@enyo-energy/energy-app-sdk 1.3.0 → 1.5.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.
- package/README.md +87 -57
- package/dist/cjs/energy-app.cjs +12 -6
- package/dist/cjs/energy-app.d.cts +12 -6
- package/dist/cjs/implementations/appliances/appliance-manager.cjs +6 -0
- package/dist/cjs/implementations/pricing/compose-electricity-prices.cjs +47 -10
- package/dist/cjs/implementations/pricing/compose-electricity-prices.d.cts +34 -9
- package/dist/cjs/implementations/pricing/price-component-validators.cjs +0 -57
- package/dist/cjs/implementations/pricing/price-component-validators.d.cts +1 -34
- package/dist/cjs/packages/energy-app-charge.d.cts +8 -81
- package/dist/cjs/packages/energy-app-electricity-tariff.d.cts +148 -69
- package/dist/cjs/packages/energy-app-grid-fee.d.cts +105 -87
- package/dist/cjs/packages/energy-app-modbus.d.cts +12 -0
- package/dist/cjs/types/enyo-charge.d.cts +0 -71
- package/dist/cjs/types/enyo-data-bus-value.cjs +35 -1
- package/dist/cjs/types/enyo-data-bus-value.d.cts +88 -0
- package/dist/cjs/types/enyo-electricity-tariff.cjs +64 -36
- package/dist/cjs/types/enyo-electricity-tariff.d.cts +192 -139
- package/dist/cjs/types/enyo-energy-prices.d.cts +9 -3
- package/dist/cjs/types/enyo-grid-fee.cjs +14 -3
- package/dist/cjs/types/enyo-grid-fee.d.cts +149 -31
- package/dist/cjs/types/enyo-price-composition.d.cts +35 -14
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/energy-app.d.ts +12 -6
- package/dist/energy-app.js +12 -6
- package/dist/implementations/appliances/appliance-manager.js +6 -0
- package/dist/implementations/pricing/compose-electricity-prices.d.ts +34 -9
- package/dist/implementations/pricing/compose-electricity-prices.js +46 -10
- package/dist/implementations/pricing/price-component-validators.d.ts +1 -34
- package/dist/implementations/pricing/price-component-validators.js +0 -55
- package/dist/packages/energy-app-charge.d.ts +8 -81
- package/dist/packages/energy-app-electricity-tariff.d.ts +148 -69
- package/dist/packages/energy-app-grid-fee.d.ts +105 -87
- package/dist/packages/energy-app-modbus.d.ts +12 -0
- package/dist/types/enyo-charge.d.ts +0 -71
- package/dist/types/enyo-data-bus-value.d.ts +88 -0
- package/dist/types/enyo-data-bus-value.js +34 -0
- package/dist/types/enyo-electricity-tariff.d.ts +192 -139
- package/dist/types/enyo-electricity-tariff.js +63 -35
- package/dist/types/enyo-energy-prices.d.ts +9 -3
- package/dist/types/enyo-grid-fee.d.ts +149 -31
- package/dist/types/enyo-grid-fee.js +13 -2
- package/dist/types/enyo-price-composition.d.ts +35 -14
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
-
|
|
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.
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
-
|
|
1249
|
-
|
|
1272
|
+
await tariffs.publishPrices(EnyoTariffDirectionEnum.Consumption, {
|
|
1273
|
+
includes: [EnyoPriceComponentEnum.GridFee], // ← we folded the fee in ourselves
|
|
1274
|
+
entries,
|
|
1275
|
+
});
|
|
1250
1276
|
```
|
|
1251
1277
|
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
-
|
|
1256
|
-
|
|
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
|
|
1260
|
-
if (event.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
2994
|
-
const fees = await energyApp.useGridFee().
|
|
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:
|
|
3000
|
-
gridFees: fees,
|
|
3001
|
-
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 `
|
|
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
|
|
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().
|
|
3048
|
-
|
|
3049
|
-
|
|
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
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
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
|
-
|
|
3075
|
-
`
|
|
3076
|
-
|
|
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
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
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()` / `
|
|
3166
|
+
| `validateDynamicGridFee()` / `validateTariffBonuses()` | catch overlapping windows and negative amounts before registering |
|
|
3137
3167
|
|
|
3138
3168
|
### Not covered
|
|
3139
3169
|
|
package/dist/cjs/energy-app.cjs
CHANGED
|
@@ -444,14 +444,20 @@ class EnergyApp {
|
|
|
444
444
|
return this.energyAppSdk.useEpexSpotPrices();
|
|
445
445
|
}
|
|
446
446
|
/**
|
|
447
|
-
* Gets the Grid Fee API for publishing
|
|
448
|
-
*
|
|
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
|
|
452
|
-
*
|
|
453
|
-
* `
|
|
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
|
|
372
|
-
*
|
|
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
|
|
376
|
-
*
|
|
377
|
-
* `
|
|
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
|
|
35
|
-
*
|
|
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
|
|
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
|
|
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
|
|
146
|
-
* const fees = await energyApp.useGridFee().
|
|
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:
|
|
188
|
+
* energyPriceComposition: series?.includes,
|
|
152
189
|
* gridFees: fees,
|
|
153
|
-
* 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?.
|
|
163
|
-
const bonusesIncluded = input.energyPriceComposition?.
|
|
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 {
|
|
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
|
|
21
|
-
*
|
|
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
|
|
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
|
|
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
|
|
86
|
-
* const fees = await energyApp.useGridFee().
|
|
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:
|
|
116
|
+
* energyPriceComposition: series?.includes,
|
|
92
117
|
* gridFees: fees,
|
|
93
|
-
* 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 {
|
|
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;
|