BESS-SCADA-Data / PLAN.md
Claude
Initial implementation: NEM Battery SCADA Data Explorer
fd0bd3b
|
Raw
History Blame Contribute Delete
8.62 kB
# 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 |