@enyo-energy/energy-app-sdk 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +87 -57
  2. package/dist/cjs/energy-app.cjs +12 -6
  3. package/dist/cjs/energy-app.d.cts +12 -6
  4. package/dist/cjs/implementations/appliances/appliance-manager.cjs +6 -0
  5. package/dist/cjs/implementations/pricing/compose-electricity-prices.cjs +47 -10
  6. package/dist/cjs/implementations/pricing/compose-electricity-prices.d.cts +34 -9
  7. package/dist/cjs/implementations/pricing/price-component-validators.cjs +0 -57
  8. package/dist/cjs/implementations/pricing/price-component-validators.d.cts +1 -34
  9. package/dist/cjs/packages/energy-app-charge.d.cts +8 -81
  10. package/dist/cjs/packages/energy-app-electricity-tariff.d.cts +148 -69
  11. package/dist/cjs/packages/energy-app-grid-fee.d.cts +105 -87
  12. package/dist/cjs/packages/energy-app-modbus.d.cts +12 -0
  13. package/dist/cjs/types/enyo-charge.d.cts +0 -71
  14. package/dist/cjs/types/enyo-data-bus-value.d.cts +7 -0
  15. package/dist/cjs/types/enyo-electricity-tariff.cjs +64 -36
  16. package/dist/cjs/types/enyo-electricity-tariff.d.cts +192 -139
  17. package/dist/cjs/types/enyo-energy-prices.d.cts +9 -3
  18. package/dist/cjs/types/enyo-grid-fee.cjs +14 -3
  19. package/dist/cjs/types/enyo-grid-fee.d.cts +149 -31
  20. package/dist/cjs/types/enyo-price-composition.d.cts +35 -14
  21. package/dist/cjs/version.cjs +1 -1
  22. package/dist/cjs/version.d.cts +1 -1
  23. package/dist/energy-app.d.ts +12 -6
  24. package/dist/energy-app.js +12 -6
  25. package/dist/implementations/appliances/appliance-manager.js +6 -0
  26. package/dist/implementations/pricing/compose-electricity-prices.d.ts +34 -9
  27. package/dist/implementations/pricing/compose-electricity-prices.js +46 -10
  28. package/dist/implementations/pricing/price-component-validators.d.ts +1 -34
  29. package/dist/implementations/pricing/price-component-validators.js +0 -55
  30. package/dist/packages/energy-app-charge.d.ts +8 -81
  31. package/dist/packages/energy-app-electricity-tariff.d.ts +148 -69
  32. package/dist/packages/energy-app-grid-fee.d.ts +105 -87
  33. package/dist/packages/energy-app-modbus.d.ts +12 -0
  34. package/dist/types/enyo-charge.d.ts +0 -71
  35. package/dist/types/enyo-data-bus-value.d.ts +7 -0
  36. package/dist/types/enyo-electricity-tariff.d.ts +192 -139
  37. package/dist/types/enyo-electricity-tariff.js +63 -35
  38. package/dist/types/enyo-energy-prices.d.ts +9 -3
  39. package/dist/types/enyo-grid-fee.d.ts +149 -31
  40. package/dist/types/enyo-grid-fee.js +13 -2
  41. package/dist/types/enyo-price-composition.d.ts +35 -14
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +2 -2
@@ -1,93 +1,20 @@
1
- import { EnyoCharge, EnyoChargeFilter, EnyoDefaultChargeMode, EnyoStartChargeRequest, EnyoStopChargeRequest } from "../types/enyo-charge.cjs";
1
+ import { EnyoCharge, EnyoChargeFilter, EnyoDefaultChargeMode } from "../types/enyo-charge.cjs";
2
2
  /**
3
3
  * Interface for managing charging sessions in enyo packages.
4
4
  * Provides operations for charge session tracking and management.
5
5
  */
