frequenz-cs-reporting 0.4.4__tar.gz → 0.4.6__tar.gz
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.
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/PKG-INFO +1 -1
- frequenz_cs_reporting-0.4.6/RELEASE_NOTES.md +18 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/app_pages/reporting.py +21 -1
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/components/tables.py +70 -26
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/services/data_service.py +75 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/styles/kpi.css +56 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/views/dashboard.py +34 -9
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/views/metric_renderers.py +92 -8
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/views/plot_renderers.py +99 -31
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/views/table_renderers.py +0 -72
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz_cs_reporting.egg-info/PKG-INFO +1 -1
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz_cs_reporting.egg-info/scm_file_list.json +68 -68
- frequenz_cs_reporting-0.4.6/src/frequenz_cs_reporting.egg-info/scm_version.json +8 -0
- frequenz_cs_reporting-0.4.4/RELEASE_NOTES.md +0 -21
- frequenz_cs_reporting-0.4.4/src/frequenz_cs_reporting.egg-info/scm_version.json +0 -8
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/.streamlit/config.toml +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/Dockerfile +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/LICENSE +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/MANIFEST.in +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/README.md +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/app.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/pyproject.toml +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/setup.cfg +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/__init__.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/app_pages/__init__.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/app_pages/home.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/app_pages/solar.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/assets/__init__.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/assets/neustrom_background.png +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/assets/neustrom_logo.png +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/components/__init__.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/components/inputs.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/components/plot_charts.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/components/sidebar_inputs.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/components/ui.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/conftest.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/constants.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/py.typed +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/rep_cs_core/__init__.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/rep_cs_core/config.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/rep_cs_core/page_spec.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/services/__init__.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/services/client_factory.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/services/solar_workflow.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/styles/dashboard.css +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/styles/home.css +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/styles/plot_card.css +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/templates/dashboard_section_divider.html +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/templates/home_feature_grid.html +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/templates/home_feature_item.html +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/templates/home_footer.html +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/templates/home_hero.html +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/templates/home_logo.html +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/templates/home_stat_item.html +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/templates/home_stats.html +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/ui_resources.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/utils/__init__.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/utils/env.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/utils/time.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/views/__init__.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/views/sections.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/views/solar_results.py +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz_cs_reporting.egg-info/SOURCES.txt +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz_cs_reporting.egg-info/dependency_links.txt +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz_cs_reporting.egg-info/requires.txt +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz_cs_reporting.egg-info/top_level.txt +0 -0
- {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/toml_directory/__init__.py +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Frequenz CS Reporting Library Release Notes
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Upgrading
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## New Features
|
|
10
|
+
|
|
11
|
+
- Add forecasted grid import cost, grid feed-in revenue, and average day-ahead
|
|
12
|
+
price KPI cards for MID231.
|
|
13
|
+
- Add a separate battery KPI card section with battery flow metrics.
|
|
14
|
+
|
|
15
|
+
## Bug Fixes
|
|
16
|
+
|
|
17
|
+
- Add Battery SOC plot tab only for the applicable cases.
|
|
18
|
+
- Use PSC stack mode for the overview time-series plot.
|
|
@@ -22,7 +22,10 @@ from frequenz.cs_reporting.services.client_factory import (
|
|
|
22
22
|
get_component_types,
|
|
23
23
|
get_microgrid_config,
|
|
24
24
|
)
|
|
25
|
-
from frequenz.cs_reporting.services.data_service import
|
|
25
|
+
from frequenz.cs_reporting.services.data_service import (
|
|
26
|
+
get_microgrid_data,
|
|
27
|
+
get_microgrid_soc_data,
|
|
28
|
+
)
|
|
26
29
|
from frequenz.cs_reporting.views.dashboard import build_master_df, render_dashboard
|
|
27
30
|
|
|
28
31
|
|
|
@@ -206,6 +209,21 @@ def render() -> None:
|
|
|
206
209
|
end_date=end_time,
|
|
207
210
|
resolution=resolution,
|
|
208
211
|
)
|
|
212
|
+
battery_soc_df = None
|
|
213
|
+
if "battery" in component_types:
|
|
214
|
+
try:
|
|
215
|
+
fetched_soc_df = get_microgrid_soc_data(
|
|
216
|
+
microgrid_id=microgrid_id,
|
|
217
|
+
start_date=start_time,
|
|
218
|
+
end_date=end_time,
|
|
219
|
+
resolution=resolution,
|
|
220
|
+
)
|
|
221
|
+
except Exception as exc: # pylint: disable=broad-except
|
|
222
|
+
st.warning(
|
|
223
|
+
f"Batterie-SOC-Daten konnten nicht geladen werden: {exc}"
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
battery_soc_df = None if fetched_soc_df.empty else fetched_soc_df
|
|
209
227
|
except Exception as exc: # pylint: disable=broad-except
|
|
210
228
|
st.error(_format_data_loading_error(exc, microgrid_id))
|
|
211
229
|
st.stop()
|
|
@@ -226,12 +244,14 @@ def render() -> None:
|
|
|
226
244
|
mcfg,
|
|
227
245
|
mapper,
|
|
228
246
|
timezone=timezone,
|
|
247
|
+
battery_soc_df=battery_soc_df,
|
|
229
248
|
)
|
|
230
249
|
render_dashboard(
|
|
231
250
|
master_df,
|
|
232
251
|
resolution=resolution,
|
|
233
252
|
component_types=component_types,
|
|
234
253
|
mapper=mapper,
|
|
254
|
+
microgrid_id=microgrid_id,
|
|
235
255
|
)
|
|
236
256
|
_scroll_to_section_if_requested()
|
|
237
257
|
|
|
@@ -19,6 +19,7 @@ _GRID_HEADER_HEIGHT = 40
|
|
|
19
19
|
_GRID_ROW_HEIGHT = 44
|
|
20
20
|
_GRID_PAGINATION_HEIGHT = 56
|
|
21
21
|
_GRID_BORDER_HEIGHT = 2
|
|
22
|
+
_GRID_TOOLBAR_HEIGHT = 40
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
# pylint: disable=too-many-arguments
|
|
@@ -90,39 +91,82 @@ def aggrid_table(
|
|
|
90
91
|
suppressAutoSize=True,
|
|
91
92
|
)
|
|
92
93
|
|
|
93
|
-
# Fit columns on first load
|
|
94
94
|
grid_options = gb.build()
|
|
95
95
|
grid_options["onGridReady"] = JsCode(f"""
|
|
96
96
|
function(params) {{
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const previousHandler =
|
|
103
|
-
window.__csReportingAgGridResetHandlers[gridKey];
|
|
104
|
-
if (previousHandler) {{
|
|
105
|
-
window.removeEventListener("message", previousHandler);
|
|
97
|
+
const buttonId = {json.dumps(f"{key_prefix}_reset_table_filters")};
|
|
98
|
+
const toolbarHeight = {_GRID_TOOLBAR_HEIGHT};
|
|
99
|
+
const existingButton = document.getElementById(buttonId);
|
|
100
|
+
if (existingButton) {{
|
|
101
|
+
return;
|
|
106
102
|
}}
|
|
107
103
|
|
|
108
|
-
const
|
|
104
|
+
const gridRoot = document.querySelector(".ag-root-wrapper");
|
|
105
|
+
const gridParent = gridRoot ? gridRoot.parentElement : null;
|
|
106
|
+
if (!gridRoot || !gridParent) {{
|
|
107
|
+
return;
|
|
108
|
+
}}
|
|
109
|
+
|
|
110
|
+
const currentGridHeight = gridRoot.getBoundingClientRect().height;
|
|
111
|
+
if (currentGridHeight > toolbarHeight) {{
|
|
112
|
+
gridRoot.style.height = `${{currentGridHeight - toolbarHeight}}px`;
|
|
113
|
+
}}
|
|
114
|
+
|
|
115
|
+
const toolbar = document.createElement("div");
|
|
116
|
+
Object.assign(toolbar.style, {{
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
backgroundColor: "transparent",
|
|
119
|
+
boxSizing: "border-box",
|
|
120
|
+
display: "flex",
|
|
121
|
+
height: `${{toolbarHeight}}px`,
|
|
122
|
+
justifyContent: "flex-start",
|
|
123
|
+
padding: "4px 0 6px 0",
|
|
124
|
+
}});
|
|
125
|
+
|
|
126
|
+
const button = document.createElement("button");
|
|
127
|
+
button.id = buttonId;
|
|
128
|
+
button.type = "button";
|
|
129
|
+
button.textContent = "Filter zurücksetzen";
|
|
130
|
+
button.title = "Filter der Tabelle zurücksetzen";
|
|
131
|
+
button.setAttribute("aria-label", "Filter der Tabelle zurücksetzen");
|
|
132
|
+
|
|
133
|
+
Object.assign(button.style, {{
|
|
134
|
+
backgroundColor: "#fff4bf",
|
|
135
|
+
border: "1px solid #e4c34a",
|
|
136
|
+
color: "#4f3f00",
|
|
137
|
+
borderRadius: "6px",
|
|
138
|
+
cursor: "pointer",
|
|
139
|
+
fontFamily: "inherit",
|
|
140
|
+
fontSize: "12px",
|
|
141
|
+
fontWeight: "600",
|
|
142
|
+
lineHeight: "1.1",
|
|
143
|
+
minHeight: "26px",
|
|
144
|
+
padding: "4px 8px",
|
|
145
|
+
whiteSpace: "nowrap",
|
|
146
|
+
}});
|
|
147
|
+
|
|
148
|
+
button.addEventListener("mouseenter", function() {{
|
|
149
|
+
button.style.backgroundColor = "#ffe88a";
|
|
150
|
+
button.style.borderColor = "#c7a629";
|
|
151
|
+
button.style.color = "#3f3200";
|
|
152
|
+
}});
|
|
153
|
+
button.addEventListener("mouseleave", function() {{
|
|
154
|
+
button.style.backgroundColor = "#fff4bf";
|
|
155
|
+
button.style.borderColor = "#e4c34a";
|
|
156
|
+
button.style.color = "#4f3f00";
|
|
157
|
+
}});
|
|
158
|
+
|
|
159
|
+
button.addEventListener("click", function(event) {{
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
event.stopPropagation();
|
|
162
|
+
|
|
109
163
|
params.api.setFilterModel(null);
|
|
110
164
|
params.api.onFilterChanged();
|
|
111
165
|
params.api.paginationGoToFirstPage();
|
|
112
|
-
}};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
event.data &&
|
|
117
|
-
event.data.type === messageType &&
|
|
118
|
-
event.data.gridKey === gridKey
|
|
119
|
-
) {{
|
|
120
|
-
resetFilters();
|
|
121
|
-
}}
|
|
122
|
-
}};
|
|
123
|
-
|
|
124
|
-
window.__csReportingAgGridResetHandlers[gridKey] = handler;
|
|
125
|
-
window.addEventListener("message", handler);
|
|
166
|
+
}});
|
|
167
|
+
|
|
168
|
+
toolbar.appendChild(button);
|
|
169
|
+
gridParent.insertBefore(toolbar, gridRoot);
|
|
126
170
|
}}
|
|
127
171
|
""")
|
|
128
172
|
|
|
@@ -160,7 +204,7 @@ def aggrid_table(
|
|
|
160
204
|
_ = AgGrid(
|
|
161
205
|
df,
|
|
162
206
|
gridOptions=grid_options,
|
|
163
|
-
height=height,
|
|
207
|
+
height=height + _GRID_TOOLBAR_HEIGHT,
|
|
164
208
|
theme=theme,
|
|
165
209
|
allow_unsafe_jscode=True,
|
|
166
210
|
update_mode=GridUpdateMode.NO_UPDATE,
|
|
@@ -99,6 +99,43 @@ async def fetch_microgrid_data(
|
|
|
99
99
|
return df.copy()
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
async def fetch_microgrid_soc_data(
|
|
103
|
+
microgrid_id: int,
|
|
104
|
+
start_date: datetime,
|
|
105
|
+
end_date: datetime,
|
|
106
|
+
resolution: timedelta,
|
|
107
|
+
timeout: float = 30.0,
|
|
108
|
+
) -> pd.DataFrame:
|
|
109
|
+
"""Fetch battery SOC for a microgrid in ``[start_date, end_date)``.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
microgrid_id: Identifier for the target microgrid.
|
|
113
|
+
start_date: Inclusive start date of the query range.
|
|
114
|
+
end_date: Exclusive end date of the query range.
|
|
115
|
+
resolution: Resampling period for the returned data.
|
|
116
|
+
timeout: Request timeout in seconds.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Dataframe of battery SOC values. Empty when no SOC data is available.
|
|
120
|
+
"""
|
|
121
|
+
start_iso, end_iso = validate_range(start_date, end_date)
|
|
122
|
+
client = get_microgrid_client(microgrid_id)
|
|
123
|
+
|
|
124
|
+
coro = client.soc(
|
|
125
|
+
microgrid_id=microgrid_id,
|
|
126
|
+
start=start_iso,
|
|
127
|
+
end=end_iso,
|
|
128
|
+
resampling_period=resolution,
|
|
129
|
+
keep_components=False,
|
|
130
|
+
)
|
|
131
|
+
with _quiet_expected_component_data_warnings():
|
|
132
|
+
df = await asyncio.wait_for(coro, timeout)
|
|
133
|
+
|
|
134
|
+
if df is None or (hasattr(df, "empty") and df.empty):
|
|
135
|
+
return pd.DataFrame()
|
|
136
|
+
return df.copy()
|
|
137
|
+
|
|
138
|
+
|
|
102
139
|
# Cached sync wrapper for Streamlit pages
|
|
103
140
|
@st.cache_data(ttl=300, show_spinner=False)
|
|
104
141
|
def get_microgrid_data(
|
|
@@ -139,3 +176,41 @@ def get_microgrid_data(
|
|
|
139
176
|
"get_microgrid_data() called from within an active event loop. "
|
|
140
177
|
"Use `await fetch_microgrid_data(...)` in async contexts."
|
|
141
178
|
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@st.cache_data(ttl=300, show_spinner=False)
|
|
182
|
+
def get_microgrid_soc_data(
|
|
183
|
+
microgrid_id: int,
|
|
184
|
+
start_date: datetime,
|
|
185
|
+
end_date: datetime,
|
|
186
|
+
resolution: timedelta,
|
|
187
|
+
timeout: float = 30.0,
|
|
188
|
+
) -> pd.DataFrame:
|
|
189
|
+
"""Sync wrapper for battery SOC data with caching.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
microgrid_id: Identifier for the target microgrid.
|
|
193
|
+
start_date: Inclusive start date of the query range.
|
|
194
|
+
end_date: Exclusive end date of the query range.
|
|
195
|
+
resolution: Resampling period for the returned data.
|
|
196
|
+
timeout: Request timeout in seconds.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Dataframe of battery SOC values. Empty when no SOC data is available.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
RuntimeError: If invoked from within an active event loop instead of
|
|
203
|
+
using the async ``fetch_microgrid_soc_data``.
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
asyncio.get_running_loop()
|
|
207
|
+
except RuntimeError:
|
|
208
|
+
return asyncio.run(
|
|
209
|
+
fetch_microgrid_soc_data(
|
|
210
|
+
microgrid_id, start_date, end_date, resolution, timeout=timeout
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
raise RuntimeError(
|
|
214
|
+
"get_microgrid_soc_data() called from within an active event loop. "
|
|
215
|
+
"Use `await fetch_microgrid_soc_data(...)` in async contexts."
|
|
216
|
+
)
|
{frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.6}/src/frequenz/cs_reporting/styles/kpi.css
RENAMED
|
@@ -24,6 +24,62 @@
|
|
|
24
24
|
color: #4a5568;
|
|
25
25
|
margin: 0;
|
|
26
26
|
}
|
|
27
|
+
.kpi-section-warning {
|
|
28
|
+
position: relative;
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
width: 24px;
|
|
33
|
+
height: 24px;
|
|
34
|
+
margin-left: 2px;
|
|
35
|
+
border: 1px solid #facc15;
|
|
36
|
+
border-radius: 6px;
|
|
37
|
+
background: #fef9c3;
|
|
38
|
+
color: #854d0e;
|
|
39
|
+
cursor: help;
|
|
40
|
+
flex: 0 0 auto;
|
|
41
|
+
}
|
|
42
|
+
.kpi-section-warning:hover,
|
|
43
|
+
.kpi-section-warning:focus {
|
|
44
|
+
background: #fde68a;
|
|
45
|
+
outline: 2px solid rgba(250, 204, 21, 0.35);
|
|
46
|
+
outline-offset: 1px;
|
|
47
|
+
}
|
|
48
|
+
.kpi-section-warning__icon {
|
|
49
|
+
color: #ca8a04;
|
|
50
|
+
font-size: 0.82rem;
|
|
51
|
+
line-height: 1;
|
|
52
|
+
}
|
|
53
|
+
.kpi-section-warning__tooltip {
|
|
54
|
+
position: absolute;
|
|
55
|
+
top: calc(100% + 8px);
|
|
56
|
+
left: 0;
|
|
57
|
+
z-index: 10;
|
|
58
|
+
width: max-content;
|
|
59
|
+
max-width: 240px;
|
|
60
|
+
padding: 8px 10px;
|
|
61
|
+
border: 1px solid #facc15;
|
|
62
|
+
border-radius: 6px;
|
|
63
|
+
background: #422006;
|
|
64
|
+
box-shadow: 0 8px 22px rgba(15,41,74,0.18);
|
|
65
|
+
color: #fef9c3;
|
|
66
|
+
font-size: 0.72rem;
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
line-height: 1.3;
|
|
69
|
+
text-align: left;
|
|
70
|
+
text-transform: none;
|
|
71
|
+
letter-spacing: 0;
|
|
72
|
+
white-space: normal;
|
|
73
|
+
opacity: 0;
|
|
74
|
+
pointer-events: none;
|
|
75
|
+
transform: translateY(-3px);
|
|
76
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
77
|
+
}
|
|
78
|
+
.kpi-section-warning:hover .kpi-section-warning__tooltip,
|
|
79
|
+
.kpi-section-warning:focus .kpi-section-warning__tooltip {
|
|
80
|
+
opacity: 1;
|
|
81
|
+
transform: translateY(0);
|
|
82
|
+
}
|
|
27
83
|
|
|
28
84
|
/* ── Individual KPI card ── */
|
|
29
85
|
.kpi-card {
|
|
@@ -56,6 +56,25 @@ def _section_divider(label: str = "", badge: str = "") -> None:
|
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def _numeric_metric(metrics: dict[str, float | str | None], key: str) -> float | None:
|
|
60
|
+
"""Return a metric value as a float when it is numeric."""
|
|
61
|
+
value = metrics.get(key)
|
|
62
|
+
if isinstance(value, (int, float)):
|
|
63
|
+
return float(value)
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _average_price_ct_per_kwh(
|
|
68
|
+
metrics: dict[str, float | str | None], value_key: str, energy_key: str
|
|
69
|
+
) -> float | None:
|
|
70
|
+
"""Calculate an average price in ct/kWh from euro and kWh metric values."""
|
|
71
|
+
value = _numeric_metric(metrics, value_key)
|
|
72
|
+
energy = _numeric_metric(metrics, energy_key)
|
|
73
|
+
if value is None or energy is None or energy == 0.0:
|
|
74
|
+
return None
|
|
75
|
+
return (value * 100) / energy
|
|
76
|
+
|
|
77
|
+
|
|
59
78
|
@st.cache_data(show_spinner="Preparing analysis tables…")
|
|
60
79
|
def _build_tables(
|
|
61
80
|
master_df: pd.DataFrame,
|
|
@@ -91,7 +110,13 @@ def _build_tables(
|
|
|
91
110
|
is automatically invalidated when any input parameters change.
|
|
92
111
|
"""
|
|
93
112
|
power_table = compute_energy_summary(master_df, resolution)
|
|
94
|
-
metrics = aggregate_metrics(master_df, resolution)
|
|
113
|
+
metrics = aggregate_metrics(master_df, resolution, price_column="day_ahead_price")
|
|
114
|
+
metrics["average_da_price_grid_import"] = _average_price_ct_per_kwh(
|
|
115
|
+
metrics, "grid_import_cost_sum", "grid_consumption_sum"
|
|
116
|
+
)
|
|
117
|
+
metrics["average_da_price_grid_feed_in"] = _average_price_ct_per_kwh(
|
|
118
|
+
metrics, "grid_feed_in_revenue_sum", "grid_feed_in_sum"
|
|
119
|
+
)
|
|
95
120
|
|
|
96
121
|
# Build component analyses using configuration
|
|
97
122
|
analyses = {
|
|
@@ -105,12 +130,6 @@ def _build_tables(
|
|
|
105
130
|
}
|
|
106
131
|
|
|
107
132
|
overview_df = build_overview_df(master_df, component_types)
|
|
108
|
-
if "day_ahead_price" in master_df.columns and "timestamp" in overview_df.columns:
|
|
109
|
-
overview_df = overview_df.merge(
|
|
110
|
-
master_df[["timestamp", "day_ahead_price"]],
|
|
111
|
-
on="timestamp",
|
|
112
|
-
how="left",
|
|
113
|
-
)
|
|
114
133
|
|
|
115
134
|
return {
|
|
116
135
|
"power_table": power_table,
|
|
@@ -124,12 +143,14 @@ def _build_tables(
|
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
|
|
146
|
+
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
127
147
|
def build_master_df(
|
|
128
148
|
raw_df: pd.DataFrame,
|
|
129
149
|
component_types: Iterable[str],
|
|
130
150
|
mcfg: Any,
|
|
131
151
|
mapper: ColumnMapper,
|
|
132
152
|
timezone: str = "Europe/Berlin",
|
|
153
|
+
battery_soc_df: pd.DataFrame | None = None,
|
|
133
154
|
) -> pd.DataFrame:
|
|
134
155
|
"""Transform raw microgrid data into master analysis dataframe.
|
|
135
156
|
|
|
@@ -146,6 +167,7 @@ def build_master_df(
|
|
|
146
167
|
site-specific settings.
|
|
147
168
|
mapper: Column name mapper for converting internal names to display names.
|
|
148
169
|
timezone: Timezone name to use for the report timestamps.
|
|
170
|
+
battery_soc_df: Optional battery SOC dataframe to merge into the report.
|
|
149
171
|
|
|
150
172
|
Returns:
|
|
151
173
|
Master dataframe with processed and aggregated component data, ready for
|
|
@@ -171,6 +193,7 @@ def build_master_df(
|
|
|
171
193
|
tz_name=timezone,
|
|
172
194
|
assume_tz="UTC",
|
|
173
195
|
component_display_names=component_display_names,
|
|
196
|
+
battery_soc_df=battery_soc_df,
|
|
174
197
|
)
|
|
175
198
|
try:
|
|
176
199
|
master_df = merge_day_ahead_prices(
|
|
@@ -187,6 +210,7 @@ def render_dashboard(
|
|
|
187
210
|
resolution: timedelta,
|
|
188
211
|
component_types: Iterable[str],
|
|
189
212
|
mapper: ColumnMapper,
|
|
213
|
+
microgrid_id: int,
|
|
190
214
|
) -> None:
|
|
191
215
|
"""Render the complete microgrid reporting dashboard.
|
|
192
216
|
|
|
@@ -202,6 +226,7 @@ def render_dashboard(
|
|
|
202
226
|
component_types: List of component types present in the microgrid for
|
|
203
227
|
dynamic tab generation.
|
|
204
228
|
mapper: Column name mapper for display name standardization.
|
|
229
|
+
microgrid_id: Identifier of the selected microgrid.
|
|
205
230
|
|
|
206
231
|
Returns:
|
|
207
232
|
Renders Streamlit components directly to the app interface.
|
|
@@ -214,11 +239,11 @@ def render_dashboard(
|
|
|
214
239
|
|
|
215
240
|
# --- Overview section---
|
|
216
241
|
_section_divider("Übersicht", "KPIs")
|
|
217
|
-
sections.render_summary_boxes(tables["metrics"], component_types)
|
|
242
|
+
sections.render_summary_boxes(tables["metrics"], component_types, microgrid_id)
|
|
218
243
|
|
|
219
244
|
# --- Plots section---
|
|
220
245
|
_section_divider("Diagramme & Zeitreihen")
|
|
221
|
-
sections.render_plots_tabs(tables, mapper)
|
|
246
|
+
sections.render_plots_tabs(tables, mapper, component_types)
|
|
222
247
|
|
|
223
248
|
# --- Tables section---
|
|
224
249
|
st.markdown('<div id="data-export-section"></div>', unsafe_allow_html=True)
|
|
@@ -26,6 +26,7 @@ _SECTION_ACCENTS: dict[str, str] = {
|
|
|
26
26
|
"Netzkennzahlen": "#3b82f6", # blue – grid
|
|
27
27
|
"(Eigen-)Erzeugungskennzahlen": "#10b981", # green – generation
|
|
28
28
|
"Verbrauchskennzahlen": "#f59e0b", # amber – consumption
|
|
29
|
+
"Batteriekennzahlen": "#14b8a6", # teal – battery
|
|
29
30
|
"Bilanzkennzahlen": "#8b5cf6", # purple – balance ratios
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -33,6 +34,7 @@ _SECTION_ICONS: dict[str, str] = {
|
|
|
33
34
|
"Netzkennzahlen": "⚡",
|
|
34
35
|
"(Eigen-)Erzeugungskennzahlen": "☀",
|
|
35
36
|
"Verbrauchskennzahlen": "📊",
|
|
37
|
+
"Batteriekennzahlen": "🔋",
|
|
36
38
|
"Bilanzkennzahlen": "⚖",
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -47,13 +49,44 @@ def _peak_label(metrics: dict[str, object]) -> str:
|
|
|
47
49
|
return f"Lastspitze (kW) — {metrics.get('peak_date')}"
|
|
48
50
|
|
|
49
51
|
|
|
52
|
+
_INVOICING_WARNING = (
|
|
53
|
+
"Die dargestellten Preise und Daten basieren auf einer Prognose eigener "
|
|
54
|
+
"gemessener Werte und entsprechen nicht den offiziellen Daten des "
|
|
55
|
+
"Verteilernetzbetreibers. Für Abrechnungen sind ausschließlich die "
|
|
56
|
+
"Netzbetreiber-Daten maßgeblich, daher sind Abweichungen zu Rechnungswerten "
|
|
57
|
+
"möglich."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
50
61
|
SECTION_SPECS: list[dict[str, Any]] = [
|
|
51
62
|
{
|
|
52
63
|
"title": "Netzkennzahlen",
|
|
64
|
+
"warning": _INVOICING_WARNING,
|
|
53
65
|
"boxes": [
|
|
54
66
|
{"label": "Netzbezug (kWh)", "key": "grid_consumption_sum"},
|
|
55
67
|
{"label": "Netzeinspeisung (kWh)", "key": "grid_feed_in_sum"},
|
|
56
68
|
{"label_fn": _peak_label, "key": "peak"},
|
|
69
|
+
{
|
|
70
|
+
"label": "Prognostizierte Netzbezugskosten (€)",
|
|
71
|
+
"key": "grid_import_cost_sum",
|
|
72
|
+
"microgrid_ids": {231},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"label": "Prognostizierte Einspeiseerloese (€)",
|
|
76
|
+
"key": "grid_feed_in_revenue_sum",
|
|
77
|
+
"microgrid_ids": {231},
|
|
78
|
+
},
|
|
79
|
+
{"label": "", "key": None, "microgrid_ids": {231}},
|
|
80
|
+
{
|
|
81
|
+
"label": "Prognostizierter Preis (ct/kWh)",
|
|
82
|
+
"key": "average_da_price_grid_import",
|
|
83
|
+
"microgrid_ids": {231},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"label": "Prognostizierter Preis (ct/kWh)",
|
|
87
|
+
"key": "average_da_price_grid_feed_in",
|
|
88
|
+
"microgrid_ids": {231},
|
|
89
|
+
},
|
|
57
90
|
],
|
|
58
91
|
},
|
|
59
92
|
{
|
|
@@ -85,6 +118,33 @@ SECTION_SPECS: list[dict[str, Any]] = [
|
|
|
85
118
|
{"label": "Eigenverbrauch (kWh)", "key": "prod_self_consumption_sum"},
|
|
86
119
|
],
|
|
87
120
|
},
|
|
121
|
+
{
|
|
122
|
+
"title": "Batteriekennzahlen",
|
|
123
|
+
"per_row": 3,
|
|
124
|
+
"boxes": [
|
|
125
|
+
{
|
|
126
|
+
"label": "Erzeugung zu Batterie (kWh)",
|
|
127
|
+
"key": "production_to_battery_sum",
|
|
128
|
+
"component_type": "battery",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"label": "Netz zu Batterie (kWh)",
|
|
132
|
+
"key": "grid_to_battery_sum",
|
|
133
|
+
"component_type": "battery",
|
|
134
|
+
},
|
|
135
|
+
{"label": "", "key": None},
|
|
136
|
+
{
|
|
137
|
+
"label": "Batterie zu Netz (kWh)",
|
|
138
|
+
"key": "battery_to_grid_sum",
|
|
139
|
+
"component_type": "battery",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"label": "Batterie zu Verbrauch (kWh)",
|
|
143
|
+
"key": "battery_to_consumption_sum",
|
|
144
|
+
"component_type": "battery",
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
88
148
|
{
|
|
89
149
|
"title": "Bilanzkennzahlen",
|
|
90
150
|
"per_row": 3,
|
|
@@ -197,9 +257,11 @@ def _build_consumption_breakdown(metrics: dict[str, Any]) -> dict[str, float | N
|
|
|
197
257
|
return {k: (float(v) if v is not None else 0.0) for k, v in values.items()}
|
|
198
258
|
|
|
199
259
|
|
|
260
|
+
# pylint: disable=too-many-locals
|
|
200
261
|
def render_summary_boxes(
|
|
201
262
|
metrics: dict[str, Any],
|
|
202
263
|
component_types: Iterable[str] | None = None,
|
|
264
|
+
microgrid_id: int | None = None,
|
|
203
265
|
) -> None:
|
|
204
266
|
"""Render overview metrics grouped into styled subsections.
|
|
205
267
|
|
|
@@ -207,6 +269,8 @@ def render_summary_boxes(
|
|
|
207
269
|
metrics: Metrics dictionary containing aggregated KPI values.
|
|
208
270
|
component_types: Optional iterable of component type identifiers present
|
|
209
271
|
in the microgrid (e.g., ``{"pv", "chp"}``).
|
|
272
|
+
microgrid_id: Optional microgrid identifier used for microgrid-specific
|
|
273
|
+
KPI cards.
|
|
210
274
|
|
|
211
275
|
Returns:
|
|
212
276
|
Streamlit components are rendered directly.
|
|
@@ -230,29 +294,49 @@ def render_summary_boxes(
|
|
|
230
294
|
)
|
|
231
295
|
|
|
232
296
|
for section in SECTION_SPECS:
|
|
297
|
+
box_specs = section["boxes"]
|
|
298
|
+
if component_type_set:
|
|
299
|
+
box_specs = [
|
|
300
|
+
spec
|
|
301
|
+
for spec in box_specs
|
|
302
|
+
if spec.get("component_type") is None
|
|
303
|
+
or spec.get("component_type") in component_type_set
|
|
304
|
+
]
|
|
305
|
+
box_specs = [
|
|
306
|
+
spec
|
|
307
|
+
for spec in box_specs
|
|
308
|
+
if spec.get("microgrid_ids") is None
|
|
309
|
+
or microgrid_id in spec.get("microgrid_ids", set())
|
|
310
|
+
]
|
|
311
|
+
if not box_specs:
|
|
312
|
+
continue
|
|
313
|
+
|
|
233
314
|
title = section["title"]
|
|
234
315
|
accent = _SECTION_ACCENTS.get(title, "#3b82f6")
|
|
235
316
|
icon = _SECTION_ICONS.get(title, "●")
|
|
236
317
|
icon_bg = accent + "22" # ~13% opacity hex approximation
|
|
318
|
+
warning = section.get("warning")
|
|
319
|
+
warning_html = (
|
|
320
|
+
f'<span class="kpi-section-warning" role="button" tabindex="0" '
|
|
321
|
+
f'aria-label="{warning}">'
|
|
322
|
+
'<span class="kpi-section-warning__icon">⚠</span>'
|
|
323
|
+
f'<span class="kpi-section-warning__tooltip">{warning}</span>'
|
|
324
|
+
"</span>"
|
|
325
|
+
if warning
|
|
326
|
+
else ""
|
|
327
|
+
)
|
|
237
328
|
|
|
238
329
|
st.markdown(
|
|
239
330
|
f"""
|
|
240
331
|
<div class="kpi-section-header">
|
|
241
332
|
<div class="kpi-section-icon" style="background:{icon_bg};">{icon}</div>
|
|
242
333
|
<p class="kpi-section-title">{title}</p>
|
|
334
|
+
{warning_html}
|
|
243
335
|
</div>
|
|
244
336
|
""",
|
|
245
337
|
unsafe_allow_html=True,
|
|
246
338
|
)
|
|
247
339
|
|
|
248
|
-
box_specs = section["boxes"]
|
|
249
|
-
if component_type_set:
|
|
250
|
-
box_specs = [
|
|
251
|
-
spec
|
|
252
|
-
for spec in box_specs
|
|
253
|
-
if spec.get("component_type") is None
|
|
254
|
-
or spec.get("component_type") in component_type_set
|
|
255
|
-
]
|
|
256
340
|
boxes = _materialize_boxes(box_specs, metrics)
|
|
257
341
|
per_row = section.get("per_row", 3)
|
|
258
342
|
render_box_grid(boxes, per_row=per_row, accent=accent)
|