@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.
- package/README.md +211 -1
- package/dist/cjs/energy-app-package-definition.cjs +1 -0
- package/dist/cjs/energy-app-package-definition.d.cts +1 -0
- package/dist/cjs/energy-app-permission.type.cjs +2 -0
- package/dist/cjs/energy-app-permission.type.d.cts +3 -1
- package/dist/cjs/energy-app.cjs +16 -0
- package/dist/cjs/energy-app.d.cts +15 -0
- package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-provider-validators.cjs +20 -2
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +32 -0
- package/dist/cjs/implementations/pricing/compose-electricity-prices.cjs +218 -0
- package/dist/cjs/implementations/pricing/compose-electricity-prices.d.cts +100 -0
- package/dist/cjs/implementations/pricing/price-component-validators.cjs +295 -0
- package/dist/cjs/implementations/pricing/price-component-validators.d.cts +90 -0
- package/dist/cjs/implementations/pricing/price-schedule-resolver.cjs +182 -0
- package/dist/cjs/implementations/pricing/price-schedule-resolver.d.cts +95 -0
- package/dist/cjs/index.cjs +7 -0
- package/dist/cjs/index.d.cts +7 -0
- package/dist/cjs/packages/energy-app-electricity-tariff.d.cts +34 -1
- package/dist/cjs/packages/energy-app-grid-fee.cjs +2 -0
- package/dist/cjs/packages/energy-app-grid-fee.d.cts +181 -0
- package/dist/cjs/packages/energy-app-onboarding-v2.d.cts +140 -0
- package/dist/cjs/types/enyo-electricity-tariff.cjs +33 -1
- package/dist/cjs/types/enyo-electricity-tariff.d.cts +161 -4
- package/dist/cjs/types/enyo-grid-fee.cjs +15 -0
- package/dist/cjs/types/enyo-grid-fee.d.cts +129 -0
- package/dist/cjs/types/enyo-onboarding-v2.d.cts +29 -0
- package/dist/cjs/types/enyo-price-composition.cjs +15 -0
- package/dist/cjs/types/enyo-price-composition.d.cts +126 -0
- package/dist/cjs/types/enyo-price-schedule.cjs +45 -0
- package/dist/cjs/types/enyo-price-schedule.d.cts +137 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/energy-app-package-definition.d.ts +1 -0
- package/dist/energy-app-package-definition.js +1 -0
- package/dist/energy-app-permission.type.d.ts +3 -1
- package/dist/energy-app-permission.type.js +2 -0
- package/dist/energy-app.d.ts +15 -0
- package/dist/energy-app.js +16 -0
- package/dist/enyo-energy-app-sdk.d.ts +3 -0
- package/dist/implementations/onboarding-v2/onboarding-v2-provider-validators.js +20 -2
- package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +32 -0
- package/dist/implementations/pricing/compose-electricity-prices.d.ts +100 -0
- package/dist/implementations/pricing/compose-electricity-prices.js +212 -0
- package/dist/implementations/pricing/price-component-validators.d.ts +90 -0
- package/dist/implementations/pricing/price-component-validators.js +285 -0
- package/dist/implementations/pricing/price-schedule-resolver.d.ts +95 -0
- package/dist/implementations/pricing/price-schedule-resolver.js +175 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/packages/energy-app-electricity-tariff.d.ts +34 -1
- package/dist/packages/energy-app-grid-fee.d.ts +181 -0
- package/dist/packages/energy-app-grid-fee.js +1 -0
- package/dist/packages/energy-app-onboarding-v2.d.ts +140 -0
- package/dist/types/enyo-electricity-tariff.d.ts +161 -4
- package/dist/types/enyo-electricity-tariff.js +32 -0
- package/dist/types/enyo-grid-fee.d.ts +129 -0
- package/dist/types/enyo-grid-fee.js +12 -0
- package/dist/types/enyo-onboarding-v2.d.ts +29 -0
- package/dist/types/enyo-price-composition.d.ts +126 -0
- package/dist/types/enyo-price-composition.js +12 -0
- package/dist/types/enyo-price-schedule.d.ts +137 -0
- package/dist/types/enyo-price-schedule.js +42 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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({
|
|
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
|
/**
|
|
@@ -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
|
package/dist/cjs/energy-app.cjs
CHANGED
|
@@ -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
|
|
51
|
-
*
|
|
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
|
+
}
|