Skip to content

Commit

Permalink
Using new nordpool data portal
Browse files Browse the repository at this point in the history
  • Loading branch information
fredli74 committed Oct 15, 2024
1 parent e4ea3c2 commit 0caa013
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 52 deletions.
124 changes: 79 additions & 45 deletions providers/nordpool/nordpool-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import nordpoolAPI from "./nordpool-api";
import { DateTime } from "luxon";
import { GQLUpdatePriceInput } from "@shared/sc-schema";
import { v5 as uuidv5 } from "uuid";
import { RestClientError } from "@shared/restclient";

const NORDPOOL_NAMESPACE = uuidv5("agent.nordpool.smartcharge.dev", uuidv5.DNS);

Expand All @@ -33,65 +34,98 @@ export class NordpoolAgent extends AbstractAgent {
}

private areaIDmap: Record<string, string> = {};
private deliveryDate = DateTime.utc().startOf("day");

public async globalWork(job: AgentWork) {
const now = Date.now();
const res = await nordpoolAPI.getPrices(config.PAGE, config.CURRENCY);

const areas: Record<string, GQLUpdatePriceInput> = {};
try {
const res:{
updatedAt: string,
multiAreaEntries: {
deliveryStart: string,
deliveryEnd: string,
entryPerArea: Record<string, number>
}[],
areaStates: {
state: string,
areas: string[]
}[]
} = await nordpoolAPI.getPrices(this.deliveryDate.toISODate(), config.AREAS, config.CURRENCY);

// remap table
for (const row of res.data.Rows) {
if (row.IsExtraRow) continue;
const startAt = DateTime.fromISO(row.StartTime, {
zone: "Europe/Oslo",
}).toISO();
for (const col of row.Columns) {
const price =
parseFloat(col.Value.replace(/ /g, "").replace(/,/g, ".")) / 1e3;
if (!isNaN(price)) {
if (!areas[col.Name]) {
if (this.areaIDmap[col.Name] === undefined) {
const id = uuidv5(`${col.Name}.pricelist`, NORDPOOL_NAMESPACE);
try {
// Check that list exits on server
await this.scClient.getPriceList(id);
} catch {
const list = await this.scClient.newPriceList(
`EU.${col.Name}`,
true,
id
);
if (list.id !== id) {
throw "Unable to create price list on server";
const areas: Record<string, GQLUpdatePriceInput> = {};

// populate areas based on final areas
for (const stateEntry of res.areaStates) {
if (stateEntry.state === "Final") {
for (const area of stateEntry.areas) {
if (!areas[area]) {
if (this.areaIDmap[area] === undefined) {
const id = uuidv5(`${area}.pricelist`, NORDPOOL_NAMESPACE);
try {
// Check that list exits on server
await this.scClient.getPriceList(id);
} catch {
const list = await this.scClient.newPriceList(`EU.${area}`, true, id);
if (list.id !== id) {
throw "Unable to create price list on server";
}
}
this.areaIDmap[area] = id;
}
this.areaIDmap[col.Name] = id;

areas[area] = {
priceListID: this.areaIDmap[area],
prices: [],
};
}
}
}
}

areas[col.Name] = {
priceListID: this.areaIDmap[col.Name],
prices: [],
};
// remap area prices to GQLUpdatePriceInput
for (const entry of res.multiAreaEntries) {
for (const [area, price] of Object.entries(entry.entryPerArea)) {
if (areas[area]) {
areas[area].prices.push({
startAt: entry.deliveryStart,
price: price / 1e3,
});
} else {
log(LogLevel.Trace, `Unknown area ${area}`);
}
areas[col.Name].prices.push({
startAt,
price: price,
});
}
}
}

for (const [name, update] of Object.entries(areas)) {
log(
LogLevel.Trace,
`Sending updatePrice for ${name} => ${JSON.stringify(update)}`
);
await this.scClient.updatePrice(update);
}
// Updating prices
for (const [name, update] of Object.entries(areas)) {
if (update.prices.length === 0) {
log(LogLevel.Warning, `No prices for ${name}`);
continue;
}
log(
LogLevel.Trace,
`Sending updatePrice for ${name} => ${JSON.stringify(update)}`
);
await this.scClient.updatePrice(update);
}

const nextUpdate = (Math.floor(now / 3600e3) + 1) * 3600e3 + 120e3;
job.interval = Math.max(60, (nextUpdate - now) / 1e3);
this.deliveryDate = this.deliveryDate.plus({ days: 1 });
const updatedAt = DateTime.fromISO(res.updatedAt, { zone: "utc" });
const nextUpdate = (updatedAt.plus({ hours: 22 }).startOf("hour").toMillis() + 60e3);
job.interval = Math.max(60, (nextUpdate - now) / 1e3);

} catch (e: any) {
if (e instanceof RestClientError && e.code === 204) {
// No content = no prices set yet
log(LogLevel.Info, `No nordpool prices set for ${this.deliveryDate.toISODate()} yet, will try again next hour`);
const nextUpdate = (Math.floor(now / 3600e3) + 1) * 3600e3 + 120e3;
job.interval = Math.max(60, (nextUpdate - now) / 1e3);
} else {
log(LogLevel.Error, `Error fetching prices: ${e.message}, will retry in 15 minutes`);
job.interval = 900;
}
}
}
}

Expand Down
7 changes: 3 additions & 4 deletions providers/nordpool/nordpool-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,16 @@ import provider from ".";
import { PROJECT_AGENT } from "@shared/smartcharge-defines";

export class NordpoolAPI extends RestClient {
public async getPrices(page: number, currency: string) {
// add &endDate=29-03-2020 if we need to poll historic data
public async getPrices(date: string, deliveryArea: string, currency: string) {
return this.get(
`/marketdata/page/${page}?currency=${currency},${currency},EUR,EUR`
`/DayAheadPrices?date=${date}&market=DayAhead&deliveryArea=${deliveryArea}&currency=${currency}`
);
}
}
const nordpoolAPI = new NordpoolAPI({
baseURL: config.NORDPOOL_API_BASE_URL,
headers: {
"User-Agent": `${PROJECT_AGENT} ${provider.name}/${provider.version}`,
"User-Agent": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36`,
},
timeout: 120e3,
});
Expand Down
4 changes: 2 additions & 2 deletions providers/nordpool/nordpool-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const config = {
NORDPOOL_API_BASE_URL: `https://www.nordpoolgroup.com/api`,
PAGE: 29,
NORDPOOL_API_BASE_URL: `https://dataportal-api.nordpoolgroup.com/api`,
AREAS: "SE1,SE2,SE3,SE4",
CURRENCY: "SEK",
};

Expand Down
2 changes: 1 addition & 1 deletion shared/restclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export class RestClient {
} catch (e: any) {
console.log(`RestClientError: Unable to parse JSON`);
console.log(`RestClientError: ${body}`);
dispatchError(e.message);
dispatchError(e.message, res.statusCode);
}
} else {
console.log(`RestClientError: Non-2xx status: ${res.statusCode}`);
Expand Down

0 comments on commit 0caa013

Please sign in to comment.