@enyo-energy/energy-app-sdk 1.3.0 → 1.4.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 (44) hide show
  1. package/README.md +87 -57
  2. package/dist/cjs/energy-app.cjs +12 -6
  3. package/dist/cjs/energy-app.d.cts +12 -6
  4. package/dist/cjs/implementations/appliances/appliance-manager.cjs +6 -0
  5. package/dist/cjs/implementations/pricing/compose-electricity-prices.cjs +47 -10
  6. package/dist/cjs/implementations/pricing/compose-electricity-prices.d.cts +34 -9
  7. package/dist/cjs/implementations/pricing/price-component-validators.cjs +0 -57
  8. package/dist/cjs/implementations/pricing/price-component-validators.d.cts +1 -34
  9. package/dist/cjs/packages/energy-app-charge.d.cts +8 -81
  10. package/dist/cjs/packages/energy-app-electricity-tariff.d.cts +148 -69
  11. package/dist/cjs/packages/energy-app-grid-fee.d.cts +105 -87
  12. package/dist/cjs/packages/energy-app-modbus.d.cts +12 -0
  13. package/dist/cjs/types/enyo-charge.d.cts +0 -71
  14. package/dist/cjs/types/enyo-data-bus-value.d.cts +7 -0
  15. package/dist/cjs/types/enyo-electricity-tariff.cjs +64 -36
  16. package/dist/cjs/types/enyo-electricity-tariff.d.cts +192 -139
  17. package/dist/cjs/types/enyo-energy-prices.d.cts +9 -3
  18. package/dist/cjs/types/enyo-grid-fee.cjs +14 -3
  19. package/dist/cjs/types/enyo-grid-fee.d.cts +149 -31
  20. package/dist/cjs/types/enyo-price-composition.d.cts +35 -14
  21. package/dist/cjs/version.cjs +1 -1
  22. package/dist/cjs/version.d.cts +1 -1
  23. package/dist/energy-app.d.ts +12 -6
  24. package/dist/energy-app.js +12 -6
  25. package/dist/implementations/appliances/appliance-manager.js +6 -0
  26. package/dist/implementations/pricing/compose-electricity-prices.d.ts +34 -9
  27. package/dist/implementations/pricing/compose-electricity-prices.js +46 -10
  28. package/dist/implementations/pricing/price-component-validators.d.ts +1 -34
  29. package/dist/implementations/pricing/price-component-validators.js +0 -55
  30. package/dist/packages/energy-app-charge.d.ts +8 -81
  31. package/dist/packages/energy-app-electricity-tariff.d.ts +148 -69
  32. package/dist/packages/energy-app-grid-fee.d.ts +105 -87
  33. package/dist/packages/energy-app-modbus.d.ts +12 -0
  34. package/dist/types/enyo-charge.d.ts +0 -71
  35. package/dist/types/enyo-data-bus-value.d.ts +7 -0
  36. package/dist/types/enyo-electricity-tariff.d.ts +192 -139
  37. package/dist/types/enyo-electricity-tariff.js +63 -35
  38. package/dist/types/enyo-energy-prices.d.ts +9 -3
  39. package/dist/types/enyo-grid-fee.d.ts +149 -31
  40. package/dist/types/enyo-grid-fee.js +13 -2
  41. package/dist/types/enyo-price-composition.d.ts +35 -14
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +2 -2
package/README.md CHANGED
@@ -1232,38 +1232,70 @@ Three things to get right:
1232
1232
 
1233
1233
  #### `useElectricityTariff(): EnergyAppElectricityTariff`
1234
1234
 
1235
- Register, retrieve, and manage electricity tariffs. One tariff can be marked as the system default.
1235
+ A site has **two tariff slots** — one for consumption, one for feed-in — and every method addresses
1236
+ them by `EnyoTariffDirectionEnum`. There is no tariff list, no default flag and no tariff id:
1237
+ occupying a slot is what it means to be the tariff in force.
1238
+
1239
+ Consumers read:
1236
1240
 
