Spaces:
Running
Running
File size: 8,623 Bytes
fd0bd3b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | # 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 `Dockerfile` for HuggingFace Spaces (Python 3.11 slim, expose port 7860)
- Create `requirements.txt`: fastapi, uvicorn, polars, httpx, pyarrow
- Create HuggingFace Spaces `README.md` with metadata (sdk: docker)
### Step 2: Static BESS List
- Parse the AEMO NEM Generation Information Excel manually
- Create `bess_list.json` with structure:
```json
{
"NSW": [
{"duid": "BESS1", "name": "Battery Name", "capacity_mw": 100, "region": "NSW1"}
],
"VIC": [...],
"QLD": [...],
"SA": [...],
"TAS": [...]
}
```
- Create `quality_flags.json`:
```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 `httpx` with 30-second timeout
- Extract CSV from ZIP in-memory using `zipfile` module
- 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.
### Step 6: FastAPI Backend Endpoints
- `GET /` β Serve main HTML page
- `GET /api/bess` β Return BESS list grouped by state
- `GET /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 CSV
- `GET /api/download/parquet?duid={duid}&date={YYYY-MM-DD}` β Download as Parquet
- `GET /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.json` when 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 |
|