6
6
  export interface EnergyAppCharge {
7
- /**
8
- * Opens a charging session for an appliance and returns it.
9
- *
10
- * Use this to start a session — not {@link save}. `save` writes whatever
11
- * object it is given and offers no uniqueness guarantee, so two
12
- * StartTransactions arriving a few milliseconds apart both pass an app-side
13
- * "is one already active?" check and create two sessions for one plug-in.
14
- * This call is handled by the host, which holds the invariant that an
15
- * appliance has at most one active charge.
16
- *
17
- * **Idempotent.** When a session is already active for the same
18
- * `applianceId` + `transactionId`, that session is returned unchanged
19
- * rather than a second one being created — a repeated StartTransaction is
20
- * safe. A session that is active on the appliance under a *different*
21
- * transaction ID is treated as over: the host finalises it with best-effort
22
- * end values before opening this one. If it is really the same physical
23
- * session continuing under a new transaction ID (a reconnect, a
24
- * re-authorization), use {@link addTransactionIdToActiveCharge} instead.
25
- *
26
- * The returned charge carries its `id`, so the session can be updated and
27
- * later stopped without a {@link findActiveCharge} round-trip first.
28
- *
29
- * **Required permission:** `Charge`.
30
- *
31
- * @param request - What is known at plug-in time.
32
- * @returns Promise resolving to the started session — or the session that
33
- * was already running for this appliance and transaction.
34
- *
35
- * @example
36
- * ```typescript
37
- * const charge = await energyApp.useCharge().startCharge({
38
- * applianceId,
39
- * transactionId: ocppTransactionId,
40
- * meterStartValueWh: meterValue,
41
- * connectorId: 1,
42
- * });
43
- * ```
44
- */
45
- startCharge: (request: EnyoStartChargeRequest) => Promise<EnyoCharge>;
46
- /**
47
- * Closes a charging session and returns the final record.
48
- *
49
- * The host does the arithmetic, and that is the point of this call: it
50
- * derives {@link EnyoCharge.totalEnergyKwh} and
51
- * {@link EnyoCharge.paidPriceEuroCent} from the session's meter readings
52
- * and the electricity tariff in force while the energy flowed, then moves
53
- * the session to a terminal status and notifies
54
- * {@link listenForChargeStopped}. An app must not price a session itself —
55
- * totals computed app-side would differ between apps, and a customer's bill
56
- * would depend on which app happened to close the charge.
57
- *
58
- * Supply readings, not totals: `meterEndValueWh` and, when the charger
59
- * reports it, `energyDeliveredWh`. Anything an app writes into
60
- * `paidPriceEuroCent` via {@link save} is overwritten here.
61
- *
62
- * Stopping a session that is already in a terminal status is a no-op and
63
- * returns the stored record — a duplicate StopTransaction will not re-price
64
- * a finished charge.
65
- *
66
- * **Required permission:** `Charge`.
67
- *
68
- * @param chargeId - The session to close, as returned by
69
- * {@link startCharge} or {@link findActiveCharge}.
70
- * @param request - How the session ended.
71
- * @returns Promise resolving to the final charge record, totals included.
72
- *
73
- * @example
74
- * ```typescript
75
- * const finished = await energyApp.useCharge().stopCharge(charge.id, {
76
- * meterEndValueWh: meterValue,
77
- * energyDeliveredWh: stopTransaction.energyWh,
78
- * });
79
- * console.log(`${finished.totalEnergyKwh} kWh for ${finished.paidPriceEuroCent} ct`);
80
- * ```
81
- */
82
- stopCharge: (chargeId: string, request: EnyoStopChargeRequest) => Promise<EnyoCharge>;
83
7
  /**
84
8
  * Save or update a charging session in the system.
85
9
  *
86
10
  * This is a whole-object write for a session that already exists — a
87
- * changed vehicle, a corrected reading. Use {@link startCharge} to open a
88
- * session and {@link stopCharge} to close one: calling this without a
89
- * `chargeId` creates a row with no uniqueness guarantee, and writing
90
- * terminal values by hand skips the host's energy and cost calculation.
11
+ * changed vehicle, a corrected reading. Calling it without a `chargeId`
12
+ * creates a row with no uniqueness guarantee, so two StartTransactions
13
+ * arriving a few milliseconds apart both pass an app-side "is one already
14
+ * active?" check and create two sessions for one plug-in. Writing terminal
15
+ * values by hand also skips the host's energy and cost calculation, which
16
+ * is what turns meter readings into {@link EnyoCharge.totalEnergyKwh} and
17
+ * {@link EnyoCharge.paidPriceEuroCent}.
91
18
  */
92
19
  save: (charge: Omit<EnyoCharge, 'id'>, chargeId?: string) => Promise<void>;
93
20
  /** Get a list of charging sessions with optional filtering */
@@ -1,96 +1,175 @@
1
- import { EnyoElectricityTariff, EnyoElectricityTariffWithDefault, EnyoTariffChangeEvent } from "../types/enyo-electricity-tariff.cjs";
1
+ import { EnyoElectricityTariff, EnyoTariffActivationResult, EnyoTariffChangeEvent, EnyoTariffDirectionEnum, EnyoTariffPricePublication, EnyoTariffPriceSeries } from "../types/enyo-electricity-tariff.cjs";
2
2
  /**
3
- * Interface for managing electricity tariffs.
4
- * Provides methods to register, retrieve, and remove electricity tariffs
5
- * used for energy pricing and consumption calculations.
3
+ * The time range a price query covers.
4
+ */
5
+ export interface EnyoTariffPriceRange {
6
+ /** Start of the requested range in ISO format (inclusive). */
7
+ fromIso: string;
8
+ /** End of the requested range in ISO format (exclusive). */
9
+ untilIso: string;
10
+ }
11
+ /**
12
+ * Interface for reading and supplying the site's electricity tariffs.
13
+ *
14
+ * A site has **two tariff slots** — one for consumption, one for feed-in — and
15
+ * everything here addresses them by {@link EnyoTariffDirectionEnum}. There is no
16
+ * list of tariffs, no default flag and no tariff id: occupying a slot is what it
17
+ * means to be the tariff in force.
18
+ *
19
+ * Six methods, split by role. A **consumer** asks {@link getTariff} what applies
20
+ * and {@link getPrices} what it costs, and follows {@link onTariffChanged}. An
21
+ * **owner** — the app integrating a provider like Tibber or Ostrom — answers
22
+ * {@link onTariffSelected} when the user picks it, calls {@link setTariff} once
23
+ * its tariff is usable, and feeds {@link publishPrices} as new prices arrive.
24
+ *
25
+ * A tariff carries no grid fee. A network charge belongs to the site's grid
26
+ * connection, not to the supplier contract — read it from `useGridFee()` and
27
+ * combine the two with `composeElectricityPrices()`.
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const tariffs = energyApp.useElectricityTariff();
32
+ *
33
+ * // Owner: the user picked us in the app UI.
34
+ * tariffs.onTariffSelected(async direction => {
35
+ * if (!await isAuthenticated()) {
36
+ * return {
37
+ * status: EnyoTariffActivationStatusEnum.AuthenticationRequired,
38
+ * authenticationUrl: buildOAuthUrl(direction),
39
+ * };
40
+ * }
41
+ * await tariffs.setTariff(direction, myTariff);
42
+ * return {status: EnyoTariffActivationStatusEnum.Success};
43
+ * });
44
+ * ```
6
45
  */
7
46
  export interface EnergyAppElectricityTariff {
8
47
  /**
9
- * Registers a new electricity tariff or updates an existing one.
10
- * Uses upsert logic based on tariffId - if a tariff with the same ID exists,
11
- * it will be updated; otherwise, a new tariff will be created.
48
+ * Returns the tariff currently filling a direction slot, whichever app owns
49
+ * it, or `null` when the slot is empty.
12
50
  *
13
- * @param tariff - The tariff registration data including ID, type, name, and vendor
14
- * @returns Promise that resolves when the tariff has been registered
15
- */
16
- registerTariff(tariff: Omit<EnyoElectricityTariff, 'id'>): Promise<EnyoElectricityTariffWithDefault>;
17
- /**
18
- * Retrieves all registered electricity tariffs.
19
- * Each tariff includes a defaultTariff boolean indicating whether
20
- * it is the system default tariff.
51
+ * `null` is a normal answer — a site with no feed-in contract has no feed-in
52
+ * tariff — and not an error.
21
53
  *
22
- * @returns Promise that resolves to an array of all registered tariffs with default indicators
54
+ * @param direction - Which slot to read
55
+ * @returns Promise resolving to the tariff in that slot, or `null`
23
56
  */
24
- getAllTariffs(filter?: {
25
- myTariffs?: boolean;
26
- }): Promise<EnyoElectricityTariffWithDefault[]>;
57
+ getTariff(direction: EnyoTariffDirectionEnum): Promise<EnyoElectricityTariff | null>;
27
58
  /**
28
- * Removes an electricity tariff by its ID.
29
- * If the tariff does not exist, this operation is a no-op.
59
+ * Returns prices for one direction over a time range, as a **15-minute
60
+ * series** — the same resolution `useGridFee().getGridFeeValues()` and
61
+ * `useEpexSpotPrices().getPrices()` use, so all three zip index-by-index.
30
62
  *
31
- * @param id - The unique identifier of the tariff to remove
32
- * @returns Promise that resolves when the tariff has been removed
33
- */
34
- removeTariff(id: string): Promise<void>;
35
- /**
36
- * Requests to make a tariff the default tariff.
63
+ * The host serves this from what the slot's owning app published with
64
+ * {@link publishPrices}, so a consumer never has to know which app is behind
65
+ * the tariff. Returns `null` when the slot is empty or nothing has been
66
+ * published covering the range.
67
+ *
68
+ * Read {@link EnyoTariffPriceSeries.includes} before adding anything to
69
+ * these prices: a series that already contains the grid fee must not have
70
+ * one added on top.
37
71
  *
38
- * @param id - The unique identifier of the tariff
39
- * @returns Promise that resolves with the result
72
+ * @param direction - Which slot to price
73
+ * @param range - The time range to cover
74
+ * @returns Promise resolving to the price series, or `null` when none applies
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const prices = await tariffs.getPrices(EnyoTariffDirectionEnum.Consumption, {
79
+ * fromIso, untilIso,
80
+ * });
81
+ * const needsGridFee = !prices?.includes.includes(EnyoPriceComponentEnum.GridFee);
82
+ * ```
40
83
  */
41
- makeDefaultTariff(id: string): Promise<'accepted' | 'rejected'>;
84
+ getPrices(direction: EnyoTariffDirectionEnum, range: EnyoTariffPriceRange): Promise<EnyoTariffPriceSeries | null>;
42
85
  /**
43
- * Retrieves the system default tariff information.
44
- * Returns the full tariff including pricing data.
86
+ * Puts this app's tariff into a direction slot, replacing whatever was
87
+ * there.
88
+ *
89
+ * **Calling this is the activation signal.** An app calls it once its tariff
90
+ * is actually usable — credentials valid, contract fetched — whether that is
91
+ * immediately when the user selects it or later, when an OAuth redirect
92
+ * comes back or an onboarding guide completes. There is nothing else to
93
+ * report afterwards.
94
+ *
95
+ * Full replacement is the only write there is: a partial update would let an
96
+ * app half-change a tariff another app owns. Re-setting an unchanged tariff
97
+ * is harmless, so an app that refetches its contract on every start can call
98
+ * this unconditionally.
99
+ *
100
+ * The returned {@link EnyoTariffActivationResult} is the host's answer, not
101
+ * the app's — it reports whether the slot is now live or whether the host
102
+ * still needs something from the user.
45
103
  *
46
- * @returns Promise that resolves to the default tariff info, or null if none is configured
104
+ * Requires the `ElectricityTariff` permission.
105
+ *
106
+ * @param direction - Which slot to fill
107
+ * @param tariff - The tariff and its pricing details
108
+ * @returns Promise resolving to the activation outcome
47
109
  */
48
- getDefaultTariff(): Promise<EnyoElectricityTariff | null>;
49
- findTariff(filter: {
50
- applianceId?: string;
51
- tariffId?: string;
52
- externalTariffId?: string;
53
- }): Promise<EnyoElectricityTariffWithDefault | null>;
110
+ setTariff(direction: EnyoTariffDirectionEnum, tariff: EnyoElectricityTariff): Promise<EnyoTariffActivationResult>;
54
111
  /**
55
- * Partially updates an existing electricity tariff.
56
- * Only the provided attributes will be modified; all other fields remain unchanged.
112
+ * Publishes prices for a slot this app owns.
113
+ *
114
+ * Push, not pull: providers hand out prices on their own schedule — day-ahead
115
+ * prices land in the afternoon, and provider APIs are rate-limited — so an
116
+ * app fetches when it makes sense and publishes what it got. The host serves
117
+ * those to every {@link getPrices} caller afterwards.
118
+ *
119
+ * Entries replace previously published entries with the same timestamp and
120
+ * leave the rest alone, so republishing a corrected day does not erase the
121
+ * days around it.
122
+ *
123
+ * Declare in {@link EnyoTariffPricePublication.includes} what the prices
124
+ * already contain. An app that folds the grid fee in before publishing must
125
+ * say so, or every consumer will add it a second time.
126
+ *
127
+ * Fails when this app does not own the slot — only the tariff's owner may
128
+ * price it.
129
+ *
130
+ * Requires the `ElectricityTariff` permission.
57
131
  *
58
- * @param id - The unique identifier of the tariff to update
59
- * @param attributes - A partial set of tariff fields to update (excluding `id`)
60
- * @returns Promise that resolves to the full updated tariff with its default indicator
132
+ * @param direction - Which slot the prices belong to
133
+ * @param prices - The priced intervals and what they already contain
134
+ * @returns Promise that resolves once the prices are stored and listeners dispatched
61
135
  */
62
- updateTariff(id: string, attributes: Partial<Omit<EnyoElectricityTariff, 'id'>>): Promise<EnyoElectricityTariffWithDefault>;
136
+ publishPrices(direction: EnyoTariffDirectionEnum, prices: EnyoTariffPricePublication): Promise<void>;
63
137
  /**
64
- * Registers a listener invoked whenever any tariff is registered, updated
65
- * or removed, or whenever a different tariff becomes the system default.
138
+ * Registers the handler the host calls when the user picks this app's tariff
139
+ * for a direction — "this should be my consumption tariff".
66
140
  *
67
- * Listeners see changes made by every energy app on the system, not just
68
- * this one — filter on {@link EnyoTariffChangeEvent.tariffId} when only one
69
- * tariff matters. This is the signal to recompose prices: a changed grid fee
70
- * link, an added or expired bonus, or a new default tariff all invalidate a
71
- * previously composed price series.
141
+ * Not a notification: the handler runs the app's activation flow and reports
142
+ * what it found. Returning
143
+ * {@link EnyoTariffActivationStatusEnum.AuthenticationRequired} or
144
+ * {@link EnyoTariffActivationStatusEnum.OnboardingRequired} tells the host to
145
+ * send the user somewhere, so carry the `authenticationUrl` or
146
+ * `onboardingGuideId` that makes it actionable. When the flow later
147
+ * completes, call {@link setTariff} — that is what marks the tariff live.
72
148
  *
73
- * Requires the `ElectricityTariff` permission.
149
+ * Returning {@link EnyoTariffActivationStatusEnum.Success} without having
150
+ * called {@link setTariff} leaves the slot empty; return it only when the
151
+ * tariff is already set.
74
152
  *
75
- * @param listener - Callback invoked with the tariff change event
76
- * @returns A unique listener id that can be passed to {@link offTariffChanged}
153
+ * One handler per app. Registering again replaces the previous one.
77
154
  *
78
- * @example
79
- * ```typescript
80
- * const listenerId = tariffs.onTariffChanged(async event => {
81
- * if (event.changeType === EnyoTariffChangeTypeEnum.Removed) return;
82
- * await recomposePrices(event.tariff);
83
- * });
84
- * // later
85
- * tariffs.offTariffChanged(listenerId);
86
- * ```
155
+ * @param handler - Called with the direction the user selected this app for
156
+ * @returns A function that removes the handler when called
87
157
  */
88
- onTariffChanged(listener: (event: EnyoTariffChangeEvent) => void | Promise<void>): string;
158
+ onTariffSelected(handler: (direction: EnyoTariffDirectionEnum) => Promise<EnyoTariffActivationResult>): () => void;
89
159
  /**
90
- * Removes a previously registered tariff change listener. If the listener id
91
- * is unknown, this operation is a no-op.
160
+ * Registers a listener invoked whenever either slot changes — a tariff set,
161
+ * replaced, or cleared.
162
+ *
163
+ * This is the signal to recompose prices. It fires for changes made by every
164
+ * app, so filter on {@link EnyoTariffChangeEvent.direction} when only one
165
+ * slot matters, and compare
166
+ * {@link EnyoElectricityTariff.externalTariffId} against your own to notice
167
+ * that a slot you were publishing for is now owned by someone else.
168
+ *
169
+ * Returns the unsubscribe function directly; calling it twice is a no-op.
92
170
  *
93
- * @param listenerId - The id returned from {@link onTariffChanged}
171
+ * @param listener - Callback invoked with the change event
172
+ * @returns A function that removes this listener when called
94
173
  */
95
- offTariffChanged(listenerId: string): void;
174
+ onTariffChanged(listener: (event: EnyoTariffChangeEvent) => void | Promise<void>): () => void;
96
175
  }
@@ -1,33 +1,42 @@
1
- import { EnyoDynamicGridFee, EnyoDynamicGridFeeFilter, EnyoDynamicGridFeeRegistration, EnyoDynamicGridFeeSeries, EnyoGridFeeChangeEvent } from '../types/enyo-grid-fee.cjs';
2
- import { EnyoAbsolutePriceWindow } from '../types/enyo-price-schedule.cjs';
1
+ import { EnyoDynamicGridFee, EnyoDynamicGridFeeRegistration, EnyoGridFeeChangeEvent, EnyoGridFeeInfo, EnyoGridFeeSeries, EnyoGridFeeValuesFilter } from '../types/enyo-grid-fee.cjs';
3
2
  /**
4
- * Interface for publishing and consuming dynamic grid fees (network charges).
3
+ * Interface for publishing and consuming grid fees (network charges).
5
4
  *
6
- * Any energy app can act as a producer by registering a grid fee — typically an
7
- * app that knows the local grid operator's tariff sheet or fetches a published
8
- * series — and any energy app can act as a consumer by resolving that fee for a
9
- * time range with {@link getDynamicGridFees}.
5
+ * Five methods, split by role. A **producer** — an app that knows the local grid
6
+ * operator's tariff sheet or fetches a published series — owns
7
+ * {@link registerGridFee} and {@link removeGridFee}. A **consumer** asks
8
+ * {@link getGridFee} what applies and {@link getGridFeeValues} for the numbers
9
+ * over a range, and subscribes with {@link onGridFeeChanged}. Nothing else is
10
+ * needed, so nothing else is offered.
10
11
  *
11
12
  * Publishing requires the `GridFeeRegister` permission; consuming requires
12
13
  * `GridFeeUse`.
13
14
  *
14
15
  * A grid fee describes the site's **grid connection**, not a supplier contract:
15
16
  * it is registered and resolved independently of any tariff, and the same fee
16
- * applies whichever supplier the site buys from.
17
+ * applies whichever supplier the site buys from. This API is the *only* place a
18
+ * grid fee lives — a tariff declares nothing about how the fee is determined,
19
+ * so there is no second source to reconcile against.
20
+ *
21
+ * **A site has exactly one grid fee.** It has one grid connection, and the
22
+ * operator of that connection publishes one network charge for it. So nothing
23
+ * here takes an identifier: registering replaces whatever was there, and reading
24
+ * needs no selector.
17
25
  *
18
26
  * **Nothing here is applied to a price automatically.** The host never folds a
19
27
  * grid fee into {@link EnergyAppEnergyPrices.getPrices}, because provider APIs
20
28
  * differ: some already return grid-fee-inclusive prices, some do not. The app
21
29
  * that owns the tariff knows which is the case — it declares it via the
22
- * tariff's `priceComposition` and composes the effective price itself with
23
- * `composeElectricityPrices()`.
30
+ * price series' `includes` list and composes the effective price
31
+ * itself with `composeElectricityPrices()`. That flag is the one thing a tariff
32
+ * says about grid fees, and it describes the provider's price feed rather than
33
+ * the fee.
24
34
  *
25
35
  * @example
26
36
  * ```typescript
27
37
  * // Producer: a §14a EnWG module 3 HT/NT network charge
28
38
  * const gridFee = energyApp.useGridFee();
29
39
  * await gridFee.registerGridFee({
30
- * id: 'dso-hd-2026',
31
40
  * name: 'Netzentgelt HT/NT 2026',
32
41
  * gridOperator: 'Netze BW',
33
42
  * currency: EnyoCurrencyEnum.EUR,
@@ -44,7 +53,7 @@ import { EnyoAbsolutePriceWindow } from '../types/enyo-price-schedule.cjs';
44
53
  * });
45
54
  *
46
55
  * // Consumer: resolve it for the next 24 hours
47
- * const fees = await gridFee.getDynamicGridFees({
56
+ * const fees = await gridFee.getGridFeeValues({
48
57
  * fromIso: new Date().toISOString(),
49
58
  * untilIso: new Date(Date.now() + 86_400_000).toISOString(),
50
59
  * });
@@ -52,130 +61,139 @@ import { EnyoAbsolutePriceWindow } from '../types/enyo-price-schedule.cjs';
52
61
  */
53
62
  export interface EnergyAppGridFee {
54
63
  /**
55
- * Registers a new dynamic grid fee or updates an existing one. Uses upsert
56
- * logic based on `id` — if a fee with the same id already exists it is
57
- * replaced, otherwise a new one is created.
64
+ * Sets the site's grid fee, replacing whatever was registered before.
58
65
  *
59
- * Requires the `GridFeeRegister` permission.
66
+ * Full replacement is the only write there is, and it is enough for every
67
+ * case — correcting a name, moving to next year's tariff sheet, or
68
+ * republishing a day-ahead schedule are all "here is the fee now". A partial
69
+ * update would only add a second way to say the same thing, and a
70
+ * schedule-only write would let metadata and windows drift apart.
60
71
  *
61
- * @param registration - The grid fee to register, including its schedule
62
- * @returns Promise that resolves to the stored grid fee, including the host-assigned `publishedAtIso`
63
- */
64
- registerGridFee(registration: EnyoDynamicGridFeeRegistration): Promise<EnyoDynamicGridFee>;
65
- /**
66
- * Partially updates a registered grid fee. Only the provided attributes are
67
- * modified; all other fields remain unchanged.
72
+ * There is no identifier and no create-versus-update distinction: the site
73
+ * has one grid connection and therefore one grid fee. Two apps that both
74
+ * register one will overwrite each other, so an app should only publish a
75
+ * fee it is actually authoritative for.
68
76
  *
69
77
  * Requires the `GridFeeRegister` permission.
70
78
  *
71
- * @param id - The unique identifier of the grid fee to update
72
- * @param attributes - A partial set of grid fee fields to update (excluding `id`)
73
- * @returns Promise that resolves to the full updated grid fee
79
+ * @param registration - The complete grid fee, including its schedule
80
+ * @returns Promise that resolves to the stored grid fee, including the host-assigned `publishedAtIso`
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * // Republishing tomorrow's day-ahead windows is the same call.
85
+ * await gridFee.registerGridFee({...fee, schedule: {
86
+ * type: EnyoPriceScheduleTypeEnum.Absolute,
87
+ * windows: tomorrowsWindows,
88
+ * }});
89
+ * ```
74
90
  */
75
- updateGridFee(id: string, attributes: Partial<Omit<EnyoDynamicGridFeeRegistration, 'id'>>): Promise<EnyoDynamicGridFee>;
91
+ registerGridFee(registration: EnyoDynamicGridFeeRegistration): Promise<EnyoDynamicGridFee>;
76
92
  /**
77
- * Removes a registered grid fee. {@link getDynamicGridFees} resolves to
78
- * `null` for it afterwards. If the fee does not exist, this operation is a
79
- * no-op.
93
+ * Removes the site's grid fee. {@link getGridFee} and
94
+ * {@link getGridFeeValues} resolve to `null` afterwards. If no fee is
95
+ * registered, this operation is a no-op.
80
96
  *
81
97
  * Requires the `GridFeeRegister` permission.
82
98
  *
83
- * @param id - The unique identifier of the grid fee to remove
84
99
  * @returns Promise that resolves when the grid fee has been removed
85
100
  */
86
- removeGridFee(id: string): Promise<void>;
101
+ removeGridFee(): Promise<void>;
87
102
  /**
88
- * Retrieves all registered grid fees across all energy apps on the system.
103
+ * Answers "what grid fee applies here?" in one call.
89
104
  *
90
- * Requires the `GridFeeUse` permission.
105
+ * This is the entry point for consuming a grid fee. The returned
106
+ * {@link EnyoGridFeeInfo} says whether the fee is
107
+ * {@link EnyoGridFeeTypeEnum.Static static} or
108
+ * {@link EnyoGridFeeTypeEnum.Dynamic dynamic}, carries the gross cent per
109
+ * kWh outright when it is static, and reports any additional fixed charges
110
+ * separately. A static fee needs no second call; a dynamic one is fetched
111
+ * interval by interval with {@link getGridFeeValues}.
91
112
  *
92
- * @returns Promise that resolves to an array of all registered grid fees
93
- */
94
- listGridFees(): Promise<EnyoDynamicGridFee[]>;
95
- /**
96
- * Retrieves a single registered grid fee by its id, or `null` when no fee
97
- * with that id exists.
113
+ * Takes no arguments — the site has one grid fee, so there is nothing to
114
+ * select. Returns `null` when none is registered, which is a normal answer
115
+ * and not an error.
98
116
  *
99
117
  * Requires the `GridFeeUse` permission.
100
118
  *
101
- * @param id - The unique identifier of the grid fee
102
- * @returns Promise that resolves to the grid fee, or `null`
103
- */
104
- getGridFee(id: string): Promise<EnyoDynamicGridFee | null>;
105
- /**
106
- * Replaces the absolute windows of a grid fee whose schedule is
107
- * {@link EnyoPriceScheduleTypeEnum.Absolute}. Use this for fees the grid
108
- * operator publishes as a dated series (for example day-ahead), where the
109
- * schedule changes far more often than the fee's metadata.
110
- *
111
- * Windows must not overlap and are stored sorted ascending by `startIso`.
112
- * Publishing replaces the previously stored windows in full.
119
+ * @returns Promise resolving to the site's grid fee, or `null` when none is registered
113
120
  *
114
- * Requires the `GridFeeRegister` permission.
115
- *
116
- * @param id - The unique identifier of the registered grid fee
117
- * @param windows - The complete set of absolute windows to store
118
- * @returns Promise that resolves when the windows have been stored and change listeners dispatched
121
+ * @example
122
+ * ```typescript
123
+ * const fee = await gridFee.getGridFee();
124
+ * if (fee?.type === EnyoGridFeeTypeEnum.Static) {
125
+ * show(fee.grossCentPerKwh, fee.additionalFeesGrossCentPerKwh);
126
+ * } else if (fee) {
127
+ * const series = await gridFee.getGridFeeValues({fromIso, untilIso});
128
+ * }
129
+ * ```
119
130
  */
120
- publishGridFeeWindows(id: string, windows: EnyoAbsolutePriceWindow[]): Promise<void>;
131
+ getGridFee(): Promise<EnyoGridFeeInfo | null>;
121
132
  /**
122
- * Resolves a dynamic grid fee to a flat **15-minute series** over the
123
- * requested range — the same resolution
133
+ * Resolves the applicable grid fee to a flat **15-minute series** of gross
134
+ * cent per kWh over the requested range — the same resolution
124
135
  * {@link EnergyAppEnergyPrices.getPrices} uses, so both series can be
125
136
  * combined interval by interval.
126
137
  *
127
- * The fee is selected by `gridFeeId` or `meteringPointId`; with no selector
128
- * the grid fee that applies to this device is used. Grid fees are **not**
129
- * bound to a tariff — a network charge is a property of the grid connection,
130
- * and the same fee applies no matter which supplier the site buys from.
131
- * Returns `null` when no fee matches the selector.
138
+ * Works for every fee, static or dynamic: a flat charge resolves to a series
139
+ * whose entries all carry the same amount, so a caller that only wants
140
+ * numbers over a range never has to branch on {@link EnyoGridFeeInfo.type}
141
+ * at all.
132
142
  *
133
- * Intervals no window covers resolve to `feePerKwh: 0`, and intervals
134
- * outside the fee's `validFromIso` / `validUntilIso` window are omitted.
143
+ * Set `includeAdditionalFees` to fold the fee's fixed charges into every
144
+ * interval and get the full gross charge per kWh; leave it off for the
145
+ * network charge alone. The result reports which of the two you got in
146
+ * {@link EnyoGridFeeSeries.includesAdditionalFees}, so a fee that declares
147
+ * no additional charges is never mistaken for a total.
148
+ *
149
+ * Returns `null` when the site has no grid fee registered. Intervals no
150
+ * window covers resolve to `0`, and intervals outside the fee's
151
+ * `validFromIso` / `validUntilIso` window are omitted.
135
152
  *
136
153
  * Requires the `GridFeeUse` permission.
137
154
  *
138
- * @param filter - The time range and the selector identifying which fee to resolve
155
+ * @param filter - The time range and whether to include additional fees
139
156
  * @returns Promise that resolves to the 15-minute fee series, or `null` when no fee applies
140
157
  *
141
158
  * @example
142
159
  * ```typescript
143
- * const fees = await gridFee.getDynamicGridFees({
144
- * fromIso: '2026-09-02T00:00:00Z',
145
- * untilIso: '2026-09-03T00:00:00Z',
160
+ * // network charge only
161
+ * const net = await gridFee.getGridFeeValues({fromIso, untilIso});
162
+ *
163
+ * // everything the customer pays per kWh
164
+ * const total = await gridFee.getGridFeeValues({
165
+ * fromIso,
166
+ * untilIso,
167
+ * includeAdditionalFees: true,
146
168
  * });
147
- * fees?.entries.forEach(e => console.log(e.timestampIso, e.feePerKwh));
169
+ * total?.entries.forEach(e => console.log(e.timestampIso, e.grossCentPerKwh));
148
170
  * ```
149
171
  */
150
- getDynamicGridFees(filter: EnyoDynamicGridFeeFilter): Promise<EnyoDynamicGridFeeSeries | null>;
172
+ getGridFeeValues(filter: EnyoGridFeeValuesFilter): Promise<EnyoGridFeeSeries | null>;
151
173
  /**
152
- * Registers a listener invoked whenever any grid fee is registered, updated
153
- * or removed — including when its absolute windows are republished.
174
+ * Registers a listener invoked whenever any grid fee is registered, replaced
175
+ * or removed.
154
176
  *
155
177
  * Use it to recompute composed prices without polling: a grid operator that
156
178
  * publishes a new day-ahead series mid-day changes the cost of every future
157
179
  * interval.
158
180
  *
181
+ * Returns the unsubscribe function directly rather than an id to hand back
182
+ * to a second method — calling it twice is a no-op.
183
+ *
159
184
  * Requires the `GridFeeUse` permission.
160
185
  *
161
186
  * @param listener - Callback invoked with the change event
162
- * @returns A unique listener id that can be passed to {@link offGridFeeChanged}
187
+ * @returns A function that removes this listener when called
163
188
  *
164
189
  * @example
165
190
  * ```typescript
166
- * const id = gridFee.onGridFeeChanged(async event => {
167
- * if (event.gridFeeId === myFeeId) await recomputePrices();
191
+ * const unsubscribe = gridFee.onGridFeeChanged(async () => {
192
+ * await recomputePrices();
168
193
  * });
169
194
  * // later
170
- * gridFee.offGridFeeChanged(id);
195
+ * unsubscribe();
171
196
  * ```
172
197
  */
173
- onGridFeeChanged(listener: (event: EnyoGridFeeChangeEvent) => void | Promise<void>): string;
174
- /**
175
- * Removes a previously registered grid fee change listener. If the listener
176
- * id is unknown, this operation is a no-op.
177
- *
178
- * @param listenerId - The id returned from {@link onGridFeeChanged}
179
- */
180
- offGridFeeChanged(listenerId: string): void;
198
+ onGridFeeChanged(listener: (event: EnyoGridFeeChangeEvent) => void | Promise<void>): () => void;
181
199
  }