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:
    {
      "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 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