Skip to content

Commit 8d1685d

Browse files
committed
Add data fetching for asset optimization
Add helper functions to initialize an microgrid data instance from environment variables. Those can exist in the environment or optionally be specified in an env file that can be passed to the function. In addition to that a function for fetching and preparing the data as required for the visualization functions is added. Signed-off-by: cwasicki <[email protected]>
1 parent e2ce760 commit 8d1685d

File tree

1 file changed

+145
-0
lines changed
  • src/frequenz/lib/notebooks/reporting/asset_optimization

1 file changed

+145
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Data fetching for asset optimization reporting."""
5+
6+
7+
import logging
8+
import os
9+
from datetime import datetime, timedelta
10+
11+
import numpy as np
12+
import pandas as pd
13+
from dotenv import load_dotenv
14+
15+
from frequenz.data.microgrid import MicrogridConfig, MicrogridData
16+
17+
_logger = logging.getLogger(__name__)
18+
19+
20+
def init_microgrid_data(
21+
*,
22+
microgrid_config_dir: str,
23+
dotenv_path: str | None = None,
24+
) -> MicrogridData:
25+
"""Load MicrogridData instance using environment variables.
26+
27+
Args:
28+
microgrid_config_dir: Directory containing microgrid configuration files.
29+
dotenv_path: Optional path to an environment variable file.
30+
31+
Returns:
32+
MicrogridData instance.
33+
"""
34+
if dotenv_path is not None:
35+
load_dotenv(dotenv_path=dotenv_path)
36+
37+
service_address = os.environ["REPORTING_API_URL"]
38+
api_key = os.environ["REPORTING_API_KEY"]
39+
api_secret = os.environ["REPORTING_API_SECRET"]
40+
41+
mcfg = MicrogridConfig.load_configs(
42+
microgrid_config_dir=microgrid_config_dir,
43+
)
44+
return MicrogridData(
45+
server_url=service_address,
46+
auth_key=api_key,
47+
sign_secret=api_secret,
48+
microgrid_configs=mcfg,
49+
)
50+
51+
52+
# pylint: disable=too-many-arguments
53+
async def fetch_data(
54+
mdata: MicrogridData,
55+
*,
56+
component_types: tuple[str],
57+
mid: int,
58+
start_time: datetime,
59+
end_time: datetime,
60+
resampling_period: timedelta,
61+
splits: bool = False,
62+
fetch_soc: bool = False,
63+
) -> pd.DataFrame:
64+
"""
65+
Fetch data of a microgrid and processes it for plotting.
66+
67+
Args:
68+
mdata: MicrogridData object to fetch data from.
69+
component_types: List of component types to fetch data for.
70+
mid: Microgrid ID.
71+
start_time: Start time for data fetching.
72+
end_time: End time for data fetching.
73+
resampling_period: Time resolution for data fetching.
74+
splits: Whether to split the data into positive and negative parts.
75+
fetch_soc: Whether to fetch state of charge (SOC) data.
76+
77+
Returns:
78+
DataFrame containing the processed data.
79+
80+
Raises:
81+
ValueError: If no data is found for the given microgrid and time range or if
82+
unexpected component types are present in the data.
83+
"""
84+
_logger.info(
85+
"Requesting data from %s to %s at %s resolution",
86+
start_time,
87+
end_time,
88+
resampling_period,
89+
)
90+
df = await mdata.ac_active_power(
91+
microgrid_id=mid,
92+
component_types=component_types,
93+
start=start_time,
94+
end=end_time,
95+
resampling_period=resampling_period,
96+
keep_components=False,
97+
splits=splits,
98+
unit="kW",
99+
)
100+
if df is None or df.empty:
101+
raise ValueError(
102+
f"No data found for microgrid {mid} between {start_time} and {end_time}"
103+
)
104+
105+
_logger.debug("Received %s rows and %s columns", df.shape[0], df.shape[1])
106+
107+
if fetch_soc:
108+
soc_df = await mdata.soc(
109+
microgrid_id=mid,
110+
start=start_time,
111+
end=end_time,
112+
resampling_period=resampling_period,
113+
keep_components=False,
114+
)
115+
if soc_df is None or soc_df.empty:
116+
raise ValueError(
117+
f"No SOC data found for microgrid {mid} between {start_time} and {end_time}"
118+
)
119+
120+
# Concat in case indices mismatch
121+
df = pd.concat([df, soc_df["battery"].rename("soc")], axis=1)
122+
123+
# Default to nan for missing SOC data
124+
df["soc"] = df.get("soc", np.nan)
125+
126+
# For later visualization we default to zero so we can use
127+
# the same plots for different microgrid setups
128+
df["battery"] = df.get("battery", 0)
129+
df["pv"] = df.get("pv", 0)
130+
df["chp"] = df.get("chp", 0)
131+
132+
# We only care about the generation part for this analysis
133+
df["pv"] = df["pv"].clip(upper=0)
134+
df["chp"] = df["chp"].clip(upper=0)
135+
136+
# Determine consumption if not present
137+
if "consumption" not in df.columns:
138+
cols = df.columns.tolist()
139+
if any(ct not in ["grid", "pv", "battery", "chp", "soc"] for ct in cols):
140+
raise ValueError(
141+
f"Consumption not found in data and unexpected component types present: {cols}."
142+
)
143+
df["consumption"] = df["grid"] - (df["chp"] + df["pv"] + df["battery"])
144+
145+
return df

0 commit comments

Comments
 (0)