1237
1241
  ```typescript
1238
1242
  const tariffs = energyApp.useElectricityTariff();
1239
1243
 
1240
- await tariffs.registerTariff({
1241
- tariffType: ElectricityTariffTypeEnum.Dynamic,
1242
- tariffName: 'Spot 2026',
1243
- vendorName: 'enyo',
1244
- dynamicTariffData: { currency: 'EUR', gridFeePerKwh: 0.0912 },
1244
+ const tariff = await tariffs.getTariff(EnyoTariffDirectionEnum.Consumption);
1245
+ const prices = await tariffs.getPrices(EnyoTariffDirectionEnum.Consumption, { fromIso, untilIso });
1246
+
1247
+ // Never add a component the series already contains.
1248
+ const needsGridFee = !prices?.includes.includes(EnyoPriceComponentEnum.GridFee);
1249
+ ```
1250
+
1251
+ The app that integrates a provider owns the other side. It answers when the user picks it, sets the
1252
+ tariff once it is actually usable, and pushes prices as they arrive:
1253
+
1254
+ ```typescript
1255
+ tariffs.onTariffSelected(async direction => {
1256
+ if (!await isAuthenticated()) {
1257
+ return {
1258
+ status: EnyoTariffActivationStatusEnum.AuthenticationRequired,
1259
+ authenticationUrl: buildOAuthUrl(direction),
1260
+ };
1261
+ }
1262
+ await tariffs.setTariff(direction, {
1263
+ name: 'Spot 2026',
1264
+ vendorName: 'enyo',
1265
+ currency: EnyoCurrencyEnum.EUR,
1266
+ pricing: { type: EnyoTariffPricingTypeEnum.Dynamic },
1267
+ externalTariffId: contract.id,
1268
+ });
1269
+ return { status: EnyoTariffActivationStatusEnum.Success };
1245
1270
  });
1246
- await tariffs.makeDefaultTariff('t1');
1247
1271
 
1248
- const defaultTariff = await tariffs.getDefaultTariff();
1249
- const all = await tariffs.getAllTariffs();
1272
+ await tariffs.publishPrices(EnyoTariffDirectionEnum.Consumption, {
1273
+ includes: [EnyoPriceComponentEnum.GridFee], // ← we folded the fee in ourselves
1274
+ entries,
1275
+ });
1250
1276
  ```
1251
1277
 
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).
1278
+ **Calling `setTariff` is the activation signal.** Return `AuthenticationRequired` or
1279
+ `OnboardingRequired` from the handler to have the host send the user somewhere, and carry the
1280
+ `authenticationUrl` / `onboardingGuideId` that makes it actionable; when that flow later completes,
1281
+ call `setTariff` and the slot goes live. There is nothing else to report.
1254
1282
 
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:
1283
+ Prices are **pushed, not pulled** — providers publish day-ahead prices on their own schedule and
1284
+ rate-limit their APIs, so an app fetches when it makes sense and publishes what it got.
1285
+
1286
+ Listen for slot changes to know when to recompose:
1257
1287
 
1258
1288
  ```typescript
1259
- const listenerId = tariffs.onTariffChanged(async event => {
1260
- if (event.changeType === EnyoTariffChangeTypeEnum.Removed) return;
1289
+ const unsubscribe = tariffs.onTariffChanged(async event => {
1290
+ if (event.direction !== EnyoTariffDirectionEnum.Consumption) return;
1261
1291
  await recomposePrices(event.tariff);
1262
1292
  });
1263
1293
  // later
1264
- tariffs.offTariffChanged(listenerId);
1294
+ unsubscribe();
1265
1295
  ```
1266
1296
 
