A full-stack application for visualizing real-time and historical Australian electricity market data from AEMO's NEMWEB portal.
The National Electricity Market (NEM) Dashboard provides real-time monitoring and historical analysis of Australia's electricity market across five regions: New South Wales (NSW), Victoria (VIC), Queensland (QLD), South Australia (SA), and Tasmania (TAS).
- Live Prices: Real-time electricity prices updated every 30 seconds with interactive map visualization
- State Drilldown: Click any region for detailed price history, fuel mix, and generation data
- Extended Time Ranges: View data from 6 hours to 365 days with automatic aggregation
- Price History: Historical price charts with multi-region comparison
- Interconnector Flows: Power flow visualization between states
- Generator Data: SCADA dispatch data with fuel source classification
- Dark Mode: Full dark/light theme support
Stack: FastAPI backend + React frontend + PostgreSQL database
┌─────────────────────────────────────────────────────────────────────┐
│ NEM Dashboard │
├─────────────────────────────┬───────────────────────────────────────┤
│ Frontend (React) │ Backend (FastAPI) │
│ http://localhost:3000 │ http://localhost:8000 │
├─────────────────────────────┼───────────────────────────────────────┤
│ • Live Prices Page │ • Data Ingestion Pipeline │
│ • State Detail Page │ - NEMDispatchClient (SCADA data) │
│ • Price History Page │ - NEMPriceClient (prices/flows) │
│ • Australia Map SVG │ - DataIngester (orchestration) │
│ • Interconnector Flows │ • REST API (14+ endpoints) │
│ • Plotly Charts │ • PostgreSQL Database (async) │
└─────────────────────────────┴───────────────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ AEMO NEMWEB │
│ https://www.nemweb.com.au │
│ • Dispatch SCADA (5-min) │
│ • Trading Prices (30-min) │
│ • Interconnector Flows │
│ • Public Price Archives │
└───────────────────────────────┘
- Python 3.8+ with pip
- Node.js 14+ with npm
- PostgreSQL 15+ (or Docker)
- Git
-
Clone the repository
git clone <repository-url> cd curitiba
-
Start PostgreSQL (using Docker)
cd nem-dashboard-backend docker-compose up -d -
Verify environment and install dependencies
make check # Verify Python and Node.js are installed make install # Install all dependencies (backend + frontend)
Or install manually:
# Backend cd nem-dashboard-backend python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate pip install -r requirements.txt cp .env.example .env # Frontend cd ../nem-dashboard-frontend npm install
One-Command Start (macOS):
make devThis opens two Terminal windows - one for backend (localhost:8000) and one for frontend (localhost:3000).
Manual Start (any platform):
Terminal 1 - Backend:
make run-backend
# Or: cd nem-dashboard-backend && python run.pyTerminal 2 - Frontend:
make run-frontend
# Or: cd nem-dashboard-frontend && npm start# Check if servers are running
make health
# Or manually:
curl http://localhost:8000/health
curl http://localhost:8000/api/summaryRun make help to see all available commands:
Setup: make install, make install-backend, make install-frontend
Development: make dev, make run-backend, make run-frontend
Database: make db, make db-stop
Verification: make check, make check-deps, make health
Build: make build, make test, make clean
curitiba/
├── README.md # This file
├── Makefile # Development commands
├── scripts/
│ └── dev.sh # macOS script to open two Terminal windows
├── nem-dashboard-backend/
│ ├── README.md # Backend documentation
│ ├── docker-compose.yml # PostgreSQL container config
│ ├── app/
│ │ ├── main.py # FastAPI app & endpoints
│ │ ├── database.py # PostgreSQL operations (asyncpg)
│ │ ├── models.py # Pydantic schemas
│ │ ├── nem_client.py # Dispatch data client
│ │ ├── nem_price_client.py # Price/flow client
│ │ └── data_ingester.py # Data pipeline
│ ├── scripts/
│ │ ├── migrate_to_postgres.py # SQLite migration tool
│ │ └── setup_postgres.sh # PostgreSQL setup script
│ ├── run.py # Application entry point
│ ├── import_geninfo_csv.py # Generator data import
│ ├── requirements.txt # Python dependencies
│ └── data/ # Reference data (GenInfo.csv)
└── nem-dashboard-frontend/
├── README.md # Frontend documentation
├── src/
│ ├── App.js # Main container
│ └── components/ # React components
├── public/
│ └── australia-map.svg # Map visualization
└── package.json # Node dependencies
Create a .env file in nem-dashboard-backend/:
| Variable | Default | Description |
|---|---|---|
HOST |
0.0.0.0 |
Server bind address |
PORT |
8000 |
Server port |
RELOAD |
True |
Auto-reload on code changes |
LOG_LEVEL |
info |
Logging level |
DATABASE_URL |
postgresql://postgres:localdev@localhost:5432/nem_dashboard |
PostgreSQL connection URL |
NEM_API_BASE_URL |
https://www.nemweb.com.au |
NEMWEB base URL |
UPDATE_INTERVAL_MINUTES |
5 |
Data fetch interval |
BACKFILL_DAYS_ON_STARTUP |
30 |
Days to backfill on startup |
The frontend uses a proxy configuration in package.json to route API requests to the backend:
"proxy": "http://localhost:8000"No additional environment variables are required for development.
- NEMDispatchClient (nem_client.py) - Downloads CSV/ZIP files from NEMWEB
- NEMPriceClient (nem_price_client.py) - Fetches price and interconnector data
- DataIngester (data_ingester.py) - Orchestrates continuous data ingestion (5-min intervals)
- NEMDatabase (database.py) - PostgreSQL operations with async asyncpg
- FastAPI (main.py) - REST API endpoints with CORS enabled
- dispatch_data - Generator SCADA values (5-min intervals)
- Unique constraint: (settlementdate, duid)
- Key columns: scadavalue, uigf, totalcleared
- price_data - Regional electricity prices (dispatch/trading/public)
- Unique constraint: (settlementdate, region, price_type)
- Three price types: DISPATCH (5-min), TRADING (30-min), PUBLIC (historical)
- interconnector_data - Power flows between states
- Unique constraint: (settlementdate, interconnector)
- generator_info - Generator metadata (region, fuel_source, capacity_mw)
The NEMWEB CSV format is non-standard:
- Files are ZIP-compressed with CSV inside
- CSV records prefixed with record types (e.g., "D,DISPATCH,UNIT_SCADA")
- Parse by filtering specific record types, not standard CSV headers
- Settlement dates in format: "YYYY/MM/DD HH:MM:SS"
- Example:
D,DISPATCH,UNIT_SCADA,1,"2025/01/15 10:30:00",DUID,123.45,"2025/01/15 10:30:05"
lifespancontext manager in main.py handles startup/shutdownDataIngester.run_continuous_ingestion()runs as asyncio background task- Default 5-minute update interval (configurable via UPDATE_INTERVAL_MINUTES env var)
- Automatic backfill on startup: Fills missing price data for the last 30 days
- Fetches: dispatch data, dispatch prices, trading prices, interconnector flows, and daily public prices
Health & Status:
GET /- Root endpoint (returns API name and version)GET /health- Health check (returns status, database connection, timestamp)
Dispatch Data:
GET /api/dispatch/latest- Latest SCADA data (limit parameter)GET /api/dispatch/range?start_date=...&end_date=...&duid=...- Dispatch data for date range
Price Data:
GET /api/prices/latest?price_type=DISPATCH|TRADING|PUBLIC- Latest prices by typeGET /api/prices/history?start_date=...&end_date=...®ion=...&price_type=...- Historical range queries
Interconnector Data:
GET /api/interconnectors/latest- Current interconnector flowsGET /api/interconnectors/history?start_date=...&end_date=...&interconnector=...- Interconnector flow history
Generator Data:
GET /api/generators/filter?region=NSW&fuel_source=Coal- Filtered generator dataGET /api/duids- List of all unique generator DUIDs
Analysis & Summary:
GET /api/generation/by-fuel?start_date=...&end_date=...- Aggregated generation by fuel typeGET /api/summary- Database summary statisticsGET /api/data/coverage?table=price_data- Data coverage for backfill planning
Data Ingestion (Manual Triggers):
POST /api/ingest/current- Trigger current data ingestionPOST /api/ingest/historical?start_date=...&end_date=...- Trigger historical dispatch ingestionPOST /api/ingest/historical-prices?start_date=...&end_date=...- Trigger historical price ingestion
Region-Specific Endpoints (State Drilldown):
GET /api/region/{region}/generation/current- Current fuel mix breakdown for a regionGET /api/region/{region}/generation/history?hours=24- Generation history with auto-aggregationGET /api/region/{region}/prices/history?hours=24&price_type=MERGED- Price history for a regionGET /api/region/{region}/summary- Summary statistics for a region (price, demand, generation)
- DISPATCH: 5-minute interval real-time prices
- TRADING: 30-minute interval prices (standard market interval)
- PUBLIC: Historical daily price data from NEMWEB archives
- MERGED: Combines PUBLIC (where available) with DISPATCH (for recent gaps)
- App.js - Main container with dark mode toggle and tab navigation
- LivePricesPage.js - Real-time prices with map visualization
- Fetches trading prices for display
- Polls every 30 seconds
- Falls back to empty data on error
- Supports state drilldown: Click on a region to navigate to StateDetailPage
- StateDetailPage.js - Detailed view for a specific NEM region
- Price history chart (Plotly, configurable time range: 6h-365d)
- Fuel mix donut chart (current generation by fuel type)
- Summary cards (price, demand, generation, generator count)
- Fuel breakdown table
- Auto-refresh every 60 seconds
- Back button to return to overview
- PriceHistoryPage.js - Historical price charts
- RegionSidebar.js - Regional price cards with hover and click effects
- RegionCard.js - Individual region card component (price display, styling)
- AustraliaMap.js - SVG map with region highlighting and click navigation
- InterconnectorFlow.js - Power flow visualization
- Local component state with useState/useEffect hooks
- No global state management (Redux/Context not used)
- Dark mode prop passed from App.js to child components
// Standard pattern used throughout
const fetchData = async () => {
try {
const response = await axios.get('/api/endpoint');
setData(response.data.data || []);
setLastUpdated(new Date().toLocaleTimeString());
} catch (error) {
console.error('Error:', error);
// Always provide fallback sample data
setData(sampleData);
}
};- CSS modules per component (e.g., LivePricesPage.css)
- Dark mode support via className conditionals:
className={darkMode ? 'dark' : 'light'} - Consistent color scheme for NEM regions:
- NSW: Blue (#1f77b4)
- VIC: Orange (#ff7f0e)
- QLD: Green (#2ca02c)
- SA: Red (#d62728)
- TAS: Purple (#9467bd)
Extended time range queries use automatic aggregation to maintain performance:
| Time Range | Aggregation Level |
|---|---|
| < 48 hours | 5 min (raw data) |
| 48h - 7 days | 30 min averages |
| 7d - 30 days | Hourly averages |
| 30d - 90 days | Daily averages |
| > 90 days | Weekly averages |
- Define response model in
app/models.py - Add database query method in
database.py - Create endpoint in
main.pywith proper error handling - Follow existing pattern: async def, HTTPException for errors, log errors
- Create client method in nem_client.py or nem_price_client.py
- Parse CSV/ZIP following NEM format conventions
- Add database table/insertion method if new table needed
- Update DataIngester.ingest_current_data() to fetch new data source
- Create corresponding API endpoint in main.py
- Create component file in src/components/
- Create matching CSS file
- Follow useState/useEffect pattern for data fetching
- Add darkMode prop support
- Provide error fallback with sample data
All data tables use ON CONFLICT DO UPDATE to handle duplicate records gracefully. When inserting data, duplicates based on unique constraints (settlementdate + identifying column) will update existing records rather than error.
Entire backend is async: asyncpg for database, httpx for HTTP requests, FastAPI async endpoints. Do not mix sync database operations.
CORS middleware configured to allow frontend origins (localhost:3000, localhost:8050). Add new origins to allow_origins list in main.py if deploying to different ports/domains.
Generator metadata enriches dispatch data with fuel source, region, and capacity information. The system includes sample generator data in data_ingester.py. For complete data:
python import_geninfo_csv.pyThis imports from data/GenInfo.csv which should contain columns: duid, station_name, region, fuel_source, technology_type, capacity_mw.
cd nem-dashboard-backend
export DATABASE_URL=postgresql://postgres:localdev@localhost:5432/nem_dashboard
pytest tests/Manual API testing:
curl http://localhost:8000/health
curl http://localhost:8000/api/summary
curl http://localhost:8000/api/prices/latestcd nem-dashboard-frontend
npm test- Generator info sample data is limited to 10 common units (run
import_geninfo_csv.pyfor full dataset) - Frontend has no authentication/authorization
- No data retention policy - database grows indefinitely
- Interconnector data parsing may fail if NEMWEB changes file naming conventions
- Backend requires Python 3.8+ with asyncio support
- Frontend requires Node.js 14+ for React 18
- PostgreSQL 15+ required (use docker-compose for local development)
- NEMWEB may rate-limit requests - respect 5-minute intervals
- Add reverse proxy (nginx) for production frontend serving
- Configure proper CORS origins in main.py for production domains
[Add license information]
[Add contribution guidelines]
For issues or questions, please [add support information].