@uk-energy/elexon 0.0.0-stage → 0.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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +150 -2
- package/dist/index.cjs +493 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +31221 -0
- package/dist/index.d.ts +31221 -0
- package/dist/index.js +482 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -3
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@uk-energy/core');
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
var DEFAULT_FUEL_TYPES = ["WIND"];
|
|
7
|
+
function compareIds(a, b) {
|
|
8
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
9
|
+
}
|
|
10
|
+
function num(s) {
|
|
11
|
+
if (s == null || s === "") return null;
|
|
12
|
+
const n = Number(s);
|
|
13
|
+
return Number.isFinite(n) ? n : null;
|
|
14
|
+
}
|
|
15
|
+
function aggregateCurtailment(settlementDate, range, units, volumes, cashflows, boalf) {
|
|
16
|
+
const byId = /* @__PURE__ */ new Map();
|
|
17
|
+
for (const u of units) {
|
|
18
|
+
if (u.elexonBmUnit) byId.set(u.elexonBmUnit, u);
|
|
19
|
+
if (u.nationalGridBmUnit) byId.set(u.nationalGridBmUnit, u);
|
|
20
|
+
}
|
|
21
|
+
const soByAcceptance = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const r of boalf) {
|
|
23
|
+
if (r.acceptanceNumber != null) {
|
|
24
|
+
soByAcceptance.set(
|
|
25
|
+
r.acceptanceNumber,
|
|
26
|
+
(soByAcceptance.get(r.acceptanceNumber) ?? false) || r.soFlag === true
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const cashByKey = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const c of cashflows) {
|
|
32
|
+
const sp = c.settlementPeriod;
|
|
33
|
+
if (!c.bmUnit || sp === void 0 || sp < range.from || sp > range.to) continue;
|
|
34
|
+
const key = `${c.bmUnit}|${sp}`;
|
|
35
|
+
cashByKey.set(key, (cashByKey.get(key) ?? 0) + (c.totalCashflow ?? 0));
|
|
36
|
+
}
|
|
37
|
+
const periods = /* @__PURE__ */ new Map();
|
|
38
|
+
const seen = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const v of volumes) {
|
|
40
|
+
const sp = v.settlementPeriod;
|
|
41
|
+
const acceptance = v.acceptanceId;
|
|
42
|
+
if (!v.bmUnit || sp === void 0 || acceptance === void 0) continue;
|
|
43
|
+
if (sp < range.from || sp > range.to) continue;
|
|
44
|
+
const mwh = -(v.totalVolumeAccepted ?? 0);
|
|
45
|
+
if (!(mwh > 0)) continue;
|
|
46
|
+
const key = `${v.bmUnit}|${sp}`;
|
|
47
|
+
let p = periods.get(key);
|
|
48
|
+
let ids = seen.get(key);
|
|
49
|
+
if (!p || !ids) {
|
|
50
|
+
const ref = byId.get(v.bmUnit);
|
|
51
|
+
p = {
|
|
52
|
+
settlementDate: v.settlementDate ?? settlementDate,
|
|
53
|
+
settlementPeriod: sp,
|
|
54
|
+
startTime: v.startTime ?? "",
|
|
55
|
+
bmUnit: v.bmUnit,
|
|
56
|
+
nationalGridBmUnit: v.nationalGridBmUnit ?? ref?.nationalGridBmUnit ?? null,
|
|
57
|
+
leadPartyName: v.leadPartyName ?? ref?.leadPartyName ?? null,
|
|
58
|
+
fuelType: ref?.fuelType ?? null,
|
|
59
|
+
gspGroupId: ref?.gspGroupId ?? null,
|
|
60
|
+
capacityMw: num(ref?.generationCapacity),
|
|
61
|
+
curtailedMwh: 0,
|
|
62
|
+
soFlaggedMwh: 0,
|
|
63
|
+
cashflowGbp: cashByKey.get(key) ?? null,
|
|
64
|
+
acceptances: 0,
|
|
65
|
+
soFlaggedAcceptances: 0
|
|
66
|
+
};
|
|
67
|
+
ids = /* @__PURE__ */ new Set();
|
|
68
|
+
periods.set(key, p);
|
|
69
|
+
seen.set(key, ids);
|
|
70
|
+
}
|
|
71
|
+
p.curtailedMwh += mwh;
|
|
72
|
+
const so = soByAcceptance.get(acceptance) === true;
|
|
73
|
+
if (so) p.soFlaggedMwh += mwh;
|
|
74
|
+
if (!ids.has(acceptance)) {
|
|
75
|
+
ids.add(acceptance);
|
|
76
|
+
p.acceptances += 1;
|
|
77
|
+
if (so) p.soFlaggedAcceptances += 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const rows = [...periods.values()].sort(
|
|
81
|
+
(a, b) => a.settlementPeriod - b.settlementPeriod || compareIds(a.bmUnit, b.bmUnit)
|
|
82
|
+
);
|
|
83
|
+
const unitTotals = /* @__PURE__ */ new Map();
|
|
84
|
+
const unitAcceptances = /* @__PURE__ */ new Map();
|
|
85
|
+
const allAcceptances = /* @__PURE__ */ new Set();
|
|
86
|
+
const total = { curtailedMwh: 0, soFlaggedMwh: 0, cashflowGbp: 0, units: 0, acceptances: 0 };
|
|
87
|
+
for (const p of rows) {
|
|
88
|
+
let u = unitTotals.get(p.bmUnit);
|
|
89
|
+
if (!u) {
|
|
90
|
+
u = {
|
|
91
|
+
bmUnit: p.bmUnit,
|
|
92
|
+
nationalGridBmUnit: p.nationalGridBmUnit,
|
|
93
|
+
leadPartyName: p.leadPartyName,
|
|
94
|
+
fuelType: p.fuelType,
|
|
95
|
+
gspGroupId: p.gspGroupId,
|
|
96
|
+
capacityMw: p.capacityMw,
|
|
97
|
+
curtailedMwh: 0,
|
|
98
|
+
soFlaggedMwh: 0,
|
|
99
|
+
cashflowGbp: 0,
|
|
100
|
+
periods: 0,
|
|
101
|
+
acceptances: 0
|
|
102
|
+
};
|
|
103
|
+
unitTotals.set(p.bmUnit, u);
|
|
104
|
+
}
|
|
105
|
+
u.curtailedMwh += p.curtailedMwh;
|
|
106
|
+
u.soFlaggedMwh += p.soFlaggedMwh;
|
|
107
|
+
u.cashflowGbp += p.cashflowGbp ?? 0;
|
|
108
|
+
u.periods += 1;
|
|
109
|
+
const ids = seen.get(`${p.bmUnit}|${p.settlementPeriod}`) ?? /* @__PURE__ */ new Set();
|
|
110
|
+
let acc = unitAcceptances.get(p.bmUnit);
|
|
111
|
+
if (!acc) {
|
|
112
|
+
acc = /* @__PURE__ */ new Set();
|
|
113
|
+
unitAcceptances.set(p.bmUnit, acc);
|
|
114
|
+
}
|
|
115
|
+
for (const id of ids) {
|
|
116
|
+
acc.add(id);
|
|
117
|
+
allAcceptances.add(id);
|
|
118
|
+
}
|
|
119
|
+
u.acceptances = acc.size;
|
|
120
|
+
total.curtailedMwh += p.curtailedMwh;
|
|
121
|
+
total.soFlaggedMwh += p.soFlaggedMwh;
|
|
122
|
+
total.cashflowGbp += p.cashflowGbp ?? 0;
|
|
123
|
+
}
|
|
124
|
+
total.units = unitTotals.size;
|
|
125
|
+
total.acceptances = allAcceptances.size;
|
|
126
|
+
return {
|
|
127
|
+
settlementDate,
|
|
128
|
+
settlementPeriodFrom: range.from,
|
|
129
|
+
settlementPeriodTo: range.to,
|
|
130
|
+
periods: rows,
|
|
131
|
+
units: [...unitTotals.values()].sort((a, b) => b.curtailedMwh - a.curtailedMwh),
|
|
132
|
+
total
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function curtailmentForDay(client, settlementDate, opts = {}) {
|
|
136
|
+
const day = core.formatDate(settlementDate);
|
|
137
|
+
const last = core.settlementPeriodsInDay(day);
|
|
138
|
+
const from = opts.settlementPeriodFrom ?? 1;
|
|
139
|
+
const to = opts.settlementPeriodTo ?? last;
|
|
140
|
+
if (from < 1 || to > last || from > to) {
|
|
141
|
+
throw new core.UkEnergyError(`Settlement periods ${from}-${to} are outside 1-${last} for ${day}`);
|
|
142
|
+
}
|
|
143
|
+
const units = await client.reference.bmUnits();
|
|
144
|
+
let ids;
|
|
145
|
+
if (opts.bmUnit) {
|
|
146
|
+
ids = opts.bmUnit;
|
|
147
|
+
} else {
|
|
148
|
+
const fuel = opts.fuelType ?? DEFAULT_FUEL_TYPES;
|
|
149
|
+
ids = units.filter((u) => u.fuelType != null && fuel.includes(u.fuelType)).map(unitId).filter((id) => id !== "");
|
|
150
|
+
}
|
|
151
|
+
ids = [...new Set(ids)];
|
|
152
|
+
if (ids.length === 0) throw new core.UkEnergyError("No BM units to query");
|
|
153
|
+
const [volumes, cashflows, boalfParts] = await Promise.all([
|
|
154
|
+
client.balancing.acceptanceVolumes("bid", day, { bmUnit: ids }),
|
|
155
|
+
client.balancing.cashflows("bid", day, { bmUnit: ids }),
|
|
156
|
+
Promise.all(
|
|
157
|
+
chunk(ids, MAX_UNITS_PER_REQUEST).map(
|
|
158
|
+
(bmUnit) => client.datasets.boalf({
|
|
159
|
+
from: day,
|
|
160
|
+
to: day,
|
|
161
|
+
settlementPeriodFrom: 1,
|
|
162
|
+
settlementPeriodTo: last,
|
|
163
|
+
bmUnit
|
|
164
|
+
})
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
]);
|
|
168
|
+
const boalf = boalfParts.flat();
|
|
169
|
+
return {
|
|
170
|
+
unitsQueried: ids.length,
|
|
171
|
+
...aggregateCurtailment(day, { from, to }, units, volumes, cashflows, boalf)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
var DEFAULT_FUEL_TYPES2 = ["WIND"];
|
|
175
|
+
function aggregateWastedWind(settlementDate, range, units, bids, offers, fuelTypes = DEFAULT_FUEL_TYPES2) {
|
|
176
|
+
const fuelById = /* @__PURE__ */ new Map();
|
|
177
|
+
for (const u of units) if (u.elexonBmUnit) fuelById.set(u.elexonBmUnit, u.fuelType);
|
|
178
|
+
const periods = [];
|
|
179
|
+
for (let sp = range.from; sp <= range.to; sp++) {
|
|
180
|
+
const windBids = bids.filter(
|
|
181
|
+
(r) => r.settlementPeriod === sp && r.soFlag === true && r.id != null && fuelTypes.includes(fuelById.get(r.id) ?? "")
|
|
182
|
+
);
|
|
183
|
+
let volume = 0;
|
|
184
|
+
let cost = 0;
|
|
185
|
+
for (const b of windBids) {
|
|
186
|
+
const v = b.volume ?? 0;
|
|
187
|
+
volume += v;
|
|
188
|
+
cost += (b.originalPrice ?? 0) * v;
|
|
189
|
+
}
|
|
190
|
+
const need = Math.abs(volume);
|
|
191
|
+
const stack = offers.filter((r) => r.settlementPeriod === sp && !r.cadlFlag).sort(
|
|
192
|
+
(a, b) => (a.soFlag ? 1 : 0) - (b.soFlag ? 1 : 0) || (a.sequenceNumber ?? 0) - (b.sequenceNumber ?? 0)
|
|
193
|
+
);
|
|
194
|
+
let taken = 0;
|
|
195
|
+
let turnUpCost = 0;
|
|
196
|
+
let used = 0;
|
|
197
|
+
for (const o of need > 0 ? stack : []) {
|
|
198
|
+
used += 1;
|
|
199
|
+
const v = o.volume ?? 0;
|
|
200
|
+
if (taken + v > need) {
|
|
201
|
+
const fraction = Math.min(Math.max((need - taken) / v, 0), 1);
|
|
202
|
+
taken += v * fraction;
|
|
203
|
+
turnUpCost += (o.originalPrice ?? 0) * (v * fraction);
|
|
204
|
+
} else {
|
|
205
|
+
taken += v;
|
|
206
|
+
turnUpCost += (o.originalPrice ?? 0) * v;
|
|
207
|
+
}
|
|
208
|
+
if (taken >= need) break;
|
|
209
|
+
}
|
|
210
|
+
const first = windBids[0] ?? stack[0] ?? bids.find((r) => r.settlementPeriod === sp) ?? offers.find((r) => r.settlementPeriod === sp);
|
|
211
|
+
periods.push({
|
|
212
|
+
settlementDate,
|
|
213
|
+
settlementPeriod: sp,
|
|
214
|
+
startTime: first?.startTime ?? "",
|
|
215
|
+
curtailedMwh: need,
|
|
216
|
+
curtailmentCostGbp: cost,
|
|
217
|
+
turnUpMwh: taken,
|
|
218
|
+
turnUpCostGbp: turnUpCost,
|
|
219
|
+
bids: windBids.length,
|
|
220
|
+
offers: used
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const total = {
|
|
224
|
+
curtailedMwh: 0,
|
|
225
|
+
curtailmentCostGbp: 0,
|
|
226
|
+
turnUpMwh: 0,
|
|
227
|
+
turnUpCostGbp: 0,
|
|
228
|
+
totalCostGbp: 0
|
|
229
|
+
};
|
|
230
|
+
for (const p of periods) {
|
|
231
|
+
total.curtailedMwh += p.curtailedMwh;
|
|
232
|
+
total.curtailmentCostGbp += p.curtailmentCostGbp;
|
|
233
|
+
total.turnUpMwh += p.turnUpMwh;
|
|
234
|
+
total.turnUpCostGbp += p.turnUpCostGbp;
|
|
235
|
+
}
|
|
236
|
+
total.totalCostGbp = total.curtailmentCostGbp + total.turnUpCostGbp;
|
|
237
|
+
return {
|
|
238
|
+
settlementDate,
|
|
239
|
+
settlementPeriodFrom: range.from,
|
|
240
|
+
settlementPeriodTo: range.to,
|
|
241
|
+
periods,
|
|
242
|
+
total
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async function wastedWindForDay(client, settlementDate, opts = {}) {
|
|
246
|
+
const day = core.formatDate(settlementDate);
|
|
247
|
+
const last = core.settlementPeriodsInDay(day);
|
|
248
|
+
const from = opts.settlementPeriodFrom ?? 1;
|
|
249
|
+
const to = opts.settlementPeriodTo ?? last;
|
|
250
|
+
if (from < 1 || to > last || from > to) {
|
|
251
|
+
throw new core.UkEnergyError(`Settlement periods ${from}-${to} are outside 1-${last} for ${day}`);
|
|
252
|
+
}
|
|
253
|
+
const concurrency = Math.max(1, opts.concurrency ?? 10);
|
|
254
|
+
const units = await client.reference.bmUnits();
|
|
255
|
+
const jobs = [];
|
|
256
|
+
for (let sp = from; sp <= to; sp++) {
|
|
257
|
+
jobs.push(() => client.balancing.settlementStack("bid", day, sp));
|
|
258
|
+
jobs.push(() => client.balancing.settlementStack("offer", day, sp));
|
|
259
|
+
}
|
|
260
|
+
const results = new Array(jobs.length);
|
|
261
|
+
let next = 0;
|
|
262
|
+
let failed = false;
|
|
263
|
+
const worker = async () => {
|
|
264
|
+
while (next < jobs.length && !failed) {
|
|
265
|
+
const i = next++;
|
|
266
|
+
const job = jobs[i];
|
|
267
|
+
if (!job) continue;
|
|
268
|
+
try {
|
|
269
|
+
results[i] = await job();
|
|
270
|
+
} catch (err) {
|
|
271
|
+
failed = true;
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, jobs.length) }, worker));
|
|
277
|
+
const bids = [];
|
|
278
|
+
const offers = [];
|
|
279
|
+
results.forEach((rows, i) => {
|
|
280
|
+
(i % 2 === 0 ? bids : offers).push(...rows ?? []);
|
|
281
|
+
});
|
|
282
|
+
return aggregateWastedWind(
|
|
283
|
+
day,
|
|
284
|
+
{ from, to },
|
|
285
|
+
units,
|
|
286
|
+
bids,
|
|
287
|
+
offers,
|
|
288
|
+
opts.fuelType ?? DEFAULT_FUEL_TYPES2
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/client.ts
|
|
293
|
+
var BASE_URL = "https://data.elexon.co.uk/bmrs/api/v1";
|
|
294
|
+
var DEFAULT_RATE_LIMIT = { requestsPerSecond: 10, burst: 20 };
|
|
295
|
+
var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
296
|
+
function unitId(u) {
|
|
297
|
+
return u.elexonBmUnit || u.nationalGridBmUnit || "";
|
|
298
|
+
}
|
|
299
|
+
var MAX_UNITS_PER_REQUEST = 200;
|
|
300
|
+
function chunk(items, size) {
|
|
301
|
+
const out = [];
|
|
302
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
function windowQuery(w) {
|
|
306
|
+
const q = { from: core.formatDateTime(w.from), to: core.formatDateTime(w.to) };
|
|
307
|
+
if (w.settlementPeriodFrom !== void 0) {
|
|
308
|
+
core.assertSettlementPeriod(w.settlementPeriodFrom);
|
|
309
|
+
q.settlementPeriodFrom = w.settlementPeriodFrom;
|
|
310
|
+
}
|
|
311
|
+
if (w.settlementPeriodTo !== void 0) {
|
|
312
|
+
core.assertSettlementPeriod(w.settlementPeriodTo);
|
|
313
|
+
q.settlementPeriodTo = w.settlementPeriodTo;
|
|
314
|
+
}
|
|
315
|
+
return q;
|
|
316
|
+
}
|
|
317
|
+
function periodQuery(settlementDate, settlementPeriod) {
|
|
318
|
+
core.assertSettlementPeriod(settlementPeriod);
|
|
319
|
+
return { settlementDate: core.formatDate(settlementDate), settlementPeriod };
|
|
320
|
+
}
|
|
321
|
+
var ElexonClient = class {
|
|
322
|
+
http;
|
|
323
|
+
constructor(opts = {}) {
|
|
324
|
+
this.http = new core.HttpClient({
|
|
325
|
+
baseUrl: BASE_URL,
|
|
326
|
+
rateLimit: DEFAULT_RATE_LIMIT,
|
|
327
|
+
cacheTtlMs: opts.cache ? DEFAULT_CACHE_TTL_MS : 0,
|
|
328
|
+
...opts
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/** Endpoints returning a bare array. */
|
|
332
|
+
async list(path, query) {
|
|
333
|
+
const res = await this.http.get(path, query);
|
|
334
|
+
if (!Array.isArray(res)) throw new core.UkEnergyError(`Unexpected response shape for ${path}`);
|
|
335
|
+
return res;
|
|
336
|
+
}
|
|
337
|
+
/** Endpoints returning `{ data: [...] }`. */
|
|
338
|
+
async data(path, query) {
|
|
339
|
+
const res = await this.http.get(path, query);
|
|
340
|
+
if (!res || !Array.isArray(res.data)) {
|
|
341
|
+
throw new core.UkEnergyError(`Unexpected response shape for ${path}`);
|
|
342
|
+
}
|
|
343
|
+
return res.data;
|
|
344
|
+
}
|
|
345
|
+
/** `/datasets/{code}` — raw datasets. The `/stream` variants are used: lighter, no envelope. */
|
|
346
|
+
datasets = {
|
|
347
|
+
/**
|
|
348
|
+
* Any stream dataset (77 codes: BOALF, BOD, FUELINST, PN, MELS, WINDFOR, FREQ, …), with the
|
|
349
|
+
* query parameters typed from the spec. Values are sent as given (strings in API format,
|
|
350
|
+
* "YYYY-MM-DDTHH:MMZ" or "YYYY-MM-DD"); the named methods below format and validate dates.
|
|
351
|
+
*/
|
|
352
|
+
stream: async (dataset, query) => this.list(`datasets/${dataset}/stream`, query),
|
|
353
|
+
/** Bid-offer acceptance levels (BOALF). Curtailment shows as `soFlag: true` bids. */
|
|
354
|
+
boalf: async (w) => this.list("datasets/BOALF/stream", { ...windowQuery(w), bmUnit: w.bmUnit }),
|
|
355
|
+
/** Bid-offer data (BOD): submitted bid and offer prices per pair. */
|
|
356
|
+
bod: async (w) => this.list("datasets/BOD/stream", { ...windowQuery(w), bmUnit: w.bmUnit }),
|
|
357
|
+
/** Generation by fuel type at 5-minute resolution (FUELINST). */
|
|
358
|
+
fuelinst: async (q) => {
|
|
359
|
+
const query = { fuelType: q.fuelType };
|
|
360
|
+
if ("publishDateTimeFrom" in q) {
|
|
361
|
+
query.publishDateTimeFrom = core.formatDateTime(q.publishDateTimeFrom);
|
|
362
|
+
query.publishDateTimeTo = core.formatDateTime(q.publishDateTimeTo);
|
|
363
|
+
} else {
|
|
364
|
+
query.settlementDateFrom = core.formatDate(q.settlementDateFrom);
|
|
365
|
+
query.settlementDateTo = core.formatDate(q.settlementDateTo);
|
|
366
|
+
if (q.settlementPeriod) {
|
|
367
|
+
for (const p of q.settlementPeriod) core.assertSettlementPeriod(p);
|
|
368
|
+
query.settlementPeriod = q.settlementPeriod;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return this.list("datasets/FUELINST/stream", query);
|
|
372
|
+
},
|
|
373
|
+
/** Physical notifications for one settlement period (`/datasets/PN` takes exactly one). */
|
|
374
|
+
pn: async (settlementDate, settlementPeriod, filter = {}) => this.data("datasets/PN", {
|
|
375
|
+
...periodQuery(settlementDate, settlementPeriod),
|
|
376
|
+
bmUnit: filter.bmUnit
|
|
377
|
+
}),
|
|
378
|
+
/** Physical notifications over a window (`/datasets/PN/stream`). */
|
|
379
|
+
pnStream: async (w) => this.list("datasets/PN/stream", { ...windowQuery(w), bmUnit: w.bmUnit }),
|
|
380
|
+
/** Maximum export limits (MELS). */
|
|
381
|
+
mels: async (w) => this.list("datasets/MELS/stream", { ...windowQuery(w), bmUnit: w.bmUnit }),
|
|
382
|
+
/** Minimum import limits (MILS). */
|
|
383
|
+
mils: async (w) => this.list("datasets/MILS/stream", { ...windowQuery(w), bmUnit: w.bmUnit })
|
|
384
|
+
};
|
|
385
|
+
/** Reference data. */
|
|
386
|
+
reference = {
|
|
387
|
+
/** All BM units (~3,100). Capacities are strings; `fuelType` is null for many non-generators. */
|
|
388
|
+
bmUnits: async () => this.list("reference/bmunits/all"),
|
|
389
|
+
/** Fuel type codes used across the API. */
|
|
390
|
+
fuelTypes: async () => this.list("reference/fueltypes/all"),
|
|
391
|
+
/** BM units matching a filter, e.g. `{ fuelType: ["WIND"] }` (287 units). */
|
|
392
|
+
units: async (filter = {}) => {
|
|
393
|
+
const party = filter.leadParty?.toLowerCase();
|
|
394
|
+
return (await this.reference.bmUnits()).filter(
|
|
395
|
+
(u) => (!filter.fuelType || u.fuelType != null && filter.fuelType.includes(u.fuelType)) && (!filter.gspGroupId || u.gspGroupId != null && filter.gspGroupId.includes(u.gspGroupId)) && (!party || (u.leadPartyName ?? "").toLowerCase().includes(party))
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
/** Market-wide balancing views (one settlement period per call) and settlement prices. */
|
|
400
|
+
balancing = {
|
|
401
|
+
/** System buy/sell prices for a settlement day, or one period of it (DISEBSP). */
|
|
402
|
+
systemPrices: async (settlementDate, settlementPeriod) => this.data(
|
|
403
|
+
`balancing/settlement/system-prices/${periodPath(settlementDate, settlementPeriod)}`
|
|
404
|
+
),
|
|
405
|
+
/** Market index (MID) prices and volumes. */
|
|
406
|
+
marketIndex: async (w) => this.data("balancing/pricing/market-index", {
|
|
407
|
+
...windowQuery(w),
|
|
408
|
+
dataProviders: w.dataProviders
|
|
409
|
+
}),
|
|
410
|
+
/** Every acceptance in one settlement period. */
|
|
411
|
+
acceptances: async (settlementDate, settlementPeriod, filter = {}) => this.data("balancing/acceptances/all", {
|
|
412
|
+
...periodQuery(settlementDate, settlementPeriod),
|
|
413
|
+
bmUnit: filter.bmUnit
|
|
414
|
+
}),
|
|
415
|
+
/** Every bid-offer pair in one settlement period. */
|
|
416
|
+
bidOffers: async (settlementDate, settlementPeriod, filter = {}) => this.data("balancing/bid-offer/all", {
|
|
417
|
+
...periodQuery(settlementDate, settlementPeriod),
|
|
418
|
+
bmUnit: filter.bmUnit
|
|
419
|
+
}),
|
|
420
|
+
/**
|
|
421
|
+
* Settled acceptance volumes (BOAV) for a settlement day, or one period of it: one row per
|
|
422
|
+
* acceptance per period, `totalVolumeAccepted` in MWh (negative for bids). Long unit lists
|
|
423
|
+
* are split across requests.
|
|
424
|
+
*/
|
|
425
|
+
acceptanceVolumes: async (bidOffer, settlementDate, filter = {}) => this.perUnitChunks(
|
|
426
|
+
`balancing/settlement/acceptance/volumes/all/${bidOffer}/${periodPath(settlementDate, filter.settlementPeriod)}`,
|
|
427
|
+
filter.bmUnit
|
|
428
|
+
),
|
|
429
|
+
/**
|
|
430
|
+
* Indicative bid/offer cashflows (EBOCF) for a settlement day or one period: one row per unit
|
|
431
|
+
* per period, `totalCashflow` in £ paid to the unit (positive for a wind bid at a negative
|
|
432
|
+
* price).
|
|
433
|
+
*/
|
|
434
|
+
cashflows: async (bidOffer, settlementDate, filter = {}) => this.perUnitChunks(
|
|
435
|
+
`balancing/settlement/indicative/cashflows/all/${bidOffer}/${periodPath(settlementDate, filter.settlementPeriod)}`,
|
|
436
|
+
filter.bmUnit
|
|
437
|
+
),
|
|
438
|
+
/**
|
|
439
|
+
* Settlement bid or offer stack for one period (ISPSTACK): every accepted action with
|
|
440
|
+
* `originalPrice`, `volume` (negative for bids), `soFlag`, `cadlFlag` and the sequence NESO
|
|
441
|
+
* used. This is what Octopus's Wasted Wind tracker is built on.
|
|
442
|
+
*/
|
|
443
|
+
settlementStack: async (bidOffer, settlementDate, settlementPeriod) => this.data(
|
|
444
|
+
`balancing/settlement/stack/all/${bidOffer}/${periodPath(settlementDate, settlementPeriod)}`
|
|
445
|
+
),
|
|
446
|
+
/** PN / QPN / MILS / MELS for every unit in one settlement period. */
|
|
447
|
+
physical: async (dataset, settlementDate, settlementPeriod, filter = {}) => this.data("balancing/physical/all", {
|
|
448
|
+
dataset,
|
|
449
|
+
...periodQuery(settlementDate, settlementPeriod),
|
|
450
|
+
bmUnit: filter.bmUnit
|
|
451
|
+
})
|
|
452
|
+
};
|
|
453
|
+
/** Wind curtailment: SO-flagged and energy bid acceptances per unit per period, with £. */
|
|
454
|
+
curtailment = {
|
|
455
|
+
/**
|
|
456
|
+
* Curtailment for a GB settlement day (Europe/London), per unit and per settlement period.
|
|
457
|
+
* Defaults to every WIND unit; the reference list plus three requests per 200 units.
|
|
458
|
+
*/
|
|
459
|
+
forDay: async (settlementDate, opts = {}) => curtailmentForDay(this, settlementDate, opts),
|
|
460
|
+
/**
|
|
461
|
+
* Octopus's "Wasted Wind" definition for a settlement day: SO-flagged wind bids from the
|
|
462
|
+
* settlement stack (MWh and £ to switch off) plus the cost of re-buying that energy from
|
|
463
|
+
* the cheapest offers in the same period (£ to replace). Two requests per settlement period.
|
|
464
|
+
*/
|
|
465
|
+
wastedWind: async (settlementDate, opts = {}) => wastedWindForDay(this, settlementDate, opts)
|
|
466
|
+
};
|
|
467
|
+
/** `{data}` endpoints with an optional unit filter, chunked so URLs stay short. */
|
|
468
|
+
async perUnitChunks(path, bmUnit) {
|
|
469
|
+
if (!bmUnit?.length) return this.data(path);
|
|
470
|
+
const parts = await Promise.all(
|
|
471
|
+
chunk(bmUnit, MAX_UNITS_PER_REQUEST).map((ids) => this.data(path, { bmUnit: ids }))
|
|
472
|
+
);
|
|
473
|
+
return parts.flat();
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
function periodPath(settlementDate, settlementPeriod) {
|
|
477
|
+
if (settlementPeriod !== void 0) core.assertSettlementPeriod(settlementPeriod);
|
|
478
|
+
const p = settlementPeriod === void 0 ? "" : `/${settlementPeriod}`;
|
|
479
|
+
return `${core.formatDate(settlementDate)}${p}`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
exports.BASE_URL = BASE_URL;
|
|
483
|
+
exports.DEFAULT_CACHE_TTL_MS = DEFAULT_CACHE_TTL_MS;
|
|
484
|
+
exports.DEFAULT_RATE_LIMIT = DEFAULT_RATE_LIMIT;
|
|
485
|
+
exports.ElexonClient = ElexonClient;
|
|
486
|
+
exports.MAX_UNITS_PER_REQUEST = MAX_UNITS_PER_REQUEST;
|
|
487
|
+
exports.aggregateCurtailment = aggregateCurtailment;
|
|
488
|
+
exports.aggregateWastedWind = aggregateWastedWind;
|
|
489
|
+
exports.curtailmentForDay = curtailmentForDay;
|
|
490
|
+
exports.unitId = unitId;
|
|
491
|
+
exports.wastedWindForDay = wastedWindForDay;
|
|
492
|
+
//# sourceMappingURL=index.cjs.map
|
|
493
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/curtailment.ts","../src/wasted-wind.ts","../src/client.ts"],"names":["formatDate","settlementPeriodsInDay","UkEnergyError","DEFAULT_FUEL_TYPES","formatDateTime","assertSettlementPeriod","HttpClient"],"mappings":";;;;;AAqGA,IAAM,kBAAA,GAAqB,CAAC,MAAM,CAAA;AAGlC,SAAS,UAAA,CAAW,GAAW,CAAA,EAAmB;AAChD,EAAA,OAAO,CAAA,GAAI,CAAA,GAAI,EAAA,GAAK,CAAA,GAAI,IAAI,CAAA,GAAI,CAAA;AAClC;AAEA,SAAS,IAAI,CAAA,EAA6C;AACxD,EAAA,IAAI,CAAA,IAAK,IAAA,IAAQ,CAAA,KAAM,EAAA,EAAI,OAAO,IAAA;AAClC,EAAA,MAAM,CAAA,GAAI,OAAO,CAAC,CAAA;AAClB,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA,GAAI,IAAA;AAClC;AAGO,SAAS,qBACd,cAAA,EACA,KAAA,EACA,KAAA,EACA,OAAA,EACA,WACA,KAAA,EACyC;AACzC,EAAA,MAAM,IAAA,uBAAW,GAAA,EAAoB;AACrC,EAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,IAAA,IAAI,EAAE,YAAA,EAAc,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,cAAc,CAAC,CAAA;AAC9C,IAAA,IAAI,EAAE,kBAAA,EAAoB,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,oBAAoB,CAAC,CAAA;AAAA,EAC5D;AACA,EAAA,MAAM,cAAA,uBAAqB,GAAA,EAAqB;AAChD,EAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,IAAA,IAAI,CAAA,CAAE,oBAAoB,IAAA,EAAM;AAC9B,MAAA,cAAA,CAAe,GAAA;AAAA,QACb,CAAA,CAAE,gBAAA;AAAA,QAAA,CACD,eAAe,GAAA,CAAI,CAAA,CAAE,gBAAgB,CAAA,IAAK,KAAA,KAAU,EAAE,MAAA,KAAW;AAAA,OACpE;AAAA,IACF;AAAA,EACF;AACA,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAoB;AAC1C,EAAA,KAAA,MAAW,KAAK,SAAA,EAAW;AACzB,IAAA,MAAM,KAAK,CAAA,CAAE,gBAAA;AACb,IAAA,IAAI,CAAC,CAAA,CAAE,MAAA,IAAU,EAAA,KAAO,MAAA,IAAa,KAAK,KAAA,CAAM,IAAA,IAAQ,EAAA,GAAK,KAAA,CAAM,EAAA,EAAI;AACvE,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,CAAA,CAAE,MAAM,IAAI,EAAE,CAAA,CAAA;AAC7B,IAAA,SAAA,CAAU,GAAA,CAAI,MAAM,SAAA,CAAU,GAAA,CAAI,GAAG,CAAA,IAAK,CAAA,KAAM,CAAA,CAAE,aAAA,IAAiB,CAAA,CAAE,CAAA;AAAA,EACvE;AAEA,EAAA,MAAM,OAAA,uBAAc,GAAA,EAA+B;AACnD,EAAA,MAAM,IAAA,uBAAW,GAAA,EAAyB;AAC1C,EAAA,KAAA,MAAW,KAAK,OAAA,EAAS;AACvB,IAAA,MAAM,KAAK,CAAA,CAAE,gBAAA;AACb,IAAA,MAAM,aAAa,CAAA,CAAE,YAAA;AACrB,IAAA,IAAI,CAAC,CAAA,CAAE,MAAA,IAAU,EAAA,KAAO,MAAA,IAAa,eAAe,MAAA,EAAW;AAC/D,IAAA,IAAI,EAAA,GAAK,KAAA,CAAM,IAAA,IAAQ,EAAA,GAAK,MAAM,EAAA,EAAI;AACtC,IAAA,MAAM,GAAA,GAAM,EAAE,CAAA,CAAE,mBAAA,IAAuB,CAAA,CAAA;AACvC,IAAA,IAAI,EAAE,MAAM,CAAA,CAAA,EAAI;AAChB,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,CAAA,CAAE,MAAM,IAAI,EAAE,CAAA,CAAA;AAC7B,IAAA,IAAI,CAAA,GAAI,OAAA,CAAQ,GAAA,CAAI,GAAG,CAAA;AACvB,IAAA,IAAI,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA;AACtB,IAAA,IAAI,CAAC,CAAA,IAAK,CAAC,GAAA,EAAK;AACd,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,MAAM,CAAA;AAC7B,MAAA,CAAA,GAAI;AAAA,QACF,cAAA,EAAgB,EAAE,cAAA,IAAkB,cAAA;AAAA,QACpC,gBAAA,EAAkB,EAAA;AAAA,QAClB,SAAA,EAAW,EAAE,SAAA,IAAa,EAAA;AAAA,QAC1B,QAAQ,CAAA,CAAE,MAAA;AAAA,QACV,kBAAA,EAAoB,CAAA,CAAE,kBAAA,IAAsB,GAAA,EAAK,kBAAA,IAAsB,IAAA;AAAA,QACvE,aAAA,EAAe,CAAA,CAAE,aAAA,IAAiB,GAAA,EAAK,aAAA,IAAiB,IAAA;AAAA,QACxD,QAAA,EAAU,KAAK,QAAA,IAAY,IAAA;AAAA,QAC3B,UAAA,EAAY,KAAK,UAAA,IAAc,IAAA;AAAA,QAC/B,UAAA,EAAY,GAAA,CAAI,GAAA,EAAK,kBAAkB,CAAA;AAAA,QACvC,YAAA,EAAc,CAAA;AAAA,QACd,YAAA,EAAc,CAAA;AAAA,QACd,WAAA,EAAa,SAAA,CAAU,GAAA,CAAI,GAAG,CAAA,IAAK,IAAA;AAAA,QACnC,WAAA,EAAa,CAAA;AAAA,QACb,oBAAA,EAAsB;AAAA,OACxB;AACA,MAAA,GAAA,uBAAU,GAAA,EAAI;AACd,MAAA,OAAA,CAAQ,GAAA,CAAI,KAAK,CAAC,CAAA;AAClB,MAAA,IAAA,CAAK,GAAA,CAAI,KAAK,GAAG,CAAA;AAAA,IACnB;AACA,IAAA,CAAA,CAAE,YAAA,IAAgB,GAAA;AAClB,IAAA,MAAM,EAAA,GAAK,cAAA,CAAe,GAAA,CAAI,UAAU,CAAA,KAAM,IAAA;AAC9C,IAAA,IAAI,EAAA,IAAM,YAAA,IAAgB,GAAA;AAC1B,IAAA,IAAI,CAAC,GAAA,CAAI,GAAA,CAAI,UAAU,CAAA,EAAG;AACxB,MAAA,GAAA,CAAI,IAAI,UAAU,CAAA;AAClB,MAAA,CAAA,CAAE,WAAA,IAAe,CAAA;AACjB,MAAA,IAAI,EAAA,IAAM,oBAAA,IAAwB,CAAA;AAAA,IACpC;AAAA,EACF;AAEA,EAAA,MAAM,OAAO,CAAC,GAAG,OAAA,CAAQ,MAAA,EAAQ,CAAA,CAAE,IAAA;AAAA,IACjC,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,gBAAA,GAAmB,CAAA,CAAE,gBAAA,IAAoB,UAAA,CAAW,CAAA,CAAE,MAAA,EAAQ,CAAA,CAAE,MAAM;AAAA,GACpF;AAEA,EAAA,MAAM,UAAA,uBAAiB,GAAA,EAA6B;AACpD,EAAA,MAAM,eAAA,uBAAsB,GAAA,EAAyB;AACrD,EAAA,MAAM,cAAA,uBAAqB,GAAA,EAAY;AACvC,EAAA,MAAM,KAAA,GAAQ,EAAE,YAAA,EAAc,CAAA,EAAG,YAAA,EAAc,CAAA,EAAG,WAAA,EAAa,CAAA,EAAG,KAAA,EAAO,CAAA,EAAG,WAAA,EAAa,CAAA,EAAE;AAC3F,EAAA,KAAA,MAAW,KAAK,IAAA,EAAM;AACpB,IAAA,IAAI,CAAA,GAAI,UAAA,CAAW,GAAA,CAAI,CAAA,CAAE,MAAM,CAAA;AAC/B,IAAA,IAAI,CAAC,CAAA,EAAG;AACN,MAAA,CAAA,GAAI;AAAA,QACF,QAAQ,CAAA,CAAE,MAAA;AAAA,QACV,oBAAoB,CAAA,CAAE,kBAAA;AAAA,QACtB,eAAe,CAAA,CAAE,aAAA;AAAA,QACjB,UAAU,CAAA,CAAE,QAAA;AAAA,QACZ,YAAY,CAAA,CAAE,UAAA;AAAA,QACd,YAAY,CAAA,CAAE,UAAA;AAAA,QACd,YAAA,EAAc,CAAA;AAAA,QACd,YAAA,EAAc,CAAA;AAAA,QACd,WAAA,EAAa,CAAA;AAAA,QACb,OAAA,EAAS,CAAA;AAAA,QACT,WAAA,EAAa;AAAA,OACf;AACA,MAAA,UAAA,CAAW,GAAA,CAAI,CAAA,CAAE,MAAA,EAAQ,CAAC,CAAA;AAAA,IAC5B;AACA,IAAA,CAAA,CAAE,gBAAgB,CAAA,CAAE,YAAA;AACpB,IAAA,CAAA,CAAE,gBAAgB,CAAA,CAAE,YAAA;AACpB,IAAA,CAAA,CAAE,WAAA,IAAe,EAAE,WAAA,IAAe,CAAA;AAClC,IAAA,CAAA,CAAE,OAAA,IAAW,CAAA;AACb,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAA,CAAE,MAAM,CAAA,CAAA,EAAI,CAAA,CAAE,gBAAgB,CAAA,CAAE,CAAA,oBAAK,IAAI,GAAA,EAAY;AAC7E,IAAA,IAAI,GAAA,GAAM,eAAA,CAAgB,GAAA,CAAI,CAAA,CAAE,MAAM,CAAA;AACtC,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,GAAA,uBAAU,GAAA,EAAI;AACd,MAAA,eAAA,CAAgB,GAAA,CAAI,CAAA,CAAE,MAAA,EAAQ,GAAG,CAAA;AAAA,IACnC;AACA,IAAA,KAAA,MAAW,MAAM,GAAA,EAAK;AACpB,MAAA,GAAA,CAAI,IAAI,EAAE,CAAA;AACV,MAAA,cAAA,CAAe,IAAI,EAAE,CAAA;AAAA,IACvB;AACA,IAAA,CAAA,CAAE,cAAc,GAAA,CAAI,IAAA;AACpB,IAAA,KAAA,CAAM,gBAAgB,CAAA,CAAE,YAAA;AACxB,IAAA,KAAA,CAAM,gBAAgB,CAAA,CAAE,YAAA;AACxB,IAAA,KAAA,CAAM,WAAA,IAAe,EAAE,WAAA,IAAe,CAAA;AAAA,EACxC;AACA,EAAA,KAAA,CAAM,QAAQ,UAAA,CAAW,IAAA;AACzB,EAAA,KAAA,CAAM,cAAc,cAAA,CAAe,IAAA;AAEnC,EAAA,OAAO;AAAA,IACL,cAAA;AAAA,IACA,sBAAsB,KAAA,CAAM,IAAA;AAAA,IAC5B,oBAAoB,KAAA,CAAM,EAAA;AAAA,IAC1B,OAAA,EAAS,IAAA;AAAA,IACT,KAAA,EAAO,CAAC,GAAG,UAAA,CAAW,QAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,YAAA,GAAe,EAAE,YAAY,CAAA;AAAA,IAC9E;AAAA,GACF;AACF;AAEA,eAAsB,iBAAA,CACpB,MAAA,EACA,cAAA,EACA,IAAA,GAA2B,EAAC,EACA;AAC5B,EAAA,MAAM,GAAA,GAAMA,gBAAW,cAAc,CAAA;AACrC,EAAA,MAAM,IAAA,GAAOC,4BAAuB,GAAG,CAAA;AACvC,EAAA,MAAM,IAAA,GAAO,KAAK,oBAAA,IAAwB,CAAA;AAC1C,EAAA,MAAM,EAAA,GAAK,KAAK,kBAAA,IAAsB,IAAA;AACtC,EAAA,IAAI,IAAA,GAAO,CAAA,IAAK,EAAA,GAAK,IAAA,IAAQ,OAAO,EAAA,EAAI;AACtC,IAAA,MAAM,IAAIC,kBAAA,CAAc,CAAA,mBAAA,EAAsB,IAAI,CAAA,CAAA,EAAI,EAAE,CAAA,eAAA,EAAkB,IAAI,CAAA,KAAA,EAAQ,GAAG,CAAA,CAAE,CAAA;AAAA,EAC7F;AAEA,EAAA,MAAM,KAAA,GAAQ,MAAM,MAAA,CAAO,SAAA,CAAU,OAAA,EAAQ;AAC7C,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI,KAAK,MAAA,EAAQ;AACf,IAAA,GAAA,GAAM,IAAA,CAAK,MAAA;AAAA,EACb,CAAA,MAAO;AACL,IAAA,MAAM,IAAA,GAAO,KAAK,QAAA,IAAY,kBAAA;AAC9B,IAAA,GAAA,GAAM,KAAA,CACH,OAAO,CAAC,CAAA,KAAM,EAAE,QAAA,IAAY,IAAA,IAAQ,KAAK,QAAA,CAAS,CAAA,CAAE,QAAQ,CAAC,CAAA,CAC7D,IAAI,MAAM,CAAA,CACV,OAAO,CAAC,EAAA,KAAO,OAAO,EAAE,CAAA;AAAA,EAC7B;AACA,EAAA,GAAA,GAAM,CAAC,GAAG,IAAI,GAAA,CAAI,GAAG,CAAC,CAAA;AACtB,EAAA,IAAI,IAAI,MAAA,KAAW,CAAA,EAAG,MAAM,IAAIA,mBAAc,sBAAsB,CAAA;AAIpE,EAAA,MAAM,CAAC,OAAA,EAAS,SAAA,EAAW,UAAU,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,IACzD,MAAA,CAAO,UAAU,iBAAA,CAAkB,KAAA,EAAO,KAAK,EAAE,MAAA,EAAQ,KAAK,CAAA;AAAA,IAC9D,MAAA,CAAO,UAAU,SAAA,CAAU,KAAA,EAAO,KAAK,EAAE,MAAA,EAAQ,KAAK,CAAA;AAAA,IACtD,OAAA,CAAQ,GAAA;AAAA,MACN,KAAA,CAAM,GAAA,EAAK,qBAAqB,CAAA,CAAE,GAAA;AAAA,QAAI,CAAC,MAAA,KACrC,MAAA,CAAO,QAAA,CAAS,KAAA,CAAM;AAAA,UACpB,IAAA,EAAM,GAAA;AAAA,UACN,EAAA,EAAI,GAAA;AAAA,UACJ,oBAAA,EAAsB,CAAA;AAAA,UACtB,kBAAA,EAAoB,IAAA;AAAA,UACpB;AAAA,SACD;AAAA;AACH;AACF,GACD,CAAA;AACD,EAAA,MAAM,KAAA,GAAQ,WAAW,IAAA,EAAK;AAE9B,EAAA,OAAO;AAAA,IACL,cAAc,GAAA,CAAI,MAAA;AAAA,IAClB,GAAG,oBAAA,CAAqB,GAAA,EAAK,EAAE,IAAA,EAAM,IAAG,EAAG,KAAA,EAAO,OAAA,EAAS,SAAA,EAAW,KAAK;AAAA,GAC7E;AACF;ACrOA,IAAMC,mBAAAA,GAAqB,CAAC,MAAM,CAAA;AAG3B,SAAS,oBACd,cAAA,EACA,KAAA,EACA,OACA,IAAA,EACA,MAAA,EACA,YAA+BA,mBAAAA,EACb;AAClB,EAAA,MAAM,QAAA,uBAAe,GAAA,EAAuC;AAC5D,EAAA,KAAA,MAAW,CAAA,IAAK,KAAA,EAAO,IAAI,CAAA,CAAE,YAAA,WAAuB,GAAA,CAAI,CAAA,CAAE,YAAA,EAAc,CAAA,CAAE,QAAQ,CAAA;AAElF,EAAA,MAAM,UAA8B,EAAC;AACrC,EAAA,KAAA,IAAS,KAAK,KAAA,CAAM,IAAA,EAAM,EAAA,IAAM,KAAA,CAAM,IAAI,EAAA,EAAA,EAAM;AAC9C,IAAA,MAAM,WAAW,IAAA,CAAK,MAAA;AAAA,MACpB,CAAC,CAAA,KACC,CAAA,CAAE,qBAAqB,EAAA,IACvB,CAAA,CAAE,WAAW,IAAA,IACb,CAAA,CAAE,EAAA,IAAM,IAAA,IACR,UAAU,QAAA,CAAS,QAAA,CAAS,IAAI,CAAA,CAAE,EAAE,KAAK,EAAE;AAAA,KAC/C;AACA,IAAA,IAAI,MAAA,GAAS,CAAA;AACb,IAAA,IAAI,IAAA,GAAO,CAAA;AACX,IAAA,KAAA,MAAW,KAAK,QAAA,EAAU;AACxB,MAAA,MAAM,CAAA,GAAI,EAAE,MAAA,IAAU,CAAA;AACtB,MAAA,MAAA,IAAU,CAAA;AACV,MAAA,IAAA,IAAA,CAAS,CAAA,CAAE,iBAAiB,CAAA,IAAK,CAAA;AAAA,IACnC;AACA,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA;AAE5B,IAAA,MAAM,KAAA,GAAQ,MAAA,CACX,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,gBAAA,KAAqB,EAAA,IAAM,CAAC,CAAA,CAAE,QAAQ,CAAA,CACtD,IAAA;AAAA,MACC,CAAC,CAAA,EAAG,CAAA,KAAA,CACD,CAAA,CAAE,SAAS,CAAA,GAAI,CAAA,KAAM,CAAA,CAAE,MAAA,GAAS,IAAI,CAAA,CAAA,IAAA,CACpC,CAAA,CAAE,cAAA,IAAkB,CAAA,KAAM,EAAE,cAAA,IAAkB,CAAA;AAAA,KACnD;AACF,IAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,IAAA,IAAI,UAAA,GAAa,CAAA;AACjB,IAAA,IAAI,IAAA,GAAO,CAAA;AACX,IAAA,KAAA,MAAW,CAAA,IAAK,IAAA,GAAO,CAAA,GAAI,KAAA,GAAQ,EAAC,EAAG;AACrC,MAAA,IAAA,IAAQ,CAAA;AACR,MAAA,MAAM,CAAA,GAAI,EAAE,MAAA,IAAU,CAAA;AACtB,MAAA,IAAI,KAAA,GAAQ,IAAI,IAAA,EAAM;AACpB,QAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,GAAA,CAAA,CAAK,OAAO,KAAA,IAAS,CAAA,EAAG,CAAC,CAAA,EAAG,CAAC,CAAA;AAC5D,QAAA,KAAA,IAAS,CAAA,GAAI,QAAA;AACb,QAAA,UAAA,IAAA,CAAe,CAAA,CAAE,aAAA,IAAiB,CAAA,KAAM,CAAA,GAAI,QAAA,CAAA;AAAA,MAC9C,CAAA,MAAO;AACL,QAAA,KAAA,IAAS,CAAA;AACT,QAAA,UAAA,IAAA,CAAe,CAAA,CAAE,iBAAiB,CAAA,IAAK,CAAA;AAAA,MACzC;AACA,MAAA,IAAI,SAAS,IAAA,EAAM;AAAA,IACrB;AAEA,IAAA,MAAM,KAAA,GACJ,SAAS,CAAC,CAAA,IACV,MAAM,CAAC,CAAA,IACP,KAAK,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,gBAAA,KAAqB,EAAE,CAAA,IAC1C,MAAA,CAAO,KAAK,CAAC,CAAA,KAAM,CAAA,CAAE,gBAAA,KAAqB,EAAE,CAAA;AAC9C,IAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,MACX,cAAA;AAAA,MACA,gBAAA,EAAkB,EAAA;AAAA,MAClB,SAAA,EAAW,OAAO,SAAA,IAAa,EAAA;AAAA,MAC/B,YAAA,EAAc,IAAA;AAAA,MACd,kBAAA,EAAoB,IAAA;AAAA,MACpB,SAAA,EAAW,KAAA;AAAA,MACX,aAAA,EAAe,UAAA;AAAA,MACf,MAAM,QAAA,CAAS,MAAA;AAAA,MACf,MAAA,EAAQ;AAAA,KACT,CAAA;AAAA,EACH;AAEA,EAAA,MAAM,KAAA,GAAQ;AAAA,IACZ,YAAA,EAAc,CAAA;AAAA,IACd,kBAAA,EAAoB,CAAA;AAAA,IACpB,SAAA,EAAW,CAAA;AAAA,IACX,aAAA,EAAe,CAAA;AAAA,IACf,YAAA,EAAc;AAAA,GAChB;AACA,EAAA,KAAA,MAAW,KAAK,OAAA,EAAS;AACvB,IAAA,KAAA,CAAM,gBAAgB,CAAA,CAAE,YAAA;AACxB,IAAA,KAAA,CAAM,sBAAsB,CAAA,CAAE,kBAAA;AAC9B,IAAA,KAAA,CAAM,aAAa,CAAA,CAAE,SAAA;AACrB,IAAA,KAAA,CAAM,iBAAiB,CAAA,CAAE,aAAA;AAAA,EAC3B;AACA,EAAA,KAAA,CAAM,YAAA,GAAe,KAAA,CAAM,kBAAA,GAAqB,KAAA,CAAM,aAAA;AAEtD,EAAA,OAAO;AAAA,IACL,cAAA;AAAA,IACA,sBAAsB,KAAA,CAAM,IAAA;AAAA,IAC5B,oBAAoB,KAAA,CAAM,EAAA;AAAA,IAC1B,OAAA;AAAA,IACA;AAAA,GACF;AACF;AAEA,eAAsB,gBAAA,CACpB,MAAA,EACA,cAAA,EACA,IAAA,GAA0B,EAAC,EACA;AAC3B,EAAA,MAAM,GAAA,GAAMH,gBAAW,cAAc,CAAA;AACrC,EAAA,MAAM,IAAA,GAAOC,4BAAuB,GAAG,CAAA;AACvC,EAAA,MAAM,IAAA,GAAO,KAAK,oBAAA,IAAwB,CAAA;AAC1C,EAAA,MAAM,EAAA,GAAK,KAAK,kBAAA,IAAsB,IAAA;AACtC,EAAA,IAAI,IAAA,GAAO,CAAA,IAAK,EAAA,GAAK,IAAA,IAAQ,OAAO,EAAA,EAAI;AACtC,IAAA,MAAM,IAAIC,kBAAAA,CAAc,CAAA,mBAAA,EAAsB,IAAI,CAAA,CAAA,EAAI,EAAE,CAAA,eAAA,EAAkB,IAAI,CAAA,KAAA,EAAQ,GAAG,CAAA,CAAE,CAAA;AAAA,EAC7F;AACA,EAAA,MAAM,cAAc,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,eAAe,EAAE,CAAA;AAEtD,EAAA,MAAM,KAAA,GAAQ,MAAM,MAAA,CAAO,SAAA,CAAU,OAAA,EAAQ;AAC7C,EAAA,MAAM,OAAgD,EAAC;AACvD,EAAA,KAAA,IAAS,EAAA,GAAK,IAAA,EAAM,EAAA,IAAM,EAAA,EAAI,EAAA,EAAA,EAAM;AAClC,IAAA,IAAA,CAAK,IAAA,CAAK,MAAM,MAAA,CAAO,SAAA,CAAU,gBAAgB,KAAA,EAAO,GAAA,EAAK,EAAE,CAAC,CAAA;AAChE,IAAA,IAAA,CAAK,IAAA,CAAK,MAAM,MAAA,CAAO,SAAA,CAAU,gBAAgB,OAAA,EAAS,GAAA,EAAK,EAAE,CAAC,CAAA;AAAA,EACpE;AACA,EAAA,MAAM,OAAA,GAAkC,IAAI,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA;AAC7D,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,IAAI,MAAA,GAAS,KAAA;AACb,EAAA,MAAM,SAAS,YAAY;AACzB,IAAA,OAAO,IAAA,GAAO,IAAA,CAAK,MAAA,IAAU,CAAC,MAAA,EAAQ;AACpC,MAAA,MAAM,CAAA,GAAI,IAAA,EAAA;AACV,MAAA,MAAM,GAAA,GAAM,KAAK,CAAC,CAAA;AAClB,MAAA,IAAI,CAAC,GAAA,EAAK;AACV,MAAA,IAAI;AACF,QAAA,OAAA,CAAQ,CAAC,CAAA,GAAI,MAAM,GAAA,EAAI;AAAA,MACzB,SAAS,GAAA,EAAK;AACZ,QAAA,MAAA,GAAS,IAAA;AACT,QAAA,MAAM,GAAA;AAAA,MACR;AAAA,IACF;AAAA,EACF,CAAA;AACA,EAAA,MAAM,OAAA,CAAQ,GAAA,CAAI,KAAA,CAAM,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,WAAA,EAAa,IAAA,CAAK,MAAM,CAAA,EAAE,EAAG,MAAM,CAAC,CAAA;AAEpF,EAAA,MAAM,OAA6B,EAAC;AACpC,EAAA,MAAM,SAA+B,EAAC;AACtC,EAAA,OAAA,CAAQ,OAAA,CAAQ,CAAC,IAAA,EAAM,CAAA,KAAM;AAC3B,IAAA,CAAC,CAAA,GAAI,MAAM,CAAA,GAAI,IAAA,GAAO,QAAQ,IAAA,CAAK,GAAI,IAAA,IAAQ,EAAG,CAAA;AAAA,EACpD,CAAC,CAAA;AAED,EAAA,OAAO,mBAAA;AAAA,IACL,GAAA;AAAA,IACA,EAAE,MAAM,EAAA,EAAG;AAAA,IACX,KAAA;AAAA,IACA,IAAA;AAAA,IACA,MAAA;AAAA,IACA,KAAK,QAAA,IAAYC;AAAA,GACnB;AACF;;;ACnLO,IAAM,QAAA,GAAW;AAOjB,IAAM,kBAAA,GAAgC,EAAE,iBAAA,EAAmB,EAAA,EAAI,OAAO,EAAA;AAGtE,IAAM,oBAAA,GAAuB,IAAI,EAAA,GAAK;AAiCtC,SAAS,OAAO,CAAA,EAAmB;AACxC,EAAA,OAAO,CAAA,CAAE,YAAA,IAAgB,CAAA,CAAE,kBAAA,IAAsB,EAAA;AACnD;AAGO,IAAM,qBAAA,GAAwB;AAE9B,SAAS,KAAA,CAAS,OAAqB,IAAA,EAAqB;AACjE,EAAA,MAAM,MAAa,EAAC;AACpB,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,IAAK,IAAA,EAAM,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,IAAI,CAAC,CAAA;AAC9E,EAAA,OAAO,GAAA;AACT;AAYA,SAAS,YAAY,CAAA,EAA4B;AAC/C,EAAA,MAAM,CAAA,GAAW,EAAE,IAAA,EAAMC,mBAAA,CAAe,CAAA,CAAE,IAAI,CAAA,EAAG,EAAA,EAAIA,mBAAA,CAAe,CAAA,CAAE,EAAE,CAAA,EAAE;AAC1E,EAAA,IAAI,CAAA,CAAE,yBAAyB,MAAA,EAAW;AACxC,IAAAC,2BAAA,CAAuB,EAAE,oBAAoB,CAAA;AAC7C,IAAA,CAAA,CAAE,uBAAuB,CAAA,CAAE,oBAAA;AAAA,EAC7B;AACA,EAAA,IAAI,CAAA,CAAE,uBAAuB,MAAA,EAAW;AACtC,IAAAA,2BAAA,CAAuB,EAAE,kBAAkB,CAAA;AAC3C,IAAA,CAAA,CAAE,qBAAqB,CAAA,CAAE,kBAAA;AAAA,EAC3B;AACA,EAAA,OAAO,CAAA;AACT;AAEA,SAAS,WAAA,CAAY,gBAA2B,gBAAA,EAAiC;AAC/E,EAAAA,2BAAA,CAAuB,gBAAgB,CAAA;AACvC,EAAA,OAAO,EAAE,cAAA,EAAgBL,eAAAA,CAAW,cAAc,GAAG,gBAAA,EAAiB;AACxE;AAYO,IAAM,eAAN,MAAmB;AAAA,EACf,IAAA;AAAA,EAET,WAAA,CAAY,IAAA,GAA4B,EAAC,EAAG;AAC1C,IAAA,IAAA,CAAK,IAAA,GAAO,IAAIM,eAAA,CAAW;AAAA,MACzB,OAAA,EAAS,QAAA;AAAA,MACT,SAAA,EAAW,kBAAA;AAAA,MACX,UAAA,EAAY,IAAA,CAAK,KAAA,GAAQ,oBAAA,GAAuB,CAAA;AAAA,MAChD,GAAG;AAAA,KACJ,CAAA;AAAA,EACH;AAAA;AAAA,EAGA,MAAc,IAAA,CAAQ,IAAA,EAAc,KAAA,EAA6B;AAC/D,IAAA,MAAM,MAAM,MAAM,IAAA,CAAK,IAAA,CAAK,GAAA,CAAS,MAAM,KAAK,CAAA;AAChD,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA,QAAS,IAAIJ,kBAAAA,CAAc,CAAA,8BAAA,EAAiC,IAAI,CAAA,CAAE,CAAA;AACxF,IAAA,OAAO,GAAA;AAAA,EACT;AAAA;AAAA,EAGA,MAAc,IAAA,CAAQ,IAAA,EAAc,KAAA,EAA6B;AAC/D,IAAA,MAAM,MAAM,MAAM,IAAA,CAAK,IAAA,CAAK,GAAA,CAAmB,MAAM,KAAK,CAAA;AAC1D,IAAA,IAAI,CAAC,GAAA,IAAO,CAAC,MAAM,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAA,EAAG;AACpC,MAAA,MAAM,IAAIA,kBAAAA,CAAc,CAAA,8BAAA,EAAiC,IAAI,CAAA,CAAE,CAAA;AAAA,IACjE;AACA,IAAA,OAAO,GAAA,CAAI,IAAA;AAAA,EACb;AAAA;AAAA,EAGS,QAAA,GAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMlB,MAAA,EAAQ,OACN,OAAA,EACA,KAAA,KAEA,KAAK,IAAA,CAAoB,CAAA,SAAA,EAAY,OAAO,CAAA,OAAA,CAAA,EAAW,KAAyB,CAAA;AAAA;AAAA,IAGlF,KAAA,EAAO,OAAO,CAAA,KACZ,IAAA,CAAK,KAAK,uBAAA,EAAyB,EAAE,GAAG,WAAA,CAAY,CAAC,CAAA,EAAG,MAAA,EAAQ,CAAA,CAAE,QAAQ,CAAA;AAAA;AAAA,IAG5E,GAAA,EAAK,OAAO,CAAA,KACV,IAAA,CAAK,KAAK,qBAAA,EAAuB,EAAE,GAAG,WAAA,CAAY,CAAC,CAAA,EAAG,MAAA,EAAQ,CAAA,CAAE,QAAQ,CAAA;AAAA;AAAA,IAG1E,QAAA,EAAU,OAAO,CAAA,KAA6C;AAC5D,MAAA,MAAM,KAAA,GAAe,EAAE,QAAA,EAAU,CAAA,CAAE,QAAA,EAAS;AAC5C,MAAA,IAAI,yBAAyB,CAAA,EAAG;AAC9B,QAAA,KAAA,CAAM,mBAAA,GAAsBE,mBAAA,CAAe,CAAA,CAAE,mBAAmB,CAAA;AAChE,QAAA,KAAA,CAAM,iBAAA,GAAoBA,mBAAA,CAAe,CAAA,CAAE,iBAAiB,CAAA;AAAA,MAC9D,CAAA,MAAO;AACL,QAAA,KAAA,CAAM,kBAAA,GAAqBJ,eAAAA,CAAW,CAAA,CAAE,kBAAkB,CAAA;AAC1D,QAAA,KAAA,CAAM,gBAAA,GAAmBA,eAAAA,CAAW,CAAA,CAAE,gBAAgB,CAAA;AACtD,QAAA,IAAI,EAAE,gBAAA,EAAkB;AACtB,UAAA,KAAA,MAAW,CAAA,IAAK,CAAA,CAAE,gBAAA,EAAkBK,2BAAA,CAAuB,CAAC,CAAA;AAC5D,UAAA,KAAA,CAAM,mBAAmB,CAAA,CAAE,gBAAA;AAAA,QAC7B;AAAA,MACF;AACA,MAAA,OAAO,IAAA,CAAK,IAAA,CAAK,0BAAA,EAA4B,KAAK,CAAA;AAAA,IACpD,CAAA;AAAA;AAAA,IAGA,EAAA,EAAI,OACF,cAAA,EACA,gBAAA,EACA,SAAqB,EAAC,KAEtB,IAAA,CAAK,IAAA,CAAK,aAAA,EAAe;AAAA,MACvB,GAAG,WAAA,CAAY,cAAA,EAAgB,gBAAgB,CAAA;AAAA,MAC/C,QAAQ,MAAA,CAAO;AAAA,KAChB,CAAA;AAAA;AAAA,IAGH,QAAA,EAAU,OAAO,CAAA,KACf,IAAA,CAAK,KAAK,oBAAA,EAAsB,EAAE,GAAG,WAAA,CAAY,CAAC,CAAA,EAAG,MAAA,EAAQ,CAAA,CAAE,QAAQ,CAAA;AAAA;AAAA,IAGzE,IAAA,EAAM,OAAO,CAAA,KACX,IAAA,CAAK,KAAK,sBAAA,EAAwB,EAAE,GAAG,WAAA,CAAY,CAAC,CAAA,EAAG,MAAA,EAAQ,CAAA,CAAE,QAAQ,CAAA;AAAA;AAAA,IAG3E,IAAA,EAAM,OAAO,CAAA,KACX,IAAA,CAAK,KAAK,sBAAA,EAAwB,EAAE,GAAG,WAAA,CAAY,CAAC,CAAA,EAAG,MAAA,EAAQ,CAAA,CAAE,QAAQ;AAAA,GAC7E;AAAA;AAAA,EAGS,SAAA,GAAY;AAAA;AAAA,IAEnB,OAAA,EAAS,YAA+B,IAAA,CAAK,IAAA,CAAK,uBAAuB,CAAA;AAAA;AAAA,IAEzE,SAAA,EAAW,YAA+B,IAAA,CAAK,IAAA,CAAK,yBAAyB,CAAA;AAAA;AAAA,IAE7E,KAAA,EAAO,OAAO,MAAA,GAAsB,EAAC,KAAyB;AAC5D,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,SAAA,EAAW,WAAA,EAAY;AAC5C,MAAA,OAAA,CAAQ,MAAM,IAAA,CAAK,SAAA,CAAU,OAAA,EAAQ,EAAG,MAAA;AAAA,QACtC,CAAC,CAAA,KAAA,CACE,CAAC,MAAA,CAAO,YAAa,CAAA,CAAE,QAAA,IAAY,IAAA,IAAQ,MAAA,CAAO,SAAS,QAAA,CAAS,CAAA,CAAE,QAAQ,CAAA,MAC9E,CAAC,MAAA,CAAO,UAAA,IACN,CAAA,CAAE,UAAA,IAAc,IAAA,IAAQ,MAAA,CAAO,UAAA,CAAW,QAAA,CAAS,EAAE,UAAU,CAAA,CAAA,KACjE,CAAC,KAAA,IAAA,CAAU,EAAE,aAAA,IAAiB,EAAA,EAAI,WAAA,EAAY,CAAE,SAAS,KAAK,CAAA;AAAA,OACnE;AAAA,IACF;AAAA,GACF;AAAA;AAAA,EAGS,SAAA,GAAY;AAAA;AAAA,IAEnB,YAAA,EAAc,OACZ,cAAA,EACA,gBAAA,KAEA,IAAA,CAAK,IAAA;AAAA,MACH,CAAA,mCAAA,EAAsC,UAAA,CAAW,cAAA,EAAgB,gBAAgB,CAAC,CAAA;AAAA,KACpF;AAAA;AAAA,IAGF,WAAA,EAAa,OACX,CAAA,KAEA,IAAA,CAAK,KAAK,gCAAA,EAAkC;AAAA,MAC1C,GAAG,YAAY,CAAC,CAAA;AAAA,MAChB,eAAe,CAAA,CAAE;AAAA,KAClB,CAAA;AAAA;AAAA,IAGH,WAAA,EAAa,OACX,cAAA,EACA,gBAAA,EACA,SAAqB,EAAC,KAEtB,IAAA,CAAK,IAAA,CAAK,2BAAA,EAA6B;AAAA,MACrC,GAAG,WAAA,CAAY,cAAA,EAAgB,gBAAgB,CAAA;AAAA,MAC/C,QAAQ,MAAA,CAAO;AAAA,KAChB,CAAA;AAAA;AAAA,IAGH,SAAA,EAAW,OACT,cAAA,EACA,gBAAA,EACA,SAAqB,EAAC,KAEtB,IAAA,CAAK,IAAA,CAAK,yBAAA,EAA2B;AAAA,MACnC,GAAG,WAAA,CAAY,cAAA,EAAgB,gBAAgB,CAAA;AAAA,MAC/C,QAAQ,MAAA,CAAO;AAAA,KAChB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOH,mBAAmB,OACjB,QAAA,EACA,gBACA,MAAA,GAAqD,OAErD,IAAA,CAAK,aAAA;AAAA,MACH,+CAA+C,QAAQ,CAAA,CAAA,EAAI,WAAW,cAAA,EAAgB,MAAA,CAAO,gBAAgB,CAAC,CAAA,CAAA;AAAA,MAC9G,MAAA,CAAO;AAAA,KACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMF,WAAW,OACT,QAAA,EACA,gBACA,MAAA,GAAqD,OAErD,IAAA,CAAK,aAAA;AAAA,MACH,iDAAiD,QAAQ,CAAA,CAAA,EAAI,WAAW,cAAA,EAAgB,MAAA,CAAO,gBAAgB,CAAC,CAAA,CAAA;AAAA,MAChH,MAAA,CAAO;AAAA,KACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMF,eAAA,EAAiB,OACf,QAAA,EACA,cAAA,EACA,qBAEA,IAAA,CAAK,IAAA;AAAA,MACH,kCAAkC,QAAQ,CAAA,CAAA,EAAI,UAAA,CAAW,cAAA,EAAgB,gBAAgB,CAAC,CAAA;AAAA,KAC5F;AAAA;AAAA,IAEF,QAAA,EAAU,OACR,OAAA,EACA,cAAA,EACA,gBAAA,EACA,SAAqB,EAAC,KAEtB,IAAA,CAAK,IAAA,CAAK,wBAAA,EAA0B;AAAA,MAClC,OAAA;AAAA,MACA,GAAG,WAAA,CAAY,cAAA,EAAgB,gBAAgB,CAAA;AAAA,MAC/C,QAAQ,MAAA,CAAO;AAAA,KAChB;AAAA,GACL;AAAA;AAAA,EAGS,WAAA,GAAc;AAAA;AAAA;AAAA;AAAA;AAAA,IAKrB,MAAA,EAAQ,OACN,cAAA,EACA,IAAA,GAA2B,EAAC,KACG,iBAAA,CAAkB,IAAA,EAAM,cAAA,EAAgB,IAAI,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAM7E,UAAA,EAAY,OACV,cAAA,EACA,IAAA,GAA0B,EAAC,KACG,gBAAA,CAAiB,IAAA,EAAM,cAAA,EAAgB,IAAI;AAAA,GAC7E;AAAA;AAAA,EAGA,MAAc,aAAA,CAAiB,IAAA,EAAc,MAAA,EAAiC;AAC5E,IAAA,IAAI,CAAC,MAAA,EAAQ,MAAA,EAAQ,OAAO,IAAA,CAAK,KAAQ,IAAI,CAAA;AAC7C,IAAA,MAAM,KAAA,GAAQ,MAAM,OAAA,CAAQ,GAAA;AAAA,MAC1B,KAAA,CAAM,MAAA,EAAQ,qBAAqB,CAAA,CAAE,IAAI,CAAC,GAAA,KAAQ,IAAA,CAAK,IAAA,CAAQ,IAAA,EAAM,EAAE,MAAA,EAAQ,GAAA,EAAK,CAAC;AAAA,KACvF;AACA,IAAA,OAAO,MAAM,IAAA,EAAK;AAAA,EACpB;AACF;AAEA,SAAS,UAAA,CAAW,gBAA2B,gBAAA,EAAmC;AAChF,EAAA,IAAI,gBAAA,KAAqB,MAAA,EAAWA,2BAAA,CAAuB,gBAAgB,CAAA;AAC3E,EAAA,MAAM,CAAA,GAAI,gBAAA,KAAqB,MAAA,GAAY,EAAA,GAAK,IAAI,gBAAgB,CAAA,CAAA;AACpE,EAAA,OAAO,CAAA,EAAGL,eAAAA,CAAW,cAAc,CAAC,GAAG,CAAC,CAAA,CAAA;AAC1C","file":"index.cjs","sourcesContent":["/**\n * Wind curtailment from settlement data.\n *\n * A curtailment is a *bid acceptance*: NESO accepts a unit's bid to reduce output below its\n * physical notification. Three Elexon feeds describe one:\n *\n * - BOAV (`balancing.acceptanceVolumes(\"bid\", day)`): the settled volume of each acceptance in\n * each settlement period, in MWh (negative for bids). This is the quantity curtailed.\n * - EBOCF (`balancing.cashflows(\"bid\", day)`): the £ paid to the unit per period. Wind bids are\n * priced negative (the unit pays to keep generating, so is *paid* to stop), hence the\n * cashflow is positive and is what consumers fund through BSUoS.\n * - BOALF (`datasets.boalf`): the acceptance's level profile and flags. `soFlag` marks\n * actions taken for system reasons (transmission constraints: the classic \"wind farm paid\n * to switch off because the cable to England is full\"); unflagged acceptances are energy\n * balancing. `rrFlag`/`storFlag`/`deemedBoFlag` are counted, not excluded.\n *\n * `curtailmentForDay` joins the three by (unit, period) and (acceptance number) and adds the\n * unit's fuel type, GSP group and capacity from the reference list.\n *\n * Contains BMRS data © Elexon Limited copyright and database right 2026.\n */\nimport { type DateInput, formatDate, settlementPeriodsInDay, UkEnergyError } from \"@uk-energy/core\";\nimport { chunk, type ElexonClient, MAX_UNITS_PER_REQUEST, unitId } from \"./client.js\";\nimport type { AcceptanceVolumeRow, BmUnit, BoalfRow, CashflowRow } from \"./types.js\";\n\nexport interface CurtailmentOptions {\n /** Units to include. Default: every unit whose `fuelType` is in `fuelType`. */\n bmUnit?: string[];\n /** Used when `bmUnit` is omitted. Default `[\"WIND\"]`. */\n fuelType?: string[];\n /** Restrict to a range of settlement periods (1-based, inclusive). Default: the whole day. */\n settlementPeriodFrom?: number;\n settlementPeriodTo?: number;\n}\n\n/** One unit in one settlement period. */\nexport interface CurtailmentPeriod {\n settlementDate: string;\n settlementPeriod: number;\n /** UTC start of the period, from the API. */\n startTime: string;\n bmUnit: string;\n nationalGridBmUnit: string | null;\n leadPartyName: string | null;\n fuelType: string | null;\n gspGroupId: string | null;\n /** `generationCapacity` from the reference list, MW; null when unknown. */\n capacityMw: number | null;\n /** MWh curtailed in the period (positive). */\n curtailedMwh: number;\n /** Of which from SO-flagged (system / constraint) acceptances. */\n soFlaggedMwh: number;\n /**\n * £ paid to the unit for the period's bids (EBOCF); null when no cashflow row was published\n * yet. Cashflow rows for periods with no settled bid volume are not reported anywhere.\n */\n cashflowGbp: number | null;\n /** Distinct acceptances contributing to the period. */\n acceptances: number;\n /** Acceptances in the period that BOALF reported with `soFlag: true`. */\n soFlaggedAcceptances: number;\n}\n\n/** A unit's totals over the day. */\nexport interface CurtailmentUnit {\n bmUnit: string;\n nationalGridBmUnit: string | null;\n leadPartyName: string | null;\n fuelType: string | null;\n gspGroupId: string | null;\n capacityMw: number | null;\n curtailedMwh: number;\n soFlaggedMwh: number;\n cashflowGbp: number;\n /** Settlement periods with any curtailment. */\n periods: number;\n /** Distinct acceptances over the day (one acceptance can span several periods). */\n acceptances: number;\n}\n\nexport interface CurtailmentReport {\n settlementDate: string;\n settlementPeriodFrom: number;\n settlementPeriodTo: number;\n /** Units that were queried (whether or not they were curtailed). */\n unitsQueried: number;\n /** Rows ordered by settlement period then unit id; only (unit, period) pairs with volume. */\n periods: CurtailmentPeriod[];\n /** Per-unit totals, largest `curtailedMwh` first. */\n units: CurtailmentUnit[];\n total: {\n curtailedMwh: number;\n soFlaggedMwh: number;\n cashflowGbp: number;\n /** Units with any curtailment. */\n units: number;\n /** Distinct acceptances. */\n acceptances: number;\n };\n}\n\nconst DEFAULT_FUEL_TYPES = [\"WIND\"];\n\n/** Code-point order (not locale order) so TS and Python sort \"C__X\" and \"CD\" the same way. */\nfunction compareIds(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\nfunction num(s: string | null | undefined): number | null {\n if (s == null || s === \"\") return null;\n const n = Number(s);\n return Number.isFinite(n) ? n : null;\n}\n\n/** Pure aggregation over already-fetched rows; exported for tests and for callers with their own data. */\nexport function aggregateCurtailment(\n settlementDate: string,\n range: { from: number; to: number },\n units: readonly BmUnit[],\n volumes: readonly AcceptanceVolumeRow[],\n cashflows: readonly CashflowRow[],\n boalf: readonly BoalfRow[],\n): Omit<CurtailmentReport, \"unitsQueried\"> {\n const byId = new Map<string, BmUnit>();\n for (const u of units) {\n if (u.elexonBmUnit) byId.set(u.elexonBmUnit, u);\n if (u.nationalGridBmUnit) byId.set(u.nationalGridBmUnit, u);\n }\n const soByAcceptance = new Map<number, boolean>();\n for (const r of boalf) {\n if (r.acceptanceNumber != null) {\n soByAcceptance.set(\n r.acceptanceNumber,\n (soByAcceptance.get(r.acceptanceNumber) ?? false) || r.soFlag === true,\n );\n }\n }\n const cashByKey = new Map<string, number>();\n for (const c of cashflows) {\n const sp = c.settlementPeriod;\n if (!c.bmUnit || sp === undefined || sp < range.from || sp > range.to) continue;\n const key = `${c.bmUnit}|${sp}`;\n cashByKey.set(key, (cashByKey.get(key) ?? 0) + (c.totalCashflow ?? 0));\n }\n\n const periods = new Map<string, CurtailmentPeriod>();\n const seen = new Map<string, Set<number>>();\n for (const v of volumes) {\n const sp = v.settlementPeriod;\n const acceptance = v.acceptanceId;\n if (!v.bmUnit || sp === undefined || acceptance === undefined) continue;\n if (sp < range.from || sp > range.to) continue;\n const mwh = -(v.totalVolumeAccepted ?? 0);\n if (!(mwh > 0)) continue;\n const key = `${v.bmUnit}|${sp}`;\n let p = periods.get(key);\n let ids = seen.get(key);\n if (!p || !ids) {\n const ref = byId.get(v.bmUnit);\n p = {\n settlementDate: v.settlementDate ?? settlementDate,\n settlementPeriod: sp,\n startTime: v.startTime ?? \"\",\n bmUnit: v.bmUnit,\n nationalGridBmUnit: v.nationalGridBmUnit ?? ref?.nationalGridBmUnit ?? null,\n leadPartyName: v.leadPartyName ?? ref?.leadPartyName ?? null,\n fuelType: ref?.fuelType ?? null,\n gspGroupId: ref?.gspGroupId ?? null,\n capacityMw: num(ref?.generationCapacity),\n curtailedMwh: 0,\n soFlaggedMwh: 0,\n cashflowGbp: cashByKey.get(key) ?? null,\n acceptances: 0,\n soFlaggedAcceptances: 0,\n };\n ids = new Set();\n periods.set(key, p);\n seen.set(key, ids);\n }\n p.curtailedMwh += mwh;\n const so = soByAcceptance.get(acceptance) === true;\n if (so) p.soFlaggedMwh += mwh;\n if (!ids.has(acceptance)) {\n ids.add(acceptance);\n p.acceptances += 1;\n if (so) p.soFlaggedAcceptances += 1;\n }\n }\n\n const rows = [...periods.values()].sort(\n (a, b) => a.settlementPeriod - b.settlementPeriod || compareIds(a.bmUnit, b.bmUnit),\n );\n\n const unitTotals = new Map<string, CurtailmentUnit>();\n const unitAcceptances = new Map<string, Set<number>>();\n const allAcceptances = new Set<number>();\n const total = { curtailedMwh: 0, soFlaggedMwh: 0, cashflowGbp: 0, units: 0, acceptances: 0 };\n for (const p of rows) {\n let u = unitTotals.get(p.bmUnit);\n if (!u) {\n u = {\n bmUnit: p.bmUnit,\n nationalGridBmUnit: p.nationalGridBmUnit,\n leadPartyName: p.leadPartyName,\n fuelType: p.fuelType,\n gspGroupId: p.gspGroupId,\n capacityMw: p.capacityMw,\n curtailedMwh: 0,\n soFlaggedMwh: 0,\n cashflowGbp: 0,\n periods: 0,\n acceptances: 0,\n };\n unitTotals.set(p.bmUnit, u);\n }\n u.curtailedMwh += p.curtailedMwh;\n u.soFlaggedMwh += p.soFlaggedMwh;\n u.cashflowGbp += p.cashflowGbp ?? 0;\n u.periods += 1;\n const ids = seen.get(`${p.bmUnit}|${p.settlementPeriod}`) ?? new Set<number>();\n let acc = unitAcceptances.get(p.bmUnit);\n if (!acc) {\n acc = new Set();\n unitAcceptances.set(p.bmUnit, acc);\n }\n for (const id of ids) {\n acc.add(id);\n allAcceptances.add(id);\n }\n u.acceptances = acc.size;\n total.curtailedMwh += p.curtailedMwh;\n total.soFlaggedMwh += p.soFlaggedMwh;\n total.cashflowGbp += p.cashflowGbp ?? 0;\n }\n total.units = unitTotals.size;\n total.acceptances = allAcceptances.size;\n\n return {\n settlementDate,\n settlementPeriodFrom: range.from,\n settlementPeriodTo: range.to,\n periods: rows,\n units: [...unitTotals.values()].sort((a, b) => b.curtailedMwh - a.curtailedMwh),\n total,\n };\n}\n\nexport async function curtailmentForDay(\n client: ElexonClient,\n settlementDate: DateInput,\n opts: CurtailmentOptions = {},\n): Promise<CurtailmentReport> {\n const day = formatDate(settlementDate);\n const last = settlementPeriodsInDay(day);\n const from = opts.settlementPeriodFrom ?? 1;\n const to = opts.settlementPeriodTo ?? last;\n if (from < 1 || to > last || from > to) {\n throw new UkEnergyError(`Settlement periods ${from}-${to} are outside 1-${last} for ${day}`);\n }\n\n const units = await client.reference.bmUnits();\n let ids: string[];\n if (opts.bmUnit) {\n ids = opts.bmUnit;\n } else {\n const fuel = opts.fuelType ?? DEFAULT_FUEL_TYPES;\n ids = units\n .filter((u) => u.fuelType != null && fuel.includes(u.fuelType))\n .map(unitId)\n .filter((id) => id !== \"\");\n }\n ids = [...new Set(ids)]; // the reference list has duplicates (T_WLNYO-4 twice on 20 Oct 2026)\n if (ids.length === 0) throw new UkEnergyError(\"No BM units to query\");\n\n // BOALF is fetched for the whole day, not just from..to: an acceptance's level rows are\n // keyed by the periods they span, so one that started earlier would otherwise lose its flag.\n const [volumes, cashflows, boalfParts] = await Promise.all([\n client.balancing.acceptanceVolumes(\"bid\", day, { bmUnit: ids }),\n client.balancing.cashflows(\"bid\", day, { bmUnit: ids }),\n Promise.all(\n chunk(ids, MAX_UNITS_PER_REQUEST).map((bmUnit) =>\n client.datasets.boalf({\n from: day,\n to: day,\n settlementPeriodFrom: 1,\n settlementPeriodTo: last,\n bmUnit,\n }),\n ),\n ),\n ]);\n const boalf = boalfParts.flat();\n\n return {\n unitsQueried: ids.length,\n ...aggregateCurtailment(day, { from, to }, units, volumes, cashflows, boalf),\n };\n}\n","/**\n * Octopus Energy's \"Wasted Wind\" figure (https://wastedwind.energy), reproduced from the same\n * Elexon data. Per settlement period:\n *\n * - **Switch-off**: rows of the settlement *bid* stack whose unit is `fuelType: \"WIND\"` in the\n * BM unit reference and which carry `soFlag: true` (system / constraint actions). Volume is\n * the sum of their `volume` (negative MWh); cost is Σ `originalPrice × volume`, which is\n * positive for the usual negative-priced wind bid (the farm is paid to stop) and negative in\n * the rare case of a positive bid price.\n * - **Replace**: the curtailed volume bought back from the settlement *offer* stack of the same\n * period, `cadlFlag` offers excluded, walking non-SO offers first then by NESO's\n * `sequenceNumber`, pro-rating the last offer. Cost is Σ `originalPrice × volume taken`.\n *\n * Totals over a day are what the tracker shows as \"£ switching off wind turbines\" and \"£ buying\n * energy elsewhere\"; its monthly summary is the sum over days. This is a *definition* rather\n * than settlement truth: it counts SO-flagged wind bids only (see `curtailment.forDay` for the\n * settled BOAV/EBOCF view of every wind bid) and estimates replacement cost from the offer\n * stack rather than attributing actual offer cashflows.\n *\n * Contains BMRS data © Elexon Limited copyright and database right 2026.\n */\nimport { type DateInput, formatDate, settlementPeriodsInDay, UkEnergyError } from \"@uk-energy/core\";\nimport type { ElexonClient } from \"./client.js\";\nimport type { BmUnit, SettlementStackRow } from \"./types.js\";\n\nexport interface WastedWindOptions {\n /** Restrict to a range of settlement periods (1-based, inclusive). Default: the whole day. */\n settlementPeriodFrom?: number;\n settlementPeriodTo?: number;\n /** Units counted as wind. Default: `fuelType === \"WIND\"` in the reference list. */\n fuelType?: string[];\n /** Stack requests in flight at once (two per period). Default 10. */\n concurrency?: number;\n}\n\nexport interface WastedWindPeriod {\n settlementDate: string;\n settlementPeriod: number;\n /** UTC start of the period, from any stack row of the period; empty when there were none. */\n startTime: string;\n /** MWh of SO-flagged wind bids (positive). */\n curtailedMwh: number;\n /** £ paid to wind units to turn down (Σ originalPrice × volume). */\n curtailmentCostGbp: number;\n /** MWh bought back from the offer stack (≤ curtailedMwh when the stack runs out). */\n turnUpMwh: number;\n /** £ to buy that energy from the offer stack. */\n turnUpCostGbp: number;\n /** Wind bid rows and offer rows that contributed. */\n bids: number;\n offers: number;\n}\n\nexport interface WastedWindReport {\n settlementDate: string;\n settlementPeriodFrom: number;\n settlementPeriodTo: number;\n periods: WastedWindPeriod[];\n total: {\n curtailedMwh: number;\n curtailmentCostGbp: number;\n turnUpMwh: number;\n turnUpCostGbp: number;\n /** curtailmentCostGbp + turnUpCostGbp: the tracker's headline. */\n totalCostGbp: number;\n };\n}\n\nconst DEFAULT_FUEL_TYPES = [\"WIND\"];\n\n/** Pure aggregation; exported for tests and for callers with their own stack rows. */\nexport function aggregateWastedWind(\n settlementDate: string,\n range: { from: number; to: number },\n units: readonly BmUnit[],\n bids: readonly SettlementStackRow[],\n offers: readonly SettlementStackRow[],\n fuelTypes: readonly string[] = DEFAULT_FUEL_TYPES,\n): WastedWindReport {\n const fuelById = new Map<string, string | null | undefined>();\n for (const u of units) if (u.elexonBmUnit) fuelById.set(u.elexonBmUnit, u.fuelType);\n\n const periods: WastedWindPeriod[] = [];\n for (let sp = range.from; sp <= range.to; sp++) {\n const windBids = bids.filter(\n (r) =>\n r.settlementPeriod === sp &&\n r.soFlag === true &&\n r.id != null &&\n fuelTypes.includes(fuelById.get(r.id) ?? \"\"),\n );\n let volume = 0;\n let cost = 0;\n for (const b of windBids) {\n const v = b.volume ?? 0;\n volume += v;\n cost += (b.originalPrice ?? 0) * v;\n }\n const need = Math.abs(volume);\n\n const stack = offers\n .filter((r) => r.settlementPeriod === sp && !r.cadlFlag)\n .sort(\n (a, b) =>\n (a.soFlag ? 1 : 0) - (b.soFlag ? 1 : 0) ||\n (a.sequenceNumber ?? 0) - (b.sequenceNumber ?? 0),\n );\n let taken = 0;\n let turnUpCost = 0;\n let used = 0;\n for (const o of need > 0 ? stack : []) {\n used += 1;\n const v = o.volume ?? 0;\n if (taken + v > need) {\n const fraction = Math.min(Math.max((need - taken) / v, 0), 1);\n taken += v * fraction;\n turnUpCost += (o.originalPrice ?? 0) * (v * fraction);\n } else {\n taken += v;\n turnUpCost += (o.originalPrice ?? 0) * v;\n }\n if (taken >= need) break;\n }\n\n const first =\n windBids[0] ??\n stack[0] ??\n bids.find((r) => r.settlementPeriod === sp) ??\n offers.find((r) => r.settlementPeriod === sp);\n periods.push({\n settlementDate,\n settlementPeriod: sp,\n startTime: first?.startTime ?? \"\",\n curtailedMwh: need,\n curtailmentCostGbp: cost,\n turnUpMwh: taken,\n turnUpCostGbp: turnUpCost,\n bids: windBids.length,\n offers: used,\n });\n }\n\n const total = {\n curtailedMwh: 0,\n curtailmentCostGbp: 0,\n turnUpMwh: 0,\n turnUpCostGbp: 0,\n totalCostGbp: 0,\n };\n for (const p of periods) {\n total.curtailedMwh += p.curtailedMwh;\n total.curtailmentCostGbp += p.curtailmentCostGbp;\n total.turnUpMwh += p.turnUpMwh;\n total.turnUpCostGbp += p.turnUpCostGbp;\n }\n total.totalCostGbp = total.curtailmentCostGbp + total.turnUpCostGbp;\n\n return {\n settlementDate,\n settlementPeriodFrom: range.from,\n settlementPeriodTo: range.to,\n periods,\n total,\n };\n}\n\nexport async function wastedWindForDay(\n client: ElexonClient,\n settlementDate: DateInput,\n opts: WastedWindOptions = {},\n): Promise<WastedWindReport> {\n const day = formatDate(settlementDate);\n const last = settlementPeriodsInDay(day);\n const from = opts.settlementPeriodFrom ?? 1;\n const to = opts.settlementPeriodTo ?? last;\n if (from < 1 || to > last || from > to) {\n throw new UkEnergyError(`Settlement periods ${from}-${to} are outside 1-${last} for ${day}`);\n }\n const concurrency = Math.max(1, opts.concurrency ?? 10);\n\n const units = await client.reference.bmUnits();\n const jobs: (() => Promise<SettlementStackRow[]>)[] = [];\n for (let sp = from; sp <= to; sp++) {\n jobs.push(() => client.balancing.settlementStack(\"bid\", day, sp));\n jobs.push(() => client.balancing.settlementStack(\"offer\", day, sp));\n }\n const results: SettlementStackRow[][] = new Array(jobs.length);\n let next = 0;\n let failed = false;\n const worker = async () => {\n while (next < jobs.length && !failed) {\n const i = next++;\n const job = jobs[i];\n if (!job) continue;\n try {\n results[i] = await job();\n } catch (err) {\n failed = true; // stop the other workers issuing requests we will not use\n throw err;\n }\n }\n };\n await Promise.all(Array.from({ length: Math.min(concurrency, jobs.length) }, worker));\n\n const bids: SettlementStackRow[] = [];\n const offers: SettlementStackRow[] = [];\n results.forEach((rows, i) => {\n (i % 2 === 0 ? bids : offers).push(...(rows ?? []));\n });\n\n return aggregateWastedWind(\n day,\n { from, to },\n units,\n bids,\n offers,\n opts.fuelType ?? DEFAULT_FUEL_TYPES,\n );\n}\n","import {\n assertSettlementPeriod,\n type DateInput,\n formatDate,\n formatDateTime,\n HttpClient,\n type HttpClientOptions,\n type Query,\n type RateLimit,\n UkEnergyError,\n} from \"@uk-energy/core\";\nimport {\n type CurtailmentOptions,\n type CurtailmentReport,\n curtailmentForDay,\n} from \"./curtailment.js\";\nimport type {\n AcceptanceRow,\n AcceptanceVolumeRow,\n BidOffer,\n BidOfferRow,\n BmUnit,\n BoalfRow,\n BodRow,\n CashflowRow,\n DatasetQuery,\n DatasetRow,\n FuelinstRow,\n MarketIndexRow,\n MelsRow,\n PhysicalDataset,\n PhysicalRow,\n PnRow,\n SettlementStackRow,\n StreamDataset,\n SystemPriceRow,\n} from \"./types.js\";\nimport { type WastedWindOptions, type WastedWindReport, wastedWindForDay } from \"./wasted-wind.js\";\n\nexport const BASE_URL = \"https://data.elexon.co.uk/bmrs/api/v1\";\n\n/**\n * No limit is published and none was observed: 310 requests at ~120 req/s on 20 Oct 2026 all\n * returned 200 with no rate-limit headers (build-board d02). 10 rps / burst 20 keeps us a\n * polite order of magnitude below that; raise it per client if you need to.\n */\nexport const DEFAULT_RATE_LIMIT: RateLimit = { requestsPerSecond: 10, burst: 20 };\n\n/** Most datasets settle within minutes; a short default TTL avoids hammering on retries. */\nexport const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;\n\nexport type ElexonClientOptions = Partial<Omit<HttpClientOptions, \"baseUrl\">> & {\n baseUrl?: string;\n};\n\n/**\n * Time window for the dataset endpoints. With `settlementPeriodFrom`/`To` set, the API treats\n * `from`/`to` as settlement *dates* (time ignored) and filters by period instead.\n */\nexport interface SettlementWindow {\n from: DateInput;\n to: DateInput;\n settlementPeriodFrom?: number;\n settlementPeriodTo?: number;\n}\n\nexport interface UnitFilter {\n /** Elexon BM unit ids, e.g. \"T_ARCHW-1\". Omit for all units. */\n bmUnit?: string[];\n}\n\n/** Client-side filter over the BM unit reference list. All given fields must match. */\nexport interface UnitsFilter {\n /** e.g. [\"WIND\"]. */\n fuelType?: string[];\n /** GSP group ids, e.g. [\"_P\", \"_N\"] (north / south Scotland). Most wind units have none. */\n gspGroupId?: string[];\n /** Case-insensitive substring of `leadPartyName`. */\n leadParty?: string;\n}\n\n/** The id to query a unit by: `elexonBmUnit`, or `nationalGridBmUnit` for units without one (48 wind units). */\nexport function unitId(u: BmUnit): string {\n return u.elexonBmUnit || u.nationalGridBmUnit || \"\";\n}\n\n/** The API accepts the whole wind fleet (287 ids, ~4.9 KB URL); chunk well below that. */\nexport const MAX_UNITS_PER_REQUEST = 200;\n\nexport function chunk<T>(items: readonly T[], size: number): T[][] {\n const out: T[][] = [];\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));\n return out;\n}\n\nexport type FuelinstQuery = (\n | { publishDateTimeFrom: DateInput; publishDateTimeTo: DateInput }\n | { settlementDateFrom: DateInput; settlementDateTo: DateInput; settlementPeriod?: number[] }\n) & { fuelType?: string[] };\n\ninterface Envelope<T> {\n data: T;\n metadata?: { datasets?: string[] };\n}\n\nfunction windowQuery(w: SettlementWindow): Query {\n const q: Query = { from: formatDateTime(w.from), to: formatDateTime(w.to) };\n if (w.settlementPeriodFrom !== undefined) {\n assertSettlementPeriod(w.settlementPeriodFrom);\n q.settlementPeriodFrom = w.settlementPeriodFrom;\n }\n if (w.settlementPeriodTo !== undefined) {\n assertSettlementPeriod(w.settlementPeriodTo);\n q.settlementPeriodTo = w.settlementPeriodTo;\n }\n return q;\n}\n\nfunction periodQuery(settlementDate: DateInput, settlementPeriod: number): Query {\n assertSettlementPeriod(settlementPeriod);\n return { settlementDate: formatDate(settlementDate), settlementPeriod };\n}\n\n/**\n * Client for Elexon Insights (BMRS). Methods return the API's own row shapes, typed from its\n * OpenAPI spec and unwrapped from the `{ data }` envelope where there is one.\n *\n * const ex = new ElexonClient();\n * const rows = await ex.datasets.boalf({ from: \"2026-10-15T00:00Z\", to: \"2026-10-15T01:00Z\" });\n * const wind = (await ex.reference.bmUnits()).filter((u) => u.fuelType === \"WIND\");\n *\n * Contains BMRS data © Elexon Limited copyright and database right 2026. Keep the attribution.\n */\nexport class ElexonClient {\n readonly http: HttpClient;\n\n constructor(opts: ElexonClientOptions = {}) {\n this.http = new HttpClient({\n baseUrl: BASE_URL,\n rateLimit: DEFAULT_RATE_LIMIT,\n cacheTtlMs: opts.cache ? DEFAULT_CACHE_TTL_MS : 0,\n ...opts,\n });\n }\n\n /** Endpoints returning a bare array. */\n private async list<T>(path: string, query?: Query): Promise<T[]> {\n const res = await this.http.get<T[]>(path, query);\n if (!Array.isArray(res)) throw new UkEnergyError(`Unexpected response shape for ${path}`);\n return res;\n }\n\n /** Endpoints returning `{ data: [...] }`. */\n private async data<T>(path: string, query?: Query): Promise<T[]> {\n const res = await this.http.get<Envelope<T[]>>(path, query);\n if (!res || !Array.isArray(res.data)) {\n throw new UkEnergyError(`Unexpected response shape for ${path}`);\n }\n return res.data;\n }\n\n /** `/datasets/{code}` — raw datasets. The `/stream` variants are used: lighter, no envelope. */\n readonly datasets = {\n /**\n * Any stream dataset (77 codes: BOALF, BOD, FUELINST, PN, MELS, WINDFOR, FREQ, …), with the\n * query parameters typed from the spec. Values are sent as given (strings in API format,\n * \"YYYY-MM-DDTHH:MMZ\" or \"YYYY-MM-DD\"); the named methods below format and validate dates.\n */\n stream: async <D extends StreamDataset>(\n dataset: D,\n query: DatasetQuery<D>,\n ): Promise<DatasetRow<D>[]> =>\n this.list<DatasetRow<D>>(`datasets/${dataset}/stream`, query as unknown as Query),\n\n /** Bid-offer acceptance levels (BOALF). Curtailment shows as `soFlag: true` bids. */\n boalf: async (w: SettlementWindow & UnitFilter): Promise<BoalfRow[]> =>\n this.list(\"datasets/BOALF/stream\", { ...windowQuery(w), bmUnit: w.bmUnit }),\n\n /** Bid-offer data (BOD): submitted bid and offer prices per pair. */\n bod: async (w: SettlementWindow & UnitFilter): Promise<BodRow[]> =>\n this.list(\"datasets/BOD/stream\", { ...windowQuery(w), bmUnit: w.bmUnit }),\n\n /** Generation by fuel type at 5-minute resolution (FUELINST). */\n fuelinst: async (q: FuelinstQuery): Promise<FuelinstRow[]> => {\n const query: Query = { fuelType: q.fuelType };\n if (\"publishDateTimeFrom\" in q) {\n query.publishDateTimeFrom = formatDateTime(q.publishDateTimeFrom);\n query.publishDateTimeTo = formatDateTime(q.publishDateTimeTo);\n } else {\n query.settlementDateFrom = formatDate(q.settlementDateFrom);\n query.settlementDateTo = formatDate(q.settlementDateTo);\n if (q.settlementPeriod) {\n for (const p of q.settlementPeriod) assertSettlementPeriod(p);\n query.settlementPeriod = q.settlementPeriod;\n }\n }\n return this.list(\"datasets/FUELINST/stream\", query);\n },\n\n /** Physical notifications for one settlement period (`/datasets/PN` takes exactly one). */\n pn: async (\n settlementDate: DateInput,\n settlementPeriod: number,\n filter: UnitFilter = {},\n ): Promise<PnRow[]> =>\n this.data(\"datasets/PN\", {\n ...periodQuery(settlementDate, settlementPeriod),\n bmUnit: filter.bmUnit,\n }),\n\n /** Physical notifications over a window (`/datasets/PN/stream`). */\n pnStream: async (w: SettlementWindow & UnitFilter): Promise<PnRow[]> =>\n this.list(\"datasets/PN/stream\", { ...windowQuery(w), bmUnit: w.bmUnit }),\n\n /** Maximum export limits (MELS). */\n mels: async (w: SettlementWindow & UnitFilter): Promise<MelsRow[]> =>\n this.list(\"datasets/MELS/stream\", { ...windowQuery(w), bmUnit: w.bmUnit }),\n\n /** Minimum import limits (MILS). */\n mils: async (w: SettlementWindow & UnitFilter): Promise<MelsRow[]> =>\n this.list(\"datasets/MILS/stream\", { ...windowQuery(w), bmUnit: w.bmUnit }),\n };\n\n /** Reference data. */\n readonly reference = {\n /** All BM units (~3,100). Capacities are strings; `fuelType` is null for many non-generators. */\n bmUnits: async (): Promise<BmUnit[]> => this.list(\"reference/bmunits/all\"),\n /** Fuel type codes used across the API. */\n fuelTypes: async (): Promise<string[]> => this.list(\"reference/fueltypes/all\"),\n /** BM units matching a filter, e.g. `{ fuelType: [\"WIND\"] }` (287 units). */\n units: async (filter: UnitsFilter = {}): Promise<BmUnit[]> => {\n const party = filter.leadParty?.toLowerCase();\n return (await this.reference.bmUnits()).filter(\n (u) =>\n (!filter.fuelType || (u.fuelType != null && filter.fuelType.includes(u.fuelType))) &&\n (!filter.gspGroupId ||\n (u.gspGroupId != null && filter.gspGroupId.includes(u.gspGroupId))) &&\n (!party || (u.leadPartyName ?? \"\").toLowerCase().includes(party)),\n );\n },\n };\n\n /** Market-wide balancing views (one settlement period per call) and settlement prices. */\n readonly balancing = {\n /** System buy/sell prices for a settlement day, or one period of it (DISEBSP). */\n systemPrices: async (\n settlementDate: DateInput,\n settlementPeriod?: number,\n ): Promise<SystemPriceRow[]> =>\n this.data(\n `balancing/settlement/system-prices/${periodPath(settlementDate, settlementPeriod)}`,\n ),\n\n /** Market index (MID) prices and volumes. */\n marketIndex: async (\n w: SettlementWindow & { dataProviders?: string[] },\n ): Promise<MarketIndexRow[]> =>\n this.data(\"balancing/pricing/market-index\", {\n ...windowQuery(w),\n dataProviders: w.dataProviders,\n }),\n\n /** Every acceptance in one settlement period. */\n acceptances: async (\n settlementDate: DateInput,\n settlementPeriod: number,\n filter: UnitFilter = {},\n ): Promise<AcceptanceRow[]> =>\n this.data(\"balancing/acceptances/all\", {\n ...periodQuery(settlementDate, settlementPeriod),\n bmUnit: filter.bmUnit,\n }),\n\n /** Every bid-offer pair in one settlement period. */\n bidOffers: async (\n settlementDate: DateInput,\n settlementPeriod: number,\n filter: UnitFilter = {},\n ): Promise<BidOfferRow[]> =>\n this.data(\"balancing/bid-offer/all\", {\n ...periodQuery(settlementDate, settlementPeriod),\n bmUnit: filter.bmUnit,\n }),\n\n /**\n * Settled acceptance volumes (BOAV) for a settlement day, or one period of it: one row per\n * acceptance per period, `totalVolumeAccepted` in MWh (negative for bids). Long unit lists\n * are split across requests.\n */\n acceptanceVolumes: async (\n bidOffer: BidOffer,\n settlementDate: DateInput,\n filter: UnitFilter & { settlementPeriod?: number } = {},\n ): Promise<AcceptanceVolumeRow[]> =>\n this.perUnitChunks(\n `balancing/settlement/acceptance/volumes/all/${bidOffer}/${periodPath(settlementDate, filter.settlementPeriod)}`,\n filter.bmUnit,\n ),\n /**\n * Indicative bid/offer cashflows (EBOCF) for a settlement day or one period: one row per unit\n * per period, `totalCashflow` in £ paid to the unit (positive for a wind bid at a negative\n * price).\n */\n cashflows: async (\n bidOffer: BidOffer,\n settlementDate: DateInput,\n filter: UnitFilter & { settlementPeriod?: number } = {},\n ): Promise<CashflowRow[]> =>\n this.perUnitChunks(\n `balancing/settlement/indicative/cashflows/all/${bidOffer}/${periodPath(settlementDate, filter.settlementPeriod)}`,\n filter.bmUnit,\n ),\n /**\n * Settlement bid or offer stack for one period (ISPSTACK): every accepted action with\n * `originalPrice`, `volume` (negative for bids), `soFlag`, `cadlFlag` and the sequence NESO\n * used. This is what Octopus's Wasted Wind tracker is built on.\n */\n settlementStack: async (\n bidOffer: BidOffer,\n settlementDate: DateInput,\n settlementPeriod: number,\n ): Promise<SettlementStackRow[]> =>\n this.data(\n `balancing/settlement/stack/all/${bidOffer}/${periodPath(settlementDate, settlementPeriod)}`,\n ),\n /** PN / QPN / MILS / MELS for every unit in one settlement period. */\n physical: async (\n dataset: PhysicalDataset,\n settlementDate: DateInput,\n settlementPeriod: number,\n filter: UnitFilter = {},\n ): Promise<PhysicalRow[]> =>\n this.data(\"balancing/physical/all\", {\n dataset,\n ...periodQuery(settlementDate, settlementPeriod),\n bmUnit: filter.bmUnit,\n }),\n };\n\n /** Wind curtailment: SO-flagged and energy bid acceptances per unit per period, with £. */\n readonly curtailment = {\n /**\n * Curtailment for a GB settlement day (Europe/London), per unit and per settlement period.\n * Defaults to every WIND unit; the reference list plus three requests per 200 units.\n */\n forDay: async (\n settlementDate: DateInput,\n opts: CurtailmentOptions = {},\n ): Promise<CurtailmentReport> => curtailmentForDay(this, settlementDate, opts),\n /**\n * Octopus's \"Wasted Wind\" definition for a settlement day: SO-flagged wind bids from the\n * settlement stack (MWh and £ to switch off) plus the cost of re-buying that energy from\n * the cheapest offers in the same period (£ to replace). Two requests per settlement period.\n */\n wastedWind: async (\n settlementDate: DateInput,\n opts: WastedWindOptions = {},\n ): Promise<WastedWindReport> => wastedWindForDay(this, settlementDate, opts),\n };\n\n /** `{data}` endpoints with an optional unit filter, chunked so URLs stay short. */\n private async perUnitChunks<T>(path: string, bmUnit?: string[]): Promise<T[]> {\n if (!bmUnit?.length) return this.data<T>(path);\n const parts = await Promise.all(\n chunk(bmUnit, MAX_UNITS_PER_REQUEST).map((ids) => this.data<T>(path, { bmUnit: ids })),\n );\n return parts.flat();\n }\n}\n\nfunction periodPath(settlementDate: DateInput, settlementPeriod?: number): string {\n if (settlementPeriod !== undefined) assertSettlementPeriod(settlementPeriod);\n const p = settlementPeriod === undefined ? \"\" : `/${settlementPeriod}`;\n return `${formatDate(settlementDate)}${p}`;\n}\n"]}
|