@powercalc/power-router 1.2.4 → 1.2.5
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/dist/config/types.json +22 -30
- package/dist/controllers/LoadAndParse.js +6 -0
- package/dist/controllers/LoadAndParse.js.map +1 -1
- package/dist/controllers/Router.js +3 -0
- package/dist/controllers/Router.js.map +1 -1
- package/dist/services/CommonTimestamps.js +3 -0
- package/dist/services/CommonTimestamps.js.map +1 -1
- package/dist/services/LoadService.d.ts +9 -0
- package/dist/services/LoadService.js +32 -0
- package/dist/services/LoadService.js.map +1 -1
- package/dist/services/Loader.js +29 -20
- package/dist/services/Loader.js.map +1 -1
- package/dist/services/Temperature.d.ts +15 -0
- package/dist/services/Temperature.js +89 -0
- package/dist/services/Temperature.js.map +1 -0
- package/package.json +5 -2
- package/src/router/config/types.json +22 -30
- package/src/router/controllers/LoadAndParse.ts +6 -0
- package/src/router/controllers/Router.ts +3 -0
- package/src/router/services/CommonTimestamps.ts +3 -0
- package/src/router/services/LoadService.ts +60 -11
- package/src/router/services/Loader.ts +81 -55
- package/src/router/services/Temperature.ts +98 -0
|
@@ -2,7 +2,7 @@ import { parseStringPromise } from 'xml2js';
|
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { add, format, sub } from 'date-fns'
|
|
5
|
+
import { add, format, sub } from 'date-fns';
|
|
6
6
|
import { Entsoe } from '../interfaces/entsoe';
|
|
7
7
|
import { Format, QueryOptions } from '../interfaces/queryoptions';
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ export interface AllEntsoe {
|
|
|
12
12
|
|
|
13
13
|
export interface EntsoeConfig {
|
|
14
14
|
entsoeDomain: string;
|
|
15
|
-
countries: Country[]
|
|
15
|
+
countries: Country[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface Country {
|
|
@@ -26,13 +26,13 @@ export interface Data {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface LoaderConfig {
|
|
29
|
-
source?: string
|
|
30
|
-
securityToken: string
|
|
31
|
-
cacheDir?: string
|
|
29
|
+
source?: string;
|
|
30
|
+
securityToken: string;
|
|
31
|
+
cacheDir?: string;
|
|
32
32
|
entsoeDomain: string;
|
|
33
33
|
}
|
|
34
34
|
interface KV {
|
|
35
|
-
[key: string]: string
|
|
35
|
+
[key: string]: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export class EntsoeLoader {
|
|
@@ -42,19 +42,33 @@ export class EntsoeLoader {
|
|
|
42
42
|
this.config = loaderConfig;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
async load(
|
|
45
|
+
async load(
|
|
46
|
+
wanted: string[],
|
|
47
|
+
countryCode: string,
|
|
48
|
+
year: number,
|
|
49
|
+
month: number,
|
|
50
|
+
queryOptions?: QueryOptions
|
|
51
|
+
): Promise<any> {
|
|
46
52
|
const urls = this.makeUrls(wanted, countryCode, year, month);
|
|
47
|
-
if(queryOptions?.format === Format.SOURCES){
|
|
53
|
+
if (queryOptions?.format === Format.SOURCES) {
|
|
48
54
|
return urls;
|
|
49
55
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
try {
|
|
57
|
+
const allJsons = await this.fetchAndMakeJson(
|
|
58
|
+
Object.values(urls),
|
|
59
|
+
queryOptions?.reload
|
|
60
|
+
);
|
|
61
|
+
const result: AllEntsoe = {};
|
|
62
|
+
Object.keys(urls).forEach((key, i) => {
|
|
63
|
+
if (allJsons[i]) {
|
|
64
|
+
result[key] = allJsons[i] as Entsoe;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return result;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error(e);
|
|
70
|
+
throw e;
|
|
71
|
+
}
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
makeUrls(wanted: string[], countryCode: string, year: number, month: number) {
|
|
@@ -62,16 +76,22 @@ export class EntsoeLoader {
|
|
|
62
76
|
start = sub(start, { hours: 3 });
|
|
63
77
|
let end = new Date(Date.UTC(year, month));
|
|
64
78
|
end = add(end, { hours: 3 });
|
|
65
|
-
const periodStart = format(start, 'yyyyMMddHHmm')
|
|
66
|
-
const periodEnd = format(end, 'yyyyMMddHHmm')
|
|
79
|
+
const periodStart = format(start, 'yyyyMMddHHmm');
|
|
80
|
+
const periodEnd = format(end, 'yyyyMMddHHmm');
|
|
67
81
|
|
|
68
82
|
const hydroStart = sub(start, { days: 7 });
|
|
69
|
-
const hydroPeriodStart = format(hydroStart, 'yyyyMMddHHmm')
|
|
70
|
-
const hydroPeriodEnd = format(
|
|
83
|
+
const hydroPeriodStart = format(hydroStart, 'yyyyMMddHHmm');
|
|
84
|
+
const hydroPeriodEnd = format(
|
|
85
|
+
add(hydroStart, { days: 380 }),
|
|
86
|
+
'yyyyMMddHHmm'
|
|
87
|
+
);
|
|
71
88
|
|
|
72
89
|
let priceCountryCode = countryCode;
|
|
73
90
|
//germany luxemburg
|
|
74
|
-
if (
|
|
91
|
+
if (
|
|
92
|
+
countryCode === '10Y1001A1001A83F' ||
|
|
93
|
+
countryCode === '10YLU-CEGEDEL-NQ'
|
|
94
|
+
) {
|
|
75
95
|
priceCountryCode = '10Y1001A1001A82H';
|
|
76
96
|
}
|
|
77
97
|
//italy
|
|
@@ -84,51 +104,56 @@ export class EntsoeLoader {
|
|
|
84
104
|
}
|
|
85
105
|
//denmark dk1
|
|
86
106
|
if (countryCode === '10Y1001A1001A65H') {
|
|
87
|
-
priceCountryCode = '10YDK-1--------W'
|
|
107
|
+
priceCountryCode = '10YDK-1--------W';
|
|
88
108
|
}
|
|
89
109
|
//norway no1
|
|
90
110
|
if (countryCode === '10YNO-0--------C') {
|
|
91
|
-
priceCountryCode = '10YNO-1--------2'
|
|
111
|
+
priceCountryCode = '10YNO-1--------2';
|
|
92
112
|
}
|
|
93
113
|
//ireland
|
|
94
114
|
if (countryCode === '10Y1001A1001A59C') {
|
|
95
|
-
priceCountryCode = '10YIE-1001A00010'
|
|
115
|
+
priceCountryCode = '10YIE-1001A00010';
|
|
96
116
|
}
|
|
97
|
-
const generation = `${this.config.entsoeDomain}/api?documentType=A75&processType=A16&in_Domain=${countryCode}&outBiddingZone_Domain=${countryCode}&periodStart=${periodStart}&periodEnd=${periodEnd}
|
|
98
|
-
const price = `${this.config.entsoeDomain}/api?documentType=A44&in_Domain=${priceCountryCode}&out_Domain=${priceCountryCode}&periodStart=${periodStart}&periodEnd=${periodEnd}
|
|
99
|
-
const
|
|
100
|
-
const
|
|
117
|
+
const generation = `${this.config.entsoeDomain}/api?documentType=A75&processType=A16&in_Domain=${countryCode}&outBiddingZone_Domain=${countryCode}&periodStart=${periodStart}&periodEnd=${periodEnd}`;
|
|
118
|
+
const price = `${this.config.entsoeDomain}/api?documentType=A44&in_Domain=${priceCountryCode}&out_Domain=${priceCountryCode}&periodStart=${periodStart}&periodEnd=${periodEnd}`;
|
|
119
|
+
const congestion = `${this.config.entsoeDomain}/api?documentType=A92&in_Domain=${priceCountryCode}&out_Domain=${priceCountryCode}&periodStart=${periodStart}&periodEnd=${periodEnd}`;
|
|
120
|
+
const consumption = `${this.config.entsoeDomain}/api?documentType=A65&processType=A16&in_Domain=${countryCode}&outBiddingZone_Domain=${countryCode}&periodStart=${periodStart}&periodEnd=${periodEnd}`;
|
|
121
|
+
const hydroFill = `${this.config.entsoeDomain}/api?documentType=A72&processType=A16&in_Domain=${countryCode}&periodStart=${hydroPeriodStart}&periodEnd=${hydroPeriodEnd}`;
|
|
101
122
|
//const crossBorder = `${this.config.entsoeDomain}/api?documentType=A88&acquiring_Domain=10YCZ-CEPS-----N&connecting_Domain=10YSK-SEPS-----K&periodStart=${periodStart}&periodEnd=${periodEnd}`
|
|
102
123
|
const urls = {
|
|
103
124
|
generation,
|
|
104
125
|
price,
|
|
105
126
|
consumption,
|
|
106
|
-
hydroFill
|
|
107
|
-
|
|
127
|
+
hydroFill,
|
|
128
|
+
congestion,
|
|
129
|
+
};
|
|
108
130
|
const filteredUrls = this.filterURLs(urls, wanted);
|
|
109
131
|
return filteredUrls;
|
|
110
132
|
}
|
|
111
133
|
|
|
112
134
|
async loadInstalled(countryCode: string, queryOptions?: QueryOptions) {
|
|
113
|
-
const currentYear =
|
|
114
|
-
const urls = []
|
|
135
|
+
const currentYear = new Date().getFullYear();
|
|
136
|
+
const urls = [];
|
|
115
137
|
for (let year = 2015; year <= currentYear; year++) {
|
|
116
|
-
const periodStart = `${year -
|
|
117
|
-
const periodEnd = `${year
|
|
138
|
+
const periodStart = `${year - 1}12302200`;
|
|
139
|
+
const periodEnd = `${year}01020400`;
|
|
118
140
|
const installed = `${this.config.entsoeDomain}/api?documentType=A68&processType=A33&in_Domain=${countryCode}&periodStart=${periodStart}&periodEnd=${periodEnd}`;
|
|
119
|
-
urls.push(installed)
|
|
141
|
+
urls.push(installed);
|
|
120
142
|
}
|
|
121
|
-
if(queryOptions?.format===Format.SOURCES){
|
|
143
|
+
if (queryOptions?.format === Format.SOURCES) {
|
|
122
144
|
return urls;
|
|
123
145
|
}
|
|
124
|
-
const allJsons = await this.fetchAndMakeJson(
|
|
146
|
+
const allJsons = await this.fetchAndMakeJson(
|
|
147
|
+
Object.values(urls),
|
|
148
|
+
queryOptions?.reload
|
|
149
|
+
);
|
|
125
150
|
return allJsons;
|
|
126
151
|
}
|
|
127
152
|
async fetchAndMakeJson(urls: string[], reload?: boolean): Promise<Entsoe[]> {
|
|
128
153
|
const promises: Promise<Entsoe>[] = [];
|
|
129
|
-
urls.forEach(url => {
|
|
130
|
-
promises.push(this.fetchAndCache(url, reload))
|
|
131
|
-
})
|
|
154
|
+
urls.forEach((url) => {
|
|
155
|
+
promises.push(this.fetchAndCache(url, reload));
|
|
156
|
+
});
|
|
132
157
|
return await Promise.all(promises);
|
|
133
158
|
/*
|
|
134
159
|
const settled = await Promise.allSettled(promises);
|
|
@@ -138,11 +163,11 @@ export class EntsoeLoader {
|
|
|
138
163
|
}
|
|
139
164
|
|
|
140
165
|
filterEmpty(all: Data) {
|
|
141
|
-
Object.keys(all).forEach(name => {
|
|
142
|
-
if (!all[name].some(item => item !== 0)) {
|
|
166
|
+
Object.keys(all).forEach((name) => {
|
|
167
|
+
if (!all[name].some((item) => item !== 0)) {
|
|
143
168
|
delete all[name];
|
|
144
169
|
}
|
|
145
|
-
})
|
|
170
|
+
});
|
|
146
171
|
}
|
|
147
172
|
|
|
148
173
|
async fetchAndCache(url: string, reload?: boolean): Promise<Entsoe> {
|
|
@@ -160,26 +185,27 @@ export class EntsoeLoader {
|
|
|
160
185
|
return JSON.parse(jsonString) as Entsoe;
|
|
161
186
|
} catch (e) {
|
|
162
187
|
*/
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
188
|
+
const urlWithToken = `${url}&securityToken=${this.config.securityToken}`;
|
|
189
|
+
const response = await fetch(urlWithToken);
|
|
190
|
+
if (response.status !== 200) {
|
|
191
|
+
return Promise.reject(
|
|
192
|
+
new Error(`${response.status} ${response.statusText}`)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const data = await response.text();
|
|
196
|
+
const json = (await parseStringPromise(data)) as Entsoe;
|
|
197
|
+
/*
|
|
172
198
|
if (filename) {
|
|
173
199
|
//writeFile(filename, JSON.stringify(json), 'utf-8')
|
|
174
200
|
}
|
|
175
201
|
*/
|
|
176
|
-
|
|
202
|
+
return json;
|
|
177
203
|
//}
|
|
178
204
|
}
|
|
179
205
|
filterURLs(obj: KV, keys: string[]) {
|
|
180
206
|
return keys.reduce((acc: KV, key: string) => {
|
|
181
207
|
acc[key] = obj[key];
|
|
182
208
|
return acc;
|
|
183
|
-
}, {})
|
|
209
|
+
}, {});
|
|
184
210
|
}
|
|
185
|
-
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFileSync } from "fs"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
import { Country } from "./Loader"
|
|
4
|
+
import { Format, QueryOptions } from "../interfaces/queryoptions"
|
|
5
|
+
|
|
6
|
+
export class Temperature {
|
|
7
|
+
countries: Country[] = []
|
|
8
|
+
|
|
9
|
+
// Capital city coordinates for all European countries
|
|
10
|
+
private readonly coordinates: { [key: string]: { lat: number; lon: number } } = {
|
|
11
|
+
AT: { lat: 48.2082, lon: 16.3738 }, // Vienna, Austria
|
|
12
|
+
BE: { lat: 50.8503, lon: 4.3517 }, // Brussels, Belgium
|
|
13
|
+
BA: { lat: 43.8564, lon: 18.4131 }, // Sarajevo, Bosnia and Herzegovina
|
|
14
|
+
HR: { lat: 45.8150, lon: 15.9819 }, // Zagreb, Croatia
|
|
15
|
+
BG: { lat: 42.6977, lon: 23.3219 }, // Sofia, Bulgaria
|
|
16
|
+
CY: { lat: 35.1856, lon: 33.3823 }, // Nicosia, Cyprus
|
|
17
|
+
CZ: { lat: 50.0755, lon: 14.4378 }, // Prague, Czech Republic
|
|
18
|
+
DK: { lat: 55.6761, lon: 12.5683 }, // Copenhagen, Denmark
|
|
19
|
+
EE: { lat: 59.4370, lon: 24.7536 }, // Tallinn, Estonia
|
|
20
|
+
FI: { lat: 60.1699, lon: 24.9384 }, // Helsinki, Finland
|
|
21
|
+
FR: { lat: 48.8566, lon: 2.3522 }, // Paris, France
|
|
22
|
+
DE: { lat: 52.5200, lon: 13.4050 }, // Berlin, Germany
|
|
23
|
+
GR: { lat: 37.9838, lon: 23.7275 }, // Athens, Greece
|
|
24
|
+
HU: { lat: 47.4979, lon: 19.0402 }, // Budapest, Hungary
|
|
25
|
+
IE: { lat: 53.3498, lon: -6.2603 }, // Dublin, Ireland
|
|
26
|
+
IT: { lat: 41.9028, lon: 12.4964 }, // Rome, Italy
|
|
27
|
+
LV: { lat: 56.9496, lon: 24.1052 }, // Riga, Latvia
|
|
28
|
+
LT: { lat: 54.6872, lon: 25.2797 }, // Vilnius, Lithuania
|
|
29
|
+
LU: { lat: 49.6116, lon: 6.1319 }, // Luxembourg City, Luxembourg
|
|
30
|
+
ME: { lat: 42.4304, lon: 19.2594 }, // Podgorica, Montenegro
|
|
31
|
+
GB: { lat: 51.5074, lon: -0.1278 }, // London, United Kingdom
|
|
32
|
+
NL: { lat: 52.3676, lon: 4.9041 }, // Amsterdam, Netherlands
|
|
33
|
+
MK: { lat: 41.9973, lon: 21.4280 }, // Skopje, North Macedonia
|
|
34
|
+
NO: { lat: 59.9139, lon: 10.7522 }, // Oslo, Norway
|
|
35
|
+
PL: { lat: 52.2297, lon: 21.0122 }, // Warsaw, Poland
|
|
36
|
+
PT: { lat: 38.7223, lon: -9.1393 }, // Lisbon, Portugal
|
|
37
|
+
RO: { lat: 44.4268, lon: 26.1025 }, // Bucharest, Romania
|
|
38
|
+
RS: { lat: 44.7866, lon: 20.4489 }, // Belgrade, Serbia
|
|
39
|
+
SK: { lat: 48.1486, lon: 17.1077 }, // Bratislava, Slovakia
|
|
40
|
+
SI: { lat: 46.0569, lon: 14.5058 }, // Ljubljana, Slovenia
|
|
41
|
+
ES: { lat: 40.4168, lon: -3.7038 }, // Madrid, Spain
|
|
42
|
+
SE: { lat: 59.3293, lon: 18.0686 }, // Stockholm, Sweden
|
|
43
|
+
CH: { lat: 46.9480, lon: 7.4474 }, // Bern, Switzerland
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
constructor() {
|
|
47
|
+
const countryPath = join(__dirname, '..', 'config/countries.json')
|
|
48
|
+
const countriesString = readFileSync(countryPath, 'utf-8')
|
|
49
|
+
this.countries = JSON.parse(countriesString)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async load(countryCode: string, queryOptions: QueryOptions) {
|
|
53
|
+
const countryShort = this.countries.find(
|
|
54
|
+
(item) => item.code === countryCode
|
|
55
|
+
)?.short
|
|
56
|
+
|
|
57
|
+
if (!countryShort || !this.coordinates[countryShort]) {
|
|
58
|
+
throw new Error(`Country ${countryCode} not supported for temperature data`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { lat, lon } = this.coordinates[countryShort]
|
|
62
|
+
|
|
63
|
+
// Calculate date range for the month
|
|
64
|
+
//const startDate = new Date(year, month - 1, 1)
|
|
65
|
+
//const endDate = new Date(year, month, 0)
|
|
66
|
+
const startDate = new Date('2015-01-01');
|
|
67
|
+
const endDate = new Date();
|
|
68
|
+
const startDateStr = startDate.toISOString().split('T')[0]
|
|
69
|
+
const endDateStr = endDate.toISOString().split('T')[0]
|
|
70
|
+
|
|
71
|
+
// Open-Meteo API - free historical weather data
|
|
72
|
+
const url = `https://archive-api.open-meteo.com/v1/archive?latitude=${lat}&longitude=${lon}&start_date=${startDateStr}&end_date=${endDateStr}&hourly=temperature_2m&timezone=auto`
|
|
73
|
+
console.log(url)
|
|
74
|
+
|
|
75
|
+
if (queryOptions.format === Format.SOURCES) {
|
|
76
|
+
return [url]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const response = await fetch(url)
|
|
80
|
+
const json = await response.json()
|
|
81
|
+
const r = this.convert(json)
|
|
82
|
+
return r
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
convert(x: any) {
|
|
86
|
+
const timeStrings = x?.hourly?.time || []
|
|
87
|
+
const temperatures = x?.hourly?.temperature_2m || []
|
|
88
|
+
|
|
89
|
+
const time = timeStrings.map((item: string) => {
|
|
90
|
+
return new Date(item).getTime()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
time: time,
|
|
95
|
+
temperature: temperatures
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|