@enyo-energy/energy-app-sdk 1.2.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/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/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-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/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/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-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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { EnyoPriceSchedule, EnyoResolvedPriceEntry } from '../../types/enyo-price-schedule.js';
|
|
2
|
+
/**
|
|
3
|
+
* Length of one resolved interval, in minutes. Matches the resolution of
|
|
4
|
+
* `useElectricityPrices().getPrices()` so resolved schedules can be combined
|
|
5
|
+
* with a price series interval by interval.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PRICE_SCHEDULE_INTERVAL_MINUTES = 15;
|
|
8
|
+
/**
|
|
9
|
+
* Wall-clock calendar fields of an instant, as seen in a specific time zone.
|
|
10
|
+
*/
|
|
11
|
+
export interface ZonedDateParts {
|
|
12
|
+
/** Full year, e.g. `2026`. */
|
|
13
|
+
year: number;
|
|
14
|
+
/** Month, `1` = January … `12` = December. */
|
|
15
|
+
month: number;
|
|
16
|
+
/** Day of the month, starting at `1`. */
|
|
17
|
+
day: number;
|
|
18
|
+
/** Day of the week, `0` = Sunday … `6` = Saturday. */
|
|
19
|
+
weekday: number;
|
|
20
|
+
/** Hour in 24-hour notation, `0`–`23`. */
|
|
21
|
+
hour: number;
|
|
22
|
+
/** Minute, `0`–`59`. */
|
|
23
|
+
minute: number;
|
|
24
|
+
/** Minutes since local midnight (`hour * 60 + minute`). */
|
|
25
|
+
minutesOfDay: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Projects an instant onto the wall clock of a time zone.
|
|
29
|
+
*
|
|
30
|
+
* Uses `Intl.DateTimeFormat`, so daylight-saving transitions are handled by the
|
|
31
|
+
* platform's time zone database without pulling in a dependency.
|
|
32
|
+
*
|
|
33
|
+
* @param date - The instant to project
|
|
34
|
+
* @param timezone - IANA time zone identifier, e.g. `'Europe/Berlin'`
|
|
35
|
+
* @returns The wall-clock calendar fields of `date` in `timezone`
|
|
36
|
+
* @throws {RangeError} If `timezone` is not a valid IANA identifier
|
|
37
|
+
*/
|
|
38
|
+
export declare function getZonedDateParts(date: Date, timezone: string): ZonedDateParts;
|
|
39
|
+
/**
|
|
40
|
+
* Parses an `HH:mm` wall-clock time into minutes since midnight.
|
|
41
|
+
*
|
|
42
|
+
* @param value - Time in 24-hour `HH:mm` notation, e.g. `'22:00'`
|
|
43
|
+
* @returns Minutes since midnight, or `null` when the value is malformed
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseTimeOfDay(value: string): number | null;
|
|
46
|
+
/**
|
|
47
|
+
* Rounds an instant down to the start of its 15-minute interval.
|
|
48
|
+
*
|
|
49
|
+
* @param timestampMs - Epoch milliseconds
|
|
50
|
+
* @returns Epoch milliseconds of the interval start
|
|
51
|
+
*/
|
|
52
|
+
export declare function floorToInterval(timestampMs: number): number;
|
|
53
|
+
/**
|
|
54
|
+
* Options for {@link resolvePriceSchedule}.
|
|
55
|
+
*/
|
|
56
|
+
export interface ResolvePriceScheduleOptions {
|
|
57
|
+
/** Start of the range to resolve, in ISO format (inclusive). */
|
|
58
|
+
fromIso: string;
|
|
59
|
+
/** End of the range to resolve, in ISO format (exclusive). */
|
|
60
|
+
untilIso: string;
|
|
61
|
+
/** IANA time zone the schedule's wall-clock times are expressed in. */
|
|
62
|
+
timezone: string;
|
|
63
|
+
/** Optional start of the component's validity in ISO format (inclusive). */
|
|
64
|
+
validFromIso?: string;
|
|
65
|
+
/** Optional end of the component's validity in ISO format (exclusive). */
|
|
66
|
+
validUntilIso?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Expands a price schedule into a flat 15-minute series over a time range.
|
|
70
|
+
*
|
|
71
|
+
* The range start is rounded down to the enclosing 15-minute interval. Intervals
|
|
72
|
+
* no window covers resolve to `amountPerKwh: 0`; intervals outside the
|
|
73
|
+
* component's validity window are omitted entirely, so an empty result means
|
|
74
|
+
* "this component does not apply here" rather than "it is free here".
|
|
75
|
+
*
|
|
76
|
+
* Windows are evaluated in array order and the first match wins — schedules are
|
|
77
|
+
* expected to be overlap-free, which the validators enforce at registration
|
|
78
|
+
* time.
|
|
79
|
+
*
|
|
80
|
+
* @param schedule - The schedule to expand
|
|
81
|
+
* @param options - Range, time zone and optional validity bounds
|
|
82
|
+
* @returns Resolved entries sorted ascending by `timestampIso`
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const entries = resolvePriceSchedule(
|
|
87
|
+
* {
|
|
88
|
+
* type: EnyoPriceScheduleTypeEnum.Recurring,
|
|
89
|
+
* windows: [{ startTimeOfDay: '22:00', endTimeOfDay: '06:00', amountPerKwh: 0.05 }],
|
|
90
|
+
* },
|
|
91
|
+
* { fromIso: '2026-09-02T00:00:00Z', untilIso: '2026-09-03T00:00:00Z', timezone: 'Europe/Berlin' },
|
|
92
|
+
* );
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolvePriceSchedule(schedule: EnyoPriceSchedule, options: ResolvePriceScheduleOptions): EnyoResolvedPriceEntry[];
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { EnyoPriceScheduleTypeEnum, } from '../../types/enyo-price-schedule.js';
|
|
2
|
+
/**
|
|
3
|
+
* Length of one resolved interval, in minutes. Matches the resolution of
|
|
4
|
+
* `useElectricityPrices().getPrices()` so resolved schedules can be combined
|
|
5
|
+
* with a price series interval by interval.
|
|
6
|
+
*/
|
|
7
|
+
export const PRICE_SCHEDULE_INTERVAL_MINUTES = 15;
|
|
8
|
+
const INTERVAL_MS = PRICE_SCHEDULE_INTERVAL_MINUTES * 60 * 1000;
|
|
9
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
const WEEKDAY_INDEX = {
|
|
11
|
+
Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6,
|
|
12
|
+
};
|
|
13
|
+
const formatterCache = new Map();
|
|
14
|
+
function zonedFormatter(timezone) {
|
|
15
|
+
let formatter = formatterCache.get(timezone);
|
|
16
|
+
if (formatter === undefined) {
|
|
17
|
+
formatter = new Intl.DateTimeFormat('en-US', {
|
|
18
|
+
timeZone: timezone,
|
|
19
|
+
hourCycle: 'h23',
|
|
20
|
+
year: 'numeric',
|
|
21
|
+
month: 'numeric',
|
|
22
|
+
day: 'numeric',
|
|
23
|
+
weekday: 'short',
|
|
24
|
+
hour: 'numeric',
|
|
25
|
+
minute: 'numeric',
|
|
26
|
+
});
|
|
27
|
+
formatterCache.set(timezone, formatter);
|
|
28
|
+
}
|
|
29
|
+
return formatter;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Projects an instant onto the wall clock of a time zone.
|
|
33
|
+
*
|
|
34
|
+
* Uses `Intl.DateTimeFormat`, so daylight-saving transitions are handled by the
|
|
35
|
+
* platform's time zone database without pulling in a dependency.
|
|
36
|
+
*
|
|
37
|
+
* @param date - The instant to project
|
|
38
|
+
* @param timezone - IANA time zone identifier, e.g. `'Europe/Berlin'`
|
|
39
|
+
* @returns The wall-clock calendar fields of `date` in `timezone`
|
|
40
|
+
* @throws {RangeError} If `timezone` is not a valid IANA identifier
|
|
41
|
+
*/
|
|
42
|
+
export function getZonedDateParts(date, timezone) {
|
|
43
|
+
const parts = zonedFormatter(timezone).formatToParts(date);
|
|
44
|
+
const lookup = {};
|
|
45
|
+
for (const part of parts) {
|
|
46
|
+
lookup[part.type] = part.value;
|
|
47
|
+
}
|
|
48
|
+
const hour = Number(lookup.hour);
|
|
49
|
+
const minute = Number(lookup.minute);
|
|
50
|
+
return {
|
|
51
|
+
year: Number(lookup.year),
|
|
52
|
+
month: Number(lookup.month),
|
|
53
|
+
day: Number(lookup.day),
|
|
54
|
+
weekday: WEEKDAY_INDEX[lookup.weekday] ?? 0,
|
|
55
|
+
hour,
|
|
56
|
+
minute,
|
|
57
|
+
minutesOfDay: hour * 60 + minute,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parses an `HH:mm` wall-clock time into minutes since midnight.
|
|
62
|
+
*
|
|
63
|
+
* @param value - Time in 24-hour `HH:mm` notation, e.g. `'22:00'`
|
|
64
|
+
* @returns Minutes since midnight, or `null` when the value is malformed
|
|
65
|
+
*/
|
|
66
|
+
export function parseTimeOfDay(value) {
|
|
67
|
+
const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value);
|
|
68
|
+
if (match === null) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return Number(match[1]) * 60 + Number(match[2]);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Rounds an instant down to the start of its 15-minute interval.
|
|
75
|
+
*
|
|
76
|
+
* @param timestampMs - Epoch milliseconds
|
|
77
|
+
* @returns Epoch milliseconds of the interval start
|
|
78
|
+
*/
|
|
79
|
+
export function floorToInterval(timestampMs) {
|
|
80
|
+
return Math.floor(timestampMs / INTERVAL_MS) * INTERVAL_MS;
|
|
81
|
+
}
|
|
82
|
+
function matchesRecurringWindow(window, timestampMs, timezone) {
|
|
83
|
+
const start = parseTimeOfDay(window.startTimeOfDay);
|
|
84
|
+
const end = parseTimeOfDay(window.endTimeOfDay);
|
|
85
|
+
if (start === null || end === null || start === end) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const parts = getZonedDateParts(new Date(timestampMs), timezone);
|
|
89
|
+
let inWindow;
|
|
90
|
+
let anchor;
|
|
91
|
+
if (start < end) {
|
|
92
|
+
inWindow = parts.minutesOfDay >= start && parts.minutesOfDay < end;
|
|
93
|
+
anchor = parts;
|
|
94
|
+
}
|
|
95
|
+
else if (parts.minutesOfDay >= start) {
|
|
96
|
+
// Evening leg of a window that wraps past midnight.
|
|
97
|
+
inWindow = true;
|
|
98
|
+
anchor = parts;
|
|
99
|
+
}
|
|
100
|
+
else if (parts.minutesOfDay < end) {
|
|
101
|
+
// Early-morning leg — the window belongs to the previous local day.
|
|
102
|
+
inWindow = true;
|
|
103
|
+
anchor = getZonedDateParts(new Date(timestampMs - DAY_MS), timezone);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (!inWindow) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (window.daysOfWeek !== undefined && !window.daysOfWeek.includes(anchor.weekday)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return window.months === undefined || window.months.includes(anchor.month);
|
|
115
|
+
}
|
|
116
|
+
function matchesAbsoluteWindow(window, timestampMs) {
|
|
117
|
+
return timestampMs >= Date.parse(window.startIso) && timestampMs < Date.parse(window.endIso);
|
|
118
|
+
}
|
|
119
|
+
function amountAt(schedule, timestampMs, timezone) {
|
|
120
|
+
if (schedule.type === EnyoPriceScheduleTypeEnum.Constant) {
|
|
121
|
+
return schedule.amountPerKwh;
|
|
122
|
+
}
|
|
123
|
+
if (schedule.type === EnyoPriceScheduleTypeEnum.Absolute) {
|
|
124
|
+
const window = schedule.windows.find(candidate => matchesAbsoluteWindow(candidate, timestampMs));
|
|
125
|
+
return window?.amountPerKwh ?? 0;
|
|
126
|
+
}
|
|
127
|
+
const window = schedule.windows.find(candidate => matchesRecurringWindow(candidate, timestampMs, timezone));
|
|
128
|
+
return window?.amountPerKwh ?? 0;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Expands a price schedule into a flat 15-minute series over a time range.
|
|
132
|
+
*
|
|
133
|
+
* The range start is rounded down to the enclosing 15-minute interval. Intervals
|
|
134
|
+
* no window covers resolve to `amountPerKwh: 0`; intervals outside the
|
|
135
|
+
* component's validity window are omitted entirely, so an empty result means
|
|
136
|
+
* "this component does not apply here" rather than "it is free here".
|
|
137
|
+
*
|
|
138
|
+
* Windows are evaluated in array order and the first match wins — schedules are
|
|
139
|
+
* expected to be overlap-free, which the validators enforce at registration
|
|
140
|
+
* time.
|
|
141
|
+
*
|
|
142
|
+
* @param schedule - The schedule to expand
|
|
143
|
+
* @param options - Range, time zone and optional validity bounds
|
|
144
|
+
* @returns Resolved entries sorted ascending by `timestampIso`
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* const entries = resolvePriceSchedule(
|
|
149
|
+
* {
|
|
150
|
+
* type: EnyoPriceScheduleTypeEnum.Recurring,
|
|
151
|
+
* windows: [{ startTimeOfDay: '22:00', endTimeOfDay: '06:00', amountPerKwh: 0.05 }],
|
|
152
|
+
* },
|
|
153
|
+
* { fromIso: '2026-09-02T00:00:00Z', untilIso: '2026-09-03T00:00:00Z', timezone: 'Europe/Berlin' },
|
|
154
|
+
* );
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function resolvePriceSchedule(schedule, options) {
|
|
158
|
+
const untilMs = Date.parse(options.untilIso);
|
|
159
|
+
const validFromMs = options.validFromIso === undefined ? undefined : Date.parse(options.validFromIso);
|
|
160
|
+
const validUntilMs = options.validUntilIso === undefined ? undefined : Date.parse(options.validUntilIso);
|
|
161
|
+
const entries = [];
|
|
162
|
+
for (let cursor = floorToInterval(Date.parse(options.fromIso)); cursor < untilMs; cursor += INTERVAL_MS) {
|
|
163
|
+
if (validFromMs !== undefined && cursor < validFromMs) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (validUntilMs !== undefined && cursor >= validUntilMs) {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
entries.push({
|
|
170
|
+
timestampIso: new Date(cursor).toISOString(),
|
|
171
|
+
amountPerKwh: amountAt(schedule, cursor, options.timezone),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return entries;
|
|
175
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -136,3 +136,10 @@ export * from './packages/energy-app-epex-spot-price.js';
|
|
|
136
136
|
export * from './types/enyo-automation.js';
|
|
137
137
|
export * from './packages/energy-app-automation.js';
|
|
138
138
|
export * from './implementations/automation/automation-validators.js';
|
|
139
|
+
export * from './types/enyo-price-schedule.js';
|
|
140
|
+
export * from './types/enyo-grid-fee.js';
|
|
141
|
+
export * from './types/enyo-price-composition.js';
|
|
142
|
+
export * from './packages/energy-app-grid-fee.js';
|
|
143
|
+
export * from './implementations/pricing/price-schedule-resolver.js';
|
|
144
|
+
export * from './implementations/pricing/price-component-validators.js';
|
|
145
|
+
export * from './implementations/pricing/compose-electricity-prices.js';
|
package/dist/index.js
CHANGED
|
@@ -136,3 +136,10 @@ export * from './packages/energy-app-epex-spot-price.js';
|
|
|
136
136
|
export * from './types/enyo-automation.js';
|
|
137
137
|
export * from './packages/energy-app-automation.js';
|
|
138
138
|
export * from './implementations/automation/automation-validators.js';
|
|
139
|
+
export * from './types/enyo-price-schedule.js';
|
|
140
|
+
export * from './types/enyo-grid-fee.js';
|
|
141
|
+
export * from './types/enyo-price-composition.js';
|
|
142
|
+
export * from './packages/energy-app-grid-fee.js';
|
|
143
|
+
export * from './implementations/pricing/price-schedule-resolver.js';
|
|
144
|
+
export * from './implementations/pricing/price-component-validators.js';
|
|
145
|
+
export * from './implementations/pricing/compose-electricity-prices.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EnyoElectricityTariff, EnyoElectricityTariffWithDefault } from "../types/enyo-electricity-tariff.js";
|
|
1
|
+
import { EnyoElectricityTariff, EnyoElectricityTariffWithDefault, EnyoTariffChangeEvent } from "../types/enyo-electricity-tariff.js";
|
|
2
2
|
/**
|
|
3
3
|
* Interface for managing electricity tariffs.
|
|
4
4
|
* Provides methods to register, retrieve, and remove electricity tariffs
|
|
@@ -60,4 +60,37 @@ export interface EnergyAppElectricityTariff {
|
|
|
60
60
|
* @returns Promise that resolves to the full updated tariff with its default indicator
|
|
61
61
|
*/
|
|
62
62
|
updateTariff(id: string, attributes: Partial<Omit<EnyoElectricityTariff, 'id'>>): Promise<EnyoElectricityTariffWithDefault>;
|
|
63
|
+
/**
|
|
64
|
+
* Registers a listener invoked whenever any tariff is registered, updated
|
|
65
|
+
* or removed, or whenever a different tariff becomes the system default.
|
|
66
|
+
*
|
|
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.
|
|
72
|
+
*
|
|
73
|
+
* Requires the `ElectricityTariff` permission.
|
|
74
|
+
*
|
|
75
|
+
* @param listener - Callback invoked with the tariff change event
|
|
76
|
+
* @returns A unique listener id that can be passed to {@link offTariffChanged}
|
|
77
|
+
*
|
|
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
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
onTariffChanged(listener: (event: EnyoTariffChangeEvent) => void | Promise<void>): string;
|
|
89
|
+
/**
|
|
90
|
+
* Removes a previously registered tariff change listener. If the listener id
|
|
91
|
+
* is unknown, this operation is a no-op.
|
|
92
|
+
*
|
|
93
|
+
* @param listenerId - The id returned from {@link onTariffChanged}
|
|
94
|
+
*/
|
|
95
|
+
offTariffChanged(listenerId: string): void;
|
|
63
96
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { EnyoDynamicGridFee, EnyoDynamicGridFeeFilter, EnyoDynamicGridFeeRegistration, EnyoDynamicGridFeeSeries, EnyoGridFeeChangeEvent } from '../types/enyo-grid-fee.js';
|
|
2
|
+
import { EnyoAbsolutePriceWindow } from '../types/enyo-price-schedule.js';
|
|
3
|
+
/**
|
|
4
|
+
* Interface for publishing and consuming dynamic grid fees (network charges).
|
|
5
|
+
*
|
|
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}.
|
|
10
|
+
*
|
|
11
|
+
* Publishing requires the `GridFeeRegister` permission; consuming requires
|
|
12
|
+
* `GridFeeUse`.
|
|
13
|
+
*
|
|
14
|
+
* A grid fee describes the site's **grid connection**, not a supplier contract:
|
|
15
|
+
* it is registered and resolved independently of any tariff, and the same fee
|
|
16
|
+
* applies whichever supplier the site buys from.
|
|
17
|
+
*
|
|
18
|
+
* **Nothing here is applied to a price automatically.** The host never folds a
|
|
19
|
+
* grid fee into {@link EnergyAppEnergyPrices.getPrices}, because provider APIs
|
|
20
|
+
* differ: some already return grid-fee-inclusive prices, some do not. The app
|
|
21
|
+
* 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()`.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // Producer: a §14a EnWG module 3 HT/NT network charge
|
|
28
|
+
* const gridFee = energyApp.useGridFee();
|
|
29
|
+
* await gridFee.registerGridFee({
|
|
30
|
+
* id: 'dso-hd-2026',
|
|
31
|
+
* name: 'Netzentgelt HT/NT 2026',
|
|
32
|
+
* gridOperator: 'Netze BW',
|
|
33
|
+
* currency: EnyoCurrencyEnum.EUR,
|
|
34
|
+
* timezone: 'Europe/Berlin',
|
|
35
|
+
* appliesTo: EnyoPriceAppliesToEnum.Consumption,
|
|
36
|
+
* moduleThreeCompliant: true,
|
|
37
|
+
* schedule: {
|
|
38
|
+
* type: EnyoPriceScheduleTypeEnum.Recurring,
|
|
39
|
+
* windows: [
|
|
40
|
+
* { startTimeOfDay: '06:00', endTimeOfDay: '22:00', amountPerKwh: 0.0912 },
|
|
41
|
+
* { startTimeOfDay: '22:00', endTimeOfDay: '06:00', amountPerKwh: 0.0431 },
|
|
42
|
+
* ],
|
|
43
|
+
* },
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* // Consumer: resolve it for the next 24 hours
|
|
47
|
+
* const fees = await gridFee.getDynamicGridFees({
|
|
48
|
+
* fromIso: new Date().toISOString(),
|
|
49
|
+
* untilIso: new Date(Date.now() + 86_400_000).toISOString(),
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export interface EnergyAppGridFee {
|
|
54
|
+
/**
|
|
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.
|
|
58
|
+
*
|
|
59
|
+
* Requires the `GridFeeRegister` permission.
|
|
60
|
+
*
|
|
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.
|
|
68
|
+
*
|
|
69
|
+
* Requires the `GridFeeRegister` permission.
|
|
70
|
+
*
|
|
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
|
|
74
|
+
*/
|
|
75
|
+
updateGridFee(id: string, attributes: Partial<Omit<EnyoDynamicGridFeeRegistration, 'id'>>): Promise<EnyoDynamicGridFee>;
|
|
76
|
+
/**
|
|
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.
|
|
80
|
+
*
|
|
81
|
+
* Requires the `GridFeeRegister` permission.
|
|
82
|
+
*
|
|
83
|
+
* @param id - The unique identifier of the grid fee to remove
|
|
84
|
+
* @returns Promise that resolves when the grid fee has been removed
|
|
85
|
+
*/
|
|
86
|
+
removeGridFee(id: string): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Retrieves all registered grid fees across all energy apps on the system.
|
|
89
|
+
*
|
|
90
|
+
* Requires the `GridFeeUse` permission.
|
|
91
|
+
*
|
|
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.
|
|
98
|
+
*
|
|
99
|
+
* Requires the `GridFeeUse` permission.
|
|
100
|
+
*
|
|
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.
|
|
113
|
+
*
|
|
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
|
|
119
|
+
*/
|
|
120
|
+
publishGridFeeWindows(id: string, windows: EnyoAbsolutePriceWindow[]): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Resolves a dynamic grid fee to a flat **15-minute series** over the
|
|
123
|
+
* requested range — the same resolution
|
|
124
|
+
* {@link EnergyAppEnergyPrices.getPrices} uses, so both series can be
|
|
125
|
+
* combined interval by interval.
|
|
126
|
+
*
|
|
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.
|
|
132
|
+
*
|
|
133
|
+
* Intervals no window covers resolve to `feePerKwh: 0`, and intervals
|
|
134
|
+
* outside the fee's `validFromIso` / `validUntilIso` window are omitted.
|
|
135
|
+
*
|
|
136
|
+
* Requires the `GridFeeUse` permission.
|
|
137
|
+
*
|
|
138
|
+
* @param filter - The time range and the selector identifying which fee to resolve
|
|
139
|
+
* @returns Promise that resolves to the 15-minute fee series, or `null` when no fee applies
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* const fees = await gridFee.getDynamicGridFees({
|
|
144
|
+
* fromIso: '2026-09-02T00:00:00Z',
|
|
145
|
+
* untilIso: '2026-09-03T00:00:00Z',
|
|
146
|
+
* });
|
|
147
|
+
* fees?.entries.forEach(e => console.log(e.timestampIso, e.feePerKwh));
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
getDynamicGridFees(filter: EnyoDynamicGridFeeFilter): Promise<EnyoDynamicGridFeeSeries | null>;
|
|
151
|
+
/**
|
|
152
|
+
* Registers a listener invoked whenever any grid fee is registered, updated
|
|
153
|
+
* or removed — including when its absolute windows are republished.
|
|
154
|
+
*
|
|
155
|
+
* Use it to recompute composed prices without polling: a grid operator that
|
|
156
|
+
* publishes a new day-ahead series mid-day changes the cost of every future
|
|
157
|
+
* interval.
|
|
158
|
+
*
|
|
159
|
+
* Requires the `GridFeeUse` permission.
|
|
160
|
+
*
|
|
161
|
+
* @param listener - Callback invoked with the change event
|
|
162
|
+
* @returns A unique listener id that can be passed to {@link offGridFeeChanged}
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* const id = gridFee.onGridFeeChanged(async event => {
|
|
167
|
+
* if (event.gridFeeId === myFeeId) await recomputePrices();
|
|
168
|
+
* });
|
|
169
|
+
* // later
|
|
170
|
+
* gridFee.offGridFeeChanged(id);
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
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;
|
|
181
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|