@enyo-energy/energy-app-sdk 1.1.0 → 1.3.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 (66) hide show
  1. package/README.md +211 -1
  2. package/dist/cjs/energy-app-package-definition.cjs +1 -0
  3. package/dist/cjs/energy-app-package-definition.d.cts +1 -0
  4. package/dist/cjs/energy-app-permission.type.cjs +2 -0
  5. package/dist/cjs/energy-app-permission.type.d.cts +3 -1
  6. package/dist/cjs/energy-app.cjs +16 -0
  7. package/dist/cjs/energy-app.d.cts +15 -0
  8. package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
  9. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-provider-validators.cjs +20 -2
  10. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +32 -0
  11. package/dist/cjs/implementations/pricing/compose-electricity-prices.cjs +218 -0
  12. package/dist/cjs/implementations/pricing/compose-electricity-prices.d.cts +100 -0
  13. package/dist/cjs/implementations/pricing/price-component-validators.cjs +295 -0
  14. package/dist/cjs/implementations/pricing/price-component-validators.d.cts +90 -0
  15. package/dist/cjs/implementations/pricing/price-schedule-resolver.cjs +182 -0
  16. package/dist/cjs/implementations/pricing/price-schedule-resolver.d.cts +95 -0
  17. package/dist/cjs/index.cjs +7 -0
  18. package/dist/cjs/index.d.cts +7 -0
  19. package/dist/cjs/packages/energy-app-electricity-tariff.d.cts +34 -1
  20. package/dist/cjs/packages/energy-app-grid-fee.cjs +2 -0
  21. package/dist/cjs/packages/energy-app-grid-fee.d.cts +181 -0
  22. package/dist/cjs/packages/energy-app-onboarding-v2.d.cts +140 -0
  23. package/dist/cjs/types/enyo-electricity-tariff.cjs +33 -1
  24. package/dist/cjs/types/enyo-electricity-tariff.d.cts +161 -4
  25. package/dist/cjs/types/enyo-grid-fee.cjs +15 -0
  26. package/dist/cjs/types/enyo-grid-fee.d.cts +129 -0
  27. package/dist/cjs/types/enyo-onboarding-v2.d.cts +29 -0
  28. package/dist/cjs/types/enyo-price-composition.cjs +15 -0
  29. package/dist/cjs/types/enyo-price-composition.d.cts +126 -0
  30. package/dist/cjs/types/enyo-price-schedule.cjs +45 -0
  31. package/dist/cjs/types/enyo-price-schedule.d.cts +137 -0
  32. package/dist/cjs/version.cjs +1 -1
  33. package/dist/cjs/version.d.cts +1 -1
  34. package/dist/energy-app-package-definition.d.ts +1 -0
  35. package/dist/energy-app-package-definition.js +1 -0
  36. package/dist/energy-app-permission.type.d.ts +3 -1
  37. package/dist/energy-app-permission.type.js +2 -0
  38. package/dist/energy-app.d.ts +15 -0
  39. package/dist/energy-app.js +16 -0
  40. package/dist/enyo-energy-app-sdk.d.ts +3 -0
  41. package/dist/implementations/onboarding-v2/onboarding-v2-provider-validators.js +20 -2
  42. package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +32 -0
  43. package/dist/implementations/pricing/compose-electricity-prices.d.ts +100 -0
  44. package/dist/implementations/pricing/compose-electricity-prices.js +212 -0
  45. package/dist/implementations/pricing/price-component-validators.d.ts +90 -0
  46. package/dist/implementations/pricing/price-component-validators.js +285 -0
  47. package/dist/implementations/pricing/price-schedule-resolver.d.ts +95 -0
  48. package/dist/implementations/pricing/price-schedule-resolver.js +175 -0
  49. package/dist/index.d.ts +7 -0
  50. package/dist/index.js +7 -0
  51. package/dist/packages/energy-app-electricity-tariff.d.ts +34 -1
  52. package/dist/packages/energy-app-grid-fee.d.ts +181 -0
  53. package/dist/packages/energy-app-grid-fee.js +1 -0
  54. package/dist/packages/energy-app-onboarding-v2.d.ts +140 -0
  55. package/dist/types/enyo-electricity-tariff.d.ts +161 -4
  56. package/dist/types/enyo-electricity-tariff.js +32 -0
  57. package/dist/types/enyo-grid-fee.d.ts +129 -0
  58. package/dist/types/enyo-grid-fee.js +12 -0
  59. package/dist/types/enyo-onboarding-v2.d.ts +29 -0
  60. package/dist/types/enyo-price-composition.d.ts +126 -0
  61. package/dist/types/enyo-price-composition.js +12 -0
  62. package/dist/types/enyo-price-schedule.d.ts +137 -0
  63. package/dist/types/enyo-price-schedule.js +42 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/package.json +1 -1
