@uk-energy/neso 0.0.0-stage → 0.1.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 +18 -0
- package/LICENSE +21 -0
- package/README.md +95 -2
- package/dist/index.cjs +325 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +365 -0
- package/dist/index.d.ts +365 -0
- package/dist/index.js +310 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @uk-energy/neso
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- `NesoClient` for the NESO Data Portal (CKAN 2.8): `ckan.*` (`packageShow`, `packageSearch`,
|
|
6
|
+
`resourceShow`, `datastoreSearch`, `datastoreAll`, `sql`), `resources.*` (`datastore`,
|
|
7
|
+
`forFinancialYear`, `byName`), `constraints.*` (`thermalCosts`, `breakdown`, `forecast24m`,
|
|
8
|
+
`limits24m`, `dayAheadFlows`, `dailyBalancingCosts`) and `dfs.*` (`requirements`,
|
|
9
|
+
`utilisation`, `summary`).
|
|
10
|
+
- Per-financial-year resources resolved by name ("25-26" / "2025-2026"), spanning years;
|
|
11
|
+
date-range queries go through `datastore_search_sql` with quoted identifiers and literals.
|
|
12
|
+
- SQL numerics-as-strings and `_full_text` normalised away using the response `fields`.
|
|
13
|
+
- Hand-typed rows for the wrapped datasets (no OpenAPI on the portal), checked by the tests.
|
|
14
|
+
- Default 5 rps / burst 10; package metadata memoised 10 minutes.
|
|
15
|
+
- `Date` inputs are read as Europe/London calendar days; `datastoreAll` clamps the page size to
|
|
16
|
+
32,000; a malformed `package_show` result throws `UkEnergyError` and is not memoised.
|
|
17
|
+
- 19 fixture-backed tests on 22 recorded responses (incl. a 404 and a 409) plus stub-portal
|
|
18
|
+
tests for paging, memoisation and multi-year ranges, shared with `uk_energy.neso`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joshua Serpis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,96 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @uk-energy/neso
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed client for the [NESO Data Portal](https://www.neso.energy/data-portal): transmission
|
|
4
|
+
constraint costs and forecasts, day-ahead boundary flows, Demand Flexibility Service and daily
|
|
5
|
+
balancing costs, plus the raw CKAN datastore and SQL endpoints for the other ~800 datasets.
|
|
6
|
+
No API key.
|
|
7
|
+
|
|
8
|
+
> Supported by National Energy SO Open Data. Keep this line wherever you show the data: it is the
|
|
9
|
+
> attribution the [NESO Open Data Licence v1.0](https://www.neso.energy/data-portal/neso-open-licence)
|
|
10
|
+
> (OGL v3-based; commercial use and redistribution allowed) requires.
|
|
11
|
+
|
|
12
|
+
Python mirror: `uk_energy.neso` in the `uk-energy` distribution.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @uk-energy/neso
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { NesoClient } from "@uk-energy/neso";
|
|
22
|
+
|
|
23
|
+
const neso = new NesoClient();
|
|
24
|
+
|
|
25
|
+
// Thermal constraint cost per boundary per day (£) — SCOTEX, SSHARN, SEIMP are the Scottish export boundaries
|
|
26
|
+
const costs = await neso.constraints.thermalCosts("2025-04-01", "2025-04-30");
|
|
27
|
+
const scotex = costs.filter((r) => r["Constraint Group"] === "SCOTEX").reduce((s, r) => s + r["Daily Cost (GBP)"], 0);
|
|
28
|
+
|
|
29
|
+
// Daily constraint cost and volume by type (thermal, voltage, inertia, largest loss)
|
|
30
|
+
const breakdown = await neso.constraints.breakdown("2025-04-01", "2025-04-30");
|
|
31
|
+
|
|
32
|
+
// NESO's forward view: £m per month for 24 months, and MW limits per boundary per week
|
|
33
|
+
const forecast = await neso.constraints.forecast24m();
|
|
34
|
+
const limits = await neso.constraints.limits24m();
|
|
35
|
+
|
|
36
|
+
// Day-ahead forecast flow vs limit, half-hourly, per boundary (filterable)
|
|
37
|
+
const flows = await neso.constraints.dayAheadFlows("2026-10-15", "2026-10-15", "SCOTEX");
|
|
38
|
+
|
|
39
|
+
// Demand Flexibility Service: requirements, bids per unit, and per-event summary
|
|
40
|
+
const dfs = await neso.dfs.summary("2026-10-20", "2026-10-22");
|
|
41
|
+
|
|
42
|
+
// Anything else on the portal: CKAN datastore paging or read-only SQL over a resource
|
|
43
|
+
const pkg = await neso.ckan.packageShow("interconnector-register");
|
|
44
|
+
const rows = await neso.ckan.datastoreAll(pkg.resources[0].id);
|
|
45
|
+
const top = await neso.ckan.sql(`SELECT * FROM "407f3e9d-5c38-4a64-98f5-923dc4d64fb2" WHERE "Daily Cost (GBP)" > 1000000 ORDER BY "Daily Cost (GBP)" DESC LIMIT 10`);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Rows keep the portal's own column names, spaces and all (`r["Daily Cost (GBP)"]`), so they match
|
|
49
|
+
the CSV downloads and the SQL endpoint. Every method is `async`; CKAN's `{ success: false }`
|
|
50
|
+
throws `UkEnergyError`, HTTP errors throw `HttpError` with the CKAN error object in `body`
|
|
51
|
+
(404 for an unknown resource, 409 for bad SQL).
|
|
52
|
+
|
|
53
|
+
### Surface
|
|
54
|
+
|
|
55
|
+
| Namespace | Methods |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `ckan` | `packageShow(id)`, `packageSearch(q, rows)`, `resourceShow(id)`, `datastoreSearch(resourceId, { limit, offset, sort, filters, fields, q })`, `datastoreAll(resourceId, opts)`, `sql(query, extraNumeric?)` |
|
|
58
|
+
| `resources` | `datastore(packageId)`, `forFinancialYear(packageId, 2025)`, `byName(packageId, name \| RegExp)` |
|
|
59
|
+
| `constraints` | `thermalCosts(from, to, group?)`, `breakdown(from, to)`, `forecast24m()`, `limits24m()`, `dayAheadFlows(from, to, group?)`, `dailyBalancingCosts(from, to)` |
|
|
60
|
+
| `dfs` | `requirements(from, to)`, `utilisation(from, to)`, `summary(from, to)` |
|
|
61
|
+
|
|
62
|
+
`action(name, query)` runs any other CKAN action. `rangeSql`, `ident`, `literal` and
|
|
63
|
+
`normaliseRows` are exported for building your own queries; `PACKAGES` lists the dataset names
|
|
64
|
+
the helpers use.
|
|
65
|
+
|
|
66
|
+
## Things to know about the portal
|
|
67
|
+
|
|
68
|
+
- **One resource per financial year.** Thermal costs, the breakdown and daily balancing costs
|
|
69
|
+
are split into resources named like "Thermal Constraint Costs Data 25-26" (April–March). The
|
|
70
|
+
date-range helpers resolve the right resource(s), spanning years when needed; the pre-2021
|
|
71
|
+
thermal cost resources are XLSX and not in the datastore.
|
|
72
|
+
- **SQL returns numerics as strings** (`"252"`) while `datastore_search` returns numbers. Both
|
|
73
|
+
paths are normalised using the column types in `fields`; the `_full_text` search column is
|
|
74
|
+
dropped. "Thermal constraints cost" in the breakdown is typed `text` in some years and is
|
|
75
|
+
coerced too.
|
|
76
|
+
- **Local dates.** Settlement and delivery dates are GB calendar days and `Date_ Time GMT_BST`
|
|
77
|
+
in the day-ahead flows is GB local time with no offset. The helpers take "YYYY-MM-DD" strings
|
|
78
|
+
as given and read `Date` inputs as Europe/London calendar days.
|
|
79
|
+
- **A range reaching into a year NESO hasn't published yet** (1 April before the new "26-27"
|
|
80
|
+
resource appears) throws rather than returning the rows that do exist; narrow `to`.
|
|
81
|
+
- **Page size.** `datastore_search` returns everything up to 32,000 rows per request; the
|
|
82
|
+
day-ahead flows table is ~750k rows, so always filter it (the helper does, via SQL).
|
|
83
|
+
- **Rate limits.** None published; the licence only reserves the right to throttle excessive
|
|
84
|
+
use. Default 5 rps / burst 10 per host; package metadata is memoised for 10 minutes.
|
|
85
|
+
- **Dataset names** can change when NESO reorganises the portal; `PACKAGES` is one place to fix.
|
|
86
|
+
|
|
87
|
+
## Options
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
new NesoClient({
|
|
91
|
+
rateLimit: { requestsPerSecond: 5, burst: 10 }, // default
|
|
92
|
+
cache: new MemoryCache(), // optional response cache; TTL defaults to 10 minutes
|
|
93
|
+
cacheTtlMs: 10 * 60 * 1000, // also the package metadata memo TTL
|
|
94
|
+
retry: { retries: 3 }, timeoutMs: 30_000, fetch, // forwarded to @uk-energy/core HttpClient
|
|
95
|
+
});
|
|
96
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@uk-energy/core');
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
|
|
7
|
+
// src/resources.ts
|
|
8
|
+
function financialYearOf(date) {
|
|
9
|
+
const year = Number(date.slice(0, 4));
|
|
10
|
+
const month = Number(date.slice(5, 7));
|
|
11
|
+
return month >= 4 ? year : year - 1;
|
|
12
|
+
}
|
|
13
|
+
function financialYearsBetween(from, to) {
|
|
14
|
+
const first = financialYearOf(from);
|
|
15
|
+
const last = financialYearOf(to);
|
|
16
|
+
const out = [];
|
|
17
|
+
for (let y = first; y <= last; y++) out.push(y);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
var YEAR_SPAN = /(?<!\d)(\d{4}|\d{2})\s*[-/–]\s*(\d{4}|\d{2})(?!\d)/g;
|
|
21
|
+
function financialYearInName(name) {
|
|
22
|
+
for (const m of name.matchAll(YEAR_SPAN)) {
|
|
23
|
+
if (!m[1] || !m[2]) continue;
|
|
24
|
+
const start = m[1].length === 2 ? 2e3 + Number(m[1]) : Number(m[1]);
|
|
25
|
+
const end = m[2].length === 2 ? 2e3 + Number(m[2]) : Number(m[2]);
|
|
26
|
+
if (end === start + 1) return start;
|
|
27
|
+
}
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
function resourceForFinancialYear(resources, startYear) {
|
|
31
|
+
return resources.find((r) => r.datastore_active && financialYearInName(r.name) === startYear);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/client.ts
|
|
35
|
+
var BASE_URL = "https://api.neso.energy/api/3/action";
|
|
36
|
+
var DEFAULT_RATE_LIMIT = { requestsPerSecond: 5, burst: 10 };
|
|
37
|
+
var DEFAULT_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
38
|
+
var MAX_PAGE_SIZE = 32e3;
|
|
39
|
+
var PACKAGES = {
|
|
40
|
+
thermalConstraintCosts: "thermal-constraint-costs",
|
|
41
|
+
constraintBreakdown: "constraint-breakdown",
|
|
42
|
+
constraintCostForecast24m: "24-months-ahead-constraint-cost-forecast",
|
|
43
|
+
constraintLimits24m: "24-months-ahead-constraint-limits",
|
|
44
|
+
dayAheadConstraintFlows: "day-ahead-constraint-flows-and-limits",
|
|
45
|
+
dailyBalancingCosts: "daily-balancing-costs-balancing-services-use-of-system",
|
|
46
|
+
demandFlexibility: "demand-flexibility"
|
|
47
|
+
};
|
|
48
|
+
var DECIMAL = /^-?\d+(\.\d+)?([eE][-+]?\d+)?$/;
|
|
49
|
+
var NUMERIC_TYPES = /* @__PURE__ */ new Set([
|
|
50
|
+
"numeric",
|
|
51
|
+
"int4",
|
|
52
|
+
"int8",
|
|
53
|
+
"float8",
|
|
54
|
+
"float4",
|
|
55
|
+
"int",
|
|
56
|
+
"double precision"
|
|
57
|
+
]);
|
|
58
|
+
function normaliseRows(records, fields, extraNumeric = []) {
|
|
59
|
+
const numeric = new Set(extraNumeric);
|
|
60
|
+
for (const f of fields) if (NUMERIC_TYPES.has(f.type)) numeric.add(f.id);
|
|
61
|
+
return records.map((r) => {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [k, v] of Object.entries(r)) {
|
|
64
|
+
if (k === "_full_text") continue;
|
|
65
|
+
if (numeric.has(k) && typeof v === "string" && DECIMAL.test(v.trim())) {
|
|
66
|
+
const n = Number(v);
|
|
67
|
+
out[k] = Number.isFinite(n) ? n : v;
|
|
68
|
+
} else {
|
|
69
|
+
out[k] = v;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function ident(name) {
|
|
76
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
77
|
+
}
|
|
78
|
+
function literal(value) {
|
|
79
|
+
return typeof value === "number" ? String(value) : `'${value.replace(/'/g, "''")}'`;
|
|
80
|
+
}
|
|
81
|
+
function rangeSql(resourceId, column, from, to, orderBy, opts = {}) {
|
|
82
|
+
const upper = opts.toExclusive ? "<" : "<=";
|
|
83
|
+
const extra = opts.where ? ` AND ${opts.where}` : "";
|
|
84
|
+
const order = orderBy.length ? ` ORDER BY ${orderBy.map(ident).join(", ")}` : "";
|
|
85
|
+
return `SELECT * FROM ${ident(resourceId)} WHERE ${ident(column)} >= ${literal(from)} AND ${ident(column)} ${upper} ${literal(to)}${extra}${order}`;
|
|
86
|
+
}
|
|
87
|
+
function localRange(from, to) {
|
|
88
|
+
const f = core.londonDate(from);
|
|
89
|
+
const t = core.londonDate(to);
|
|
90
|
+
if (f > t) throw new core.UkEnergyError(`from (${f}) is after to (${t})`);
|
|
91
|
+
return [f, t];
|
|
92
|
+
}
|
|
93
|
+
function nextDay(date) {
|
|
94
|
+
const d = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
|
|
95
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
96
|
+
return d.toISOString().slice(0, 10);
|
|
97
|
+
}
|
|
98
|
+
var NesoClient = class {
|
|
99
|
+
http;
|
|
100
|
+
packageCache = /* @__PURE__ */ new Map();
|
|
101
|
+
packageTtlMs;
|
|
102
|
+
constructor(opts = {}) {
|
|
103
|
+
this.http = new core.HttpClient({
|
|
104
|
+
baseUrl: BASE_URL,
|
|
105
|
+
rateLimit: DEFAULT_RATE_LIMIT,
|
|
106
|
+
cacheTtlMs: opts.cache ? DEFAULT_CACHE_TTL_MS : 0,
|
|
107
|
+
...opts
|
|
108
|
+
});
|
|
109
|
+
this.packageTtlMs = opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
110
|
+
}
|
|
111
|
+
/** Any CKAN action; unwraps `{ success, result }` and throws `UkEnergyError` on `success: false`. */
|
|
112
|
+
async action(name, query) {
|
|
113
|
+
const res = await this.http.get(name, query);
|
|
114
|
+
if (!res || typeof res !== "object" || !("success" in res)) {
|
|
115
|
+
throw new core.UkEnergyError(`Unexpected response shape for ${name}`);
|
|
116
|
+
}
|
|
117
|
+
if (!res.success) {
|
|
118
|
+
throw new core.UkEnergyError(`${name}: ${res.error?.message ?? JSON.stringify(res.error)}`);
|
|
119
|
+
}
|
|
120
|
+
return res.result;
|
|
121
|
+
}
|
|
122
|
+
/** Raw CKAN API. */
|
|
123
|
+
ckan = {
|
|
124
|
+
/** Dataset metadata and its resources (memoised for `cacheTtlMs`, default 10 minutes). */
|
|
125
|
+
packageShow: async (id) => {
|
|
126
|
+
const hit = this.packageCache.get(id);
|
|
127
|
+
if (hit && Date.now() - hit.at < this.packageTtlMs) return hit.value;
|
|
128
|
+
const value = this.action("package_show", { id }).then((pkg) => {
|
|
129
|
+
if (!pkg || !Array.isArray(pkg.resources)) {
|
|
130
|
+
throw new core.UkEnergyError(`package_show: unexpected shape for ${id}`);
|
|
131
|
+
}
|
|
132
|
+
return pkg;
|
|
133
|
+
});
|
|
134
|
+
this.packageCache.set(id, { at: Date.now(), value });
|
|
135
|
+
value.catch(() => this.packageCache.delete(id));
|
|
136
|
+
return value;
|
|
137
|
+
},
|
|
138
|
+
/** Search datasets by free text. */
|
|
139
|
+
packageSearch: async (q, rows = 20) => this.action("package_search", { q, rows }),
|
|
140
|
+
resourceShow: async (id) => this.action("resource_show", { id }),
|
|
141
|
+
/** One page of a datastore resource (up to 32,000 rows). Rows are normalised (see `normaliseRows`). */
|
|
142
|
+
datastoreSearch: async (resourceId, opts = {}) => {
|
|
143
|
+
const query = {
|
|
144
|
+
resource_id: resourceId,
|
|
145
|
+
limit: opts.limit ?? MAX_PAGE_SIZE,
|
|
146
|
+
sort: opts.sort ?? "_id"
|
|
147
|
+
};
|
|
148
|
+
if (opts.offset !== void 0) query.offset = opts.offset;
|
|
149
|
+
if (opts.filters) query.filters = JSON.stringify(opts.filters);
|
|
150
|
+
if (opts.fields) query.fields = opts.fields.join(",");
|
|
151
|
+
if (opts.q) query.q = opts.q;
|
|
152
|
+
const res = await this.action("datastore_search", query);
|
|
153
|
+
if (!Array.isArray(res?.records)) {
|
|
154
|
+
throw new core.UkEnergyError("Unexpected response shape for datastore_search");
|
|
155
|
+
}
|
|
156
|
+
return { ...res, records: normaliseRows(res.records, res.fields ?? []) };
|
|
157
|
+
},
|
|
158
|
+
/** Every row of a datastore resource, paging by `limit` (default 32,000). */
|
|
159
|
+
datastoreAll: async (resourceId, opts = {}) => {
|
|
160
|
+
const pageSize = Math.min(opts.limit ?? MAX_PAGE_SIZE, MAX_PAGE_SIZE);
|
|
161
|
+
const out = [];
|
|
162
|
+
for await (const page of core.pages({
|
|
163
|
+
fetchPage: (offset) => this.ckan.datastoreSearch(resourceId, {
|
|
164
|
+
...opts,
|
|
165
|
+
limit: pageSize,
|
|
166
|
+
offset: offset ?? 0
|
|
167
|
+
}),
|
|
168
|
+
items: (p) => p.records,
|
|
169
|
+
nextCursor: core.offsetCursor(pageSize, (p) => p.records.length)
|
|
170
|
+
})) {
|
|
171
|
+
out.push(...page.records);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
},
|
|
175
|
+
/**
|
|
176
|
+
* `datastore_search_sql`: read-only PostgreSQL over the datastore. Resource ids are the
|
|
177
|
+
* table names (quote them). Numeric columns come back as strings from this endpoint; they
|
|
178
|
+
* are converted using the `fields` types, and `_full_text` is dropped.
|
|
179
|
+
*/
|
|
180
|
+
sql: async (sql, extraNumeric = []) => {
|
|
181
|
+
const res = await this.action("datastore_search_sql", {
|
|
182
|
+
sql
|
|
183
|
+
});
|
|
184
|
+
if (!Array.isArray(res?.records)) {
|
|
185
|
+
throw new core.UkEnergyError("Unexpected response shape for datastore_search_sql");
|
|
186
|
+
}
|
|
187
|
+
return normaliseRows(res.records, res.fields ?? [], extraNumeric);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
/** Resolving resources inside datasets. */
|
|
191
|
+
resources = {
|
|
192
|
+
/** All datastore-backed resources of a dataset. */
|
|
193
|
+
datastore: async (packageId) => (await this.ckan.packageShow(packageId)).resources.filter((r) => r.datastore_active),
|
|
194
|
+
/** The resource for a GB financial year (`2025` = April 2025 – March 2026), by name. */
|
|
195
|
+
forFinancialYear: async (packageId, startYear) => {
|
|
196
|
+
const pkg = await this.ckan.packageShow(packageId);
|
|
197
|
+
const r = resourceForFinancialYear(pkg.resources, startYear);
|
|
198
|
+
if (!r) {
|
|
199
|
+
throw new core.UkEnergyError(
|
|
200
|
+
`${packageId}: no datastore resource for financial year ${startYear}/${(startYear + 1) % 100}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return r;
|
|
204
|
+
},
|
|
205
|
+
/** The datastore resource whose name matches exactly (or the regex), e.g. "DFS Utilisation Report". */
|
|
206
|
+
byName: async (packageId, name) => {
|
|
207
|
+
const list = await this.resources.datastore(packageId);
|
|
208
|
+
const r = list.find(
|
|
209
|
+
(x) => typeof name === "string" ? x.name.trim() === name.trim() : name.test(x.name)
|
|
210
|
+
);
|
|
211
|
+
if (!r) throw new core.UkEnergyError(`${packageId}: no datastore resource named ${String(name)}`);
|
|
212
|
+
return r;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
/** Rows of a per-financial-year dataset for an inclusive date range, across year boundaries. */
|
|
216
|
+
async byFinancialYear(packageId, column, range, orderBy, opts = {}) {
|
|
217
|
+
const [from, to] = localRange(range.from, range.to);
|
|
218
|
+
const out = [];
|
|
219
|
+
for (const fy of financialYearsBetween(from, to)) {
|
|
220
|
+
const resource = await this.resources.forFinancialYear(packageId, fy);
|
|
221
|
+
const sql = rangeSql(resource.id, column, from, to, orderBy, { where: opts.where });
|
|
222
|
+
out.push(...await this.ckan.sql(sql, opts.extraNumeric));
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
/** Transmission constraint costs and forecasts. */
|
|
227
|
+
constraints = {
|
|
228
|
+
/**
|
|
229
|
+
* Thermal constraint cost per boundary per day (£), `thermal-constraint-costs`. One row per
|
|
230
|
+
* constraint group per settlement date, e.g. SCOTEX, SSHARN, SEIMP (Scottish export
|
|
231
|
+
* boundaries), ESTEX, SWALEX.
|
|
232
|
+
*/
|
|
233
|
+
thermalCosts: async (from, to, constraintGroup) => this.byFinancialYear(
|
|
234
|
+
PACKAGES.thermalConstraintCosts,
|
|
235
|
+
"Settlement Date",
|
|
236
|
+
{ from, to },
|
|
237
|
+
["Settlement Date", "Constraint Group"],
|
|
238
|
+
constraintGroup ? { where: `${ident("Constraint Group")} = ${literal(constraintGroup)}` } : {}
|
|
239
|
+
),
|
|
240
|
+
/**
|
|
241
|
+
* Daily constraint cost (£) and volume (MWh) by type: thermal, voltage, inertia, largest
|
|
242
|
+
* loss (`constraint-breakdown`). Thermal volume is negative (energy turned down).
|
|
243
|
+
*/
|
|
244
|
+
breakdown: async (from, to) => this.byFinancialYear(PACKAGES.constraintBreakdown, "Date", { from, to }, ["Date"], {
|
|
245
|
+
// "Thermal constraints cost" is typed text in some years
|
|
246
|
+
extraNumeric: ["Thermal constraints cost"]
|
|
247
|
+
}),
|
|
248
|
+
/** NESO's 24-month-ahead constraint cost forecast, £m per month (24 rows). */
|
|
249
|
+
forecast24m: async () => {
|
|
250
|
+
const r = await this.resources.byName(PACKAGES.constraintCostForecast24m, /Forecast/);
|
|
251
|
+
return this.ckan.datastoreAll(r.id);
|
|
252
|
+
},
|
|
253
|
+
/** 24-month-ahead boundary limits (MW) per week; boundary columns vary by publication. */
|
|
254
|
+
limits24m: async () => {
|
|
255
|
+
const r = await this.resources.byName(PACKAGES.constraintLimits24m, /limits/i);
|
|
256
|
+
return this.ckan.datastoreAll(r.id);
|
|
257
|
+
},
|
|
258
|
+
/**
|
|
259
|
+
* Day-ahead forecast flow vs limit per boundary, half-hourly, for a range of local dates
|
|
260
|
+
* (`day-ahead-constraint-flows-and-limits`, ~750k rows, so always filtered by SQL).
|
|
261
|
+
*/
|
|
262
|
+
dayAheadFlows: async (from, to, constraintGroup) => {
|
|
263
|
+
const [f, t] = localRange(from, to);
|
|
264
|
+
const r = await this.resources.byName(PACKAGES.dayAheadConstraintFlows, /Flows and Limits/);
|
|
265
|
+
const sql = rangeSql(
|
|
266
|
+
r.id,
|
|
267
|
+
"Date_ Time GMT_BST",
|
|
268
|
+
`${f}T00:00:00`,
|
|
269
|
+
`${nextDay(t)}T00:00:00`,
|
|
270
|
+
["Constraint Group", "Date_ Time GMT_BST"],
|
|
271
|
+
{
|
|
272
|
+
toExclusive: true,
|
|
273
|
+
where: constraintGroup ? `${ident("Constraint Group")} = ${literal(constraintGroup)}` : void 0
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
return this.ckan.sql(sql);
|
|
277
|
+
},
|
|
278
|
+
/** Daily balancing (BSUoS) costs per settlement period by category, incl. `Constraints` (£). */
|
|
279
|
+
dailyBalancingCosts: async (from, to) => this.byFinancialYear(PACKAGES.dailyBalancingCosts, "SETT_DATE", { from, to }, [
|
|
280
|
+
"SETT_DATE",
|
|
281
|
+
"SETT_PERIOD"
|
|
282
|
+
])
|
|
283
|
+
};
|
|
284
|
+
/** Demand Flexibility Service (current season; archives are separate resources). */
|
|
285
|
+
dfs = {
|
|
286
|
+
/** Published requirements per event window, by delivery date. */
|
|
287
|
+
requirements: async (from, to) => this.dfsRange("DFS Service Requirement", from, to, [
|
|
288
|
+
"Event ID",
|
|
289
|
+
"From_UTC"
|
|
290
|
+
]),
|
|
291
|
+
/** Accepted/rejected bids per participant unit per event window. */
|
|
292
|
+
utilisation: async (from, to) => this.dfsRange("DFS Utilisation Report", from, to, [
|
|
293
|
+
"Event ID",
|
|
294
|
+
"From_UTC",
|
|
295
|
+
"DFS Unit ID"
|
|
296
|
+
]),
|
|
297
|
+
/** Procured MW, accepted cost and settled volume/cost per event window. */
|
|
298
|
+
summary: async (from, to) => this.dfsRange("DFS Utilisation Report Summary", from, to, [
|
|
299
|
+
"Event ID",
|
|
300
|
+
"From_UTC"
|
|
301
|
+
])
|
|
302
|
+
};
|
|
303
|
+
async dfsRange(name, from, to, orderBy) {
|
|
304
|
+
const [f, t] = localRange(from, to);
|
|
305
|
+
const r = await this.resources.byName(PACKAGES.demandFlexibility, name);
|
|
306
|
+
return this.ckan.sql(rangeSql(r.id, "Delivery Date", f, t, orderBy));
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
exports.BASE_URL = BASE_URL;
|
|
311
|
+
exports.DEFAULT_CACHE_TTL_MS = DEFAULT_CACHE_TTL_MS;
|
|
312
|
+
exports.DEFAULT_RATE_LIMIT = DEFAULT_RATE_LIMIT;
|
|
313
|
+
exports.MAX_PAGE_SIZE = MAX_PAGE_SIZE;
|
|
314
|
+
exports.NesoClient = NesoClient;
|
|
315
|
+
exports.PACKAGES = PACKAGES;
|
|
316
|
+
exports.financialYearInName = financialYearInName;
|
|
317
|
+
exports.financialYearOf = financialYearOf;
|
|
318
|
+
exports.financialYearsBetween = financialYearsBetween;
|
|
319
|
+
exports.ident = ident;
|
|
320
|
+
exports.literal = literal;
|
|
321
|
+
exports.normaliseRows = normaliseRows;
|
|
322
|
+
exports.rangeSql = rangeSql;
|
|
323
|
+
exports.resourceForFinancialYear = resourceForFinancialYear;
|
|
324
|
+
//# sourceMappingURL=index.cjs.map
|
|
325
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resources.ts","../src/client.ts"],"names":["londonDate","UkEnergyError","HttpClient","pages","offsetCursor"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Financial-year arithmetic and resource-name matching for datasets split one resource per\n * GB financial year (1 April – 31 March): \"Thermal Constraint Costs Data 25-26\",\n * \"Constraint Breakdown 2025-2026\", \"Daily Balancing Costs 2025-2026\".\n */\nimport type { CkanResource } from \"./types.js\";\n\n/** Financial year a date (\"YYYY-MM-DD\") falls in, as its starting calendar year. */\nexport function financialYearOf(date: string): number {\n const year = Number(date.slice(0, 4));\n const month = Number(date.slice(5, 7));\n return month >= 4 ? year : year - 1;\n}\n\n/** Starting years of every financial year touched by an inclusive date range. */\nexport function financialYearsBetween(from: string, to: string): number[] {\n const first = financialYearOf(from);\n const last = financialYearOf(to);\n const out: number[] = [];\n for (let y = first; y <= last; y++) out.push(y);\n return out;\n}\n\nconst YEAR_SPAN = /(?<!\\d)(\\d{4}|\\d{2})\\s*[-/–]\\s*(\\d{4}|\\d{2})(?!\\d)/g;\n\n/** The financial year a resource name refers to (\"25-26\" / \"2025-2026\" → 2025), or undefined. */\nexport function financialYearInName(name: string): number | undefined {\n for (const m of name.matchAll(YEAR_SPAN)) {\n if (!m[1] || !m[2]) continue;\n const start = m[1].length === 2 ? 2000 + Number(m[1]) : Number(m[1]);\n const end = m[2].length === 2 ? 2000 + Number(m[2]) : Number(m[2]);\n if (end === start + 1) return start;\n }\n return undefined;\n}\n\n/** The datastore resource of a dataset for one financial year, if any. */\nexport function resourceForFinancialYear(\n resources: readonly CkanResource[],\n startYear: number,\n): CkanResource | undefined {\n return resources.find((r) => r.datastore_active && financialYearInName(r.name) === startYear);\n}\n","/**\n * Client for the NESO Data Portal (https://www.neso.energy/data-portal), a CKAN 2.8 instance at\n * https://api.neso.energy/api/3/action/. No API key. Datasets (\"packages\") hold \"resources\";\n * tabular resources are loaded into a Postgres datastore that `datastore_search` and\n * `datastore_search_sql` query. Many datasets split one resource per GB financial year\n * (April–March), so the helpers resolve the right resource(s) for a date range.\n *\n * const neso = new NesoClient();\n * const costs = await neso.constraints.thermalCosts(\"2025-04-01\", \"2025-04-07\"); // £/day per boundary\n * const dfs = await neso.dfs.summary(\"2026-10-20\", \"2026-10-22\");\n * const rows = await neso.ckan.sql(`SELECT * FROM \"407f3e9d-…\" WHERE \"Settlement Date\" = '2025-04-01'`);\n *\n * Licence: National Energy SO Open Data Licence v1.0 (OGL v3 based): copy, adapt, use\n * commercially, with the attribution \"Supported by National Energy SO Open Data\".\n */\nimport {\n type DateInput,\n HttpClient,\n type HttpClientOptions,\n londonDate,\n offsetCursor,\n pages,\n type Query,\n type RateLimit,\n UkEnergyError,\n} from \"@uk-energy/core\";\nimport { financialYearsBetween, resourceForFinancialYear } from \"./resources.js\";\nimport type {\n CkanPackage,\n CkanPackageSearch,\n CkanResource,\n CkanResponse,\n ConstraintBreakdownRow,\n ConstraintForecastRow,\n ConstraintLimitRow,\n DailyBalancingCostRow,\n DatastoreField,\n DatastoreRow,\n DatastoreSearchOptions,\n DatastoreSearchResult,\n DatastoreSqlResult,\n DayAheadFlowRow,\n DfsRequirementRow,\n DfsSummaryRow,\n DfsUtilisationRow,\n ThermalConstraintCostRow,\n} from \"./types.js\";\n\nexport const BASE_URL = \"https://api.neso.energy/api/3/action\";\n\n/**\n * No limit is published; the licence only reserves the right to throttle \"excessive use\".\n * 5 rps / burst 10 is comfortably polite for a shared public CKAN.\n */\nexport const DEFAULT_RATE_LIMIT: RateLimit = { requestsPerSecond: 5, burst: 10 };\n\n/** Resources update daily or weekly; package metadata is memoised for this long. */\nexport const DEFAULT_CACHE_TTL_MS = 10 * 60 * 1000;\n\n/** The portal returns up to this many rows per `datastore_search` page. */\nexport const MAX_PAGE_SIZE = 32_000;\n\n/** Dataset (package) names on the portal that the helpers use. */\nexport const PACKAGES = {\n thermalConstraintCosts: \"thermal-constraint-costs\",\n constraintBreakdown: \"constraint-breakdown\",\n constraintCostForecast24m: \"24-months-ahead-constraint-cost-forecast\",\n constraintLimits24m: \"24-months-ahead-constraint-limits\",\n dayAheadConstraintFlows: \"day-ahead-constraint-flows-and-limits\",\n dailyBalancingCosts: \"daily-balancing-costs-balancing-services-use-of-system\",\n demandFlexibility: \"demand-flexibility\",\n} as const;\n\nexport interface NesoClientOptions extends Omit<HttpClientOptions, \"baseUrl\"> {\n baseUrl?: string;\n}\n\n/** Inclusive date range; strings are \"YYYY-MM-DD\", `Date`s are GB-local calendar days. */\ninterface DateRange {\n from: DateInput;\n to: DateInput;\n}\n\nconst DECIMAL = /^-?\\d+(\\.\\d+)?([eE][-+]?\\d+)?$/;\n\nconst NUMERIC_TYPES = new Set([\n \"numeric\",\n \"int4\",\n \"int8\",\n \"float8\",\n \"float4\",\n \"int\",\n \"double precision\",\n]);\n\n/**\n * The SQL endpoint returns `numeric` columns as strings (\"252\"); `datastore_search` returns\n * numbers. Make rows consistent: numeric-typed columns become numbers, `_full_text` goes.\n */\nexport function normaliseRows<Row extends object>(\n records: readonly DatastoreRow[],\n fields: readonly DatastoreField[],\n extraNumeric: readonly string[] = [],\n): Row[] {\n const numeric = new Set(extraNumeric);\n for (const f of fields) if (NUMERIC_TYPES.has(f.type)) numeric.add(f.id);\n return records.map((r) => {\n const out: DatastoreRow = {};\n for (const [k, v] of Object.entries(r)) {\n if (k === \"_full_text\") continue;\n if (numeric.has(k) && typeof v === \"string\" && DECIMAL.test(v.trim())) {\n const n = Number(v);\n out[k] = Number.isFinite(n) ? n : v;\n } else {\n out[k] = v;\n }\n }\n return out as Row;\n });\n}\n\n/** Double-quote an identifier for the SQL endpoint. */\nexport function ident(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/** Single-quote a literal for the SQL endpoint. */\nexport function literal(value: string | number): string {\n return typeof value === \"number\" ? String(value) : `'${value.replace(/'/g, \"''\")}'`;\n}\n\n/**\n * `SELECT * FROM \"resource\" WHERE \"col\" >= 'from' AND \"col\" <= 'to' ORDER BY …` — the shape\n * every date-range helper uses. `toExclusive` switches the upper bound to `<` for timestamps.\n */\nexport function rangeSql(\n resourceId: string,\n column: string,\n from: string,\n to: string,\n orderBy: readonly string[],\n opts: { toExclusive?: boolean; where?: string | undefined } = {},\n): string {\n const upper = opts.toExclusive ? \"<\" : \"<=\";\n const extra = opts.where ? ` AND ${opts.where}` : \"\";\n const order = orderBy.length ? ` ORDER BY ${orderBy.map(ident).join(\", \")}` : \"\";\n return `SELECT * FROM ${ident(resourceId)} WHERE ${ident(column)} >= ${literal(from)} AND ${ident(column)} ${upper} ${literal(to)}${extra}${order}`;\n}\n\n/** Inclusive GB-local date range: `Date`s are read as Europe/London calendar days. */\nfunction localRange(from: DateInput, to: DateInput): [string, string] {\n const f = londonDate(from);\n const t = londonDate(to);\n if (f > t) throw new UkEnergyError(`from (${f}) is after to (${t})`);\n return [f, t];\n}\n\nfunction nextDay(date: string): string {\n const d = new Date(`${date}T00:00:00Z`);\n d.setUTCDate(d.getUTCDate() + 1);\n return d.toISOString().slice(0, 10);\n}\n\nexport class NesoClient {\n readonly http: HttpClient;\n private readonly packageCache = new Map<string, { at: number; value: Promise<CkanPackage> }>();\n private readonly packageTtlMs: number;\n\n constructor(opts: NesoClientOptions = {}) {\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 this.packageTtlMs = opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;\n }\n\n /** Any CKAN action; unwraps `{ success, result }` and throws `UkEnergyError` on `success: false`. */\n async action<T>(name: string, query?: Query): Promise<T> {\n const res = await this.http.get<CkanResponse<T>>(name, query);\n if (!res || typeof res !== \"object\" || !(\"success\" in res)) {\n throw new UkEnergyError(`Unexpected response shape for ${name}`);\n }\n if (!res.success) {\n throw new UkEnergyError(`${name}: ${res.error?.message ?? JSON.stringify(res.error)}`);\n }\n return res.result;\n }\n\n /** Raw CKAN API. */\n readonly ckan = {\n /** Dataset metadata and its resources (memoised for `cacheTtlMs`, default 10 minutes). */\n packageShow: async (id: string): Promise<CkanPackage> => {\n const hit = this.packageCache.get(id);\n if (hit && Date.now() - hit.at < this.packageTtlMs) return hit.value;\n const value = this.action<CkanPackage>(\"package_show\", { id }).then((pkg) => {\n if (!pkg || !Array.isArray(pkg.resources)) {\n throw new UkEnergyError(`package_show: unexpected shape for ${id}`);\n }\n return pkg;\n });\n this.packageCache.set(id, { at: Date.now(), value });\n value.catch(() => this.packageCache.delete(id));\n return value;\n },\n /** Search datasets by free text. */\n packageSearch: async (q: string, rows = 20): Promise<CkanPackageSearch> =>\n this.action(\"package_search\", { q, rows }),\n resourceShow: async (id: string): Promise<CkanResource> => this.action(\"resource_show\", { id }),\n /** One page of a datastore resource (up to 32,000 rows). Rows are normalised (see `normaliseRows`). */\n datastoreSearch: async <Row extends object = DatastoreRow>(\n resourceId: string,\n opts: DatastoreSearchOptions = {},\n ): Promise<DatastoreSearchResult<Row>> => {\n const query: Query = {\n resource_id: resourceId,\n limit: opts.limit ?? MAX_PAGE_SIZE,\n sort: opts.sort ?? \"_id\",\n };\n if (opts.offset !== undefined) query.offset = opts.offset;\n if (opts.filters) query.filters = JSON.stringify(opts.filters);\n if (opts.fields) query.fields = opts.fields.join(\",\");\n if (opts.q) query.q = opts.q;\n const res = await this.action<DatastoreSearchResult<DatastoreRow>>(\"datastore_search\", query);\n if (!Array.isArray(res?.records)) {\n throw new UkEnergyError(\"Unexpected response shape for datastore_search\");\n }\n return { ...res, records: normaliseRows<Row>(res.records, res.fields ?? []) };\n },\n /** Every row of a datastore resource, paging by `limit` (default 32,000). */\n datastoreAll: async <Row extends object = DatastoreRow>(\n resourceId: string,\n opts: Omit<DatastoreSearchOptions, \"offset\"> = {},\n ): Promise<Row[]> => {\n const pageSize = Math.min(opts.limit ?? MAX_PAGE_SIZE, MAX_PAGE_SIZE);\n const out: Row[] = [];\n for await (const page of pages<DatastoreSearchResult<Row>, Row, number>({\n fetchPage: (offset) =>\n this.ckan.datastoreSearch<Row>(resourceId, {\n ...opts,\n limit: pageSize,\n offset: offset ?? 0,\n }),\n items: (p) => p.records,\n nextCursor: offsetCursor(pageSize, (p) => p.records.length),\n })) {\n out.push(...page.records);\n }\n return out;\n },\n /**\n * `datastore_search_sql`: read-only PostgreSQL over the datastore. Resource ids are the\n * table names (quote them). Numeric columns come back as strings from this endpoint; they\n * are converted using the `fields` types, and `_full_text` is dropped.\n */\n sql: async <Row extends object = DatastoreRow>(\n sql: string,\n extraNumeric: readonly string[] = [],\n ): Promise<Row[]> => {\n const res = await this.action<DatastoreSqlResult<DatastoreRow>>(\"datastore_search_sql\", {\n sql,\n });\n if (!Array.isArray(res?.records)) {\n throw new UkEnergyError(\"Unexpected response shape for datastore_search_sql\");\n }\n return normaliseRows<Row>(res.records, res.fields ?? [], extraNumeric);\n },\n };\n\n /** Resolving resources inside datasets. */\n readonly resources = {\n /** All datastore-backed resources of a dataset. */\n datastore: async (packageId: string): Promise<CkanResource[]> =>\n (await this.ckan.packageShow(packageId)).resources.filter((r) => r.datastore_active),\n /** The resource for a GB financial year (`2025` = April 2025 – March 2026), by name. */\n forFinancialYear: async (packageId: string, startYear: number): Promise<CkanResource> => {\n const pkg = await this.ckan.packageShow(packageId);\n const r = resourceForFinancialYear(pkg.resources, startYear);\n if (!r) {\n throw new UkEnergyError(\n `${packageId}: no datastore resource for financial year ${startYear}/${(startYear + 1) % 100}`,\n );\n }\n return r;\n },\n /** The datastore resource whose name matches exactly (or the regex), e.g. \"DFS Utilisation Report\". */\n byName: async (packageId: string, name: string | RegExp): Promise<CkanResource> => {\n const list = await this.resources.datastore(packageId);\n const r = list.find((x) =>\n typeof name === \"string\" ? x.name.trim() === name.trim() : name.test(x.name),\n );\n if (!r) throw new UkEnergyError(`${packageId}: no datastore resource named ${String(name)}`);\n return r;\n },\n };\n\n /** Rows of a per-financial-year dataset for an inclusive date range, across year boundaries. */\n private async byFinancialYear<Row extends object>(\n packageId: string,\n column: string,\n range: DateRange,\n orderBy: readonly string[],\n opts: { where?: string | undefined; extraNumeric?: readonly string[] } = {},\n ): Promise<Row[]> {\n const [from, to] = localRange(range.from, range.to);\n const out: Row[] = [];\n for (const fy of financialYearsBetween(from, to)) {\n const resource = await this.resources.forFinancialYear(packageId, fy);\n const sql = rangeSql(resource.id, column, from, to, orderBy, { where: opts.where });\n out.push(...(await this.ckan.sql<Row>(sql, opts.extraNumeric)));\n }\n return out;\n }\n\n /** Transmission constraint costs and forecasts. */\n readonly constraints = {\n /**\n * Thermal constraint cost per boundary per day (£), `thermal-constraint-costs`. One row per\n * constraint group per settlement date, e.g. SCOTEX, SSHARN, SEIMP (Scottish export\n * boundaries), ESTEX, SWALEX.\n */\n thermalCosts: async (\n from: DateInput,\n to: DateInput,\n constraintGroup?: string,\n ): Promise<ThermalConstraintCostRow[]> =>\n this.byFinancialYear(\n PACKAGES.thermalConstraintCosts,\n \"Settlement Date\",\n { from, to },\n [\"Settlement Date\", \"Constraint Group\"],\n constraintGroup\n ? { where: `${ident(\"Constraint Group\")} = ${literal(constraintGroup)}` }\n : {},\n ),\n /**\n * Daily constraint cost (£) and volume (MWh) by type: thermal, voltage, inertia, largest\n * loss (`constraint-breakdown`). Thermal volume is negative (energy turned down).\n */\n breakdown: async (from: DateInput, to: DateInput): Promise<ConstraintBreakdownRow[]> =>\n this.byFinancialYear(PACKAGES.constraintBreakdown, \"Date\", { from, to }, [\"Date\"], {\n // \"Thermal constraints cost\" is typed text in some years\n extraNumeric: [\"Thermal constraints cost\"],\n }),\n /** NESO's 24-month-ahead constraint cost forecast, £m per month (24 rows). */\n forecast24m: async (): Promise<ConstraintForecastRow[]> => {\n const r = await this.resources.byName(PACKAGES.constraintCostForecast24m, /Forecast/);\n return this.ckan.datastoreAll<ConstraintForecastRow>(r.id);\n },\n /** 24-month-ahead boundary limits (MW) per week; boundary columns vary by publication. */\n limits24m: async (): Promise<ConstraintLimitRow[]> => {\n const r = await this.resources.byName(PACKAGES.constraintLimits24m, /limits/i);\n return this.ckan.datastoreAll<ConstraintLimitRow>(r.id);\n },\n /**\n * Day-ahead forecast flow vs limit per boundary, half-hourly, for a range of local dates\n * (`day-ahead-constraint-flows-and-limits`, ~750k rows, so always filtered by SQL).\n */\n dayAheadFlows: async (\n from: DateInput,\n to: DateInput,\n constraintGroup?: string,\n ): Promise<DayAheadFlowRow[]> => {\n const [f, t] = localRange(from, to);\n const r = await this.resources.byName(PACKAGES.dayAheadConstraintFlows, /Flows and Limits/);\n const sql = rangeSql(\n r.id,\n \"Date_ Time GMT_BST\",\n `${f}T00:00:00`,\n `${nextDay(t)}T00:00:00`,\n [\"Constraint Group\", \"Date_ Time GMT_BST\"],\n {\n toExclusive: true,\n where: constraintGroup\n ? `${ident(\"Constraint Group\")} = ${literal(constraintGroup)}`\n : undefined,\n },\n );\n return this.ckan.sql<DayAheadFlowRow>(sql);\n },\n /** Daily balancing (BSUoS) costs per settlement period by category, incl. `Constraints` (£). */\n dailyBalancingCosts: async (from: DateInput, to: DateInput): Promise<DailyBalancingCostRow[]> =>\n this.byFinancialYear(PACKAGES.dailyBalancingCosts, \"SETT_DATE\", { from, to }, [\n \"SETT_DATE\",\n \"SETT_PERIOD\",\n ]),\n };\n\n /** Demand Flexibility Service (current season; archives are separate resources). */\n readonly dfs = {\n /** Published requirements per event window, by delivery date. */\n requirements: async (from: DateInput, to: DateInput): Promise<DfsRequirementRow[]> =>\n this.dfsRange<DfsRequirementRow>(\"DFS Service Requirement\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n ]),\n /** Accepted/rejected bids per participant unit per event window. */\n utilisation: async (from: DateInput, to: DateInput): Promise<DfsUtilisationRow[]> =>\n this.dfsRange<DfsUtilisationRow>(\"DFS Utilisation Report\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n \"DFS Unit ID\",\n ]),\n /** Procured MW, accepted cost and settled volume/cost per event window. */\n summary: async (from: DateInput, to: DateInput): Promise<DfsSummaryRow[]> =>\n this.dfsRange<DfsSummaryRow>(\"DFS Utilisation Report Summary\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n ]),\n };\n\n private async dfsRange<Row extends object>(\n name: string,\n from: DateInput,\n to: DateInput,\n orderBy: readonly string[],\n ): Promise<Row[]> {\n const [f, t] = localRange(from, to);\n const r = await this.resources.byName(PACKAGES.demandFlexibility, name);\n return this.ckan.sql<Row>(rangeSql(r.id, \"Delivery Date\", f, t, orderBy));\n }\n}\n"]}
|