@@ -117,17 +117,23 @@ def _refine_data(options, data1):
117
117
data1 .sort_index (inplace = True )
118
118
return {"data" : data1 , "refine_logs" : refine_logs }
119
119
120
+ def _convert_local_to_utc (dte ):
121
+ # datetime obj is converted from local time zone to utc
122
+ local_timezone = datetime .now ().astimezone ().tzinfo
123
+ return pd .Timestamp (dte ,tz = local_timezone ).tz_convert ('UTC' )
120
124
121
125
def _entsoe_get_actual_generation (options = {"country" : "" , "start" : "" , "end" : "" }):
122
126
"""Fetches the aggregated actual generation per production type data (16.1.B&C) for the given country within the given start and end date
123
127
params: options = {country (2 letter country code),start,end} . Both the dates are in the YYYYMMDDhhmm format and the local time zone
124
128
returns : {"data":pd.DataFrame, "duration":duration (in min) of the time series data, "refine_logs":"notes on refinements made" }
125
129
"""
130
+ utc_start = _convert_local_to_utc (options ["start" ])
131
+ utc_end = _convert_local_to_utc (options ["end" ])
126
132
client1 = entsoePandas (api_key = _get_API_token ())
127
133
data1 = client1 .query_generation (
128
134
options ["country" ],
129
- start = pd . Timestamp ( options [ "start" ], tz = "UTC" ) ,
130
- end = pd . Timestamp ( options [ "end" ], tz = "UTC" ) ,
135
+ start = utc_start ,
136
+ end = utc_end ,
131
137
psr_type = None ,
132
138
)
133
139
# drop columns with actual consumption values (we want actual aggregated generation values)
@@ -159,8 +165,8 @@ def _entsoe_get_total_forecast(options={"country": "", "start": "", "end": ""}):
159
165
client = entsoePandas (api_key = _get_API_token ())
160
166
data = client .query_generation_forecast (
161
167
options ["country" ],
162
- start = pd . Timestamp (options ["start" ], tz = "UTC" ) ,
163
- end = pd . Timestamp (options ["end" ], tz = "UTC" ),
168
+ start = _convert_local_to_utc (options ["start" ]) ,
169
+ end = _convert_local_to_utc (options ["end" ])
164
170
)
165
171
# if the data is a series instead of a dataframe, it will be converted to a dataframe
166
172
if isinstance (data , pd .Series ):
@@ -188,8 +194,8 @@ def _entsoe_get_wind_solar_forecast(options={"country": "", "start": "", "end":
188
194
client = entsoePandas (api_key = _get_API_token ())
189
195
data = client .query_wind_and_solar_forecast (
190
196
options ["country" ],
191
- start = pd . Timestamp (options ["start" ], tz = "UTC" ) ,
192
- end = pd . Timestamp (options ["end" ], tz = "UTC" ),
197
+ start = _convert_local_to_utc (options ["start" ]) ,
198
+ end = _convert_local_to_utc (options ["end" ])
193
199
)
194
200
durationMin = (data .index [1 ] - data .index [0 ]).total_seconds () / 60
195
201
# refining the data
@@ -225,6 +231,10 @@ def _convert_to_60min_interval(rawData):
225
231
# determining how many rows need to be combined to get data in 60 min format.
226
232
groupingFactor = int (60 / duration )
227
233
oldData = rawData ["data" ]
234
+ # check if there is enough data to convert to 60 min
235
+ if (len (oldData ) < groupingFactor ):
236
+ raise ValueError ("Data cannot be converted into 60 min interval since there is inadequate number of rows in the data" )
237
+
228
238
oldData ["startTimeUTC" ] = pd .to_datetime (oldData ["startTimeUTC" ])
229
239
start_time = oldData ["startTimeUTC" ].min ()
230
240
end_time = oldData ["startTimeUTC" ].max ()
@@ -246,9 +256,19 @@ def _convert_to_60min_interval(rawData):
246
256
247
257
248
258
def _convert_date_to_entsoe_format (dt : datetime ):
259
+ """ rounds the date to nearest hour """
249
260
return dt .replace (minute = 0 , second = 0 , microsecond = 0 ).strftime ("%Y%m%d%H%M" )
250
261
251
262
263
+ def _format_energy_data (df ):
264
+ start_time_column = df .pop ("startTimeUTC" )
265
+ df .insert (0 , "startTime" , start_time_column )
266
+ local_timezone = datetime .now ().astimezone ().tzinfo
267
+ df ["startTime" ] = pd .to_datetime (df ["startTime" ], format = "%Y%m%d%H%M" ).dt .tz_localize ("UTC" ).dt .tz_convert (local_timezone )
268
+ df .insert (1 , "startTimeUTC" , start_time_column )
269
+ return df
270
+
271
+
252
272
# the main methods
253
273
254
274
@@ -260,6 +280,7 @@ def get_actual_production_percentage(country, start, end, interval60=False) -> d
260
280
:param str country: The 2 alphabet country code.
261
281
:param datetime start: The start date for data retrieval. A Datetime object. Note that this date will be rounded to the nearest hour.
262
282
:param datetime end: The end date for data retrieval. A datetime object. This date is also rounded to the nearest hour.
283
+ :param boolean interval60: To convert the data into 60 min time interval. False by default
263
284
:return: A DataFrame containing the hourly energy production mix and percentage of energy generated from renewable and non renewable sources.
264
285
:return: A dictionary containing:
265
286
- `error`: A string with an error message, empty if no errors.
@@ -269,12 +290,32 @@ def get_actual_production_percentage(country, start, end, interval60=False) -> d
269
290
:rtype: dict
270
291
"""
271
292
try :
293
+ if not isinstance (country , str ):
294
+ raise ValueError ("Invalid country" )
295
+ if not isinstance (start , datetime ):
296
+ raise ValueError ("Invalid start date" )
297
+ if not isinstance (end , datetime ):
298
+ raise ValueError ("Invalid end date" )
299
+
300
+ if start > datetime .now ():
301
+ raise ValueError ("Invalid start date. Generation data is only available for the past and not the future. Use the forecast API instead" )
302
+
303
+ if start > end :
304
+ raise ValueError ("Invalid date range. End date must be greater than the start date" )
305
+
306
+ # if end date is in the future and the start date is in the past , only data till the available moment will be returned.
307
+ if end > datetime .now ():
308
+ raise ValueError ("Invalid end date. Generation data is only available for the past and not the future. Use the forecast API instead" )
309
+ # this is not allowed because the entsoe-py returns error if it's greater than the present
310
+ #warnings.warn("End date is in the future. Will fetch data only till the present")
311
+
272
312
options = {
273
313
"country" : country ,
274
- "start" : start ,
275
- "end" : end ,
314
+ "start" : start . replace ( minute = 0 , second = 0 ) ,
315
+ "end" : end . replace ( second = 0 , minute = 0 ) ,
276
316
"interval60" : interval60 ,
277
317
}
318
+ # print(options)
278
319
# get actual generation data per production type and convert it into 60 min interval if required
279
320
totalRaw = _entsoe_get_actual_generation (options )
280
321
total = totalRaw ["data" ]
@@ -327,18 +368,18 @@ def get_actual_production_percentage(country, start, end, interval60=False) -> d
327
368
table [fieldName ] = table [fieldName ].astype (int )
328
369
329
370
return {
330
- "data" : table ,
371
+ "data" : _format_energy_data ( table ) ,
331
372
"data_available" : True ,
332
- "time_interval" : totalRaw [ " duration" ] ,
373
+ "time_interval" : duration ,
333
374
}
334
375
except Exception as e :
335
- print (e )
376
+ # print(e)
336
377
print (traceback .format_exc ())
337
378
return {
338
379
"data" : None ,
339
380
"data_available" : False ,
340
- "error" : Exception ,
341
- "time_interval" : totalRaw [ "duration" ] ,
381
+ "error" : e ,
382
+ "time_interval" : 0 ,
342
383
}
343
384
344
385
@@ -364,6 +405,13 @@ def get_forecast_percent_renewable(
364
405
"""
365
406
try :
366
407
# print(country,start,end)
408
+ if not isinstance (country , str ):
409
+ raise ValueError ("Invalid country" )
410
+ if not isinstance (start , datetime ):
411
+ raise ValueError ("Invalid start date" )
412
+ if not isinstance (end , datetime ):
413
+ raise ValueError ("Invalid end date" )
414
+
367
415
start = _convert_date_to_entsoe_format (start )
368
416
end = _convert_date_to_entsoe_format (end )
369
417
options = {"country" : country , "start" : start , "end" : end }
@@ -390,7 +438,7 @@ def get_forecast_percent_renewable(
390
438
windsolar ["startTimeUTC" ], format = "%Y%m%d%H%M"
391
439
)
392
440
windsolar ["posix_timestamp" ] = windsolar ["startTimeUTC" ].astype (int ) // 10 ** 9
393
- return {"data" : windsolar , "data_available" : True , "time_interval" : 60 }
441
+ return {"data" : _format_energy_data ( windsolar ) , "data_available" : True , "time_interval" : 60 }
394
442
except Exception as e :
395
443
print (e )
396
444
print (traceback .format_exc ())
0 commit comments