package/README.md CHANGED
@@ -64,6 +64,14 @@ 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
+ - [Dynamic Grid Fees & Tariff Bonuses](#dynamic-grid-fees--tariff-bonuses)
68
+ - [The rule that shapes this API](#the-rule-that-shapes-this-api)
69
+ - [Publishing a dynamic grid fee](#publishing-a-dynamic-grid-fee)
70
+ - [Declaring it on a tariff](#declaring-it-on-a-tariff)
71
+ - [Multiple bonuses, and how they stack](#multiple-bonuses-and-how-they-stack)
72
+ - [Reading the result](#reading-the-result)
73
+ - [Staying current](#staying-current)
74
+ - [Helpers](#helpers)
67
75
  - [Automations](#automations)
68
76
  - [The model](#-the-model)
69
77
  - [Guide: Energy Manager apps](#-guide-energy-manager-apps)
@@ -170,6 +178,7 @@ The SDK exposes several layered building blocks. Pick the one that matches the k
170
178
  | Register a weather / PV / dynamic-price forecast provider | [`useWeatherForecasting()`](#useweatherforecasting-energyappweatherforecasting) / [`usePvForecasting()`](#usepvforecasting-energyapppvforecasting) / [`useDynamicPriceForecast()`](#usedynamicpriceforecast-energyappdynamicpriceforecast) |
171
179
  | Read EPEX SPOT wholesale prices (incl. negative-price windows) | [`useEpexSpotPrices()`](#useepexspotprices-energyappepexspotprice) |
172
180
  | Manage electricity tariffs (default tariff, price per kWh) | [`useElectricityTariff()`](#useelectricitytariff-energyappelectricitytariff) |
181
+ | Publish or resolve time-variable grid fees (§14a HT/NT) | [`useGridFee()`](#usegridfee-energyappgridfee) |
173
182
  | Register a PV system (kWp, DC strings, orientation) | [`usePvSystem()`](#usepvsystem-energyapppvsystem) |
174
183
  | Discover capabilities of the active energy manager | [`useEnergyManager()`](#useenergymanager-energyappenergymanager) |
175
184
  | Serve your v2 onboarding guides when the host asks for them | [`useOnboardingV2()`](#useonboardingv2-energyapponboardingv2) |
@@ -357,6 +366,7 @@ Energy Apps use a granular permissions system to control access to system resour
357
366
  - **`PvSystemRegister`** / **`PvSystemUse`**: Register / read PV system configuration
358
367
  - **`Savings`**: Publish and read back day-scoped savings reports
359
368
  - **`EpexSpotPrices`**: Read EPEX SPOT day-ahead wholesale market prices
369
+ - **`GridFeeRegister`** / **`GridFeeUse`**: Publish / resolve dynamic grid fees (time-variable network charges)
360
370
 
361
371
  #### Site & Identity Permissions
362
372
 
@@ -1227,15 +1237,47 @@ Register, retrieve, and manage electricity tariffs. One tariff can be marked as
1227
1237
  ```typescript
1228
1238
  const tariffs = energyApp.useElectricityTariff();
1229
1239
 
1230
- await tariffs.registerTariff({ id: 't1', name: 'Spot 2026', pricePerKwh: 0.21 });
1240
+ await tariffs.registerTariff({
1241
+ tariffType: ElectricityTariffTypeEnum.Dynamic,
1242
+ tariffName: 'Spot 2026',
1243
+ vendorName: 'enyo',
1244
+ dynamicTariffData: { currency: 'EUR', gridFeePerKwh: 0.0912 },
1245
+ });
1231
1246
  await tariffs.makeDefaultTariff('t1');
1232
1247
 
1233
1248
  const defaultTariff = await tariffs.getDefaultTariff();
1234
1249
  const all = await tariffs.getAllTariffs();
1235
1250
  ```
1236
1251
 
1252
+ A tariff can also declare that its grid fee is **dynamic**, state what its prices already include, and
1253
+ carry any number of **bonuses** — see [Dynamic Grid Fees & Tariff Bonuses](#dynamic-grid-fees--tariff-bonuses).
1254
+
1255
+ Register a listener to be told when any tariff changes — a new default, a changed grid fee link, an
1256
+ added or expired bonus all invalidate a previously composed price series:
1257
+
1258
+ ```typescript
1259
+ const listenerId = tariffs.onTariffChanged(async event => {
1260
+ if (event.changeType === EnyoTariffChangeTypeEnum.Removed) return;
1261
+ await recomposePrices(event.tariff);
1262
+ });
1263
+ // later
1264
+ tariffs.offTariffChanged(listenerId);
1265
+ ```
1266
+
1237
1267
  Requires the `ElectricityTariff` permission.
1238
1268
 
1269
+ #### `useGridFee(): EnergyAppGridFee`
1270
+
1271
+ Publish time-dependent grid fees (network charges) and resolve them to a 15-minute series. See
1272
+ [Dynamic Grid Fees & Tariff Bonuses](#dynamic-grid-fees--tariff-bonuses) for the full picture.
1273
+
1274
+ ```typescript
1275
+ const gridFee = energyApp.useGridFee();
1276
+ const fees = await gridFee.getDynamicGridFees({ fromIso, untilIso });
1277
+ ```
1278
+
1279
+ Publishers need `GridFeeRegister`; consumers need `GridFeeUse`.
1280
+
1239
1281
  #### `useWeatherForecasting(): EnergyAppWeatherForecasting`
1240
1282
 
1241
1283
  Register a weather-forecast provider (e.g. wraps an external API) and / or consume forecasts by zip code or coordinates.
@@ -2930,6 +2972,174 @@ try {
2930
2972
  }
2931
2973
  ```
2932
2974
 
2975
+ ## Dynamic Grid Fees & Tariff Bonuses
2976
+
2977
+ An electricity price is rarely one number. It is the energy price, plus the grid operator's network
2978
+ charge, minus whatever discounts the supplier grants. Both of the latter vary over time: §14a EnWG
2979
+ module 3 network charges switch between high- and low-tariff windows by time of day, weekday and
2980
+ season, and a supplier bonus may run "−5 ct/kWh between 22:00 and 06:00" or for a fixed promotional
2981
+ period.
2982
+
2983
+ ### The rule that shapes this API
2984
+
2985
+ **The energy app composes the price. The host never adds anything.**
2986
+
2987
+ That is not a stylistic choice. Provider APIs disagree with each other: some already return
2988
+ grid-fee-inclusive prices, others return the bare energy price. Only the app that owns the tariff
2989
+ knows which — so it *declares* it on the tariff, and composes accordingly. A host that silently added
2990
+ network charges would double-count for half the providers on the market.
2991
+
2992
+ ```typescript
2993
+ const tariff = await energyApp.useElectricityTariff().getDefaultTariff();
2994
+ const fees = await energyApp.useGridFee().getDynamicGridFees({ fromIso, untilIso });
2995
+ const spot = await energyApp.useEpexSpotPrices().getPrices({ fromIso, untilIso });
2996
+
2997
+ const prices = composeElectricityPrices({
2998
+ energyPrices: fromEpexSpotEntries(spot.entries),
2999
+ energyPriceComposition: tariff?.priceComposition, // ← what the prices already contain
3000
+ gridFees: fees,
3001
+ bonuses: tariff?.bonuses,
3002
+ currency: 'EUR',
3003
+ });
3004
+
3005
+ const cheapest = prices.reduce((a, b) => (b.effectivePricePerKwh < a.effectivePricePerKwh ? b : a));
3006
+ ```
3007
+
3008
+ ### Publishing a dynamic grid fee
3009
+
3010
+ Amounts are always **unsigned magnitudes**: a grid fee is added, a bonus is subtracted. There is no
3011
+ signed number to get backwards.
3012
+
3013
+ ```typescript
3014
+ await energyApp.useGridFee().registerGridFee({
3015
+ id: 'dso-hd-2026',
3016
+ name: 'Netzentgelt HT/NT 2026',
3017
+ gridOperator: 'Netze BW',
3018
+ currency: EnyoCurrencyEnum.EUR,
3019
+ timezone: 'Europe/Berlin', // required — windows are wall-clock, and DST is real
3020
+ appliesTo: EnyoPriceAppliesToEnum.Consumption,
3021
+ moduleThreeCompliant: true,
3022
+ schedule: {
3023
+ type: EnyoPriceScheduleTypeEnum.Recurring,
3024
+ windows: [
3025
+ { startTimeOfDay: '06:00', endTimeOfDay: '22:00', amountPerKwh: 0.0912 },
3026
+ { startTimeOfDay: '22:00', endTimeOfDay: '06:00', amountPerKwh: 0.0431 },
3027
+ ],
3028
+ },
3029
+ });
3030
+ ```
3031
+
3032
+ Three schedule types cover what grid operators and suppliers actually publish:
3033
+
3034
+ | Type | Use it for |
3035
+ |---|---|
3036
+ | `Constant` | one amount that never changes |
3037
+ | `Recurring` | HT/NT, weekday-dependent and seasonal windows (`daysOfWeek`, `months`) |
3038
+ | `Absolute` | dated series, e.g. a day-ahead publication — republish with `publishGridFeeWindows()` |
3039
+
3040
+ A `Recurring` window may wrap past midnight (`22:00` → `06:00`). When it does, `daysOfWeek` and
3041
+ `months` are evaluated against the day the window **starts** — a Monday-only 22:00–06:00 window covers
3042
+ Monday evening *and* the early hours of Tuesday.
3043
+
3044
+ ### Declaring it on a tariff
3045
+
3046
+ ```typescript
3047
+ await energyApp.useElectricityTariff().registerTariff({
3048
+ tariffType: ElectricityTariffTypeEnum.Dynamic,
3049
+ tariffName: 'Spot 2026',
3050
+ vendorName: 'enyo',
3051
+ priceComposition: { includesGridFee: false }, // provider returns the bare energy price
3052
+ dynamicTariffData: {
3053
+ currency: 'EUR',
3054
+ gridFeeMode: GridFeeModeEnum.Dynamic, // read the fee from useGridFee(), don't carry one
3055
+ },
3056
+ bonuses: [{
3057
+ id: 'night',
3058
+ name: 'Nachtbonus',
3059
+ timezone: 'Europe/Berlin',
3060
+ appliesTo: EnyoPriceAppliesToEnum.Consumption,
3061
+ schedule: {
3062
+ type: EnyoPriceScheduleTypeEnum.Recurring,
3063
+ windows: [{ startTimeOfDay: '22:00', endTimeOfDay: '06:00', amountPerKwh: 0.05 }],
3064
+ },
3065
+ }],
3066
+ });
3067
+ ```
3068
+
3069
+ The tariff declares only *that* its grid fee is time-dependent, never *which* fee applies. **A grid fee
3070
+ is not bound to a tariff** — it belongs to the site's grid connection, is registered independently, and
3071
+ stays the same whichever supplier the site buys from. That is why `getDynamicGridFees()` takes no
3072
+ tariff selector.
3073
+
3074
+ `gridFeeMode` defaults to `Static`, so tariffs written before this existed keep working unchanged, and
3075
+ `DynamicTariffData.gridFeePerKwh` remains the constant fallback (now deprecated). The declaration is
3076
+ available on **all three** tariff shapes — a fixed energy price with time-variable network charges is a
3077
+ real product.
3078
+
3079
+ If the provider ships all-in prices instead, set `priceComposition.includesGridFee: true` and leave
3080
+ `gridFeeMode` alone. Combining it with `Dynamic` is rejected by `validateElectricityTariffPricing()` —
3081
+ it is the exact configuration that double-counts.
3082
+
3083
+ ### Multiple bonuses, and how they stack
3084
+
3085
+ A tariff may carry several bonuses at once. Overlapping bonuses **add up**, unless one of them is
3086
+ `exclusive` — then the exclusive bonus with the highest `priority` applies alone.
3087
+
3088
+ ```typescript
3089
+ bonuses: [
3090
+ { id: 'night', schedule: nightWindow, /* … */ }, // −5 ct
3091
+ { id: 'summer', schedule: summerWindow, /* … */ }, // −2 ct → −7 ct together
3092
+ { id: 'launch', schedule: promo, exclusive: true, priority: 10, /* … */ }, // wins alone where it applies
3093
+ ]
3094
+ ```
3095
+
3096
+ ### Reading the result
3097
+
3098
+ Every component is reported separately, so a UI can explain the number and an optimizer can reason
3099
+ about its parts:
3100
+
3101
+ ```typescript
3102
+ {
3103
+ timestampIso: '2026-09-02T22:00:00.000Z',
3104
+ energyPricePerKwh: 0.18,
3105
+ gridFeePerKwh: 0.0431,
3106
+ gridFeeOrigin: 'added', // 'included' when the provider already had it, 'not-applicable' when none
3107
+ bonusPerKwh: 0.05,
3108
+ bonusOrigin: 'added',
3109
+ appliedBonusIds: ['night'],
3110
+ effectivePricePerKwh: 0.1731,
3111
+ currency: 'EUR',
3112
+ }
3113
+ ```
3114
+
3115
+ A component flagged `included` is still reported — you can break down an all-in price — but it is never
3116
+ added a second time. Effective prices may go negative, because wholesale prices do; pass
3117
+ `minEffectivePricePerKwh` only if your use case genuinely needs a floor.
3118
+
3119
+ ### Staying current
3120
+
3121
+ Both sides change under you. Listen to both:
3122
+
3123
+ ```typescript
3124
+ energyApp.useGridFee().onGridFeeChanged(() => recompose()); // new day-ahead fee series
3125
+ energyApp.useElectricityTariff().onTariffChanged(() => recompose()); // new default, bonus, or fee link
3126
+ ```
3127
+
3128
+ ### Helpers
3129
+
3130
+ | Function | Purpose |
3131
+ |---|---|
3132
+ | `composeElectricityPrices()` | energy price + grid fee − bonuses → effective price, with full breakdown |
3133
+ | `fromEnergyPriceEntries()` / `fromEpexSpotEntries()` | adapt the SDK's price shapes into composer input |
3134
+ | `resolveTariffBonuses()` | resolve a tariff's bonuses to 15-min intervals, stacking rule applied |
3135
+ | `resolvePriceSchedule()` | expand any schedule to a 15-min series (DST-aware, dependency-free) |
3136
+ | `validateDynamicGridFee()` / `validateElectricityTariffPricing()` | catch overlapping windows, negative amounts and double-count configurations before registering |
3137
+
3138
+ ### Not covered
3139
+
3140
+ Consumption-quota bonuses — "the first 100 kWh each month are free", "1000 free charging kWh per year" —
3141
+ are **not** expressible. They need consumption accounting that this model deliberately does not carry.
3142
+
2933
3143
  ## Examples
2934
3144
 
2935
3145
  ### Basic Energy App
@@ -18,6 +18,7 @@ var EnergyAppPackageCategory;
18
18
  EnergyAppPackageCategory["TemperatureSensor"] = "temperature-sensor";
19
19
  EnergyAppPackageCategory["SmartPlug"] = "smart-plug";
20
20
  EnergyAppPackageCategory["HeatingRod"] = "heating-rod";
21
+ EnergyAppPackageCategory["GridOperator"] = "grid-operator";
21
22
  EnergyAppPackageCategory["Other"] = "other";
22
23
  })(EnergyAppPackageCategory || (exports.EnergyAppPackageCategory = EnergyAppPackageCategory = {}));
23
24
  /**
@@ -16,6 +16,7 @@ export declare enum EnergyAppPackageCategory {
16
16
  TemperatureSensor = "temperature-sensor",
17
17
  SmartPlug = "smart-plug",
18
18
  HeatingRod = "heating-rod",
19
+ GridOperator = "grid-operator",
19
20
  Other = "other"
20
21
  }
21
22
  /**
@@ -60,4 +60,6 @@ var EnergyAppPermissionTypeEnum;
60
60
  EnergyAppPermissionTypeEnum["Savings"] = "Savings";
61
61
  EnergyAppPermissionTypeEnum["EpexSpotPrices"] = "EpexSpotPrices";
62
62
  EnergyAppPermissionTypeEnum["FirmwareRegistry"] = "FirmwareRegistry";
63
+ EnergyAppPermissionTypeEnum["GridFeeRegister"] = "GridFeeRegister";
64
+ EnergyAppPermissionTypeEnum["GridFeeUse"] = "GridFeeUse";
63
65
  })(EnergyAppPermissionTypeEnum || (exports.EnergyAppPermissionTypeEnum = EnergyAppPermissionTypeEnum = {}));
@@ -55,7 +55,9 @@ export declare enum EnergyAppPermissionTypeEnum {
55
55
  Automation = "Automation",
56
56
  Savings = "Savings",
57
57
  EpexSpotPrices = "EpexSpotPrices",
58
- FirmwareRegistry = "FirmwareRegistry"
58
+ FirmwareRegistry = "FirmwareRegistry",
59
+ GridFeeRegister = "GridFeeRegister",
60
+ GridFeeUse = "GridFeeUse"
59
61
  }
60
62
  /**
61
63
  * String union of every permission name, derived from
@@ -443,6 +443,22 @@ class EnergyApp {
443
443
  useEpexSpotPrices() {
444
444
  return this.energyAppSdk.useEpexSpotPrices();
445
445
  }
446
+ /**
447
+ * Gets the Grid Fee API for publishing time-dependent grid fees (network
448
+ * charges) and resolving them to a 15-minute series.
449
+ *
450
+ * Nothing published here is folded into a price automatically — provider
451
+ * APIs differ in whether their prices already contain the network charge, so
452
+ * the app that owns the tariff declares that via the tariff's
453
+ * `priceComposition` and composes the effective price itself with
454
+ * `composeElectricityPrices()`.
455
+ * @returns The Grid Fee API instance
456
+ * @throws {EnergyAppPermissionNotGrantedError} If the `GridFeeRegister` or
457
+ * `GridFeeUse` permission required by the called method is not granted.
458
+ */
459
+ useGridFee() {
460
+ return this.energyAppSdk.useGridFee();
461
+ }
446
462
  /**
447
463
  * Gets the current SDK version.
448
464
  * @returns The semantic version string of the SDK
@@ -45,6 +45,7 @@ import { EnergyAppAutomation } from "./packages/energy-app-automation.cjs";
45
45
  import { EnergyAppSavings } from "./packages/energy-app-savings.cjs";
46
46
  import { EnergyAppDeviceTest } from "./packages/energy-app-device-test.cjs";
47
47
  import { EnergyAppEpexSpotPrice } from "./packages/energy-app-epex-spot-price.cjs";
48
+ import { EnergyAppGridFee } from "./packages/energy-app-grid-fee.cjs";
48
49
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
49
50
  /**
50
51
  * Concrete implementation of {@link EnyoEnergyAppSdk} that delegates every call
@@ -366,6 +367,20 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
366
367
  * permission is not granted.
367
368
  */
368
369
  useEpexSpotPrices(): EnergyAppEpexSpotPrice;
370
+ /**
371
+ * Gets the Grid Fee API for publishing time-dependent grid fees (network
372
+ * charges) and resolving them to a 15-minute series.
373
+ *
374
+ * Nothing published here is folded into a price automatically — provider
375
+ * APIs differ in whether their prices already contain the network charge, so
376
+ * the app that owns the tariff declares that via the tariff's
377
+ * `priceComposition` and composes the effective price itself with
378
+ * `composeElectricityPrices()`.
379
+ * @returns The Grid Fee API instance
380
+ * @throws {EnergyAppPermissionNotGrantedError} If the `GridFeeRegister` or
381
+ * `GridFeeUse` permission required by the called method is not granted.
382
+ */
383
+ useGridFee(): EnergyAppGridFee;
369
384
  /**
370
385
  * Gets the current SDK version.
371
386
  * @returns The semantic version string of the SDK
@@ -44,6 +44,7 @@ import { EnergyAppAutomation } from "./packages/energy-app-automation.cjs";
44
44
  import { EnergyAppSavings } from "./packages/energy-app-savings.cjs";
45
45
  import { EnergyAppDeviceTest } from "./packages/energy-app-device-test.cjs";
46
46
  import { EnergyAppEpexSpotPrice } from "./packages/energy-app-epex-spot-price.cjs";
47
+ import { EnergyAppGridFee } from "./packages/energy-app-grid-fee.cjs";
47
48
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
48
49
  export declare enum EnergyAppStateEnum {
49
50
  Launching = "launching",
@@ -163,4 +164,6 @@ export interface EnyoEnergyAppSdk {
163
164
  useDeviceTest: () => EnergyAppDeviceTest;
164
165
  /** Get the EPEX SPOT Price API for reading the cleared day-ahead wholesale electricity prices that apply to this device */
165
166
  useEpexSpotPrices: () => EnergyAppEpexSpotPrice;
167
+ /** Get the Grid Fee API for publishing time-dependent network charges and resolving them to a 15-minute series */
168
+ useGridFee: () => EnergyAppGridFee;
166
169
  }
@@ -47,14 +47,18 @@ const ANY_MODEL = '*';
47
47
  /**
48
48
  * A short human-readable label for a guide, for use in messages.
49
49
  *
50
- * Prefers the first translated title, since ids are not required at guide level;
51
- * falls back to the start variant so a titleless guide is still identifiable.
50
+ * Prefers {@link EnyoOnboardingV2Guide.name} — it is the handle the author will
51
+ * search for — then the first translated title, then the start variant, so a
52
+ * guide with neither a name nor a title is still identifiable.
52
53
  *
53
54
  * @param guide - The guide to label.
54
55
  * @param index - Its position in the answer's `guides` array.
55
56
  * @returns A message prefix such as ``guides[2] ("Wallbox über OCPP")``.
56
57
  */
57
58
  function guideLabel(guide, index) {
59
+ const name = guide.name?.trim();
60
+ if (name)
61
+ return `guides[${index}] ("${name}")`;
58
62
  const title = guide.title?.[0]?.value;
59
63
  return `guides[${index}] (${title ? `"${title}"` : (guide.startVariant ?? '?')})`;
60
64
  }
@@ -122,6 +126,9 @@ function validateOnboardingV2GuidesResult(result, context) {
122
126
  }
123
127
  // Which guide(s) claimed each binding, so a collision can name both sides.
124
128
  const claimedBy = new Map();
129
+ // Same, for the explicit `name` handles — a duplicate makes an app unable to
130
+ // say which of the two it means.
131
+ const namedBy = new Map();
125
132
  for (const [i, guide] of result.guides.entries()) {
126
133
  const at = guideLabel(guide, i);
127
134
  const guideResult = (0, onboarding_v2_validators_js_1.validateOnboardingGuideV2)(guide, context);
@@ -134,6 +141,17 @@ function validateOnboardingV2GuidesResult(result, context) {
134
141
  else if (!guide.modelIds?.length) {
135
142
  warnings.push(`${at}: no modelIds — this guide applies to every model of "${guide.vendorId}".`);
136
143
  }
144
+ const name = guide.name?.trim();
145
+ if (name) {
146
+ const previouslyNamed = namedBy.get(name);
147
+ if (previouslyNamed) {
148
+ errors.push(`${at}: name "${name}" is already used by ${previouslyNamed} — ` +
149
+ 'a name is how an app addresses one specific guide, so two guides cannot share one.');
150
+ }
151
+ else {
152
+ namedBy.set(name, at);
153
+ }
154
+ }
137
155
  for (const key of bindingKeys(guide)) {
138
156
  const previous = claimedBy.get(key);
139
157
  if (previous) {
@@ -149,6 +149,7 @@ function validateOnboardingGuideV2(guide, context) {
149
149
  validateNetworkScanFlag(guide, warnings);
150
150
  validateApplianceBinding(guide, errors, warnings);
151
151
  validateUserNotification(guide, warnings);
152
+ validateGuideName(guide, errors, warnings);
152
153
  return { ok: errors.length === 0, errors, warnings };
153
154
  }
154
155
  /**
@@ -682,6 +683,37 @@ function validateApplianceBinding(guide, errors, warnings) {
682
683
  '` guide binds an existing appliance.');
683
684
  }
684
685
  }
686
+ /**
687
+ * Checks {@link EnyoOnboardingV2Guide.name} is usable as a handle.
688
+ *
689
+ * The property is optional — a guide that never needs to be addressed does not
690
+ * need one, and the host falls back to a name derived from the guide's
691
+ * bindings. But a name that is *present* and blank is worse than absent: it
692
+ * looks like a handle at the call site and matches nothing, so
693
+ * `removeOnboardingGuide('')` would silently do nothing. That is an error.
694
+ *
695
+ * Whitespace around a name is trimmed by the host before it is compared, so
696
+ * `" setup "` and `"setup"` are the same handle; a name carrying padding is
697
+ * only warned about, since it works but will not look like what the author
698
+ * typed in a log line.
699
+ *
700
+ * @param guide - The guide being validated.
701
+ * @param errors - Collector for blocking problems.
702
+ * @param warnings - Collector for advisory problems.
703
+ */
704
+ function validateGuideName(guide, errors, warnings) {
705
+ if (guide.name === undefined)
706
+ return;
707
+ if (typeof guide.name !== 'string' || guide.name.trim() === '') {
708
+ errors.push('`name` is present but blank — a guide either carries a handle an app can address ' +
709
+ 'it by or omits the property entirely; an empty name matches nothing.');
710
+ return;
711
+ }
712
+ if (guide.name !== guide.name.trim()) {
713
+ warnings.push(`\`name\` "${guide.name}" has leading or trailing whitespace, which the host trims — ` +
714
+ 'the guide is addressed as `' + guide.name.trim() + '`.');
715
+ }
716
+ }
685
717
  /**
686
718
  * Checks {@link EnyoOnboardingV2Guide.notifyUser} against the guide's
687
719
  * {@link EnyoOnboardingV2Guide.startVariant}.
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fromEnergyPriceEntries = fromEnergyPriceEntries;
4
+ exports.fromEpexSpotEntries = fromEpexSpotEntries;
5
+ exports.resolveTariffBonuses = resolveTariffBonuses;
6
+ exports.composeElectricityPrices = composeElectricityPrices;
7
+ const enyo_price_composition_js_1 = require("../../types/enyo-price-composition.cjs");
8
+ const enyo_price_schedule_js_1 = require("../../types/enyo-price-schedule.cjs");
9
+ const price_schedule_resolver_js_1 = require("./price-schedule-resolver.cjs");
10
+ /**
11
+ * Adapts a tariff price series from `useElectricityPrices().getPrices()` into
12
+ * composer input.
13
+ *
14
+ * @param entries - Price entries as returned by the electricity prices API
15
+ * @param direction - Which price to take; defaults to consumption. Feed-in
16
+ * entries without a `feedInPricePerKwh` are skipped.
17
+ * @returns Composable energy price entries
18
+ */
19
+ function fromEnergyPriceEntries(entries, direction = enyo_price_schedule_js_1.EnyoPriceAppliesToEnum.Consumption) {
20
+ if (direction === enyo_price_schedule_js_1.EnyoPriceAppliesToEnum.FeedIn) {
21
+ return entries
22
+ .filter(entry => entry.feedInPricePerKwh !== undefined)
23
+ .map(entry => ({ timestampIso: entry.timestampIso, pricePerKwh: entry.feedInPricePerKwh }));
24
+ }
25
+ return entries.map(entry => ({
26
+ timestampIso: entry.timestampIso,
27
+ pricePerKwh: entry.consumptionPricePerKwh,
28
+ }));
29
+ }
30
+ /**
31
+ * Adapts an EPEX SPOT day-ahead series into composer input.
32
+ *
33
+ * Spot prices are the raw exchange result and contain neither grid fees nor
34
+ * levies, taxes or supplier margin — so the tariff's `priceComposition` for such
35
+ * a series should leave every `includes*` flag `false`.
36
+ *
37
+ * @param entries - Spot price entries as returned by the EPEX SPOT price API
38
+ * @returns Composable energy price entries
39
+ */
40
+ function fromEpexSpotEntries(entries) {
41
+ return entries.map(entry => ({ timestampIso: entry.timestampIso, pricePerKwh: entry.pricePerKwh }));
42
+ }
43
+ function appliesToDirection(componentAppliesTo, direction) {
44
+ return componentAppliesTo === enyo_price_schedule_js_1.EnyoPriceAppliesToEnum.Both || componentAppliesTo === direction;
45
+ }
46
+ /**
47
+ * Resolves a tariff's bonuses over a time range and combines them per interval
48
+ * according to the SDK's stacking rule.
49
+ *
50
+ * **Stacking rule:** overlapping bonuses add up, unless at least one bonus
51
+ * covering the interval is {@link EnyoTariffBonus.exclusive} — then the
52
+ * exclusive bonus with the highest `priority` applies alone (ties are broken by
53
+ * array order). Bonuses whose `appliesTo` does not match the requested
54
+ * direction, and intervals outside a bonus's validity window, contribute
55
+ * nothing.
56
+ *
57
+ * @param bonuses - The tariff's bonuses
58
+ * @param options - Range and direction to resolve for
59
+ * @returns A map from interval start (ISO) to the total discount and the contributing bonus ids
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * const resolved = resolveTariffBonuses(tariff.bonuses ?? [], {
64
+ * fromIso: '2026-09-02T00:00:00Z',
65
+ * untilIso: '2026-09-03T00:00:00Z',
66
+ * });
67
+ * ```
68
+ */
69
+ function resolveTariffBonuses(bonuses, options) {
70
+ const direction = options.appliesTo ?? enyo_price_schedule_js_1.EnyoPriceAppliesToEnum.Consumption;
71
+ const resolved = bonuses
72
+ .filter(bonus => appliesToDirection(bonus.appliesTo, direction))
73
+ .map(bonus => {
74
+ const entries = (0, price_schedule_resolver_js_1.resolvePriceSchedule)(bonus.schedule, {
75
+ fromIso: options.fromIso,
76
+ untilIso: options.untilIso,
77
+ timezone: bonus.timezone,
78
+ validFromIso: bonus.validFromIso,
79
+ validUntilIso: bonus.validUntilIso,
80
+ });
81
+ const amountByTimestamp = new Map();
82
+ for (const entry of entries) {
83
+ if (entry.amountPerKwh > 0) {
84
+ amountByTimestamp.set(entry.timestampIso, entry.amountPerKwh);
85
+ }
86
+ }
87
+ return { bonus, amountByTimestamp };
88
+ });
89
+ const timestamps = new Set();
90
+ for (const candidate of resolved) {
91
+ for (const timestampIso of candidate.amountByTimestamp.keys()) {
92
+ timestamps.add(timestampIso);
93
+ }
94
+ }
95
+ const combined = new Map();
96
+ for (const timestampIso of timestamps) {
97
+ const active = resolved.filter(candidate => candidate.amountByTimestamp.has(timestampIso));
98
+ const exclusive = active.filter(candidate => candidate.bonus.exclusive === true);
99
+ if (exclusive.length > 0) {
100
+ const winner = exclusive.reduce((best, candidate) => (candidate.bonus.priority ?? 0) > (best.bonus.priority ?? 0) ? candidate : best);
101
+ combined.set(timestampIso, {
102
+ bonusPerKwh: winner.amountByTimestamp.get(timestampIso),
103
+ appliedBonusIds: [winner.bonus.id],
104
+ });
105
+ continue;
106
+ }
107
+ combined.set(timestampIso, {
108
+ bonusPerKwh: active.reduce((sum, candidate) => sum + candidate.amountByTimestamp.get(timestampIso), 0),
109
+ appliedBonusIds: active.map(candidate => candidate.bonus.id),
110
+ });
111
+ }
112
+ return combined;
113
+ }
114
+ function gridFeeByTimestamp(series) {
115
+ const map = new Map();
116
+ for (const entry of series?.entries ?? []) {
117
+ map.set(entry.timestampIso, entry.feePerKwh);
118
+ }
119
+ return map;
120
+ }
121
+ /**
122
+ * Composes the effective electricity price per 15-minute interval from an energy
123
+ * price series, a dynamic grid fee and a tariff's bonuses.
124
+ *
125
+ * This runs **in the energy app**, never in the host, because only the app knows
126
+ * what its provider's API already returns: some providers deliver all-in prices
127
+ * with the network charge baked in, others deliver the pure energy price. Pass
128
+ * the tariff's `priceComposition` as `energyPriceComposition` and the composer
129
+ * reports each component without adding one that is already included.
130
+ *
131
+ * Every component is reported separately — `gridFeePerKwh` with its
132
+ * {@link EnyoPriceComponentOriginEnum}, the total `bonusPerKwh` and the
133
+ * `appliedBonusIds` behind it — so a UI can explain the number and an optimizer
134
+ * can reason about the parts.
135
+ *
136
+ * Entries are produced for the intervals present in `energyPrices`; grid fee and
137
+ * bonus values are matched by `timestampIso`, and an interval without a matching
138
+ * fee or bonus simply contributes `0`.
139
+ *
140
+ * @param input - Energy prices, what they already include, the grid fee and the bonuses
141
+ * @returns Composed entries in the order of the input energy prices
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * const tariff = await energyApp.useElectricityTariff().getDefaultTariff();
146
+ * const fees = await energyApp.useGridFee().getDynamicGridFees({fromIso, untilIso});
147
+ * const spot = await energyApp.useEpexSpotPrices().getPrices({fromIso, untilIso});
148
+ *
149
+ * const prices = composeElectricityPrices({
150
+ * energyPrices: fromEpexSpotEntries(spot.entries),
151
+ * energyPriceComposition: tariff?.priceComposition,
152
+ * gridFees: fees,
153
+ * bonuses: tariff?.bonuses,
154
+ * currency: 'EUR',
155
+ * });
156
+ *
157
+ * const cheapest = prices.reduce((a, b) => (b.effectivePricePerKwh < a.effectivePricePerKwh ? b : a));
158
+ * ```
159
+ */
160
+ function composeElectricityPrices(input) {
161
+ const direction = input.appliesTo ?? enyo_price_schedule_js_1.EnyoPriceAppliesToEnum.Consumption;
162
+ const gridFeeIncluded = input.energyPriceComposition?.includesGridFee === true;
163
+ const bonusesIncluded = input.energyPriceComposition?.includesBonuses === true;
164
+ const gridFeeSeriesApplies = input.gridFees != null && appliesToDirection(input.gridFees.appliesTo, direction);
165
+ const resolvedGridFees = gridFeeSeriesApplies ? gridFeeByTimestamp(input.gridFees) : new Map();
166
+ const constantGridFee = !gridFeeSeriesApplies && input.constantGridFeePerKwh !== undefined ? input.constantGridFeePerKwh : undefined;
167
+ const hasGridFeeSource = gridFeeSeriesApplies || constantGridFee !== undefined;
168
+ const range = {
169
+ fromIso: input.energyPrices[0]?.timestampIso,
170
+ untilIso: input.energyPrices[input.energyPrices.length - 1]?.timestampIso,
171
+ };
172
+ const resolvedBonuses = input.bonuses === undefined || input.bonuses.length === 0 || range.fromIso === undefined
173
+ ? new Map()
174
+ : resolveTariffBonuses(input.bonuses, {
175
+ fromIso: range.fromIso,
176
+ // The last entry starts the final interval, so extend past it.
177
+ untilIso: new Date(Date.parse(range.untilIso) + 1).toISOString(),
178
+ appliesTo: direction,
179
+ });
180
+ return input.energyPrices.map(entry => {
181
+ const gridFeePerKwh = hasGridFeeSource
182
+ ? constantGridFee ?? resolvedGridFees.get(entry.timestampIso) ?? 0
183
+ : 0;
184
+ const gridFeeOrigin = !hasGridFeeSource
185
+ ? enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.NotApplicable
186
+ : gridFeeIncluded
187
+ ? enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.Included
188
+ : enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.Added;
189
+ const bonus = resolvedBonuses.get(entry.timestampIso);
190
+ const bonusPerKwh = bonus?.bonusPerKwh ?? 0;
191
+ const bonusOrigin = bonus === undefined
192
+ ? enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.NotApplicable
193
+ : bonusesIncluded
194
+ ? enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.Included
195
+ : enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.Added;
196
+ let effectivePricePerKwh = entry.pricePerKwh;
197
+ if (gridFeeOrigin === enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.Added) {
198
+ effectivePricePerKwh += gridFeePerKwh;
199
+ }
200
+ if (bonusOrigin === enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.Added) {
201
+ effectivePricePerKwh -= bonusPerKwh;
202
+ }
203
+ if (input.minEffectivePricePerKwh !== undefined) {
204
+ effectivePricePerKwh = Math.max(effectivePricePerKwh, input.minEffectivePricePerKwh);
205
+ }
206
+ return {
207
+ timestampIso: entry.timestampIso,
208
+ energyPricePerKwh: entry.pricePerKwh,
209
+ gridFeePerKwh,
210
+ gridFeeOrigin,
211
+ bonusPerKwh,
212
+ bonusOrigin,
213
+ appliedBonusIds: bonus?.appliedBonusIds ?? [],
214
+ effectivePricePerKwh,
215
+ currency: input.currency,
216
+ };
217
+ });
218
+ }