@enyo-energy/energy-app-sdk 1.2.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 (70) hide show
  1. package/README.md +245 -5
  2. package/dist/cjs/energy-app-package-definition.cjs +1 -0
  3. package/dist/cjs/energy-app-package-definition.d.cts +1 -0
  4. package/dist/cjs/energy-app-permission.type.cjs +2 -0
  5. package/dist/cjs/energy-app-permission.type.d.cts +3 -1
  6. package/dist/cjs/energy-app.cjs +22 -0
  7. package/dist/cjs/energy-app.d.cts +21 -0
  8. package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
  9. package/dist/cjs/implementations/appliances/appliance-manager.cjs +6 -0
  10. package/dist/cjs/implementations/pricing/compose-electricity-prices.cjs +255 -0
  11. package/dist/cjs/implementations/pricing/compose-electricity-prices.d.cts +125 -0
  12. package/dist/cjs/implementations/pricing/price-component-validators.cjs +238 -0
  13. package/dist/cjs/implementations/pricing/price-component-validators.d.cts +57 -0
  14. package/dist/cjs/implementations/pricing/price-schedule-resolver.cjs +182 -0
  15. package/dist/cjs/implementations/pricing/price-schedule-resolver.d.cts +95 -0
  16. package/dist/cjs/index.cjs +7 -0
  17. package/dist/cjs/index.d.cts +7 -0
  18. package/dist/cjs/packages/energy-app-charge.d.cts +8 -81
  19. package/dist/cjs/packages/energy-app-electricity-tariff.d.cts +153 -41
  20. package/dist/cjs/packages/energy-app-grid-fee.cjs +2 -0
  21. package/dist/cjs/packages/energy-app-grid-fee.d.cts +199 -0
  22. package/dist/cjs/packages/energy-app-modbus.d.cts +12 -0
  23. package/dist/cjs/types/enyo-charge.d.cts +0 -71
  24. package/dist/cjs/types/enyo-data-bus-value.d.cts +7 -0
  25. package/dist/cjs/types/enyo-electricity-tariff.cjs +73 -13
  26. package/dist/cjs/types/enyo-electricity-tariff.d.cts +260 -50
  27. package/dist/cjs/types/enyo-energy-prices.d.cts +9 -3
  28. package/dist/cjs/types/enyo-grid-fee.cjs +26 -0
  29. package/dist/cjs/types/enyo-grid-fee.d.cts +247 -0
  30. package/dist/cjs/types/enyo-price-composition.cjs +15 -0
  31. package/dist/cjs/types/enyo-price-composition.d.cts +147 -0
  32. package/dist/cjs/types/enyo-price-schedule.cjs +45 -0
  33. package/dist/cjs/types/enyo-price-schedule.d.cts +137 -0
  34. package/dist/cjs/version.cjs +1 -1
  35. package/dist/cjs/version.d.cts +1 -1
  36. package/dist/energy-app-package-definition.d.ts +1 -0
  37. package/dist/energy-app-package-definition.js +1 -0
  38. package/dist/energy-app-permission.type.d.ts +3 -1
  39. package/dist/energy-app-permission.type.js +2 -0
  40. package/dist/energy-app.d.ts +21 -0
  41. package/dist/energy-app.js +22 -0
  42. package/dist/enyo-energy-app-sdk.d.ts +3 -0
  43. package/dist/implementations/appliances/appliance-manager.js +6 -0
  44. package/dist/implementations/pricing/compose-electricity-prices.d.ts +125 -0
  45. package/dist/implementations/pricing/compose-electricity-prices.js +248 -0
  46. package/dist/implementations/pricing/price-component-validators.d.ts +57 -0
  47. package/dist/implementations/pricing/price-component-validators.js +230 -0
  48. package/dist/implementations/pricing/price-schedule-resolver.d.ts +95 -0
  49. package/dist/implementations/pricing/price-schedule-resolver.js +175 -0
  50. package/dist/index.d.ts +7 -0
  51. package/dist/index.js +7 -0
  52. package/dist/packages/energy-app-charge.d.ts +8 -81
  53. package/dist/packages/energy-app-electricity-tariff.d.ts +153 -41
  54. package/dist/packages/energy-app-grid-fee.d.ts +199 -0
  55. package/dist/packages/energy-app-grid-fee.js +1 -0
  56. package/dist/packages/energy-app-modbus.d.ts +12 -0
  57. package/dist/types/enyo-charge.d.ts +0 -71
  58. package/dist/types/enyo-data-bus-value.d.ts +7 -0
  59. package/dist/types/enyo-electricity-tariff.d.ts +260 -50
  60. package/dist/types/enyo-electricity-tariff.js +71 -11
  61. package/dist/types/enyo-energy-prices.d.ts +9 -3
  62. package/dist/types/enyo-grid-fee.d.ts +247 -0
  63. package/dist/types/enyo-grid-fee.js +23 -0
  64. package/dist/types/enyo-price-composition.d.ts +147 -0
  65. package/dist/types/enyo-price-composition.js +12 -0
  66. package/dist/types/enyo-price-schedule.d.ts +137 -0
  67. package/dist/types/enyo-price-schedule.js +42 -0
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/package.json +2 -2
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fromEnergyPriceEntries = fromEnergyPriceEntries;
4
+ exports.fromGridFeeEntries = fromGridFeeEntries;
5
+ exports.fromEpexSpotEntries = fromEpexSpotEntries;
6
+ exports.resolveTariffBonuses = resolveTariffBonuses;
7
+ exports.composeElectricityPrices = composeElectricityPrices;
8
+ const enyo_electricity_tariff_js_1 = require("../../types/enyo-electricity-tariff.cjs");
9
+ const enyo_price_composition_js_1 = require("../../types/enyo-price-composition.cjs");
10
+ const enyo_price_schedule_js_1 = require("../../types/enyo-price-schedule.cjs");
11
+ const price_schedule_resolver_js_1 = require("./price-schedule-resolver.cjs");
12
+ /**
13
+ * Adapts a tariff price series from `useElectricityPrices().getPrices()` into
14
+ * composer input.
15
+ *
16
+ * @param entries - Price entries as returned by the electricity prices API
17
+ * @param direction - Which price to take; defaults to consumption. Feed-in
18
+ * entries without a `feedInPricePerKwh` are skipped.
19
+ * @returns Composable energy price entries
20
+ */
21
+ function fromEnergyPriceEntries(entries, direction = enyo_price_schedule_js_1.EnyoPriceAppliesToEnum.Consumption) {
22
+ if (direction === enyo_price_schedule_js_1.EnyoPriceAppliesToEnum.FeedIn) {
23
+ return entries
24
+ .filter(entry => entry.feedInPricePerKwh !== undefined)
25
+ .map(entry => ({ timestampIso: entry.timestampIso, pricePerKwh: entry.feedInPricePerKwh }));
26
+ }
27
+ return entries.map(entry => ({
28
+ timestampIso: entry.timestampIso,
29
+ pricePerKwh: entry.consumptionPricePerKwh,
30
+ }));
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
+ }
63
+ /**
64
+ * Adapts an EPEX SPOT day-ahead series into composer input.
65
+ *
66
+ * Spot prices are the raw exchange result and contain neither grid fees nor
67
+ * levies, taxes or supplier margin — so a series built from them should declare
68
+ * no included components at all.
69
+ *
70
+ * @param entries - Spot price entries as returned by the EPEX SPOT price API
71
+ * @returns Composable energy price entries
72
+ */
73
+ function fromEpexSpotEntries(entries) {
74
+ return entries.map(entry => ({ timestampIso: entry.timestampIso, pricePerKwh: entry.pricePerKwh }));
75
+ }
76
+ function appliesToDirection(componentAppliesTo, direction) {
77
+ return componentAppliesTo === enyo_price_schedule_js_1.EnyoPriceAppliesToEnum.Both || componentAppliesTo === direction;
78
+ }
79
+ /**
80
+ * Resolves a tariff's bonuses over a time range and combines them per interval
81
+ * according to the SDK's stacking rule.
82
+ *
83
+ * **Stacking rule:** overlapping bonuses add up, unless at least one bonus
84
+ * covering the interval is {@link EnyoTariffBonus.exclusive} — then the
85
+ * exclusive bonus with the highest `priority` applies alone (ties are broken by
86
+ * array order). Bonuses whose `appliesTo` does not match the requested
87
+ * direction, and intervals outside a bonus's validity window, contribute
88
+ * nothing.
89
+ *
90
+ * @param bonuses - The tariff's bonuses
91
+ * @param options - Range and direction to resolve for
92
+ * @returns A map from interval start (ISO) to the total discount and the contributing bonus ids
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const resolved = resolveTariffBonuses(tariff.bonuses ?? [], {
97
+ * fromIso: '2026-09-02T00:00:00Z',
98
+ * untilIso: '2026-09-03T00:00:00Z',
99
+ * });
100
+ * ```
101
+ */
102
+ function resolveTariffBonuses(bonuses, options) {
103
+ const direction = options.appliesTo ?? enyo_price_schedule_js_1.EnyoPriceAppliesToEnum.Consumption;
104
+ const resolved = bonuses
105
+ .filter(bonus => appliesToDirection(bonus.appliesTo, direction))
106
+ .map(bonus => {
107
+ const entries = (0, price_schedule_resolver_js_1.resolvePriceSchedule)(bonus.schedule, {
108
+ fromIso: options.fromIso,
109
+ untilIso: options.untilIso,
110
+ timezone: bonus.timezone,
111
+ validFromIso: bonus.validFromIso,
112
+ validUntilIso: bonus.validUntilIso,
113
+ });
114
+ const amountByTimestamp = new Map();
115
+ for (const entry of entries) {
116
+ if (entry.amountPerKwh > 0) {
117
+ amountByTimestamp.set(entry.timestampIso, entry.amountPerKwh);
118
+ }
119
+ }
120
+ return { bonus, amountByTimestamp };
121
+ });
122
+ const timestamps = new Set();
123
+ for (const candidate of resolved) {
124
+ for (const timestampIso of candidate.amountByTimestamp.keys()) {
125
+ timestamps.add(timestampIso);
126
+ }
127
+ }
128
+ const combined = new Map();
129
+ for (const timestampIso of timestamps) {
130
+ const active = resolved.filter(candidate => candidate.amountByTimestamp.has(timestampIso));
131
+ const exclusive = active.filter(candidate => candidate.bonus.exclusive === true);
132
+ if (exclusive.length > 0) {
133
+ const winner = exclusive.reduce((best, candidate) => (candidate.bonus.priority ?? 0) > (best.bonus.priority ?? 0) ? candidate : best);
134
+ combined.set(timestampIso, {
135
+ bonusPerKwh: winner.amountByTimestamp.get(timestampIso),
136
+ appliedBonusIds: [winner.bonus.id],
137
+ });
138
+ continue;
139
+ }
140
+ combined.set(timestampIso, {
141
+ bonusPerKwh: active.reduce((sum, candidate) => sum + candidate.amountByTimestamp.get(timestampIso), 0),
142
+ appliedBonusIds: active.map(candidate => candidate.bonus.id),
143
+ });
144
+ }
145
+ return combined;
146
+ }
147
+ function gridFeeByTimestamp(series) {
148
+ const map = new Map();
149
+ for (const entry of series?.entries ?? []) {
150
+ map.set(entry.timestampIso, entry.feePerKwh);
151
+ }
152
+ return map;
153
+ }
154
+ /**
155
+ * Composes the effective electricity price per 15-minute interval from an energy
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.
161
+ *
162
+ * This runs **in the energy app**, never in the host, because only the app knows
163
+ * what its provider's API already returns: some providers deliver all-in prices
164
+ * with the network charge baked in, others deliver the pure energy price. Pass
165
+ * the price series' `includes` as `energyPriceComposition` and the composer
166
+ * reports each component without adding one that is already included.
167
+ *
168
+ * Every component is reported separately — `gridFeePerKwh` with its
169
+ * {@link EnyoPriceComponentOriginEnum}, the total `bonusPerKwh` and the
170
+ * `appliedBonusIds` behind it — so a UI can explain the number and an optimizer
171
+ * can reason about the parts.
172
+ *
173
+ * Entries are produced for the intervals present in `energyPrices`; grid fee and
174
+ * bonus values are matched by `timestampIso`, and an interval without a matching
175
+ * fee or bonus simply contributes `0`.
176
+ *
177
+ * @param input - Energy prices, what they already include, the grid fee and the bonuses
178
+ * @returns Composed entries in the order of the input energy prices
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * const series = await energyApp.useElectricityTariff().getPrices(direction, {fromIso, untilIso});
183
+ * const fees = await energyApp.useGridFee().getGridFeeValues({fromIso, untilIso});
184
+ * const spot = await energyApp.useEpexSpotPrices().getPrices({fromIso, untilIso});
185
+ *
186
+ * const prices = composeElectricityPrices({
187
+ * energyPrices: fromEpexSpotEntries(spot.entries),
188
+ * energyPriceComposition: series?.includes,
189
+ * gridFees: fees,
190
+ * bonuses: myBonuses,
191
+ * currency: 'EUR',
192
+ * });
193
+ *
194
+ * const cheapest = prices.reduce((a, b) => (b.effectivePricePerKwh < a.effectivePricePerKwh ? b : a));
195
+ * ```
196
+ */
197
+ function composeElectricityPrices(input) {
198
+ const direction = input.appliesTo ?? enyo_price_schedule_js_1.EnyoPriceAppliesToEnum.Consumption;
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;
201
+ const gridFeeSeriesApplies = input.gridFees != null && appliesToDirection(input.gridFees.appliesTo, direction);
202
+ const resolvedGridFees = gridFeeSeriesApplies ? gridFeeByTimestamp(input.gridFees) : new Map();
203
+ const constantGridFee = !gridFeeSeriesApplies && input.constantGridFeePerKwh !== undefined ? input.constantGridFeePerKwh : undefined;
204
+ const hasGridFeeSource = gridFeeSeriesApplies || constantGridFee !== undefined;
205
+ const range = {
206
+ fromIso: input.energyPrices[0]?.timestampIso,
207
+ untilIso: input.energyPrices[input.energyPrices.length - 1]?.timestampIso,
208
+ };
209
+ const resolvedBonuses = input.bonuses === undefined || input.bonuses.length === 0 || range.fromIso === undefined
210
+ ? new Map()
211
+ : resolveTariffBonuses(input.bonuses, {
212
+ fromIso: range.fromIso,
213
+ // The last entry starts the final interval, so extend past it.
214
+ untilIso: new Date(Date.parse(range.untilIso) + 1).toISOString(),
215
+ appliesTo: direction,
216
+ });
217
+ return input.energyPrices.map(entry => {
218
+ const gridFeePerKwh = hasGridFeeSource
219
+ ? constantGridFee ?? resolvedGridFees.get(entry.timestampIso) ?? 0
220
+ : 0;
221
+ const gridFeeOrigin = !hasGridFeeSource
222
+ ? enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.NotApplicable
223
+ : gridFeeIncluded
224
+ ? enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.Included
225
+ : enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.Added;
226
+ const bonus = resolvedBonuses.get(entry.timestampIso);
227
+ const bonusPerKwh = bonus?.bonusPerKwh ?? 0;
228
+ const bonusOrigin = bonus === undefined
229
+ ? enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.NotApplicable
230
+ : bonusesIncluded
231
+ ? enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.Included
232
+ : enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.Added;
233
+ let effectivePricePerKwh = entry.pricePerKwh;
234
+ if (gridFeeOrigin === enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.Added) {
235
+ effectivePricePerKwh += gridFeePerKwh;
236
+ }
237
+ if (bonusOrigin === enyo_price_composition_js_1.EnyoPriceComponentOriginEnum.Added) {
238
+ effectivePricePerKwh -= bonusPerKwh;
239
+ }
240
+ if (input.minEffectivePricePerKwh !== undefined) {
241
+ effectivePricePerKwh = Math.max(effectivePricePerKwh, input.minEffectivePricePerKwh);
242
+ }
243
+ return {
244
+ timestampIso: entry.timestampIso,
245
+ energyPricePerKwh: entry.pricePerKwh,
246
+ gridFeePerKwh,
247
+ gridFeeOrigin,
248
+ bonusPerKwh,
249
+ bonusOrigin,
250
+ appliedBonusIds: bonus?.appliedBonusIds ?? [],
251
+ effectivePricePerKwh,
252
+ currency: input.currency,
253
+ };
254
+ });
255
+ }
@@ -0,0 +1,125 @@
1
+ import { EnyoTariffBonus } from '../../types/enyo-electricity-tariff.cjs';
2
+ import { EnyoEnergyPriceEntry } from '../../types/enyo-energy-prices.cjs';
3
+ import { EnyoEpexSpotPriceEntry } from '../../types/enyo-epex-spot-price.cjs';
4
+ import { EnyoGridFeeSeries } from '../../types/enyo-grid-fee.cjs';
5
+ import { EnyoComposableEnergyPriceEntry, EnyoComposableGridFeeSeries, EnyoComposeElectricityPricesInput, EnyoComposedElectricityPriceEntry } from '../../types/enyo-price-composition.cjs';
6
+ import { EnyoPriceAppliesToEnum } from '../../types/enyo-price-schedule.cjs';
7
+ /**
8
+ * Adapts a tariff price series from `useElectricityPrices().getPrices()` into
9
+ * composer input.
10
+ *
11
+ * @param entries - Price entries as returned by the electricity prices API
12
+ * @param direction - Which price to take; defaults to consumption. Feed-in
13
+ * entries without a `feedInPricePerKwh` are skipped.
14
+ * @returns Composable energy price entries
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;
37
+ /**
38
+ * Adapts an EPEX SPOT day-ahead series into composer input.
39
+ *
40
+ * Spot prices are the raw exchange result and contain neither grid fees nor
41
+ * levies, taxes or supplier margin — so a series built from them should declare
42
+ * no included components at all.
43
+ *
44
+ * @param entries - Spot price entries as returned by the EPEX SPOT price API
45
+ * @returns Composable energy price entries
46
+ */
47
+ export declare function fromEpexSpotEntries(entries: EnyoEpexSpotPriceEntry[]): EnyoComposableEnergyPriceEntry[];
48
+ /**
49
+ * Resolves a tariff's bonuses over a time range and combines them per interval
50
+ * according to the SDK's stacking rule.
51
+ *
52
+ * **Stacking rule:** overlapping bonuses add up, unless at least one bonus
53
+ * covering the interval is {@link EnyoTariffBonus.exclusive} — then the
54
+ * exclusive bonus with the highest `priority` applies alone (ties are broken by
55
+ * array order). Bonuses whose `appliesTo` does not match the requested
56
+ * direction, and intervals outside a bonus's validity window, contribute
57
+ * nothing.
58
+ *
59
+ * @param bonuses - The tariff's bonuses
60
+ * @param options - Range and direction to resolve for
61
+ * @returns A map from interval start (ISO) to the total discount and the contributing bonus ids
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const resolved = resolveTariffBonuses(tariff.bonuses ?? [], {
66
+ * fromIso: '2026-09-02T00:00:00Z',
67
+ * untilIso: '2026-09-03T00:00:00Z',
68
+ * });
69
+ * ```
70
+ */
71
+ export declare function resolveTariffBonuses(bonuses: EnyoTariffBonus[], options: {
72
+ /** Start of the range in ISO format (inclusive). */
73
+ fromIso: string;
74
+ /** End of the range in ISO format (exclusive). */
75
+ untilIso: string;
76
+ /** Direction to resolve for; defaults to consumption. */
77
+ appliesTo?: EnyoPriceAppliesToEnum;
78
+ }): Map<string, {
79
+ bonusPerKwh: number;
80
+ appliedBonusIds: string[];
81
+ }>;
82
+ /**
83
+ * Composes the effective electricity price per 15-minute interval from an energy
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.
89
+ *
90
+ * This runs **in the energy app**, never in the host, because only the app knows
91
+ * what its provider's API already returns: some providers deliver all-in prices
92
+ * with the network charge baked in, others deliver the pure energy price. Pass
93
+ * the price series' `includes` as `energyPriceComposition` and the composer
94
+ * reports each component without adding one that is already included.
95
+ *
96
+ * Every component is reported separately — `gridFeePerKwh` with its
97
+ * {@link EnyoPriceComponentOriginEnum}, the total `bonusPerKwh` and the
98
+ * `appliedBonusIds` behind it — so a UI can explain the number and an optimizer
99
+ * can reason about the parts.
100
+ *
101
+ * Entries are produced for the intervals present in `energyPrices`; grid fee and
102
+ * bonus values are matched by `timestampIso`, and an interval without a matching
103
+ * fee or bonus simply contributes `0`.
104
+ *
105
+ * @param input - Energy prices, what they already include, the grid fee and the bonuses
106
+ * @returns Composed entries in the order of the input energy prices
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * const series = await energyApp.useElectricityTariff().getPrices(direction, {fromIso, untilIso});
111
+ * const fees = await energyApp.useGridFee().getGridFeeValues({fromIso, untilIso});
112
+ * const spot = await energyApp.useEpexSpotPrices().getPrices({fromIso, untilIso});
113
+ *
114
+ * const prices = composeElectricityPrices({
115
+ * energyPrices: fromEpexSpotEntries(spot.entries),
116
+ * energyPriceComposition: series?.includes,
117
+ * gridFees: fees,
118
+ * bonuses: myBonuses,
119
+ * currency: 'EUR',
120
+ * });
121
+ *
122
+ * const cheapest = prices.reduce((a, b) => (b.effectivePricePerKwh < a.effectivePricePerKwh ? b : a));
123
+ * ```
124
+ */
125
+ export declare function composeElectricityPrices(input: EnyoComposeElectricityPricesInput): EnyoComposedElectricityPriceEntry[];
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PriceComponentValidationError = void 0;
4
+ exports.validatePriceSchedule = validatePriceSchedule;
5
+ exports.validateDynamicGridFee = validateDynamicGridFee;
6
+ exports.validateTariffBonus = validateTariffBonus;
7
+ exports.validateTariffBonuses = validateTariffBonuses;
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
+ * Thrown when a grid fee, tariff bonus or price schedule violates one of the
12
+ * invariants enforced by this module. The message names the offending field or
13
+ * window index so callers can surface it directly to the user.
14
+ */
15
+ class PriceComponentValidationError extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = 'PriceComponentValidationError';
19
+ }
20
+ }
21
+ exports.PriceComponentValidationError = PriceComponentValidationError;
22
+ function fail(message) {
23
+ throw new PriceComponentValidationError(message);
24
+ }
25
+ function requireNonEmpty(value, field) {
26
+ if (value === undefined || value.trim().length === 0) {
27
+ fail(`${field} must be a non-empty string`);
28
+ }
29
+ }
30
+ function requireValidTimezone(timezone, field) {
31
+ requireNonEmpty(timezone, field);
32
+ try {
33
+ new Intl.DateTimeFormat('en-US', { timeZone: timezone });
34
+ }
35
+ catch {
36
+ fail(`${field} must be a valid IANA time zone identifier, got '${timezone}'`);
37
+ }
38
+ }
39
+ function requireNonNegativeAmount(amount, field) {
40
+ if (!Number.isFinite(amount)) {
41
+ fail(`${field} must be a finite number`);
42
+ }
43
+ if (amount < 0) {
44
+ fail(`${field} must not be negative — amounts are unsigned magnitudes; ` +
45
+ `grid fees are added to and bonuses subtracted from the energy price`);
46
+ }
47
+ }
48
+ function requireIsoTimestamp(value, field) {
49
+ const parsed = Date.parse(value);
50
+ if (Number.isNaN(parsed)) {
51
+ fail(`${field} must be a valid ISO timestamp, got '${value}'`);
52
+ }
53
+ return parsed;
54
+ }
55
+ function requireValidityRange(validFromIso, validUntilIso, label) {
56
+ const from = validFromIso === undefined ? undefined : requireIsoTimestamp(validFromIso, `${label}.validFromIso`);
57
+ const until = validUntilIso === undefined ? undefined : requireIsoTimestamp(validUntilIso, `${label}.validUntilIso`);
58
+ if (from !== undefined && until !== undefined && until <= from) {
59
+ fail(`${label}.validUntilIso must be after ${label}.validFromIso`);
60
+ }
61
+ }
62
+ function requireAppliesTo(appliesTo, field) {
63
+ if (!Object.values(enyo_price_schedule_js_1.EnyoPriceAppliesToEnum).includes(appliesTo)) {
64
+ fail(`${field} must be one of ${Object.values(enyo_price_schedule_js_1.EnyoPriceAppliesToEnum).join(', ')}`);
65
+ }
66
+ }
67
+ function toMinuteIntervals(start, end) {
68
+ return start < end
69
+ ? [{ start, end }]
70
+ : [{ start, end: 1440 }, { start: 0, end }];
71
+ }
72
+ function intervalsOverlap(a, b) {
73
+ return a.some(left => b.some(right => left.start < right.end && right.start < left.end));
74
+ }
75
+ function setsIntersect(a, b) {
76
+ if (a === undefined || b === undefined) {
77
+ return true;
78
+ }
79
+ return a.some(value => b.includes(value));
80
+ }
81
+ /**
82
+ * Validates a {@link EnyoPriceSchedule}. Throws on the first violation.
83
+ *
84
+ * Enforces finite, non-negative amounts, well-formed `HH:mm` times, day and
85
+ * month ranges, ordered absolute windows, and that no two windows which can
86
+ * occur on the same day overlap in time — an overlap would make the resolved
87
+ * amount depend on array order rather than on the schedule itself.
88
+ *
89
+ * Overlap detection for recurring windows is conservative: two windows are only
90
+ * compared when their day-of-week and month sets can intersect. A window that
91
+ * wraps past midnight is compared on both of its legs.
92
+ *
93
+ * @param schedule - The schedule to validate
94
+ * @param label - Prefix used in error messages, e.g. `'gridFee.schedule'`
95
+ * @throws {PriceComponentValidationError} On the first invariant violation
96
+ */
97
+ function validatePriceSchedule(schedule, label = 'schedule') {
98
+ if (schedule === null || typeof schedule !== 'object') {
99
+ fail(`${label} must be an object`);
100
+ }
101
+ if (schedule.type === enyo_price_schedule_js_1.EnyoPriceScheduleTypeEnum.Constant) {
102
+ requireNonNegativeAmount(schedule.amountPerKwh, `${label}.amountPerKwh`);
103
+ return;
104
+ }
105
+ if (schedule.type === enyo_price_schedule_js_1.EnyoPriceScheduleTypeEnum.Absolute) {
106
+ if (schedule.windows.length === 0) {
107
+ fail(`${label}.windows must contain at least one window`);
108
+ }
109
+ const ranges = schedule.windows.map((window, index) => {
110
+ const start = requireIsoTimestamp(window.startIso, `${label}.windows[${index}].startIso`);
111
+ const end = requireIsoTimestamp(window.endIso, `${label}.windows[${index}].endIso`);
112
+ if (end <= start) {
113
+ fail(`${label}.windows[${index}].endIso must be after startIso`);
114
+ }
115
+ requireNonNegativeAmount(window.amountPerKwh, `${label}.windows[${index}].amountPerKwh`);
116
+ return { start, end, index };
117
+ }).sort((a, b) => a.start - b.start);
118
+ for (let i = 1; i < ranges.length; i++) {
119
+ if (ranges[i].start < ranges[i - 1].end) {
120
+ fail(`${label}.windows[${ranges[i].index}] overlaps windows[${ranges[i - 1].index}]`);
121
+ }
122
+ }
123
+ return;
124
+ }
125
+ if (schedule.type === enyo_price_schedule_js_1.EnyoPriceScheduleTypeEnum.Recurring) {
126
+ if (schedule.windows.length === 0) {
127
+ fail(`${label}.windows must contain at least one window`);
128
+ }
129
+ const parsed = schedule.windows.map((window, index) => {
130
+ const start = (0, price_schedule_resolver_js_1.parseTimeOfDay)(window.startTimeOfDay);
131
+ const end = (0, price_schedule_resolver_js_1.parseTimeOfDay)(window.endTimeOfDay);
132
+ if (start === null) {
133
+ fail(`${label}.windows[${index}].startTimeOfDay must be a 'HH:mm' time, got '${window.startTimeOfDay}'`);
134
+ }
135
+ if (end === null) {
136
+ fail(`${label}.windows[${index}].endTimeOfDay must be a 'HH:mm' time, got '${window.endTimeOfDay}'`);
137
+ }
138
+ if (start === end) {
139
+ fail(`${label}.windows[${index}] must not start and end at the same time of day`);
140
+ }
141
+ if (window.daysOfWeek !== undefined) {
142
+ if (window.daysOfWeek.length === 0) {
143
+ fail(`${label}.windows[${index}].daysOfWeek must not be empty — omit it for 'every day'`);
144
+ }
145
+ for (const day of window.daysOfWeek) {
146
+ if (!Number.isInteger(day) || day < 0 || day > 6) {
147
+ fail(`${label}.windows[${index}].daysOfWeek must contain integers 0 (Sunday) to 6 (Saturday), got ${day}`);
148
+ }
149
+ }
150
+ }
151
+ if (window.months !== undefined) {
152
+ if (window.months.length === 0) {
153
+ fail(`${label}.windows[${index}].months must not be empty — omit it for 'every month'`);
154
+ }
155
+ for (const month of window.months) {
156
+ if (!Number.isInteger(month) || month < 1 || month > 12) {
157
+ fail(`${label}.windows[${index}].months must contain integers 1 to 12, got ${month}`);
158
+ }
159
+ }
160
+ }
161
+ requireNonNegativeAmount(window.amountPerKwh, `${label}.windows[${index}].amountPerKwh`);
162
+ return { window, index, intervals: toMinuteIntervals(start, end) };
163
+ });
164
+ for (let i = 0; i < parsed.length; i++) {
165
+ for (let j = i + 1; j < parsed.length; j++) {
166
+ const left = parsed[i];
167
+ const right = parsed[j];
168
+ if (!setsIntersect(left.window.daysOfWeek, right.window.daysOfWeek)) {
169
+ continue;
170
+ }
171
+ if (!setsIntersect(left.window.months, right.window.months)) {
172
+ continue;
173
+ }
174
+ if (intervalsOverlap(left.intervals, right.intervals)) {
175
+ fail(`${label}.windows[${right.index}] overlaps windows[${left.index}]`);
176
+ }
177
+ }
178
+ }
179
+ return;
180
+ }
181
+ fail(`${label}.type must be one of ${Object.values(enyo_price_schedule_js_1.EnyoPriceScheduleTypeEnum).join(', ')}`);
182
+ }
183
+ /**
184
+ * Validates a dynamic grid fee registration. Throws on the first violation.
185
+ *
186
+ * @param registration - The grid fee to validate
187
+ * @throws {PriceComponentValidationError} On the first invariant violation
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * validateDynamicGridFee(registration);
192
+ * await energyApp.useGridFee().registerGridFee(registration);
193
+ * ```
194
+ */
195
+ function validateDynamicGridFee(registration) {
196
+ requireNonEmpty(registration.name, 'gridFee.name');
197
+ requireNonEmpty(registration.gridOperator, 'gridFee.gridOperator');
198
+ requireNonEmpty(registration.currency, 'gridFee.currency');
199
+ requireValidTimezone(registration.timezone, 'gridFee.timezone');
200
+ requireAppliesTo(registration.appliesTo, 'gridFee.appliesTo');
201
+ requireValidityRange(registration.validFromIso, registration.validUntilIso, 'gridFee');
202
+ validatePriceSchedule(registration.schedule, 'gridFee.schedule');
203
+ }
204
+ /**
205
+ * Validates a single tariff bonus. Throws on the first violation.
206
+ *
207
+ * @param bonus - The bonus to validate
208
+ * @param label - Prefix used in error messages, e.g. `'bonuses[0]'`
209
+ * @throws {PriceComponentValidationError} On the first invariant violation
210
+ */
211
+ function validateTariffBonus(bonus, label = 'bonus') {
212
+ requireNonEmpty(bonus.id, `${label}.id`);
213
+ requireNonEmpty(bonus.name, `${label}.name`);
214
+ requireValidTimezone(bonus.timezone, `${label}.timezone`);
215
+ requireAppliesTo(bonus.appliesTo, `${label}.appliesTo`);
216
+ requireValidityRange(bonus.validFromIso, bonus.validUntilIso, label);
217
+ if (bonus.priority !== undefined && !Number.isFinite(bonus.priority)) {
218
+ fail(`${label}.priority must be a finite number`);
219
+ }
220
+ validatePriceSchedule(bonus.schedule, `${label}.schedule`);
221
+ }
222
+ /**
223
+ * Validates a tariff's bonus list: every bonus is individually valid and ids are
224
+ * unique within the tariff.
225
+ *
226
+ * @param bonuses - The bonuses to validate
227
+ * @throws {PriceComponentValidationError} On the first invariant violation
228
+ */
229
+ function validateTariffBonuses(bonuses) {
230
+ const seen = new Set();
231
+ bonuses.forEach((bonus, index) => {
232
+ validateTariffBonus(bonus, `bonuses[${index}]`);
233
+ if (seen.has(bonus.id)) {
234
+ fail(`bonuses[${index}].id '${bonus.id}' is used more than once in the same tariff`);
235
+ }
236
+ seen.add(bonus.id);
237
+ });
238
+ }
@@ -0,0 +1,57 @@
1
+ import { EnyoTariffBonus } from '../../types/enyo-electricity-tariff.cjs';
2
+ import { EnyoDynamicGridFeeRegistration } from '../../types/enyo-grid-fee.cjs';
3
+ import { EnyoPriceSchedule } from '../../types/enyo-price-schedule.cjs';
4
+ /**
5
+ * Thrown when a grid fee, tariff bonus or price schedule violates one of the
6
+ * invariants enforced by this module. The message names the offending field or
7
+ * window index so callers can surface it directly to the user.
8
+ */
9
+ export declare class PriceComponentValidationError extends Error {
10
+ constructor(message: string);
11
+ }
12
+ /**
13
+ * Validates a {@link EnyoPriceSchedule}. Throws on the first violation.
14
+ *
15
+ * Enforces finite, non-negative amounts, well-formed `HH:mm` times, day and
16
+ * month ranges, ordered absolute windows, and that no two windows which can
17
+ * occur on the same day overlap in time — an overlap would make the resolved
18
+ * amount depend on array order rather than on the schedule itself.
19
+ *
20
+ * Overlap detection for recurring windows is conservative: two windows are only
21
+ * compared when their day-of-week and month sets can intersect. A window that
22
+ * wraps past midnight is compared on both of its legs.
23
+ *
24
+ * @param schedule - The schedule to validate
25
+ * @param label - Prefix used in error messages, e.g. `'gridFee.schedule'`
26
+ * @throws {PriceComponentValidationError} On the first invariant violation
27
+ */
28
+ export declare function validatePriceSchedule(schedule: EnyoPriceSchedule, label?: string): void;
29
+ /**
30
+ * Validates a dynamic grid fee registration. Throws on the first violation.
31
+ *
32
+ * @param registration - The grid fee to validate
33
+ * @throws {PriceComponentValidationError} On the first invariant violation
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * validateDynamicGridFee(registration);
38
+ * await energyApp.useGridFee().registerGridFee(registration);
39
+ * ```
40
+ */
41
+ export declare function validateDynamicGridFee(registration: EnyoDynamicGridFeeRegistration): void;
42
+ /**
43
+ * Validates a single tariff bonus. Throws on the first violation.
44
+ *
45
+ * @param bonus - The bonus to validate
46
+ * @param label - Prefix used in error messages, e.g. `'bonuses[0]'`
47
+ * @throws {PriceComponentValidationError} On the first invariant violation
48
+ */
49
+ export declare function validateTariffBonus(bonus: EnyoTariffBonus, label?: string): void;
50
+ /**
51
+ * Validates a tariff's bonus list: every bonus is individually valid and ids are
52
+ * unique within the tariff.
53
+ *
54
+ * @param bonuses - The bonuses to validate
55
+ * @throws {PriceComponentValidationError} On the first invariant violation
56
+ */
57
+ export declare function validateTariffBonuses(bonuses: EnyoTariffBonus[]): void;