1297
+ A tariff carries **no grid fee** — see [Dynamic Grid Fees & Tariff Bonuses](#dynamic-grid-fees--tariff-bonuses).
1298
+
1267
1299
  Requires the `ElectricityTariff` permission.
1268
1300
 
1269
1301
  #### `useGridFee(): EnergyAppGridFee`
@@ -1273,7 +1305,7 @@ Publish time-dependent grid fees (network charges) and resolve them to a 15-minu
1273
1305
 
1274
1306
  ```typescript
1275
1307
  const gridFee = energyApp.useGridFee();
1276
- const fees = await gridFee.getDynamicGridFees({ fromIso, untilIso });
1308
+ const fees = await gridFee.getGridFeeValues({ fromIso, untilIso });
1277
1309
  ```
1278
1310
 
1279
1311
  Publishers need `GridFeeRegister`; consumers need `GridFeeUse`.
@@ -2986,19 +3018,19 @@ period.
2986
3018
 
2987
3019
  That is not a stylistic choice. Provider APIs disagree with each other: some already return
2988
3020
  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.
3021
+ knows which — so it *declares* it on the price series it publishes, and composes accordingly. A host
3022
+ that silently added network charges would double-count for half the providers on the market.
2991
3023
 
2992
3024
  ```typescript
2993
- const tariff = await energyApp.useElectricityTariff().getDefaultTariff();
2994
- const fees = await energyApp.useGridFee().getDynamicGridFees({ fromIso, untilIso });
3025
+ const series = await energyApp.useElectricityTariff().getPrices(direction, { fromIso, untilIso });
3026
+ const fees = await energyApp.useGridFee().getGridFeeValues({ fromIso, untilIso });
2995
3027
  const spot = await energyApp.useEpexSpotPrices().getPrices({ fromIso, untilIso });
2996
3028
 
2997
3029
  const prices = composeElectricityPrices({
2998
3030
  energyPrices: fromEpexSpotEntries(spot.entries),
2999
- energyPriceComposition: tariff?.priceComposition, // ← what the prices already contain
3000
- gridFees: fees,
3001
- bonuses: tariff?.bonuses,
3031
+ energyPriceComposition: series?.includes, // ← what the prices already contain
3032
+ gridFees: fromGridFeeEntries(fees), // ← gross cent → currency units
3033
+ bonuses: myBonuses,
3002
3034
  currency: 'EUR',
3003
3035
  });
3004
3036
 
@@ -3035,50 +3067,48 @@ Three schedule types cover what grid operators and suppliers actually publish:
3035
3067
  |---|---|
3036
3068
  | `Constant` | one amount that never changes |
3037
3069
  | `Recurring` | HT/NT, weekday-dependent and seasonal windows (`daysOfWeek`, `months`) |
3038
- | `Absolute` | dated series, e.g. a day-ahead publication — republish with `publishGridFeeWindows()` |
3070
+ | `Absolute` | dated series, e.g. a day-ahead publication — republish with `registerGridFee()` |
3039
3071
 
3040
3072
  A `Recurring` window may wrap past midnight (`22:00` → `06:00`). When it does, `daysOfWeek` and
3041
3073
  `months` are evaluated against the day the window **starts** — a Monday-only 22:00–06:00 window covers
3042
3074
  Monday evening *and* the early hours of Tuesday.
3043
3075
 
3044
- ### Declaring it on a tariff
3076
+ ### Declaring it on a price series
3077
+
3078
+ A tariff carries **no grid fee at all**. It is neither a field on the tariff nor something the tariff
3079
+ declares a mode for: a network charge belongs to the site's grid connection, is registered
3080
+ independently with `useGridFee()`, and stays the same whichever supplier the site buys from. That is
3081
+ why `getGridFee()` takes no arguments and `getGridFeeValues()` takes no tariff selector.
3082
+
3083
+ What the owning app *does* declare is what its published prices already contain:
3045
3084
 
3046
3085
  ```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
- }],
3086
+ await energyApp.useElectricityTariff().publishPrices(EnyoTariffDirectionEnum.Consumption, {
3087
+ includes: [], // the bare energy price — the consumer adds the grid fee
3088
+ entries,
3066
3089
  });
3067
3090
  ```
3068
3091
 
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.
3092
+ If the provider ships all-in prices instead, publish `includes: [EnyoPriceComponentEnum.GridFee]` and
3093
+ consumers will report the fee without adding it a second time. Declaring an included component you did
3094
+ not actually apply is the one mistake that silently under-prices every interval.
3073
3095
 
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.
3096
+ Bonuses work the same way — either apply them yourself and declare
3097
+ `EnyoPriceComponentEnum.Bonuses`, or hand them to `composeElectricityPrices()` and have them applied
3098
+ and reported separately:
3078
3099
 
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.
3100
+ ```typescript
3101
+ const bonuses = [{
3102
+ id: 'night',
3103
+ name: 'Nachtbonus',
3104
+ timezone: 'Europe/Berlin',
3105
+ appliesTo: EnyoPriceAppliesToEnum.Consumption,
3106
+ schedule: {
3107
+ type: EnyoPriceScheduleTypeEnum.Recurring,
3108
+ windows: [{ startTimeOfDay: '22:00', endTimeOfDay: '06:00', amountPerKwh: 0.05 }],
3109
+ },
3110
+ }];
3111
+ ```
3082
3112
 
3083
3113
  ### Multiple bonuses, and how they stack
3084
3114
 
@@ -3133,7 +3163,7 @@ energyApp.useElectricityTariff().onTariffChanged(() => recompose()); // new defa
3133
3163
  | `fromEnergyPriceEntries()` / `fromEpexSpotEntries()` | adapt the SDK's price shapes into composer input |
3134
3164
  | `resolveTariffBonuses()` | resolve a tariff's bonuses to 15-min intervals, stacking rule applied |
3135
3165
  | `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 |
