@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.
- package/README.md +245 -5
- 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 +22 -0
- package/dist/cjs/energy-app.d.cts +21 -0
- package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
- package/dist/cjs/implementations/appliances/appliance-manager.cjs +6 -0
- package/dist/cjs/implementations/pricing/compose-electricity-prices.cjs +255 -0
- package/dist/cjs/implementations/pricing/compose-electricity-prices.d.cts +125 -0
- package/dist/cjs/implementations/pricing/price-component-validators.cjs +238 -0
- package/dist/cjs/implementations/pricing/price-component-validators.d.cts +57 -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-charge.d.cts +8 -81
- package/dist/cjs/packages/energy-app-electricity-tariff.d.cts +153 -41
- package/dist/cjs/packages/energy-app-grid-fee.cjs +2 -0
- package/dist/cjs/packages/energy-app-grid-fee.d.cts +199 -0
- package/dist/cjs/packages/energy-app-modbus.d.cts +12 -0
- package/dist/cjs/types/enyo-charge.d.cts +0 -71
- package/dist/cjs/types/enyo-data-bus-value.d.cts +7 -0
- package/dist/cjs/types/enyo-electricity-tariff.cjs +73 -13
- package/dist/cjs/types/enyo-electricity-tariff.d.cts +260 -50
- package/dist/cjs/types/enyo-energy-prices.d.cts +9 -3
- package/dist/cjs/types/enyo-grid-fee.cjs +26 -0
- package/dist/cjs/types/enyo-grid-fee.d.cts +247 -0
- package/dist/cjs/types/enyo-price-composition.cjs +15 -0
- package/dist/cjs/types/enyo-price-composition.d.cts +147 -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 +21 -0
- package/dist/energy-app.js +22 -0
- package/dist/enyo-energy-app-sdk.d.ts +3 -0
- package/dist/implementations/appliances/appliance-manager.js +6 -0
- package/dist/implementations/pricing/compose-electricity-prices.d.ts +125 -0
- package/dist/implementations/pricing/compose-electricity-prices.js +248 -0
- package/dist/implementations/pricing/price-component-validators.d.ts +57 -0
- package/dist/implementations/pricing/price-component-validators.js +230 -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-charge.d.ts +8 -81
- package/dist/packages/energy-app-electricity-tariff.d.ts +153 -41
- package/dist/packages/energy-app-grid-fee.d.ts +199 -0
- package/dist/packages/energy-app-grid-fee.js +1 -0
- package/dist/packages/energy-app-modbus.d.ts +12 -0
- package/dist/types/enyo-charge.d.ts +0 -71
- package/dist/types/enyo-data-bus-value.d.ts +7 -0
- package/dist/types/enyo-electricity-tariff.d.ts +260 -50
- package/dist/types/enyo-electricity-tariff.js +71 -11
- package/dist/types/enyo-energy-prices.d.ts +9 -3
- package/dist/types/enyo-grid-fee.d.ts +247 -0
- package/dist/types/enyo-grid-fee.js +23 -0
- package/dist/types/enyo-price-composition.d.ts +147 -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 +2 -2
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PRICE_SCHEDULE_INTERVAL_MINUTES = void 0;
|
|
4
|
+
exports.getZonedDateParts = getZonedDateParts;
|
|
5
|
+
exports.parseTimeOfDay = parseTimeOfDay;
|
|
6
|
+
exports.floorToInterval = floorToInterval;
|
|
7
|
+
exports.resolvePriceSchedule = resolvePriceSchedule;
|
|
8
|
+
const enyo_price_schedule_js_1 = require("../../types/enyo-price-schedule.cjs");
|
|
9
|
+
/**
|
|
10
|
+
* Length of one resolved interval, in minutes. Matches the resolution of
|
|
11
|
+
* `useElectricityPrices().getPrices()` so resolved schedules can be combined
|
|
12
|
+
* with a price series interval by interval.
|
|
13
|
+
*/
|
|
14
|
+
exports.PRICE_SCHEDULE_INTERVAL_MINUTES = 15;
|
|
15
|
+
const INTERVAL_MS = exports.PRICE_SCHEDULE_INTERVAL_MINUTES * 60 * 1000;
|
|
16
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
const WEEKDAY_INDEX = {
|
|
18
|
+
Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6,
|
|
19
|
+
};
|
|
20
|
+
const formatterCache = new Map();
|
|
21
|
+
function zonedFormatter(timezone) {
|
|
22
|
+
let formatter = formatterCache.get(timezone);
|
|
23
|
+
if (formatter === undefined) {
|
|
24
|
+
formatter = new Intl.DateTimeFormat('en-US', {
|
|
25
|
+
timeZone: timezone,
|
|
26
|
+
hourCycle: 'h23',
|
|
27
|
+
year: 'numeric',
|
|
28
|
+
month: 'numeric',
|
|
29
|
+
day: 'numeric',
|
|
30
|
+
weekday: 'short',
|
|
31
|
+
hour: 'numeric',
|
|
32
|
+
minute: 'numeric',
|
|
33
|
+
});
|
|
34
|
+
formatterCache.set(timezone, formatter);
|
|
35
|
+
}
|
|
36
|
+
return formatter;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Projects an instant onto the wall clock of a time zone.
|
|
40
|
+
*
|
|
41
|
+
* Uses `Intl.DateTimeFormat`, so daylight-saving transitions are handled by the
|
|
42
|
+
* platform's time zone database without pulling in a dependency.
|
|
43
|
+
*
|
|
44
|
+
* @param date - The instant to project
|
|
45
|
+
* @param timezone - IANA time zone identifier, e.g. `'Europe/Berlin'`
|
|
46
|
+
* @returns The wall-clock calendar fields of `date` in `timezone`
|
|
47
|
+
* @throws {RangeError} If `timezone` is not a valid IANA identifier
|
|
48
|
+
*/
|
|
49
|
+
function getZonedDateParts(date, timezone) {
|
|
50
|
+
const parts = zonedFormatter(timezone).formatToParts(date);
|
|
51
|
+
const lookup = {};
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
lookup[part.type] = part.value;
|
|
54
|
+
}
|
|
55
|
+
const hour = Number(lookup.hour);
|
|
56
|
+
const minute = Number(lookup.minute);
|
|
57
|
+
return {
|
|
58
|
+
year: Number(lookup.year),
|
|
59
|
+
month: Number(lookup.month),
|
|
60
|
+
day: Number(lookup.day),
|
|
61
|
+
weekday: WEEKDAY_INDEX[lookup.weekday] ?? 0,
|
|
62
|
+
hour,
|
|
63
|
+
minute,
|
|
64
|
+
minutesOfDay: hour * 60 + minute,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parses an `HH:mm` wall-clock time into minutes since midnight.
|
|
69
|
+
*
|
|
70
|
+
* @param value - Time in 24-hour `HH:mm` notation, e.g. `'22:00'`
|
|
71
|
+
* @returns Minutes since midnight, or `null` when the value is malformed
|
|
72
|
+
*/
|
|
73
|
+
function parseTimeOfDay(value) {
|
|
74
|
+
const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value);
|
|
75
|
+
if (match === null) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return Number(match[1]) * 60 + Number(match[2]);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Rounds an instant down to the start of its 15-minute interval.
|
|
82
|
+
*
|
|
83
|
+
* @param timestampMs - Epoch milliseconds
|
|
84
|
+
* @returns Epoch milliseconds of the interval start
|
|
85
|
+
*/
|
|
86
|
+
function floorToInterval(timestampMs) {
|
|
87
|
+
return Math.floor(timestampMs / INTERVAL_MS) * INTERVAL_MS;
|
|
88
|
+
}
|
|
89
|
+
function matchesRecurringWindow(window, timestampMs, timezone) {
|
|
90
|
+
const start = parseTimeOfDay(window.startTimeOfDay);
|
|
91
|
+
const end = parseTimeOfDay(window.endTimeOfDay);
|
|
92
|
+
if (start === null || end === null || start === end) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const parts = getZonedDateParts(new Date(timestampMs), timezone);
|
|
96
|
+
let inWindow;
|
|
97
|
+
let anchor;
|
|
98
|
+
if (start < end) {
|
|
99
|
+
inWindow = parts.minutesOfDay >= start && parts.minutesOfDay < end;
|
|
100
|
+
anchor = parts;
|
|
101
|
+
}
|
|
102
|
+
else if (parts.minutesOfDay >= start) {
|
|
103
|
+
// Evening leg of a window that wraps past midnight.
|
|
104
|
+
inWindow = true;
|
|
105
|
+
anchor = parts;
|
|
106
|
+
}
|
|
107
|
+
else if (parts.minutesOfDay < end) {
|
|
108
|
+
// Early-morning leg — the window belongs to the previous local day.
|
|
109
|
+
inWindow = true;
|
|
110
|
+
anchor = getZonedDateParts(new Date(timestampMs - DAY_MS), timezone);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (!inWindow) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (window.daysOfWeek !== undefined && !window.daysOfWeek.includes(anchor.weekday)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return window.months === undefined || window.months.includes(anchor.month);
|
|
122
|
+
}
|
|
123
|
+
function matchesAbsoluteWindow(window, timestampMs) {
|
|
124
|
+
return timestampMs >= Date.parse(window.startIso) && timestampMs < Date.parse(window.endIso);
|
|
125
|
+
}
|
|
126
|
+
function amountAt(schedule, timestampMs, timezone) {
|
|
127
|
+
if (schedule.type === enyo_price_schedule_js_1.EnyoPriceScheduleTypeEnum.Constant) {
|
|
128
|
+
return schedule.amountPerKwh;
|
|
129
|
+
}
|
|
130
|
+
if (schedule.type === enyo_price_schedule_js_1.EnyoPriceScheduleTypeEnum.Absolute) {
|
|
131
|
+
const window = schedule.windows.find(candidate => matchesAbsoluteWindow(candidate, timestampMs));
|
|
132
|
+
return window?.amountPerKwh ?? 0;
|
|
133
|
+
}
|
|
134
|
+
const window = schedule.windows.find(candidate => matchesRecurringWindow(candidate, timestampMs, timezone));
|
|
135
|
+
return window?.amountPerKwh ?? 0;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Expands a price schedule into a flat 15-minute series over a time range.
|
|
139
|
+
*
|
|
140
|
+
* The range start is rounded down to the enclosing 15-minute interval. Intervals
|
|
141
|
+
* no window covers resolve to `amountPerKwh: 0`; intervals outside the
|
|
142
|
+
* component's validity window are omitted entirely, so an empty result means
|
|
143
|
+
* "this component does not apply here" rather than "it is free here".
|
|
144
|
+
*
|
|
145
|
+
* Windows are evaluated in array order and the first match wins — schedules are
|
|
146
|
+
* expected to be overlap-free, which the validators enforce at registration
|
|
147
|
+
* time.
|
|
148
|
+
*
|
|
149
|
+
* @param schedule - The schedule to expand
|
|
150
|
+
* @param options - Range, time zone and optional validity bounds
|
|
151
|
+
* @returns Resolved entries sorted ascending by `timestampIso`
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* const entries = resolvePriceSchedule(
|
|
156
|
+
* {
|
|
157
|
+
* type: EnyoPriceScheduleTypeEnum.Recurring,
|
|
158
|
+
* windows: [{ startTimeOfDay: '22:00', endTimeOfDay: '06:00', amountPerKwh: 0.05 }],
|
|
159
|
+
* },
|
|
160
|
+
* { fromIso: '2026-09-02T00:00:00Z', untilIso: '2026-09-03T00:00:00Z', timezone: 'Europe/Berlin' },
|
|
161
|
+
* );
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
function resolvePriceSchedule(schedule, options) {
|
|
165
|
+
const untilMs = Date.parse(options.untilIso);
|
|
166
|
+
const validFromMs = options.validFromIso === undefined ? undefined : Date.parse(options.validFromIso);
|
|
167
|
+
const validUntilMs = options.validUntilIso === undefined ? undefined : Date.parse(options.validUntilIso);
|
|
168
|
+
const entries = [];
|
|
169
|
+
for (let cursor = floorToInterval(Date.parse(options.fromIso)); cursor < untilMs; cursor += INTERVAL_MS) {
|
|
170
|
+
if (validFromMs !== undefined && cursor < validFromMs) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (validUntilMs !== undefined && cursor >= validUntilMs) {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
entries.push({
|
|
177
|
+
timestampIso: new Date(cursor).toISOString(),
|
|
178
|
+
amountPerKwh: amountAt(schedule, cursor, options.timezone),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return entries;
|
|
182
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { EnyoPriceSchedule, EnyoResolvedPriceEntry } from '../../types/enyo-price-schedule.cjs';
|
|
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[];
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -152,3 +152,10 @@ __exportStar(require("./packages/energy-app-epex-spot-price.cjs"), exports);
|
|
|
152
152
|
__exportStar(require("./types/enyo-automation.cjs"), exports);
|
|
153
153
|
__exportStar(require("./packages/energy-app-automation.cjs"), exports);
|
|
154
154
|
__exportStar(require("./implementations/automation/automation-validators.cjs"), exports);
|
|
155
|
+
__exportStar(require("./types/enyo-price-schedule.cjs"), exports);
|
|
156
|
+
__exportStar(require("./types/enyo-grid-fee.cjs"), exports);
|
|
157
|
+
__exportStar(require("./types/enyo-price-composition.cjs"), exports);
|
|
158
|
+
__exportStar(require("./packages/energy-app-grid-fee.cjs"), exports);
|
|
159
|
+
__exportStar(require("./implementations/pricing/price-schedule-resolver.cjs"), exports);
|
|
160
|
+
__exportStar(require("./implementations/pricing/price-component-validators.cjs"), exports);
|
|
161
|
+
__exportStar(require("./implementations/pricing/compose-electricity-prices.cjs"), exports);
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -136,3 +136,10 @@ export * from './packages/energy-app-epex-spot-price.cjs';
|
|
|
136
136
|
export * from './types/enyo-automation.cjs';
|
|
137
137
|
export * from './packages/energy-app-automation.cjs';
|
|
138
138
|
export * from './implementations/automation/automation-validators.cjs';
|
|
139
|
+
export * from './types/enyo-price-schedule.cjs';
|
|
140
|
+
export * from './types/enyo-grid-fee.cjs';
|
|
141
|
+
export * from './types/enyo-price-composition.cjs';
|
|
142
|
+
export * from './packages/energy-app-grid-fee.cjs';
|
|
143
|
+
export * from './implementations/pricing/price-schedule-resolver.cjs';
|
|
144
|
+
export * from './implementations/pricing/price-component-validators.cjs';
|
|
145
|
+
export * from './implementations/pricing/compose-electricity-prices.cjs';
|
|
@@ -1,93 +1,20 @@
|
|
|
1
|
-
import { EnyoCharge, EnyoChargeFilter, EnyoDefaultChargeMode
|
|
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.
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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,63 +1,175 @@
|
|
|
1
|
-
import { EnyoElectricityTariff,
|
|
1
|
+
import { EnyoElectricityTariff, EnyoTariffActivationResult, EnyoTariffChangeEvent, EnyoTariffDirectionEnum, EnyoTariffPricePublication, EnyoTariffPriceSeries } from "../types/enyo-electricity-tariff.cjs";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
51
|
+
* `null` is a normal answer — a site with no feed-in contract has no feed-in
|
|
52
|
+
* tariff — and not an error.
|
|
53
|
+
*
|
|
54
|
+
* @param direction - Which slot to read
|
|
55
|
+
* @returns Promise resolving to the tariff in that slot, or `null`
|
|
15
56
|
*/
|
|
16
|
-
|
|
57
|
+
getTariff(direction: EnyoTariffDirectionEnum): Promise<EnyoElectricityTariff | null>;
|
|
17
58
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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.
|
|
62
|
+
*
|
|
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.
|
|
21
71
|
*
|
|
22
|
-
* @
|
|
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
|
+
* ```
|
|
23
83
|
*/
|
|
24
|
-
|
|
25
|
-
myTariffs?: boolean;
|
|
26
|
-
}): Promise<EnyoElectricityTariffWithDefault[]>;
|
|
84
|
+
getPrices(direction: EnyoTariffDirectionEnum, range: EnyoTariffPriceRange): Promise<EnyoTariffPriceSeries | null>;
|
|
27
85
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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.
|
|
103
|
+
*
|
|
104
|
+
* Requires the `ElectricityTariff` permission.
|
|
30
105
|
*
|
|
31
|
-
* @param
|
|
32
|
-
* @
|
|
106
|
+
* @param direction - Which slot to fill
|
|
107
|
+
* @param tariff - The tariff and its pricing details
|
|
108
|
+
* @returns Promise resolving to the activation outcome
|
|
33
109
|
*/
|
|
34
|
-
|
|
110
|
+
setTariff(direction: EnyoTariffDirectionEnum, tariff: EnyoElectricityTariff): Promise<EnyoTariffActivationResult>;
|
|
35
111
|
/**
|
|
36
|
-
*
|
|
112
|
+
* Publishes prices for a slot this app owns.
|
|
37
113
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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.
|
|
131
|
+
*
|
|
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
|
|
40
135
|
*/
|
|
41
|
-
|
|
136
|
+
publishPrices(direction: EnyoTariffDirectionEnum, prices: EnyoTariffPricePublication): Promise<void>;
|
|
42
137
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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".
|
|
45
140
|
*
|
|
46
|
-
*
|
|
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.
|
|
148
|
+
*
|
|
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.
|
|
152
|
+
*
|
|
153
|
+
* One handler per app. Registering again replaces the previous one.
|
|
154
|
+
*
|
|
155
|
+
* @param handler - Called with the direction the user selected this app for
|
|
156
|
+
* @returns A function that removes the handler when called
|
|
47
157
|
*/
|
|
48
|
-
|
|
49
|
-
findTariff(filter: {
|
|
50
|
-
applianceId?: string;
|
|
51
|
-
tariffId?: string;
|
|
52
|
-
externalTariffId?: string;
|
|
53
|
-
}): Promise<EnyoElectricityTariffWithDefault | null>;
|
|
158
|
+
onTariffSelected(handler: (direction: EnyoTariffDirectionEnum) => Promise<EnyoTariffActivationResult>): () => void;
|
|
54
159
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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.
|
|
57
170
|
*
|
|
58
|
-
* @param
|
|
59
|
-
* @
|
|
60
|
-
* @returns Promise that resolves to the full updated tariff with its default indicator
|
|
171
|
+
* @param listener - Callback invoked with the change event
|
|
172
|
+
* @returns A function that removes this listener when called
|
|
61
173
|
*/
|
|
62
|
-
|
|
174
|
+
onTariffChanged(listener: (event: EnyoTariffChangeEvent) => void | Promise<void>): () => void;
|
|
63
175
|
}
|