Spaces:
Running
Running
BESS SCADA Data Web App β Implementation Plan
Architecture Overview
Hosting: HuggingFace Spaces (Docker SDK) Backend: FastAPI (Python) Frontend: HTML/CSS/JS served by FastAPI (lightweight, no React build step needed) Data Processing: Polars (memory-efficient, faster than pandas for large CSVs) Charts: Plotly.js (interactive, zoom/pan, rendered client-side) Analytics: Lightweight SQLite logging (ephemeral but rebuilt; optionally push to free Supabase) BESS List: Static JSON file in repo, manually updated monthly
Data Flow
Student selects: State β BESS β Date
β
Frontend sends request to FastAPI: GET /api/data?duid=XXXX&date=2026-03-01
β
Backend constructs AEMO URL:
- Current: https://www.nemweb.com.au/REPORTS/Current/FPPDAILY/
- Archive: https://nemweb.com.au/Reports/Archive/FPPDAILY/
β
Backend downloads ZIP from AEMO (~20-50 MB compressed)
β
Backend extracts CSV in-memory (streaming, never full file to disk)
β
Backend filters rows: FPP_UNITID == selected DUID
Result: ~21,600 rows (~1-2 MB) for one BESS for one day
β
Backend returns JSON (for chart/table display) or CSV/Parquet (for download)
β
Frontend renders interactive chart + paginated table
Project Structure
BESS-SCADA-Data/
βββ Dockerfile # HuggingFace Spaces Docker config
βββ requirements.txt # Python dependencies
βββ README.md # HuggingFace Spaces metadata
βββ app/
β βββ main.py # FastAPI application entry point
β βββ config.py # Constants, URLs, settings
β βββ data/
β β βββ bess_list.json # Static BESS list by state (updated monthly)
β β βββ quality_flags.json # MW_QUALITY_FLAG descriptions
β βββ services/
β β βββ aemo_fetcher.py # Download & extract ZIP from AEMO
β β βββ data_processor.py # Filter, transform CSV data using Polars
β β βββ analytics.py # Log requests (IP, timestamp, BESS, date)
β βββ routers/
β β βββ api.py # REST API endpoints
β β βββ pages.py # Serve HTML pages
β βββ static/
β βββ index.html # Main single-page app
β βββ css/
β β βββ style.css # Styling
β βββ js/
β βββ app.js # Frontend logic (fetch, render chart/table)
Implementation Steps
Step 1: Project Setup & Docker Configuration
- Initialize project structure
- Create
Dockerfilefor HuggingFace Spaces (Python 3.11 slim, expose port 7860) - Create
requirements.txt: fastapi, uvicorn, polars, httpx, pyarrow - Create HuggingFace Spaces
README.mdwith metadata (sdk: docker)
Step 2: Static BESS List
- Parse the AEMO NEM Generation Information Excel manually
- Create
bess_list.jsonwith structure:{ "NSW": [ {"duid": "BESS1", "name": "Battery Name", "capacity_mw": 100, "region": "NSW1"} ], "VIC": [...], "QLD": [...], "SA": [...], "TAS": [...] } - Create
quality_flags.json:{ "0": {"label": "Good", "description": "Good quality data", "color": "green"}, "1": {"label": "Bad", "description": "Sustained communication failure or manual override", "color": "red"}, "-1": {"label": "N/A", "description": "Not applicable", "color": "gray"} }
Step 3: AEMO Data Fetcher Service
aemo_fetcher.py:- Given a date, construct the correct AEMO URL (Current vs Archive)
- Download ZIP using
httpxwith 30-second timeout - Extract CSV from ZIP in-memory using
zipfilemodule - Return raw CSV bytes/stream
- Error handling: timeout β retry once β user-friendly error
- Need to investigate the exact filename pattern in the ZIP to construct URLs correctly
Step 4: Data Processor Service
data_processor.py:- Read CSV bytes with Polars (lazy mode for memory efficiency)
- Filter by
FPP_UNITID == selected_duid - Select only relevant columns: INTERVAL_DATETIME, MEASUREMENT_DATETIME, FPP_UNITID, MEASURED_MW, MW_QUALITY_FLAG
- Return as:
- Polars DataFrame (for JSON serialization to frontend)
- CSV bytes (for CSV download)
- Parquet bytes (for Parquet download)
- Compute summary statistics: min, max, mean MW, % good quality, % bad quality
Step 5: Analytics Service
analytics.py:- On each data request, log to SQLite (
/tmp/analytics.db):- Timestamp
- IP address (from request headers, respecting X-Forwarded-For)
- Selected BESS (DUID)
- Selected date
- Download format (view/csv/parquet)
- Provide endpoint to view analytics (protected by simple query param token)
- Note: SQLite on ephemeral storage will be lost on restart β acceptable for basic tracking. Could optionally push to free Supabase (500 MB free) for persistence.
- On each data request, log to SQLite (
Step 6: FastAPI Backend Endpoints
GET /β Serve main HTML pageGET /api/bessβ Return BESS list grouped by stateGET /api/data?duid={duid}&date={YYYY-MM-DD}β Return filtered SCADA data as JSON- Response includes: data rows, summary stats, quality flag breakdown
GET /api/download/csv?duid={duid}&date={YYYY-MM-DD}β Download as CSVGET /api/download/parquet?duid={duid}&date={YYYY-MM-DD}β Download as ParquetGET /api/analytics?token={secret}β View usage analytics (admin only)
Step 7: Frontend (Single Page App)
- State selector: Dropdown for Australian state (NSW, VIC, QLD, SA, TAS)
- BESS selector: Populated dynamically based on selected state
- Date picker: Single date (max 1 day), with validation (not future dates, not before data availability)
- Load button: Fetches data from backend
- Loading state: Spinner with message "Fetching data from AEMO... this may take 10-15 seconds"
- Chart: Plotly.js time-series chart of MEASURED_MW vs MEASUREMENT_DATETIME
- Color-coded by MW_QUALITY_FLAG (green=good, red=bad, gray=N/A)
- Interactive zoom, pan, hover tooltips
- Data quality banner:
- "X% of measurements are good quality, Y% flagged as bad quality (communication failure or manual override)"
- Data table: Paginated table showing all rows (virtual scrolling for performance)
- Download buttons: CSV and Parquet download buttons
- Summary stats panel: Min, Max, Mean, Std Dev of MEASURED_MW
Step 8: Error Handling & UX
- AEMO server unavailable β "AEMO data source is temporarily unavailable. Please try again in a few minutes."
- No data for selected BESS/date β "No SCADA data found for [BESS name] on [date]. This unit may not have been operational on this date."
- Large response handling β Stream response, show progress
- Rate limiting β Simple in-memory rate limit (10 requests/minute per IP) to prevent abuse
Step 9: Deployment to HuggingFace Spaces
- Create HuggingFace Space with Docker SDK
- Push code to HuggingFace repo
- Configure Space settings
- Test with sample BESS queries
Step 10: Documentation & BESS List Maintenance
- Document how to update
bess_list.jsonwhen AEMO publishes new generation info - Provide a helper script to parse the AEMO Excel and generate the JSON
Key Technical Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Polars over Pandas | Polars | 2-10x faster, much lower memory usage, lazy evaluation |
| FastAPI over Flask | FastAPI | Async support (critical for downloading from AEMO), auto-docs, modern |
| Plotly.js over server-side charts | Plotly.js | Client-side rendering reduces server load, interactive |
| Single HTML page over React | Vanilla JS | No build step, simpler deployment, fewer dependencies |
| Parquet for download | PyArrow | Parquet is 5-10x smaller than CSV, native to Python data science workflows |
| httpx over requests | httpx | Async support, connection pooling, timeout handling |
Risks & Mitigations
| Risk | Mitigation |
|---|---|
| AEMO changes URL structure | Configurable URLs in config.py, easy to update |
| HF Space sleeps after inactivity | Acceptable for academic use; wakes in ~30 seconds |
| 16 GB RAM exceeded | On-demand fetch + Polars lazy mode keeps memory under 500 MB per request |
| AEMO rate-limits our requests | Cache recently fetched data in /tmp (ephemeral but helps during active sessions) |
| BESS list becomes stale | Monthly manual update process documented; helper script provided |