3166
+ | `validateDynamicGridFee()` / `validateTariffBonuses()` | catch overlapping windows and negative amounts before registering |
3137
3167
 
3138
3168
  ### Not covered
3139
3169
 
@@ -444,14 +444,20 @@ class EnergyApp {
444
444
  return this.energyAppSdk.useEpexSpotPrices();
445
445
  }
446
446
  /**
447
- * Gets the Grid Fee API for publishing time-dependent grid fees (network
448
- * charges) and resolving them to a 15-minute series.
447
+ * Gets the Grid Fee API for publishing grid fees (network charges), asking
448
+ * which one applies, and resolving it to a 15-minute series.
449
+ *
450
+ * This is the single source for grid fees. A network charge belongs to the
451
+ * site's grid connection rather than to a supplier contract, so a tariff
452
+ * carries no fee of its own and declares nothing about how one is
453
+ * determined — constant and time-dependent fees are both registered and read
454
+ * here.
449
455
  *
450
456
  * 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()`.
457
+ * APIs differ in whether their prices already contain the network charge.
458
+ * The app that owns the tariff declares that one fact via the tariff's
459
+ * the price series' `includes` and composes the effective price itself
460
+ * with `composeElectricityPrices()`.
455
461
  * @returns The Grid Fee API instance
456
462
  * @throws {EnergyAppPermissionNotGrantedError} If the `GridFeeRegister` or
457
463
  * `GridFeeUse` permission required by the called method is not granted.
@@ -368,14 +368,20 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
368
368
  */
369
369
  useEpexSpotPrices(): EnergyAppEpexSpotPrice;
