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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +245 -5
  2. package/dist/cjs/energy-app-package-definition.cjs +1 -0
  3. package/dist/cjs/energy-app-package-definition.d.cts +1 -0
  4. package/dist/cjs/energy-app-permission.type.cjs +2 -0
  5. package/dist/cjs/energy-app-permission.type.d.cts +3 -1
  6. package/dist/cjs/energy-app.cjs +22 -0
  7. package/dist/cjs/energy-app.d.cts +21 -0
  8. package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
  9. package/dist/cjs/implementations/appliances/appliance-manager.cjs +6 -0
  10. package/dist/cjs/implementations/pricing/compose-electricity-prices.cjs +255 -0
  11. package/dist/cjs/implementations/pricing/compose-electricity-prices.d.cts +125 -0
  12. package/dist/cjs/implementations/pricing/price-component-validators.cjs +238 -0
  13. package/dist/cjs/implementations/pricing/price-component-validators.d.cts +57 -0
  14. package/dist/cjs/implementations/pricing/price-schedule-resolver.cjs +182 -0
  15. package/dist/cjs/implementations/pricing/price-schedule-resolver.d.cts +95 -0
  16. package/dist/cjs/index.cjs +7 -0
  17. package/dist/cjs/index.d.cts +7 -0
  18. package/dist/cjs/packages/energy-app-charge.d.cts +8 -81
  19. package/dist/cjs/packages/energy-app-electricity-tariff.d.cts +153 -41
  20. package/dist/cjs/packages/energy-app-grid-fee.cjs +2 -0
  21. package/dist/cjs/packages/energy-app-grid-fee.d.cts +199 -0
  22. package/dist/cjs/packages/energy-app-modbus.d.cts +12 -0
  23. package/dist/cjs/types/enyo-charge.d.cts +0 -71
  24. package/dist/cjs/types/enyo-data-bus-value.d.cts +7 -0
  25. package/dist/cjs/types/enyo-electricity-tariff.cjs +73 -13
  26. package/dist/cjs/types/enyo-electricity-tariff.d.cts +260 -50
  27. package/dist/cjs/types/enyo-energy-prices.d.cts +9 -3
  28. package/dist/cjs/types/enyo-grid-fee.cjs +26 -0
  29. package/dist/cjs/types/enyo-grid-fee.d.cts +247 -0
  30. package/dist/cjs/types/enyo-price-composition.cjs +15 -0
  31. package/dist/cjs/types/enyo-price-composition.d.cts +147 -0
  32. package/dist/cjs/types/enyo-price-schedule.cjs +45 -0
  33. package/dist/cjs/types/enyo-price-schedule.d.cts +137 -0
  34. package/dist/cjs/version.cjs +1 -1
  35. package/dist/cjs/version.d.cts +1 -1
  36. package/dist/energy-app-package-definition.d.ts +1 -0
  37. package/dist/energy-app-package-definition.js +1 -0
  38. package/dist/energy-app-permission.type.d.ts +3 -1
  39. package/dist/energy-app-permission.type.js +2 -0
  40. package/dist/energy-app.d.ts +21 -0
  41. package/dist/energy-app.js +22 -0
  42. package/dist/enyo-energy-app-sdk.d.ts +3 -0
  43. package/dist/implementations/appliances/appliance-manager.js +6 -0
  44. package/dist/implementations/pricing/compose-electricity-prices.d.ts +125 -0
  45. package/dist/implementations/pricing/compose-electricity-prices.js +248 -0
  46. package/dist/implementations/pricing/price-component-validators.d.ts +57 -0
  47. package/dist/implementations/pricing/price-component-validators.js +230 -0
  48. package/dist/implementations/pricing/price-schedule-resolver.d.ts +95 -0
  49. package/dist/implementations/pricing/price-schedule-resolver.js +175 -0
  50. package/dist/index.d.ts +7 -0
  51. package/dist/index.js +7 -0
  52. package/dist/packages/energy-app-charge.d.ts +8 -81
  53. package/dist/packages/energy-app-electricity-tariff.d.ts +153 -41
  54. package/dist/packages/energy-app-grid-fee.d.ts +199 -0
  55. package/dist/packages/energy-app-grid-fee.js +1 -0
  56. package/dist/packages/energy-app-modbus.d.ts +12 -0
  57. package/dist/types/enyo-charge.d.ts +0 -71
  58. package/dist/types/enyo-data-bus-value.d.ts +7 -0
  59. package/dist/types/enyo-electricity-tariff.d.ts +260 -50
  60. package/dist/types/enyo-electricity-tariff.js +71 -11
  61. package/dist/types/enyo-energy-prices.d.ts +9 -3
  62. package/dist/types/enyo-grid-fee.d.ts +247 -0
  63. package/dist/types/enyo-grid-fee.js +23 -0
  64. package/dist/types/enyo-price-composition.d.ts +147 -0
  65. package/dist/types/enyo-price-composition.js +12 -0
  66. package/dist/types/enyo-price-schedule.d.ts +137 -0
  67. package/dist/types/enyo-price-schedule.js +42 -0
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/package.json +2 -2
@@ -0,0 +1,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[];
@@ -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);
@@ -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, 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,63 +1,175 @@
1
- import { EnyoElectricityTariff, EnyoElectricityTariffWithDefault } 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
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
- registerTariff(tariff: Omit<EnyoElectricityTariff, 'id'>): Promise<EnyoElectricityTariffWithDefault>;
57
+ getTariff(direction: EnyoTariffDirectionEnum): Promise<EnyoElectricityTariff | null>;
17
58
  /**
18
- * Retrieves all registered electricity tariffs.
19
- * Each tariff includes a defaultTariff boolean indicating whether
20
- * it is the system default tariff.
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
- * @returns Promise that resolves to an array of all registered tariffs with default indicators
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
- getAllTariffs(filter?: {
25
- myTariffs?: boolean;
26
- }): Promise<EnyoElectricityTariffWithDefault[]>;
84
+ getPrices(direction: EnyoTariffDirectionEnum, range: EnyoTariffPriceRange): Promise<EnyoTariffPriceSeries | null>;
27
85
  /**
28
- * Removes an electricity tariff by its ID.
29
- * If the tariff does not exist, this operation is a no-op.
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 id - The unique identifier of the tariff to remove
32
- * @returns Promise that resolves when the tariff has been removed
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
- removeTariff(id: string): Promise<void>;
110
+ setTariff(direction: EnyoTariffDirectionEnum, tariff: EnyoElectricityTariff): Promise<EnyoTariffActivationResult>;
35
111
  /**
36
- * Requests to make a tariff the default tariff.
112
+ * Publishes prices for a slot this app owns.
37
113
  *
38
- * @param id - The unique identifier of the tariff
39
- * @returns Promise that resolves with the result
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
- makeDefaultTariff(id: string): Promise<'accepted' | 'rejected'>;
136
+ publishPrices(direction: EnyoTariffDirectionEnum, prices: EnyoTariffPricePublication): Promise<void>;
42
137
  /**
43
- * Retrieves the system default tariff information.
44
- * Returns the full tariff including pricing data.
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
- * @returns Promise that resolves to the default tariff info, or null if none is configured
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
- getDefaultTariff(): Promise<EnyoElectricityTariff | null>;
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
- * Partially updates an existing electricity tariff.
56
- * Only the provided attributes will be modified; all other fields remain unchanged.
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 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
171
+ * @param listener - Callback invoked with the change event
172
+ * @returns A function that removes this listener when called
61
173
  */
62
- updateTariff(id: string, attributes: Partial<Omit<EnyoElectricityTariff, 'id'>>): Promise<EnyoElectricityTariffWithDefault>;
174
+ onTariffChanged(listener: (event: EnyoTariffChangeEvent) => void | Promise<void>): () => void;
63
175
  }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });