frequenz-cs-reporting 0.4.4__tar.gz → 0.4.5__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.
Files changed (67) hide show
  1. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/PKG-INFO +1 -1
  2. frequenz_cs_reporting-0.4.5/RELEASE_NOTES.md +16 -0
  3. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/app_pages/reporting.py +20 -1
  4. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/components/tables.py +70 -26
  5. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/services/data_service.py +75 -0
  6. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/views/dashboard.py +5 -7
  7. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/views/plot_renderers.py +91 -24
  8. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/views/table_renderers.py +0 -72
  9. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz_cs_reporting.egg-info/PKG-INFO +1 -1
  10. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz_cs_reporting.egg-info/scm_file_list.json +68 -68
  11. frequenz_cs_reporting-0.4.5/src/frequenz_cs_reporting.egg-info/scm_version.json +8 -0
  12. frequenz_cs_reporting-0.4.4/RELEASE_NOTES.md +0 -21
  13. frequenz_cs_reporting-0.4.4/src/frequenz_cs_reporting.egg-info/scm_version.json +0 -8
  14. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/.streamlit/config.toml +0 -0
  15. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/Dockerfile +0 -0
  16. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/LICENSE +0 -0
  17. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/MANIFEST.in +0 -0
  18. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/README.md +0 -0
  19. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/app.py +0 -0
  20. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/pyproject.toml +0 -0
  21. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/setup.cfg +0 -0
  22. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/__init__.py +0 -0
  23. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/app_pages/__init__.py +0 -0
  24. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/app_pages/home.py +0 -0
  25. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/app_pages/solar.py +0 -0
  26. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/assets/__init__.py +0 -0
  27. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/assets/neustrom_background.png +0 -0
  28. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/assets/neustrom_logo.png +0 -0
  29. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/components/__init__.py +0 -0
  30. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/components/inputs.py +0 -0
  31. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/components/plot_charts.py +0 -0
  32. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/components/sidebar_inputs.py +0 -0
  33. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/components/ui.py +0 -0
  34. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/conftest.py +0 -0
  35. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/constants.py +0 -0
  36. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/py.typed +0 -0
  37. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/rep_cs_core/__init__.py +0 -0
  38. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/rep_cs_core/config.py +0 -0
  39. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/rep_cs_core/page_spec.py +0 -0
  40. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/services/__init__.py +0 -0
  41. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/services/client_factory.py +0 -0
  42. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/services/solar_workflow.py +0 -0
  43. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/styles/dashboard.css +0 -0
  44. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/styles/home.css +0 -0
  45. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/styles/kpi.css +0 -0
  46. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/styles/plot_card.css +0 -0
  47. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/templates/dashboard_section_divider.html +0 -0
  48. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/templates/home_feature_grid.html +0 -0
  49. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/templates/home_feature_item.html +0 -0
  50. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/templates/home_footer.html +0 -0
  51. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/templates/home_hero.html +0 -0
  52. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/templates/home_logo.html +0 -0
  53. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/templates/home_stat_item.html +0 -0
  54. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/templates/home_stats.html +0 -0
  55. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/ui_resources.py +0 -0
  56. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/utils/__init__.py +0 -0
  57. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/utils/env.py +0 -0
  58. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/utils/time.py +0 -0
  59. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/views/__init__.py +0 -0
  60. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/views/metric_renderers.py +0 -0
  61. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/views/sections.py +0 -0
  62. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz/cs_reporting/views/solar_results.py +0 -0
  63. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz_cs_reporting.egg-info/SOURCES.txt +0 -0
  64. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz_cs_reporting.egg-info/dependency_links.txt +0 -0
  65. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz_cs_reporting.egg-info/requires.txt +0 -0
  66. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/src/frequenz_cs_reporting.egg-info/top_level.txt +0 -0
  67. {frequenz_cs_reporting-0.4.4 → frequenz_cs_reporting-0.4.5}/toml_directory/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frequenz-cs-reporting