370
370
  /**
371
- * Gets the Grid Fee API for publishing time-dependent grid fees (network
372
- * charges) and resolving them to a 15-minute series.
371
+ * Gets the Grid Fee API for publishing grid fees (network charges), asking
372
+ * which one applies, and resolving it to a 15-minute series.
373
+ *
374
+ * This is the single source for grid fees. A network charge belongs to the
375
+ * site's grid connection rather than to a supplier contract, so a tariff
376
+ * carries no fee of its own and declares nothing about how one is
377
+ * determined — constant and time-dependent fees are both registered and read
378
+ * here.
373
379
  *
374
380
  * 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()`.
381
+ * APIs differ in whether their prices already contain the network charge.
382
+ * The app that owns the tariff declares that one fact via the tariff's
383
+ * the price series' `includes` and composes the effective price itself
384
+ * with `composeElectricityPrices()`.
379
385
  * @returns The Grid Fee API instance
380
386
  * @throws {EnergyAppPermissionNotGrantedError} If the `GridFeeRegister` or
381
387
  * `GridFeeUse` permission required by the called method is not granted.
@@ -290,12 +290,18 @@ class ApplianceManager {
290
290
  const updatedListenerId = applianceService.listenForApplianceUpdated((appliance) => {
291
291
  if (this.disposed)
292
292
  return;
293
+ if (this.config.enableLogging) {
294
+ console.debug(`Appliance updated event for ${appliance.id}`);
295
+ }
293
296
  this.updateCache(appliance);
294
297
  });
295
298
  this.listenerIds.push(updatedListenerId);
296
299
  const removedListenerId = applianceService.listenForApplianceRemoved((applianceId) => {
297
300
  if (this.disposed)
298
301
  return;
302
+ if (this.config.enableLogging) {
303
+ console.debug(`Appliance removed event for ${applianceId}`);
304
+ }
299
305
  this.removeFromCache(applianceId);
300
306
  });
301
307
  this.listenerIds.push(removedListenerId);
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fromEnergyPriceEntries = fromEnergyPriceEntries;
4
+ exports.fromGridFeeEntries = fromGridFeeEntries;
4
5
  exports.fromEpexSpotEntries = fromEpexSpotEntries;
5
6
  exports.resolveTariffBonuses = resolveTariffBonuses;
6
7
  exports.composeElectricityPrices = composeElectricityPrices;
8
+ const enyo_electricity_tariff_js_1 = require("../../types/enyo-electricity-tariff.cjs");
7
9
  const enyo_price_composition_js_1 = require("../../types/enyo-price-composition.cjs");
8
10
  const enyo_price_schedule_js_1 = require("../../types/enyo-price-schedule.cjs");
9
11
  const price_schedule_resolver_js_1 = require("./price-schedule-resolver.cjs");
@@ -27,12 +29,43 @@ function fromEnergyPriceEntries(entries, direction = enyo_price_schedule_js_1.En
27
29
  pricePerKwh: entry.consumptionPricePerKwh,
28
30
  }));
29
31
  }
32
+ /**
33
+ * Converts a grid fee series from `useGridFee().getGridFeeValues()` into the
34
+ * unit the composer works in.
35
+ *
36
+ * This is the **only** place cent meets currency in the pricing path, and it
37
+ * exists so the conversion happens once, visibly, instead of at every call site.
38
+ * Grid fees are published and read as gross **cent** per kWh because that is how
39
+ * they are quoted and configured; energy prices are in whole currency units
40
+ * (EUR/kWh), so a fee added to a price without dividing by 100 overstates it by
41
+ * a factor of 100 — a mistake that survives review because both numbers look
42
+ * plausible.
43
+ *
44
+ * Pass the result as {@link EnyoComposeElectricityPricesInput.gridFees}. Whether
45
+ * the amounts include the fee's additional charges is decided when the series is
46
+ * fetched, not here.
47
+ *
48
+ * @param series - The series returned by `getGridFeeValues()`, or `null`
49
+ * @returns The same series with amounts in currency per kWh, or `null` when the input was
50
+ */
51
+ function fromGridFeeEntries(series) {
52
+ if (series == null) {
53
+ return null;
54
+ }
55
+ return {
56
+ appliesTo: series.appliesTo,
57
+ entries: series.entries.map(entry => ({
58
+ timestampIso: entry.timestampIso,
59
+ feePerKwh: entry.grossCentPerKwh / 100,
60
+ })),
61
+ };
62
+ }
30
63
  /**
31
64
  * Adapts an EPEX SPOT day-ahead series into composer input.
32
65
  *
33
66
  * 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`.
67
+ * levies, taxes or supplier margin — so a series built from them should declare
68
+ * no included components at all.
36
69
  *
37
70
  * @param entries - Spot price entries as returned by the EPEX SPOT price API
38
71
  * @returns Composable energy price entries
@@ -120,12 +153,16 @@ function gridFeeByTimestamp(series) {
120
153
  }
121
154
  /**
122
155
  * Composes the effective electricity price per 15-minute interval from an energy
123
- * price series, a dynamic grid fee and a tariff's bonuses.
156
+ * price series, a grid fee and a tariff's bonuses.
157
+ *
158
+ * The three inputs come from three independent sources and are deliberately not
159
+ * coupled: energy prices from the tariff or the spot market, the grid fee from
160
+ * `useGridFee()`, the bonuses from the tariff. Pass whichever apply.
124
161
  *
125
162
  * This runs **in the energy app**, never in the host, because only the app knows
126
163
  * what its provider's API already returns: some providers deliver all-in prices
127
164
  * with the network charge baked in, others deliver the pure energy price. Pass
128
- * the tariff's `priceComposition` as `energyPriceComposition` and the composer
165
+ * the price series' `includes` as `energyPriceComposition` and the composer
129
166
  * reports each component without adding one that is already included.
130
167
  *
131
168
  * Every component is reported separately — `gridFeePerKwh` with its
@@ -142,15 +179,15 @@ function gridFeeByTimestamp(series) {
142
179
  *
143
180
  * @example
144
181
  * ```typescript
145
- * const tariff = await energyApp.useElectricityTariff().getDefaultTariff();
146
- * const fees = await energyApp.useGridFee().getDynamicGridFees({fromIso, untilIso});
182
+ * const series = await energyApp.useElectricityTariff().getPrices(direction, {fromIso, untilIso});
183
+ * const fees = await energyApp.useGridFee().getGridFeeValues({fromIso, untilIso});
147
184
  * const spot = await energyApp.useEpexSpotPrices().getPrices({fromIso, untilIso});
148
185
  *
149
186
  * const prices = composeElectricityPrices({
150
187
  * energyPrices: fromEpexSpotEntries(spot.entries),
151
- * energyPriceComposition: tariff?.priceComposition,
188
+ * energyPriceComposition: series?.includes,
152
189
  * gridFees: fees,
153
- * bonuses: tariff?.bonuses,
190
+ * bonuses: myBonuses,
154
191
  * currency: 'EUR',
155
192
  * });
156
193
  *
@@ -159,8 +196,8 @@ function gridFeeByTimestamp(series) {
159
196
  */
