@uk-energy/neso
v0.1.1
Published
Typed client for the NESO Data Portal (CKAN): datastore search and SQL, thermal constraint costs, constraint breakdown and forecasts, day-ahead boundary flows, Demand Flexibility Service, daily balancing costs. Part of uk-energy-sdk.
Maintainers
Readme
@uk-energy/neso
Typed client for the NESO Data Portal: transmission constraint costs and forecasts, day-ahead boundary flows, Demand Flexibility Service and daily balancing costs, plus the raw CKAN datastore and SQL endpoints for the other ~800 datasets. No API key.
Supported by National Energy SO Open Data. Keep this line wherever you show the data: it is the attribution the NESO Open Data Licence v1.0 (OGL v3-based; commercial use and redistribution allowed) requires.
Python mirror: uk_energy.neso in the uk-energy distribution.
pnpm add @uk-energy/nesoUsage
import { NesoClient } from "@uk-energy/neso";
const neso = new NesoClient();
// Thermal constraint cost per boundary per day (£) — SCOTEX, SSHARN, SEIMP are the Scottish export boundaries
const costs = await neso.constraints.thermalCosts("2025-04-01", "2025-04-30");
const scotex = costs.filter((r) => r["Constraint Group"] === "SCOTEX").reduce((s, r) => s + r["Daily Cost (GBP)"], 0);
// Daily constraint cost and volume by type (thermal, voltage, inertia, largest loss)
const breakdown = await neso.constraints.breakdown("2025-04-01", "2025-04-30");
// NESO's forward view: £m per month for 24 months, and MW limits per boundary per week
const forecast = await neso.constraints.forecast24m();
const limits = await neso.constraints.limits24m();
// Day-ahead forecast flow vs limit, half-hourly, per boundary (filterable)
const flows = await neso.constraints.dayAheadFlows("2026-10-15", "2026-10-15", "SCOTEX");
// Demand Flexibility Service: requirements, bids per unit, and per-event summary
const dfs = await neso.dfs.summary("2026-10-20", "2026-10-22");
// Anything else on the portal: CKAN datastore paging or read-only SQL over a resource
const pkg = await neso.ckan.packageShow("interconnector-register");
const rows = await neso.ckan.datastoreAll(pkg.resources[0].id);
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`);Rows keep the portal's own column names, spaces and all (r["Daily Cost (GBP)"]), so they match
the CSV downloads and the SQL endpoint. Every method is async; CKAN's { success: false }
throws UkEnergyError, HTTP errors throw HttpError with the CKAN error object in body
(404 for an unknown resource, 409 for bad SQL).
Surface
| Namespace | Methods |
| --- | --- |
| ckan | packageShow(id), packageSearch(q, rows), resourceShow(id), datastoreSearch(resourceId, { limit, offset, sort, filters, fields, q }), datastoreAll(resourceId, opts), sql(query, extraNumeric?) |
| resources | datastore(packageId), forFinancialYear(packageId, 2025), byName(packageId, name \| RegExp) |
| constraints | thermalCosts(from, to, group?), breakdown(from, to), forecast24m(), limits24m(), dayAheadFlows(from, to, group?), dailyBalancingCosts(from, to) |
| dfs | requirements(from, to), utilisation(from, to), summary(from, to) |
action(name, query) runs any other CKAN action. rangeSql, ident, literal and
normaliseRows are exported for building your own queries; PACKAGES lists the dataset names
the helpers use.
Things to know about the portal
- One resource per financial year. Thermal costs, the breakdown and daily balancing costs are split into resources named like "Thermal Constraint Costs Data 25-26" (April–March). The date-range helpers resolve the right resource(s), spanning years when needed; the pre-2021 thermal cost resources are XLSX and not in the datastore.
- SQL returns numerics as strings (
"252") whiledatastore_searchreturns numbers. Both paths are normalised using the column types infields; the_full_textsearch column is dropped. "Thermal constraints cost" in the breakdown is typedtextin some years and is coerced too. - Local dates. Settlement and delivery dates are GB calendar days and
Date_ Time GMT_BSTin the day-ahead flows is GB local time with no offset. The helpers take "YYYY-MM-DD" strings as given and readDateinputs as Europe/London calendar days. - A range reaching into a year NESO hasn't published yet (1 April before the new "26-27"
resource appears) throws rather than returning the rows that do exist; narrow
to. - Page size.
datastore_searchreturns everything up to 32,000 rows per request; the day-ahead flows table is ~750k rows, so always filter it (the helper does, via SQL). - Rate limits. None published; the licence only reserves the right to throttle excessive use. Default 5 rps / burst 10 per host; package metadata is memoised for 10 minutes.
- Dataset names can change when NESO reorganises the portal;
PACKAGESis one place to fix.
Options
new NesoClient({
rateLimit: { requestsPerSecond: 5, burst: 10 }, // default
cache: new MemoryCache(), // optional response cache; TTL defaults to 10 minutes
cacheTtlMs: 10 * 60 * 1000, // also the package metadata memo TTL
retry: { retries: 3 }, timeoutMs: 30_000, fetch, // forwarded to @uk-energy/core HttpClient
});