Data Engineering ยท Open Source ยท Technical Deep Dive

Technical Deep Dive: Electricity Prices Local Pipeline

Docker Compose stack details, and the three real bugs found and fixed while getting the dashboard right

Goal: a self-contained, Dockerized system fetching German electricity prices, storing them locally, and rendering interactive Plotly dashboards.

Result: โœ… Working end-to-end โ€” dashboard live, both charts rendering, three real bugs found and fixed along the way.

Stack

Data sourceaWATTar API โ€” free, no key
StorageLocal filesystem โ€” raw JSON (bronze) โ†’ merged Parquet (silver)
VisualizationsPlotly heatmap, time series, stats dashboard
ScheduleRuns once on startup, then daily at 13:00 UTC
Cost$0 โ€” everything runs locally
Docker Compose Stack
โ”œโ”€โ”€ seaweedfs   โ€“ S3-compatible storage (present, unused by current pipeline)
โ”œโ”€โ”€ etl         โ€“ Python service: fetch โ†’ process โ†’ merge โ†’ visualize
โ””โ”€โ”€ webdav      โ€“ Nginx server exposing the dashboard + charts

Data flow: aWATTar API โ†’ JSON saved to data/bronze/new/ โ†’ flattened & merged into data/silver/prices.parquet โ†’ Plotly charts + dashboard HTML written to webdav/ โ†’ served at localhost:8081.

Key classes/methods

ElectricityPricePipeline โ€” fetch_prices() pulls today's prices from aWATTar; process_bronze_files() reads raw JSON, flattens, merges, triggers visualization; _flatten_prices() converts the API response into row dicts; _merge_to_silver() dedupes and merges into the master table; generate_visualizations() orchestrates chart + dashboard generation; run_once() / run_scheduled().

Bug 1 โ€” zero-price records could be silently overwritten by stale data

Symptom: 0 prices weren't reliably reflected in the dashboard.

Root cause: _merge_to_silver() deduplicated on start_utc using pandas' default keep='first'. If an hour was already stored from an earlier fetch, that old row always won over a fresh fetch for the same hour โ€” so a corrected price of 0 from a later fetch could never overwrite a stale non-zero value.

combined_df = combined_df.drop_duplicates(subset=['start_utc'], keep='last')

Also cleaned up _flatten_prices(), which filtered records using a mixed truthy/is not None check inside all([...]) โ€” fragile and easy to misread:

if start_ts is None or end_ts is None or marketprice is None:
    continue

This makes it unambiguous that a price of 0 is valid data, not "missing."

Bug 2 โ€” charts never appeared in the dashboard (blank iframes)

Symptom: the heatmap and time-series <iframe>s in the dashboard were empty.

Root cause: the dashboard HTML requests /visualizations/heatmap.html and /visualizations/timeseries.html, but generate_visualizations() was writing those files directly into viz_dir โ€” the same directory Nginx serves as its web root. That put the files one level too high (reachable at /heatmap.html, not /visualizations/heatmap.html), so the iframe requests 404'd.

Fix: introduced a viz_subdir (/visualizations/visualizations inside the container, mapped to webdav/visualizations/ on the host) and pointed the heatmap/time-series writers at it, while dashboard.html and index.html stay at the viz_dir root โ€” matching the URL paths the HTML already used. No docker-compose.yml changes were needed.

Bug 3 โ€” emoji/symbols rendering as garbled text (mojibake)

Symptom: โšก displayed as รขลกยก in the browser.

Root cause: the generated HTML had no <meta charset="UTF-8"> tag, and the file was written with open(path, 'w') โ€” using the container's default locale encoding rather than an explicit one. The browser guessed the wrong encoding (typically Latin-1) when decoding the UTF-8 emoji bytes.

Fix: added <meta charset="UTF-8"> to the generated dashboard's <head>, and wrote the file explicitly as UTF-8: open(path, 'w', encoding='utf-8'). Optional hardening also recommended: charset utf-8; inside Nginx's server {} block, to force the correct Content-Type response header.

Verifying the fixes

# 1. Clear old/possibly-corrupted state
docker-compose down -v
rm -rf data webdav/*.html webdav/visualizations

# 2. Rebuild with the fixed function_app.py
docker-compose up -d --build

# 3. Run the pipeline once
docker-compose exec -e RUN_ONCE=true etl python function_app.py

# 4. Open the dashboard
open http://localhost:8081

Expected result, confirmed: stats card shows the correct min/current price (including any 0.00 values), both charts render inline, and all emoji/symbols display correctly.

Quick reference

ActionCommand
Start servicesdocker-compose up -d
Run ETL oncedocker-compose exec -e RUN_ONCE=true etl python function_app.py
Regenerate charts onlydocker-compose exec etl python visualize.py
View logsdocker-compose logs -f etl
Check datadocker-compose exec etl python -c "import pandas as pd; print(pd.read_parquet('/data/silver/prices.parquet'))"
Full resetdocker-compose down -v && rm -rf data webdav/*.html webdav/visualizations
Being upfront about scope: the SeaweedFS container sits in the Compose file but this pipeline routes storage straight to the local filesystem instead โ€” no real S3-API parity attempt like the Bee Haven open-source build has. A reasonable next step if this project gets picked back up, not a claim being made now.

Full story: project overview. Compare against the Azure build and its technical deep-dive.