160
197
  function composeElectricityPrices(input) {
161
198
  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;
199
+ const gridFeeIncluded = input.energyPriceComposition?.includes(enyo_electricity_tariff_js_1.EnyoPriceComponentEnum.GridFee) === true;
200
+ const bonusesIncluded = input.energyPriceComposition?.includes(enyo_electricity_tariff_js_1.EnyoPriceComponentEnum.Bonuses) === true;
164
201
  const gridFeeSeriesApplies = input.gridFees != null && appliesToDirection(input.gridFees.appliesTo, direction);
165
202
  const resolvedGridFees = gridFeeSeriesApplies ? gridFeeByTimestamp(input.gridFees) : new Map();
166
203
  const constantGridFee = !gridFeeSeriesApplies && input.constantGridFeePerKwh !== undefined ? input.constantGridFeePerKwh : undefined;
@@ -1,7 +1,8 @@
1
1
  import { EnyoTariffBonus } from '../../types/enyo-electricity-tariff.cjs';
2
2
  import { EnyoEnergyPriceEntry } from '../../types/enyo-energy-prices.cjs';
3
3
  import { EnyoEpexSpotPriceEntry } from '../../types/enyo-epex-spot-price.cjs';
4
- import { EnyoComposableEnergyPriceEntry, EnyoComposeElectricityPricesInput, EnyoComposedElectricityPriceEntry } from '../../types/enyo-price-composition.cjs';
4
+ import { EnyoGridFeeSeries } from '../../types/enyo-grid-fee.cjs';
5
+ import { EnyoComposableEnergyPriceEntry, EnyoComposableGridFeeSeries, EnyoComposeElectricityPricesInput, EnyoComposedElectricityPriceEntry } from '../../types/enyo-price-composition.cjs';
5
6
  import { EnyoPriceAppliesToEnum } from '../../types/enyo-price-schedule.cjs';
6
7
  /**
7
8
  * Adapts a tariff price series from `useElectricityPrices().getPrices()` into
@@ -13,12 +14,32 @@ import { EnyoPriceAppliesToEnum } from '../../types/enyo-price-schedule.cjs';
13
14
  * @returns Composable energy price entries
14
15
  */
15
16
  export declare function fromEnergyPriceEntries(entries: EnyoEnergyPriceEntry[], direction?: EnyoPriceAppliesToEnum): EnyoComposableEnergyPriceEntry[];
17
+ /**
18
+ * Converts a grid fee series from `useGridFee().getGridFeeValues()` into the
19
+ * unit the composer works in.
20
+ *
21
+ * This is the **only** place cent meets currency in the pricing path, and it
22
+ * exists so the conversion happens once, visibly, instead of at every call site.
23
+ * Grid fees are published and read as gross **cent** per kWh because that is how
24
+ * they are quoted and configured; energy prices are in whole currency units
25
+ * (EUR/kWh), so a fee added to a price without dividing by 100 overstates it by
26
+ * a factor of 100 — a mistake that survives review because both numbers look
27
+ * plausible.
28
+ *
29
+ * Pass the result as {@link EnyoComposeElectricityPricesInput.gridFees}. Whether
30
+ * the amounts include the fee's additional charges is decided when the series is
31
+ * fetched, not here.
32
+ *
33
+ * @param series - The series returned by `getGridFeeValues()`, or `null`
34
+ * @returns The same series with amounts in currency per kWh, or `null` when the input was
35
+ */
36
+ export declare function fromGridFeeEntries(series: EnyoGridFeeSeries | null | undefined): EnyoComposableGridFeeSeries | null;
16
37
  /**
17
38
  * Adapts an EPEX SPOT day-ahead series into composer input.
18
39
  *
19
40
  * Spot prices are the raw exchange result and contain neither grid fees nor
20
- * levies, taxes or supplier margin — so the tariff's `priceComposition` for such
21
- * a series should leave every `includes*` flag `false`.
41
+ * levies, taxes or supplier margin — so a series built from them should declare
42
+ * no included components at all.
22
43
  *
23
44
  * @param entries - Spot price entries as returned by the EPEX SPOT price API
24
45
  * @returns Composable energy price entries
@@ -60,12 +81,16 @@ export declare function resolveTariffBonuses(bonuses: EnyoTariffBonus[], options
60
81
  }>;
61
82
  /**
62
83
  * Composes the effective electricity price per 15-minute interval from an energy
63
- * price series, a dynamic grid fee and a tariff's bonuses.
84
+ * price series, a grid fee and a tariff's bonuses.
85
+ *
86
+ * The three inputs come from three independent sources and are deliberately not
87
+ * coupled: energy prices from the tariff or the spot market, the grid fee from
88
+ * `useGridFee()`, the bonuses from the tariff. Pass whichever apply.
64
89
  *
65
90
  * This runs **in the energy app**, never in the host, because only the app knows
66
91
  * what its provider's API already returns: some providers deliver all-in prices
67
92
  * with the network charge baked in, others deliver the pure energy price. Pass
68
- * the tariff's `priceComposition` as `energyPriceComposition` and the composer
93
+ * the price series' `includes` as `energyPriceComposition` and the composer
69
94
  * reports each component without adding one that is already included.
70
95
  *
71
96
  * Every component is reported separately — `gridFeePerKwh` with its
@@ -82,15 +107,15 @@ export declare function resolveTariffBonuses(bonuses: EnyoTariffBonus[], options
82
107
  *
83
108
  * @example
84
109
  * ```typescript
85
- * const tariff = await energyApp.useElectricityTariff().getDefaultTariff();
86
- * const fees = await energyApp.useGridFee().getDynamicGridFees({fromIso, untilIso});
110
+ * const series = await energyApp.useElectricityTariff().getPrices(direction, {fromIso, untilIso});
111
+ * const fees = await energyApp.useGridFee().getGridFeeValues({fromIso, untilIso});
87
112
  * const spot = await energyApp.useEpexSpotPrices().getPrices({fromIso, untilIso});
88
113
  *
89
114
  * const prices = composeElectricityPrices({
90
115
  * energyPrices: fromEpexSpotEntries(spot.entries),
91
- * energyPriceComposition: tariff?.priceComposition,
116
+ * energyPriceComposition: series?.includes,
92
117
  * gridFees: fees,
93
- * bonuses: tariff?.bonuses,
118
+ * bonuses: myBonuses,
94
119
  * currency: 'EUR',
95
120
  * });
96
121
  *
@@ -5,9 +5,6 @@ exports.validatePriceSchedule = validatePriceSchedule;
5
5
  exports.validateDynamicGridFee = validateDynamicGridFee;
6
6
  exports.validateTariffBonus = validateTariffBonus;
7
7
  exports.validateTariffBonuses = validateTariffBonuses;
8
- exports.validateTariffGridFeeConfig = validateTariffGridFeeConfig;
9
- exports.validateElectricityTariffPricing = validateElectricityTariffPricing;
10
- const enyo_electricity_tariff_js_1 = require("../../types/enyo-electricity-tariff.cjs");
11
8
  const enyo_price_schedule_js_1 = require("../../types/enyo-price-schedule.cjs");
12
9
  const price_schedule_resolver_js_1 = require("./price-schedule-resolver.cjs");
13
10
  /**
@@ -196,7 +193,6 @@ function validatePriceSchedule(schedule, label = 'schedule') {
196
193
  * ```
197
194
  */
198
195
  function validateDynamicGridFee(registration) {
199
- requireNonEmpty(registration.id, 'gridFee.id');
200
196
  requireNonEmpty(registration.name, 'gridFee.name');
201
197
  requireNonEmpty(registration.gridOperator, 'gridFee.gridOperator');
202
198
  requireNonEmpty(registration.currency, 'gridFee.currency');
@@ -240,56 +236,3 @@ function validateTariffBonuses(bonuses) {
240
236
  seen.add(bonus.id);
241
237
  });
242
238
  }
