Skip to content

Commit be7873c

Browse files
committed
hotfix in entsoe date timezone
1 parent d276c6e commit be7873c

File tree

4 files changed

+123
-11
lines changed

4 files changed

+123
-11
lines changed

codegreen_core/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
from .utilities.config import Config
2-
3-
Config.load_config()
1+
from . import utilities
2+
from . import data
3+
from . import tools
4+
from . import models
5+
utilities.config.Config.load_config()

codegreen_core/data/entsoe.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,23 @@ def _refine_data(options, data1):
117117
data1.sort_index(inplace=True)
118118
return {"data": data1, "refine_logs": refine_logs}
119119

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')
120124

121125
def _entsoe_get_actual_generation(options={"country": "", "start": "", "end": ""}):
122126
"""Fetches the aggregated actual generation per production type data (16.1.B&C) for the given country within the given start and end date
123127
params: options = {country (2 letter country code),start,end} . Both the dates are in the YYYYMMDDhhmm format and the local time zone
124128
returns : {"data":pd.DataFrame, "duration":duration (in min) of the time series data, "refine_logs":"notes on refinements made" }
125129
"""
130+
utc_start = _convert_local_to_utc(options["start"])
131+
utc_end = _convert_local_to_utc(options["end"])
126132
client1 = entsoePandas(api_key=_get_API_token())
127133
data1 = client1.query_generation(
128134
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 ,
131137
psr_type=None,
132138
)
133139
# 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": ""}):
159165
client = entsoePandas(api_key=_get_API_token())
160166
data = client.query_generation_forecast(
161167
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"])
164170
)
165171
# if the data is a series instead of a dataframe, it will be converted to a dataframe
166172
if isinstance(data, pd.Series):
@@ -188,8 +194,8 @@ def _entsoe_get_wind_solar_forecast(options={"country": "", "start": "", "end":
188194
client = entsoePandas(api_key=_get_API_token())
189195
data = client.query_wind_and_solar_forecast(
190196
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"])
193199
)
194200
durationMin = (data.index[1] - data.index[0]).total_seconds() / 60
195201
# refining the data
@@ -246,6 +252,7 @@ def _convert_to_60min_interval(rawData):
246252

247253

248254
def _convert_date_to_entsoe_format(dt: datetime):
255+
""" rounds the date to nearest hour """
249256
return dt.replace(minute=0, second=0, microsecond=0).strftime("%Y%m%d%H%M")
250257

251258

@@ -260,6 +267,7 @@ def get_actual_production_percentage(country, start, end, interval60=False) -> d
260267
:param str country: The 2 alphabet country code.
261268
:param datetime start: The start date for data retrieval. A Datetime object. Note that this date will be rounded to the nearest hour.
262269
:param datetime end: The end date for data retrieval. A datetime object. This date is also rounded to the nearest hour.
270+
:param boolean interval60: To convert the data into 60 min time interval. False by default
263271
:return: A DataFrame containing the hourly energy production mix and percentage of energy generated from renewable and non renewable sources.
264272
:return: A dictionary containing:
265273
- `error`: A string with an error message, empty if no errors.
@@ -269,6 +277,25 @@ def get_actual_production_percentage(country, start, end, interval60=False) -> d
269277
:rtype: dict
270278
"""
271279
try:
280+
if not isinstance(country, str):
281+
raise ValueError("Invalid country")
282+
if not isinstance(start, datetime):
283+
raise ValueError("Invalid start date")
284+
if not isinstance(end, datetime):
285+
raise ValueError("Invalid end date")
286+
287+
if start > datetime.now():
288+
raise ValueError("Invalid start date. Generation data is only available for the past and not the future. Use the forecast API instead")
289+
290+
if start > end :
291+
raise ValueError("Invalid date range. End date must be greater than the start date")
292+
293+
# if end date is in the future and the start date is in the past , only data till the available moment will be returned.
294+
if end > datetime.now():
295+
raise ValueError("Invalid end date. Generation data is only available for the past and not the future. Use the forecast API instead")
296+
# this is not allowed because the entsoe-py returns error if it's greater than the present
297+
#warnings.warn("End date is in the future. Will fetch data only till the present")
298+
272299
options = {
273300
"country": country,
274301
"start": start,
@@ -332,12 +359,12 @@ def get_actual_production_percentage(country, start, end, interval60=False) -> d
332359
"time_interval": duration,
333360
}
334361
except Exception as e:
335-
print(e)
362+
# print(e)
336363
print(traceback.format_exc())
337364
return {
338365
"data": None,
339366
"data_available": False,
340-
"error": Exception,
367+
"error": e,
341368
"time_interval": 0,
342369
}
343370

@@ -364,6 +391,13 @@ def get_forecast_percent_renewable(
364391
"""
365392
try:
366393
# print(country,start,end)
394+
if not isinstance(country, str):
395+
raise ValueError("Invalid country")
396+
if not isinstance(start, datetime):
397+
raise ValueError("Invalid start date")
398+
if not isinstance(end, datetime):
399+
raise ValueError("Invalid end date")
400+
367401
start = _convert_date_to_entsoe_format(start)
368402
end = _convert_date_to_entsoe_format(end)
369403
options = {"country": country, "start": start, "end": end}

codegreen_core/utilities/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from . import metadata
2+
from . import config

tests/test_entsoe.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import pytest
2+
from codegreen_core.data.entsoe import *
3+
from codegreen_core.utilities.message import CodegreenDataError
4+
from datetime import datetime
5+
import pandas as pd
6+
7+
8+
class TestEntsoeData:
9+
def test_actual_time_interval_original(self):
10+
data = get_actual_production_percentage("DE",datetime.now()-timedelta(hours=2),datetime.now())
11+
assert data["time_interval"] == 15 and data["data_available"] == True
12+
def test_actual_time_interval_60min(self):
13+
data = get_actual_production_percentage("DE",datetime.now()-timedelta(hours=2),datetime.now(),True)
14+
assert data["time_interval"] == 60 and data["data_available"] == True
15+
def test_actual_invalid_country1(self):
16+
data = get_actual_production_percentage("DE1",datetime.now()-timedelta(hours=3),datetime.now(),True)
17+
assert data["data_available"] == False and isinstance(data["error"],ValueError)
18+
def test_actual_invalid_country2(self):
19+
data = get_actual_production_percentage(1234,datetime.now()-timedelta(hours=3),datetime.now(),True)
20+
assert data["data_available"] == False and isinstance(data["error"],ValueError)
21+
def test_actual_invalid_start(self):
22+
data = get_actual_production_percentage("DE","invalid",datetime.now(),True)
23+
assert data["data_available"] == False and isinstance(data["error"],ValueError)
24+
def test_actual_invalid_end(self):
25+
data = get_actual_production_percentage("DE",datetime.now(),"invalid",True)
26+
assert data["data_available"] == False and isinstance(data["error"],ValueError)
27+
def test_actual_invalid_date_range(self):
28+
# start > end
29+
data = get_actual_production_percentage("DE",datetime.now(),datetime.now()-timedelta(hours=3),True)
30+
assert data["data_available"] == False and isinstance(data["error"],ValueError)
31+
def test_actual_invalid_date_range2(self):
32+
# start > now
33+
data = get_actual_production_percentage("DE",datetime.now()+timedelta(hours=3),datetime.now()+timedelta(hours=4),True)
34+
assert data["data_available"] == False and isinstance(data["error"],ValueError)
35+
def test_actual_invalid_date_range3(self):
36+
# end > now
37+
data = get_actual_production_percentage("DE",datetime.now()-timedelta(hours=3),datetime.now()+timedelta(hours=3),True)
38+
assert data["data_available"] == False and isinstance(data["error"],ValueError)
39+
40+
41+
def test_forecast_time_interval_60(self):
42+
data = get_forecast_percent_renewable("DE",datetime.now()-timedelta(hours=2),datetime.now()+timedelta(hours=5))
43+
assert data["time_interval"] == 60 and data["data_available"] == True
44+
45+
46+
class TestActualDataFrame:
47+
@classmethod
48+
def setup_class(cls):
49+
"""Fetch data once for all tests."""
50+
# Simulate fetching data from an API
51+
cls.country="DE"
52+
cls.start1 = datetime(2024,5,1)
53+
cls.end1 = datetime (2024,5,1,10,0,0)
54+
cls.row_count_check_60 = int(((cls.end1- cls.start1).total_seconds()/60)/60)
55+
cls.row_count_check_15 = cls.row_count_check_60*4
56+
# de1 is 15 min interval
57+
# de2 is 60 min interval
58+
cls.de1 = get_actual_production_percentage(cls.country,cls.start1,cls.end1,False)["data"]
59+
cls.de2 = get_actual_production_percentage(cls.country,cls.start1,cls.end1,True)["data"]
60+
def test_dataframe_nonempty(self):
61+
"""Test that the DataFrame is not empty."""
62+
assert not self.de1.empty, "The DataFrame should not be empty."
63+
def test_dataframe_nonempty1(self):
64+
"""Test that the DataFrame is not empty."""
65+
assert not self.de2.empty, "The DataFrame should not be empty."
66+
def test_column_presence(self):
67+
"""Test that required columns are present in the DataFrame."""
68+
required_columns = ["startTimeUTC", "total", "percentRenewable"]
69+
for col in required_columns:
70+
assert col in self.de1.columns
71+
def test_check_row_count_1(self):
72+
assert len(self.de2) == self.row_count_check_60
73+
def test_check_row_count_2(self):
74+
assert len(self.de1) == self.row_count_check_15
75+

0 commit comments

Comments
 (0)