import { db } from '#lib/server/db/index.js'; import { sql } from 'drizzle-orm'; import type { EnergyRow } from '#lib/energy/data.js'; // Read-only queries over the ned-collector tables in the `energy` schema. The app role // already has USAGE + SELECT there; nothing on the site writes to these tables. type Raw = Record; const num = (value: unknown) => (value === null || value === undefined ? null : Number(value)); const time = (value: unknown) => new Date(value as string | Date).getTime(); const rows = (result: unknown) => result as Raw[]; function energyRows(result: unknown): EnergyRow[] { return rows(result).map((row) => ({ t: time(row.t), type: String(row.type), value: Number(row.value), pct: num(row.pct), ef: num(row.ef), ...(row.hours === undefined ? {} : { hours: Number(row.hours) }), ...(row.expected === undefined ? {} : { expected: Number(row.expected) }) })); } export async function loadEnergy() { const [hourly, daily, forecast, monthly, yearly, gasNl, gasSites, runs, coverage] = await Promise.all([ // Last 7 days of hourly measurements, counted back from the newest hour NED published. db.execute(sql`WITH latest AS ( SELECT max(valid_from) AS t FROM energy.ned_utilization WHERE granularity = 'Hour' AND classification = 'Current') SELECT valid_from AS t, type, volume_kwh::float8 / 1000 AS value, percentage::float8 AS pct, emission_factor::float8 AS ef FROM energy.ned_utilization, latest WHERE granularity = 'Hour' AND classification = 'Current' AND valid_from > latest.t - interval '7 days' ORDER BY 1, 2`), // Daily totals (Amsterdam days) from hourly data, last 31 days. `expected` is 23 or 25 // on DST change days so partial days can be flagged. db.execute(sql`WITH latest AS ( SELECT max(valid_from) AS t FROM energy.ned_utilization WHERE granularity = 'Hour' AND classification = 'Current'), days AS ( SELECT date_trunc('day', valid_from AT TIME ZONE 'Europe/Amsterdam') AS day, type, sum(volume_kwh) AS kwh, sum(emission_kg) AS kg, count(*)::int AS hours FROM energy.ned_utilization, latest WHERE granularity = 'Hour' AND classification = 'Current' AND valid_from > latest.t - interval '31 days' GROUP BY 1, 2) SELECT day AT TIME ZONE 'Europe/Amsterdam' AS t, type, kwh::float8 / 1000 AS value, hours, (extract(epoch FROM (day + interval '1 day') AT TIME ZONE 'Europe/Amsterdam' - day AT TIME ZONE 'Europe/Amsterdam') / 3600)::int AS expected, (kg / nullif(kwh, 0))::float8 AS ef FROM days ORDER BY 1, 2`), db.execute(sql`WITH latest AS ( SELECT max(valid_from) AS t FROM energy.ned_utilization WHERE granularity = 'Hour' AND classification = 'Current') SELECT valid_from AS t, type, volume_kwh::float8 / 1000 AS value FROM energy.ned_utilization, latest WHERE granularity = 'Hour' AND classification = 'Forecast' AND valid_from > latest.t AND valid_from <= latest.t + interval '8 days' AND type IN ('Wind', 'WindOffshoreC', 'Solar', 'ElectricityLoad', 'ElectricityMix') ORDER BY 1, 2`), db.execute(sql`SELECT valid_from AS t, type, volume_kwh::float8 / 1e6 AS value, percentage::float8 AS pct, emission_factor::float8 AS ef FROM energy.ned_utilization WHERE granularity = 'Month' AND classification = 'Current' ORDER BY 1, 2`), db.execute(sql`SELECT valid_from AS t, type, volume_kwh::float8 / 1e6 AS value, emission_factor::float8 AS ef FROM energy.ned_utilization WHERE granularity = 'Year' AND classification = 'Current' ORDER BY 1, 2`), db.execute(sql`SELECT gas_day::text AS day, fill_pct::float8 AS fill, volume_twh::float8 AS twh FROM energy.gas_storage_daily WHERE site = 'NL' ORDER BY gas_day`), db.execute(sql`SELECT site, gas_day::text AS day, fill_pct::float8 AS fill, volume_twh::float8 AS twh FROM energy.gas_storage_latest WHERE site <> 'NL' ORDER BY volume_twh DESC`), db.execute(sql`SELECT (SELECT max(started_at) FROM energy.collector_runs) AS last_run, (SELECT max(finished_at) FROM energy.collector_runs WHERE ok) AS last_ok, (SELECT count(*)::int FROM energy.collector_runs WHERE NOT ok AND started_at > now() - interval '7 days') AS failed_week`), db.execute(sql`SELECT min(valid_from) AS first_hour, max(valid_from) AS last_hour, count(DISTINCT valid_from)::int AS hours FROM energy.ned_utilization WHERE granularity = 'Hour' AND classification = 'Current'`) ]); const run = rows(runs)[0] ?? {}; const cover = rows(coverage)[0] ?? {}; return { hourly: energyRows(hourly), daily: energyRows(daily), forecast: energyRows(forecast), monthly: energyRows(monthly), yearly: energyRows(yearly), gas: rows(gasNl).map((row) => ({ day: String(row.day), fill: Number(row.fill), twh: Number(row.twh) })), gasSites: rows(gasSites).map((row) => ({ site: String(row.site), day: String(row.day), fill: Number(row.fill), twh: Number(row.twh) })), collector: { lastRun: run.last_run ? time(run.last_run) : null, lastOk: run.last_ok ? time(run.last_ok) : null, failedWeek: Number(run.failed_week ?? 0) }, coverage: { firstHour: cover.first_hour ? time(cover.first_hour) : null, lastHour: cover.last_hour ? time(cover.last_hour) : null, hours: Number(cover.hours ?? 0) } }; } export type EnergyData = Awaited>;