243
- /**
244
- * Validates how a tariff's pricing data declares its grid fee.
245
- *
246
- * Catches the configuration that silently double-charges the customer: declaring
247
- * a dynamic grid fee while also declaring that the tariff's prices already
248
- * include the grid fee. Anyone trusting the declaration would add the fee on top
249
- * of prices that already contain it.
250
- *
251
- * @param config - The grid fee configuration of one tariff pricing shape
252
- * @param priceComposition - The tariff's price composition declaration, if any
253
- * @param label - Prefix used in error messages, e.g. `'dynamicTariffData'`
254
- * @throws {PriceComponentValidationError} On the first invariant violation
255
- */
256
- function validateTariffGridFeeConfig(config, priceComposition, label = 'tariffData') {
257
- const mode = config.gridFeeMode ?? enyo_electricity_tariff_js_1.GridFeeModeEnum.Static;
258
- if (!Object.values(enyo_electricity_tariff_js_1.GridFeeModeEnum).includes(mode)) {
259
- fail(`${label}.gridFeeMode must be one of ${Object.values(enyo_electricity_tariff_js_1.GridFeeModeEnum).join(', ')}`);
260
- }
261
- if (mode === enyo_electricity_tariff_js_1.GridFeeModeEnum.Dynamic && priceComposition?.includesGridFee === true) {
262
- fail(`${label} declares a dynamic grid fee while priceComposition.includesGridFee is true — ` +
263
- `the fee is already contained in the tariff's prices and must not be added again`);
264
- }
265
- }
266
- /**
267
- * Validates the pricing-related parts of a complete electricity tariff: the grid
268
- * fee link of whichever pricing shape is present, and the tariff's bonuses.
269
- *
270
- * Call it before {@link EnergyAppElectricityTariff.registerTariff} to turn what
271
- * would otherwise be a silently mispriced tariff into an actionable error.
272
- *
273
- * @param tariff - The tariff to validate (with or without its `id`)
274
- * @throws {PriceComponentValidationError} On the first invariant violation
275
- *
276
- * @example
277
- * ```typescript
278
- * validateElectricityTariffPricing(tariff);
279
- * await energyApp.useElectricityTariff().registerTariff(tariff);
280
- * ```
281
- */
282
- function validateElectricityTariffPricing(tariff) {
283
- if (tariff.staticTariffData !== undefined) {
284
- validateTariffGridFeeConfig(tariff.staticTariffData, tariff.priceComposition, 'staticTariffData');
285
- }
286
- if (tariff.timeVariableTariffData !== undefined) {
287
- validateTariffGridFeeConfig(tariff.timeVariableTariffData, tariff.priceComposition, 'timeVariableTariffData');
288
- }
289
- if (tariff.dynamicTariffData !== undefined) {
290
- validateTariffGridFeeConfig(tariff.dynamicTariffData, tariff.priceComposition, 'dynamicTariffData');
291
- }
292
- if (tariff.bonuses !== undefined) {
293
- validateTariffBonuses(tariff.bonuses);
294
- }
295
- }
@@ -1,4 +1,4 @@
1
- import { EnyoElectricityTariff, EnyoTariffBonus, TariffGridFeeConfig, TariffPriceCompositionInfo } from '../../types/enyo-electricity-tariff.cjs';
1
+ import { EnyoTariffBonus } from '../../types/enyo-electricity-tariff.cjs';
2
2
  import { EnyoDynamicGridFeeRegistration } from '../../types/enyo-grid-fee.cjs';