3
- Version: 0.4.4
3
+ Version: 0.4.5
4
4
  Summary: Streamlit Library for reporting and solar monitoring
5
5
  Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
6
6
  License-Expression: MIT
@@ -0,0 +1,16 @@
1
+ # Frequenz CS Reporting Library Release Notes
2
+
3
+ ## Summary
4
+
5
+
6
+ ## Upgrading
7
+
8
+
9
+ ## New Features
10
+
11
+ - Fetch and cache battery state-of-charge data for microgrids with batteries.
12
+ - Add a dedicated "Batterie SOC" plot tab showing battery power flow and state of charge.
13
+
14
+ ## Bug Fixes
15
+ - Update the table filtering logic to work in deepnote.
16
+
@@ -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 get_microgrid_data
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,6 +244,7 @@ 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,
@@ -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 gridKey = {json.dumps(key_prefix)};
98
- const messageType = "cs-reporting-reset-aggrid-filters";
99
- window.__csReportingAgGridResetHandlers =
100
- window.__csReportingAgGridResetHandlers || {{}};
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 resetFilters = function() {{
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
- const handler = function(event) {{
115
- if (
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
+ )
@@ -105,12 +105,6 @@ def _build_tables(
105
105
  }
106
106
 
107
107
  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
108
 
115
109
  return {
116
110
  "power_table": power_table,
@@ -124,12 +118,14 @@ def _build_tables(
124
118
  }
125
119
 
126
120
 
121
+ # pylint: disable=too-many-arguments, too-many-positional-arguments
127
122
  def build_master_df(
128
123
  raw_df: pd.DataFrame,
129
124
  component_types: Iterable[str],
130
125
  mcfg: Any,
131
126
  mapper: ColumnMapper,
132
127
  timezone: str = "Europe/Berlin",
128
+ battery_soc_df: pd.DataFrame | None = None,
133
129
  ) -> pd.DataFrame:
134
130
  """Transform raw microgrid data into master analysis dataframe.
135
131
 
@@ -146,6 +142,7 @@ def build_master_df(
146
142
  site-specific settings.
147
143
  mapper: Column name mapper for converting internal names to display names.
148
144
  timezone: Timezone name to use for the report timestamps.
145
+ battery_soc_df: Optional battery SOC dataframe to merge into the report.
149
146
 
150
147
  Returns:
151
148
  Master dataframe with processed and aggregated component data, ready for
@@ -171,6 +168,7 @@ def build_master_df(
171
168
  tz_name=timezone,
172
169
  assume_tz="UTC",
173
170
  component_display_names=component_display_names,
171
+ battery_soc_df=battery_soc_df,
174
172
  )
175
173
  try:
176
174
  master_df = merge_day_ahead_prices(
@@ -218,7 +216,7 @@ def render_dashboard(
218
216
 
219
217
  # --- Plots section---
220
218
  _section_divider("Diagramme & Zeitreihen")
221
- sections.render_plots_tabs(tables, mapper)
219
+ sections.render_plots_tabs(tables, mapper, component_types)
222
220
 
223
221
  # --- Tables section---
224
222
  st.markdown('<div id="data-export-section"></div>', unsafe_allow_html=True)
@@ -6,23 +6,21 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  from functools import partial
9
- from typing import Callable
9
+ from typing import Callable, Iterable
10
10
 
11
11
  import pandas as pd
12
+ import plotly.graph_objects as go
12
13
  import streamlit as st
13
14
  from frequenz.lib.notebooks.reporting.plotter import (
14
15
  plot_energy_pie_chart,
15
16
  plot_time_series,
16
- plot_time_series_battery_usecase,
17
+ plot_time_series_battery_soc,
18
+ plot_time_series_battery_soc_and_usecase,
17
19
  )
18
20
  from frequenz.lib.notebooks.reporting.utils.column_mapper import ColumnMapper
19
21
 
20
22
  from frequenz.cs_reporting.components.ui import render_plot_card
21
- from frequenz.cs_reporting.constants import (
22
- COLOR_DICT,
23
- COMPONENT_CONFIGS,
24
- TablesResult,
25
- )
23
+ from frequenz.cs_reporting.constants import COLOR_DICT, COMPONENT_CONFIGS, TablesResult
26
24
 
27
25
  _COMPONENT_TABS = [
28
26
  ("PV Leistung", "pv"),
@@ -36,6 +34,12 @@ _BATTERY_OVERVIEW_MARGIN = {"t": 150, "r": 56, "b": 96, "l": 64}
36
34
  _BATTERY_OVERVIEW_RANGE_SLIDER_THICKNESS = 0.15
37
35
 
38
36
 
37
+ def _left_align_plot_title(fig: object) -> None:
38
+ """Align Plotly figure titles with the left edge of the chart container."""
39
+ if isinstance(fig, go.Figure):
40
+ fig.update_layout(title={"x": 0, "xref": "container", "xanchor": "left"})
41
+
42
+
39
43
  # pylint: disable=too-many-arguments
40
44
  def render_time_series(
41
45
  df: pd.DataFrame,
@@ -92,7 +96,7 @@ def render_time_series(
92
96
  df,
93
97
  time_col=time_col,
94
98
  cols=cols,
95
- title=title,
99
+ title="",
96
100
  xaxis_title=xaxis_title,
97
101
  yaxis_title=yaxis_title,
98
102
  legend_title=legend_title,
@@ -104,6 +108,12 @@ def render_time_series(
104
108
  dotted_cols=dotted_cols,
105
109
  plot_order=plot_order,
106
110
  )
111
+ fig.update_layout(
112
+ height=_BATTERY_OVERVIEW_HEIGHT,
113
+ margin=_BATTERY_OVERVIEW_MARGIN,
114
+ xaxis_rangeslider_thickness=_BATTERY_OVERVIEW_RANGE_SLIDER_THICKNESS,
115
+ )
116
+ _left_align_plot_title(fig)
107
117
 
108
118
  render_plot_card(title, fig)
109
119
 
@@ -194,64 +204,119 @@ def _prepare_battery_usecase_df(tables: TablesResult) -> pd.DataFrame | None:
194
204
  Returns:
195
205
  Battery-usecase dataframe, or ``None`` when unavailable.
196
206
  """
197
- overview_df = tables.get("overview_df")
198
- if overview_df is None or overview_df.empty:
207
+ battery_usecase_df = tables.get("overview_df")
208
+ if battery_usecase_df is None or battery_usecase_df.empty:
199
209
  return None
200
210
 
201
- return overview_df.drop(columns={"grid_feed_in"}, errors="ignore")
211
+ return battery_usecase_df.drop(columns={"grid_feed_in"}, errors="ignore")
202
212
 
203
213
 
204
- def _render_overview_plot(overview_df: pd.DataFrame | None) -> None:
214
+ def _render_overview_plot(battery_usecase_df: pd.DataFrame | None) -> None:
205
215
  """Render the main time-series overview plot.
206
216
 
207
217
  Args:
208
- overview_df: Battery-usecase dataframe in canonical naming convention.
218
+ battery_usecase_df: Battery-usecase dataframe in canonical naming convention.
209
219
 
210
220
  Returns:
211
221
  Streamlit components are rendered directly.
212
222
  """
213
- if overview_df is None or overview_df.empty:
223
+ if battery_usecase_df is None or battery_usecase_df.empty:
214
224
  st.info("Keine Übersichtsdaten zum Plotten verfügbar.")
215
225
  return
216
226
 
217
- helper_cols = {"battery_charge", "battery_discharge"}
218
- cols_list = [col for col in overview_df.columns if col not in helper_cols]
227
+ cols_list = battery_usecase_df.columns.tolist()
219
228
  secondary_y_cols = (
220
- ["day_ahead_price"] if "day_ahead_price" in overview_df.columns else None
229
+ ["day_ahead_price"] if "day_ahead_price" in battery_usecase_df.columns else None
221
230
  )
222
- fig = plot_time_series_battery_usecase(
223
- overview_df,
224
- time_col="timestamp",
231
+ fig = plot_time_series_battery_soc_and_usecase(
232
+ battery_usecase_df,
225
233
  cols=cols_list,
234
+ time_col="timestamp",
235
+ battery_power_flow="battery_power_flow",
236
+ soc_pct="battery_soc_pct",
226
237
  legend_title=None,
227
238
  secondary_y_cols=secondary_y_cols,
228
- secondary_y_title="EUR/MWh" if secondary_y_cols else None,
239
+ secondary_y_title="EUR/MWh",
229
240
  title="",
230
241
  dotted_cols=[
231
242
  "grid_consumption_without_battery",
232
243
  "peak_before_optimization",
233
244
  "day_ahead_price",
234
245
  ],
235
- stack_mode="psc",
246
+ stack_mode="energy_balance",
247
+ xaxis_title="Zeitpunkt",
248
+ yaxis_title="kW",
249
+ soc_secondary_y_title="SOC [%]",
236
250
  )
237
251
  fig.update_layout(
238
252
  height=_BATTERY_OVERVIEW_HEIGHT,
239
253
  margin=_BATTERY_OVERVIEW_MARGIN,
240
254
  xaxis_rangeslider_thickness=_BATTERY_OVERVIEW_RANGE_SLIDER_THICKNESS,
241
255
  )
256
+ _left_align_plot_title(fig)
242
257
  render_plot_card("Lastgang Übersicht", fig)
243
258
 
244
259
 
260
+ def _render_battery_soc_plot(battery_usecase_df: pd.DataFrame | None) -> None:
261
+ """Render the battery power flow and SOC plot.
262
+
263
+ Args:
264
+ battery_usecase_df: Battery-usecase dataframe in canonical naming convention.
265
+
266
+ Returns:
267
+ Streamlit components are rendered directly.
268
+ """
269
+ if battery_usecase_df is None or battery_usecase_df.empty:
270
+ st.info("Keine Batteriedaten zum Plotten verfügbar.")
271
+ return
272
+
273
+ required_cols = {"timestamp", "battery_power_flow", "battery_soc_pct"}
274
+ missing_cols = required_cols.difference(battery_usecase_df.columns)
275
+ if missing_cols:
276
+ st.info("Keine gültigen Batteriedaten für den SOC-Plot verfügbar.")
277
+ return
278
+
279
+ fig = plot_time_series_battery_soc(
280
+ battery_usecase_df,
281
+ time_col="timestamp",
282
+ title="",
283
+ xaxis_title="Zeitpunkt",
284
+ yaxis_title="kW",
285
+ battery_power_flow="battery_power_flow",
286
+ soc_pct="battery_soc_pct",
287
+ legend_title=None,
288
+ secondary_y_title="SOC [%]",
289
+ )
290
+ fig.update_layout(
291
+ height=_BATTERY_OVERVIEW_HEIGHT,
292
+ margin=_BATTERY_OVERVIEW_MARGIN,
293
+ xaxis_rangeslider_thickness=_BATTERY_OVERVIEW_RANGE_SLIDER_THICKNESS,
294
+ )
295
+ _left_align_plot_title(fig)
296
+ render_plot_card("Batterie Ladezustand", fig)
297
+
298
+
245
299
  def _get_active_tabs(
246
- tables: TablesResult, mapper: ColumnMapper, palette: dict[str, str]
300
+ tables: TablesResult,
301
+ mapper: ColumnMapper,
302
+ palette: dict[str, str],
303
+ component_types: Iterable[str],
247
304
  ) -> list[tuple[str, Callable[[], None]]]:
248
305
  """Determine which tabs should be rendered based on data availability."""
249
306
  tabs = []
307
+ component_type_set = set(component_types)
250
308
 
251
309
  # 1. Overview Tab
252
310
  overview_df = _prepare_battery_usecase_df(tables)
253
311
  if overview_df is not None and not overview_df.empty:
254
312
  tabs.append(("Zeitreihen-Plot", lambda: _render_overview_plot(overview_df)))
313
+ if "battery" in component_type_set:
314
+ tabs.append(
315
+ (
316
+ "Batterie SOC",
317
+ lambda: _render_battery_soc_plot(overview_df),
318
+ )
319
+ )
255
320
 
256
321
  # 2. Energy Mix Tab
257
322
  power_table = tables.get("power_table")
@@ -293,6 +358,7 @@ def _get_active_tabs(
293
358
  def render_plots_tabs(
294
359
  tables: TablesResult,
295
360
  mapper: ColumnMapper,
361
+ component_types: Iterable[str],
296
362
  color_dict: dict[str, str] | None = None,
297
363
  ) -> None:
298
364
  """
@@ -301,12 +367,13 @@ def render_plots_tabs(
301
367
  Args:
302
368
  tables: The tables result containing data for the plots.
303
369
  mapper: The column mapper for renaming columns.
370
+ component_types: Component type identifiers present in the microgrid.
304
371
  color_dict: Optional color mapping for the plots.
305
372
  """
306
373
  palette = color_dict or COLOR_DICT
307
374
 
308
375
  # Get configuration of what to render
309
- plot_tabs_config = _get_active_tabs(tables, mapper, palette)
376
+ plot_tabs_config = _get_active_tabs(tables, mapper, palette, component_types)
310
377
 
311
378
  if not plot_tabs_config:
312
379
  st.info("Keine Plot-Daten verfügbar.")
@@ -5,8 +5,6 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- import json
9
-
10
8
  import pandas as pd
11
9
  import streamlit as st
12
10
  from frequenz.lib.notebooks.reporting.utils.column_mapper import ColumnMapper
@@ -111,72 +109,6 @@ def _style_download_button(
111
109
  )
112
110
 
113
111
 
114
- def _render_reset_filters_button(key_prefix: str) -> None:
115
- """Render a no-rerun button that clears the matching AgGrid filters."""
116
- button_id = f"{key_prefix}_reset_table_filters"
117
- button_html = f"""
118
- <button id="{button_id}" type="button">
119
- Filter der Tabelle zurücksetzen
120
- </button>
121
- <script>
122
- (function() {{
123
- const button = document.getElementById({json.dumps(button_id)});
124
- const gridKey = {json.dumps(key_prefix)};
125
- const messageType = "cs-reporting-reset-aggrid-filters";
126
-
127
- if (!button) {{
128
- return;
129
- }}
130
-
131
- button.addEventListener("click", function(event) {{
132
- event.preventDefault();
133
- event.stopPropagation();
134
-
135
- for (const iframe of window.parent.document.querySelectorAll("iframe")) {{
136
- try {{
137
- iframe.contentWindow.postMessage(
138
- {{ type: messageType, gridKey: gridKey }},
139
- "*"
140
- );
141
- }} catch (error) {{
142
- // Ignore if a browser blocks access to an iframe window.
143
- }}
144
- }}
145
- }});
146
- }})();
147
- </script>
148
- <style>
149
- html,
150
- body {{
151
- margin: 0;
152
- padding: 0;
153
- overflow: hidden;
154
- background: transparent;
155
- }}
156
- #{button_id} {{
157
- background-color: #fff4bf;
158
- border: 1px solid #e4c34a;
159
- color: #4f3f00;
160
- border-radius: 6px;
161
- cursor: pointer;
162
- font-family: inherit;
163
- font-size: 0.8rem;
164
- font-weight: 600;
165
- line-height: 1.1;
166
- min-height: 1.75rem;
167
- padding: 0.15rem 0.55rem;
168
- white-space: nowrap;
169
- }}
170
- #{button_id}:hover {{
171
- background-color: #ffe88a;
172
- border-color: #c7a629;
173
- color: #3f3200;
174
- }}
175
- </style>
176
- """
177
- st.html(button_html, unsafe_allow_javascript=True)
178
-
179
-
180
112
  def render_table_section(
181
113
  df: pd.DataFrame | None,
182
114
  *,
@@ -203,8 +135,6 @@ def render_table_section(
203
135
  header_cols[0].caption(caption)
204
136
  else:
205
137
  header_cols[0].markdown("", unsafe_allow_html=True)
206
- with header_cols[0]:
207
- _render_reset_filters_button(key_prefix)
208
138
 
209
139
  if not safe_df.empty:
210
140
  csv_bytes = display_df.to_csv(index=False, sep=";", decimal=",").encode("utf-8")
@@ -236,8 +166,6 @@ def render_master_df(master_df: pd.DataFrame, mapper: ColumnMapper) -> None:
236
166
  if master_df is not None and not master_df.empty:
237
167
  header_cols = st.columns([1, 0.125])
238
168
  header_cols[0].caption("Standardisierter Haupt-DataFrame")
239
- with header_cols[0]:
240
- _render_reset_filters_button("master_df")
241
169
  display_df = _round_numeric_columns(
242
170
  mapper.to_display(master_df).rename(columns=_MASTER_DF_DISPLAY_RENAMES)
243
171
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frequenz-cs-reporting
3
- Version: 0.4.4
3
+ Version: 0.4.5
4
4
  Summary: Streamlit Library for reporting and solar monitoring
5
5
  Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
6
6
  License-Expression: MIT
@@ -1,93 +1,93 @@
1
1
  {
2
2
  "files": [
3
- "mkdocs.yml",
4
- "app.py",
5
- "MANIFEST.in",
6
- "CONTRIBUTING.md",
7
- "Dockerfile",
8
- ".editorconfig",
9
- "pyproject.toml",
10
3
  ".cookiecutter-replay.json",
11
- "noxfile.py",
12
- "CODEOWNERS",
13
- "LICENSE",
14
- "RELEASE_NOTES.md",
15
- "README.md",
16
- ".gitignore",
4
+ ".editorconfig",
5
+ ".github/ISSUE_TEMPLATE/bug.yml",
6
+ ".github/ISSUE_TEMPLATE/config.yml",
7
+ ".github/ISSUE_TEMPLATE/feature.yml",
17
8
  ".github/RELEASE_NOTES.template.md",
18
- ".github/labeler.yml",
19
9
  ".github/dependabot.yml",
20
10
  ".github/keylabeler.yml",
21
- ".github/ISSUE_TEMPLATE/config.yml",
22
- ".github/ISSUE_TEMPLATE/bug.yml",
23
- ".github/ISSUE_TEMPLATE/feature.yml",
24
- ".github/workflows/dco-merge-queue.yml",
11
+ ".github/labeler.yml",
12
+ ".github/workflows/auto-dependabot.yaml",
13
+ ".github/workflows/black-migration.yaml",
14
+ ".github/workflows/ci-pr.yaml",
25
15
  ".github/workflows/ci.yaml",
16
+ ".github/workflows/dco-merge-queue.yml",
26
17
  ".github/workflows/isort-migration.yaml",
27
- ".github/workflows/release.yaml",
28
18
  ".github/workflows/labeler.yml",
29
- ".github/workflows/repo-config-migration.yaml",
30
- ".github/workflows/auto-dependabot.yaml",
31
- ".github/workflows/black-migration.yaml",
32
19
  ".github/workflows/release-notes-check.yml",
33
- ".github/workflows/ci-pr.yaml",
34
- "tests/test_frequenz_cs_reporting.py",
35
- "toml_directory/__init__.py",
20
+ ".github/workflows/release.yaml",
21
+ ".github/workflows/repo-config-migration.yaml",
22
+ ".gitignore",
36
23
  ".streamlit/config.toml",
37
- "src/frequenz/cs_reporting/py.typed",
24
+ "CODEOWNERS",
25
+ "CONTRIBUTING.md",
26
+ "Dockerfile",
27
+ "LICENSE",
28
+ "MANIFEST.in",
29
+ "README.md",
30
+ "RELEASE_NOTES.md",
31
+ "app.py",
32
+ "docs/CONTRIBUTING.md",
33
+ "docs/SUMMARY.md",
34
+ "docs/_css/mkdocstrings.css",
35
+ "docs/_css/style.css",
36
+ "docs/_img/logo.png",
37
+ "docs/_overrides/main.html",
38
+ "docs/_scripts/mkdocstrings_autoapi.py",
39
+ "docs/index.md",
40
+ "mkdocs.yml",
41
+ "noxfile.py",
42
+ "pyproject.toml",
38
43
  "src/frequenz/cs_reporting/__init__.py",
39
- "src/frequenz/cs_reporting/ui_resources.py",
40
- "src/frequenz/cs_reporting/conftest.py",
41
- "src/frequenz/cs_reporting/constants.py",
42
- "src/frequenz/cs_reporting/utils/__init__.py",
43
- "src/frequenz/cs_reporting/utils/env.py",
44
- "src/frequenz/cs_reporting/utils/time.py",
45
44
  "src/frequenz/cs_reporting/app_pages/__init__.py",
46
- "src/frequenz/cs_reporting/app_pages/solar.py",
47
- "src/frequenz/cs_reporting/app_pages/reporting.py",
48
45
  "src/frequenz/cs_reporting/app_pages/home.py",
46
+ "src/frequenz/cs_reporting/app_pages/reporting.py",
47
+ "src/frequenz/cs_reporting/app_pages/solar.py",
48
+ "src/frequenz/cs_reporting/assets/__init__.py",
49
+ "src/frequenz/cs_reporting/assets/neustrom_background.png",
50
+ "src/frequenz/cs_reporting/assets/neustrom_logo.png",
51
+ "src/frequenz/cs_reporting/components/__init__.py",
52
+ "src/frequenz/cs_reporting/components/inputs.py",
53
+ "src/frequenz/cs_reporting/components/plot_charts.py",
54
+ "src/frequenz/cs_reporting/components/sidebar_inputs.py",
55
+ "src/frequenz/cs_reporting/components/tables.py",
56
+ "src/frequenz/cs_reporting/components/ui.py",
57
+ "src/frequenz/cs_reporting/conftest.py",
58
+ "src/frequenz/cs_reporting/constants.py",
59
+ "src/frequenz/cs_reporting/py.typed",
60
+ "src/frequenz/cs_reporting/rep_cs_core/__init__.py",
61
+ "src/frequenz/cs_reporting/rep_cs_core/config.py",
62
+ "src/frequenz/cs_reporting/rep_cs_core/page_spec.py",
49
63
  "src/frequenz/cs_reporting/services/__init__.py",
50
64
  "src/frequenz/cs_reporting/services/client_factory.py",
51
- "src/frequenz/cs_reporting/services/solar_workflow.py",
52
65
  "src/frequenz/cs_reporting/services/data_service.py",
53
- "src/frequenz/cs_reporting/styles/plot_card.css",
54
- "src/frequenz/cs_reporting/styles/home.css",
66
+ "src/frequenz/cs_reporting/services/solar_workflow.py",
55
67
  "src/frequenz/cs_reporting/styles/dashboard.css",
68
+ "src/frequenz/cs_reporting/styles/home.css",
56
69
  "src/frequenz/cs_reporting/styles/kpi.css",
57
- "src/frequenz/cs_reporting/views/metric_renderers.py",
58
- "src/frequenz/cs_reporting/views/sections.py",
59
- "src/frequenz/cs_reporting/views/plot_renderers.py",
60
- "src/frequenz/cs_reporting/views/__init__.py",
61
- "src/frequenz/cs_reporting/views/solar_results.py",
62
- "src/frequenz/cs_reporting/views/dashboard.py",
63
- "src/frequenz/cs_reporting/views/table_renderers.py",
64
- "src/frequenz/cs_reporting/templates/home_stats.html",
65
- "src/frequenz/cs_reporting/templates/home_hero.html",
66
- "src/frequenz/cs_reporting/templates/home_feature_grid.html",
70
+ "src/frequenz/cs_reporting/styles/plot_card.css",
67
71
  "src/frequenz/cs_reporting/templates/dashboard_section_divider.html",
72
+ "src/frequenz/cs_reporting/templates/home_feature_grid.html",
68
73
  "src/frequenz/cs_reporting/templates/home_feature_item.html",
74
+ "src/frequenz/cs_reporting/templates/home_footer.html",
75
+ "src/frequenz/cs_reporting/templates/home_hero.html",
69
76
  "src/frequenz/cs_reporting/templates/home_logo.html",
70
77
  "src/frequenz/cs_reporting/templates/home_stat_item.html",
71
- "src/frequenz/cs_reporting/templates/home_footer.html",
72
- "src/frequenz/cs_reporting/rep_cs_core/page_spec.py",
73
- "src/frequenz/cs_reporting/rep_cs_core/config.py",
74
- "src/frequenz/cs_reporting/rep_cs_core/__init__.py",
75
- "src/frequenz/cs_reporting/components/plot_charts.py",
76
- "src/frequenz/cs_reporting/components/__init__.py",
77
- "src/frequenz/cs_reporting/components/tables.py",
78
- "src/frequenz/cs_reporting/components/sidebar_inputs.py",
79
- "src/frequenz/cs_reporting/components/ui.py",
80
- "src/frequenz/cs_reporting/components/inputs.py",
81
- "src/frequenz/cs_reporting/assets/neustrom_logo.png",
82
- "src/frequenz/cs_reporting/assets/__init__.py",
83
- "src/frequenz/cs_reporting/assets/neustrom_background.png",
84
- "docs/CONTRIBUTING.md",
85
- "docs/SUMMARY.md",
86
- "docs/index.md",
87
- "docs/_css/mkdocstrings.css",
88
- "docs/_css/style.css",
89
- "docs/_img/logo.png",
90
- "docs/_overrides/main.html",
91
- "docs/_scripts/mkdocstrings_autoapi.py"
78
+ "src/frequenz/cs_reporting/templates/home_stats.html",
79
+ "src/frequenz/cs_reporting/ui_resources.py",
80
+ "src/frequenz/cs_reporting/utils/__init__.py",
81
+ "src/frequenz/cs_reporting/utils/env.py",
82
+ "src/frequenz/cs_reporting/utils/time.py",
83
+ "src/frequenz/cs_reporting/views/__init__.py",
84
+ "src/frequenz/cs_reporting/views/dashboard.py",
85
+ "src/frequenz/cs_reporting/views/metric_renderers.py",
86
+ "src/frequenz/cs_reporting/views/plot_renderers.py",
87
+ "src/frequenz/cs_reporting/views/sections.py",
88
+ "src/frequenz/cs_reporting/views/solar_results.py",
89
+ "src/frequenz/cs_reporting/views/table_renderers.py",
90
+ "tests/test_frequenz_cs_reporting.py",
91
+ "toml_directory/__init__.py"
92
92
  ]
93
93
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.4.5",
3
+ "distance": 0,
4
+ "node": "g7f0f65b50e6b0a4e9783114113eca0100344ec0e",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-10"
8
+ }
@@ -1,21 +0,0 @@
1
- # Frequenz CS Reporting Library Release Notes
2
-
3
- ## Summary
4
-
5
-
6
- ## Upgrading
7
-
8
- - Update dockerfile to have grpcio.
9
-
10
- ## New Features
11
-
12
- - Added a "Filter der Tabelle zurücksetzen" control to reporting tables, allowing
13
- users to clear AgGrid filters without rerunning the page.
14
-
15
- ## Bug Fixes
16
-
17
- - Fixed reporting table pagination and sizing by using a stable page size and
18
- calculated grid height instead of automatic page sizing.
19
- - Improved reporting dashboard error handling for timeouts, missing microgrid
20
- configuration, invalid filter parameters, service configuration problems, and
21
- API/network failures.
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.4.4",
3
- "distance": 0,
4
- "node": "g547b1f4fb14278e0722c43562d10c16b33a6d896",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-04"
8
- }