pvar_dashboard / src /data_fetcher.py
F1nnSBK's picture
Deploy Photovoltaik Risiko-Manager dashboard to Hugging Face Spaces
5841846
Raw
History Blame Contribute Delete
5.56 kB
import logging
import os
import time
from pathlib import Path
import httpx
import pandas as pd
logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] %(message)s",
datefmt="%H:%M:%S",
force=True
)
logging.getLogger("httpx").setLevel(logging.WARNING)
log = logging.getLogger(__name__)
class SmardFetcher:
BASE_URL = "https://www.smard.de/app/chart_data"
RESOLUTION = "hour"
START_DATE = pd.Timestamp("2020-01-01", tz="UTC")
def __init__(self, data_dir: str = None):
target_dir = data_dir or os.getenv("DATA_DIR", "data")
self.raw_dir = Path(target_dir) / "raw"
self.raw_dir.mkdir(parents=True, exist_ok=True)
self.dataset_file = self.raw_dir / "pvar_dataset.csv"
def _get_relevant_timestamps(self, client: httpx.Client, filter_id: str, region_id: str, last_dt: pd.Timestamp = None) -> list:
url = f"{self.BASE_URL}/{filter_id}/{region_id}/index_{self.RESOLUTION}.json"
response = client.get(url)
if response.status_code != 200:
return []
timestamps = response.json().get("timestamps", [])
start_ms = self.START_DATE.timestamp() * 1000
valid_ts = [ts for ts in timestamps if ts >= start_ms]
if last_dt:
last_ms = last_dt.timestamp() * 1000
valid_ts = [ts for ts in valid_ts if ts > last_ms]
return valid_ts
def _fetch_series(self, client: httpx.Client, filter_id: str, region_id: str, timestamp: int, col_name: str, retries=3) -> pd.DataFrame:
url = f"{self.BASE_URL}/{filter_id}/{region_id}/{filter_id}_{region_id}_{self.RESOLUTION}_{timestamp}.json"
for attempt in range(retries):
response = client.get(url)
if response.status_code == 200:
data = response.json().get("series", [])
if not data:
return pd.DataFrame()
df = pd.DataFrame(data, columns=["timestamp", col_name])
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
return df.set_index("timestamp")
elif response.status_code == 429:
time.sleep(2)
else:
break
return pd.DataFrame()
def _sync_feature(self, client: httpx.Client, filter_id: str, region_id: str, col_name: str, last_dt: pd.Timestamp = None) -> pd.DataFrame:
timestamps = self._get_relevant_timestamps(client, filter_id, region_id, last_dt)
if not timestamps:
return pd.DataFrame()
log.info(f" -> {col_name:<18} ({region_id}) | Downloading {len(timestamps)} blocks...")
dfs = []
for ts in timestamps:
dfs.append(self._fetch_series(client, filter_id, region_id, ts, col_name))
time.sleep(0.02) # Rate limit safety
valid_dfs = [df for df in dfs if not df.empty]
return pd.concat(valid_dfs).sort_index() if valid_dfs else pd.DataFrame()
def sync_all(self, progress_cb=None) -> pd.DataFrame:
# Hard wipe of the corrupted file to start 100% fresh
if self.dataset_file.exists():
log.warning("Wiping corrupted dataset to build a seamless combined version...")
self.dataset_file.unlink(missing_ok=True)
feature_dfs = []
with httpx.Client(timeout=30.0) as client:
# 1. Fetch Era 1 & 2 Prices
if progress_cb:
progress_cb("Lade Strompreise herunter...")
df_price_legacy = self._sync_feature(client, "4169", "DE-LU", "price_mwh")
df_price_modern = self._sync_feature(client, "4169", "DE", "price_mwh")
# Combine both price eras vertically, drop overlapping duplicates
df_price = pd.concat([df_price_legacy, df_price_modern]).sort_index()
df_price = df_price[~df_price.index.duplicated(keep='first')]
if not df_price.empty:
feature_dfs.append(df_price)
# 3. Fetch grid physics (Always under DE)
grid_filters = {
"410": "load_total",
"125": "prog_pv",
"123": "prog_wind_onshore",
"3791": "prog_wind_offshore"
}
grid_names = {
"load_total": "Netzlast",
"prog_pv": "Solarprognose",
"prog_wind_onshore": "Wind Onshore Prognose",
"prog_wind_offshore": "Wind Offshore Prognose"
}
for fid, col in grid_filters.items():
if progress_cb:
progress_cb(f"Lade {grid_names.get(col, col)} herunter...")
df_feat = self._sync_feature(client, fid, "DE", col)
if not df_feat.empty:
feature_dfs.append(df_feat)
if not feature_dfs:
log.error("No data could be retrieved.")
return pd.DataFrame()
if progress_cb:
progress_cb("Verarbeite Daten...")
# Merge all features side-by-side on the timestamp index
df_final = pd.concat(feature_dfs, axis=1).sort_index()
# Keep only data from 2020 onwards
df_final = df_final[df_final.index >= self.START_DATE]
df_final.to_csv(self.dataset_file)
log.info(f"Successfully generated clean dataset: {self.dataset_file} ({len(df_final)} rows)")
return df_final
if __name__ == "__main__":
fetcher = SmardFetcher()
fetcher.sync_all()