3
3
  import { EnyoPriceSchedule } from '../../types/enyo-price-schedule.cjs';
4
4
  /**
@@ -55,36 +55,3 @@ export declare function validateTariffBonus(bonus: EnyoTariffBonus, label?: stri
55
55
  * @throws {PriceComponentValidationError} On the first invariant violation
56
56
  */
57
57
  export declare function validateTariffBonuses(bonuses: EnyoTariffBonus[]): void;
58
- /**
59
- * Validates how a tariff's pricing data declares its grid fee.
60
- *
61
- * Catches the configuration that silently double-charges the customer: declaring
62
- * a dynamic grid fee while also declaring that the tariff's prices already
63
- * include the grid fee. Anyone trusting the declaration would add the fee on top
64
- * of prices that already contain it.
65
- *
66
- * @param config - The grid fee configuration of one tariff pricing shape
67
- * @param priceComposition - The tariff's price composition declaration, if any
68
- * @param label - Prefix used in error messages, e.g. `'dynamicTariffData'`
69
- * @throws {PriceComponentValidationError} On the first invariant violation
70
- */
71
- export declare function validateTariffGridFeeConfig(config: TariffGridFeeConfig, priceComposition: TariffPriceCompositionInfo | undefined, label?: string): void;
72
- /**
73
- * Validates the pricing-related parts of a complete electricity tariff: the grid
74
- * fee link of whichever pricing shape is present, and the tariff's bonuses.
75
- *
76
- * Call it before {@link EnergyAppElectricityTariff.registerTariff} to turn what
77
- * would otherwise be a silently mispriced tariff into an actionable error.
78
- *
79
- * @param tariff - The tariff to validate (with or without its `id`)
80
- * @throws {PriceComponentValidationError} On the first invariant violation
81
- *
82
- * @example
83
- * ```typescript
84
- * validateElectricityTariffPricing(tariff);
85
- * await energyApp.useElectricityTariff().registerTariff(tariff);
86
- * ```
87
- */
88
- export declare function validateElectricityTariffPricing(tariff: Omit<EnyoElectricityTariff, 'id'> & {
89
- id?: string;
90
- }): void;