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 |