Real-time European energy grid monitor with AI-powered anomaly detection, short-range forecasting, and Telegram alerts.
Grid-Watch polls the Energy Charts API every hour, analyses live power generation data for Germany (or any ENTSO-E bidding zone), detects anomalies using a four-detector stack, forecasts the next 4 hours with Facebook Prophet, generates an Excel report, and fires a Telegram alert — all in under 4 seconds per cycle.
🟠 HIGH ALERT
[HIGH] 5 anomaly instance(s) — consumption_mw_zscore, isolation_forest_multivariate
Window: 2026-05-24 22:00 UTC → 2026-05-25 17:15 UTC.
Analysed 78 record(s) and detected 5 anomaly instance(s).
Renewable share averaged 65.1% (min 11.5%).
Consumption averaged 39,167 MW with a peak of 45,050 MW.
Grid status: HIGH severity — operator review recommended.
──────────────────────
🕐 2026-05-25 18:32:16 UTC
🔖 Run: 407ad34b
Every cycle runs a deterministic state machine — no branching, no surprises:
FETCHING
│ 78 EnergyRecords from Energy Charts API
▼
VALIDATING
│ Window bounds logged; empty dataset flagged as partial
▼
ANALYSING
│ Four anomaly detectors run in sequence
│ 1. Z-score on renewable_percentage (|z| > 2.5)
│ 2. Z-score on consumption_mw (|z| > 2.5)
│ 3. Threshold policy (renewable% < threshold, solar hours only)
│ 4. Isolation Forest (multivariate, 5-D feature space)
▼
FORECASTING
│ Prophet model → next 4h in 15-min steps
│ risk_detected = True when predicted min < RENEWABLE_THRESHOLD
▼
DECIDING
│ Current anomalies → HIGH / CRITICAL alert
│ Forecast risk only → LOW proactive alert
│ All clear → no alert sent
▼
REPORTING
│ 4-sheet Excel workbook saved to data/reports/
▼
ALERTING
│ Telegram message delivered
▼
COMPLETE → RunMetadata JSON persisted to data/last_run.json
- Four-detector anomaly stack — statistical z-score, hard threshold policy, and a multivariate Isolation Forest that learns time-of-day patterns to eliminate overnight false positives
- Proactive forecasting — Prophet predicts renewable share 4 hours ahead; alerts fire before a dip happens, not after
- Solar-hour gating — threshold check is suppressed overnight (07:00–21:00 UTC) since solar output is structurally zero; only genuine daytime violations are flagged
- 4-sheet Excel report — Raw Data, Analysis Summary, Renewable Trend chart, Forecast chart with confidence bands
- Telegram alerts — severity-coloured, HTML-formatted, delivered in under 1 second
- Fully typed — Pydantic v2 models at every boundary; config validated at startup with clear error messages
- Async pipeline — httpx + asyncio; non-blocking file I/O; daily rotating compressed log files
| Component | Library | What it does |
|---|---|---|
| Isolation Forest | scikit-learn |
Learns the joint distribution of [production_mw, consumption_mw, renewable%, sin(hour), cos(hour)] and flags records in low-density regions. The cyclical hour encoding lets the model distinguish expected overnight lows from genuine mid-day collapses. Runs entirely locally — no API, no cost. |
| Prophet Forecast | prophet (Meta) |
Fits a daily-seasonality time-series model on renewable_percentage and projects 16 × 15-min periods forward. Proactive LOW alerts fire when the forecast minimum crosses the threshold — even if the current window looks clean. Runs entirely locally. |
grid-watch/
├── main.py # CLI entry point (typer) — run-once / start / status
├── .env # Secrets and config (not committed)
├── .env.example # Template — copy to .env and fill in
├── requirements.txt # Pinned dependencies
│
├── grid_watch/ # Core package
│ ├── config.py # Pydantic-settings; validates all env vars at import time
│ ├── models.py # Data contracts: EnergyRecord → ForecastResult
│ ├── fetcher.py # Async HTTP client → list[EnergyRecord]; caches raw JSON
│ ├── analyser.py # 4-detector anomaly engine → AnalysisResult
│ ├── forecaster.py # Prophet short-range forecast → ForecastResult
│ ├── reporter.py # openpyxl 4-sheet Excel report generator
│ ├── alerter.py # Telegram bot delivery (never raises)
│ └── orchestrator.py # State machine — wires all modules together
│
├── data/
│ ├── raw/ # Raw API JSON snapshots (auto-created)
│ └── reports/ # Generated .xlsx files (auto-created)
│
└── logs/ # Daily rotating log files, gzip-compressed (auto-created)
1. Clone and create a virtual environment
git clone https://github.com/Yadav108/grid-watch.git
cd grid-watch
python -m venv venv
# Windows
venv\Scripts\activate
# macOS / Linux
source venv/bin/activate2. Install dependencies
pip install -r requirements.txt3. Configure
cp .env.example .env
# Edit .env with your Telegram credentials and API URL4. Run
python main.py run-once --bzn DE-LUAll settings live in .env at the project root.
| Variable | Type | Default | Description |
|---|---|---|---|
TELEGRAM_BOT_TOKEN |
required | — | Bot token from @BotFather |
TELEGRAM_CHAT_ID |
required | — | Chat or channel ID to deliver alerts to |
ENERGY_API_URL |
required | — | https://api.energy-charts.info |
RENEWABLE_THRESHOLD |
optional | 15.0 |
Renewable % below which a daytime record is anomalous |
CHECK_INTERVAL_MINUTES |
optional | 60 |
How often start polls the API |
REPORT_DIR |
optional | data/reports |
Where Excel reports are saved |
RAW_DIR |
optional | data/raw |
Where raw JSON snapshots are cached |
LOG_DIR |
optional | logs |
Where rotating log files are written |
Minimal .env:
TELEGRAM_BOT_TOKEN=123456789:ABCdef...
TELEGRAM_CHAT_ID=-1001234567890
ENERGY_API_URL=https://api.energy-charts.info
RENEWABLE_THRESHOLD=15.0
CHECK_INTERVAL_MINUTES=60Getting your Telegram credentials
- Message @BotFather →
/newbot→ copy the token- Add the bot to your group or channel
- Visit
https://api.telegram.org/bot<TOKEN>/getUpdatesto find thechat_id
# One full cycle, then exit
python main.py run-once
python main.py run-once --bzn AT
python main.py run-once --bzn DE-LU --start 2026-05-01 --end 2026-05-02
# Continuous monitoring — runs every CHECK_INTERVAL_MINUTES (Ctrl+C to stop)
python main.py start
python main.py start --bzn FR
python main.py start --bzn ES --no-run-now # skip the immediate first cycle
# Show metadata from the last completed run (no network needed)
python main.py status
# Version
python main.py --versionExit codes: 0 = success or partial output produced · 1 = fetch failed, no data
Supported bidding zones (--bzn): DE-LU, AT, FR, ES, BE, NL, and all other ENTSO-E BZN codes.
Each run writes grid_watch_<run_id[:8]>_<YYYYMMDDTHHMMSSz>.xlsx with four sheets:
| Sheet | Contents |
|---|---|
| Raw Data | Full EnergyRecord table — filterable headers, alternating row colours, number formatting |
| Analysis Summary | Key metrics, colour-coded severity badge, full summary paragraph |
| Renewable Trend | Line chart of renewable % over the window with a red threshold reference line |
| Forecast | Prophet prediction table + line chart with 80% confidence band and threshold line |
| Level | Triggers |
|---|---|
| 🚨 CRITICAL | ≥ 6 anomaly instances, or renewable min < threshold × 0.5 during solar hours |
| 🟠 HIGH | ≥ 3 anomaly instances, or renewable avg < threshold |
| 🟡 LOW | 1–2 instances, or forecast predicts a dip below threshold (proactive, no current anomalies) |
| ✅ NONE | Clean cycle — no message sent |
Adjustable constants in grid_watch/analyser.py:
| Constant | Default | Effect |
|---|---|---|
_Z_THRESHOLD |
2.5 |
Lower → more z-score alerts; raise to reduce sensitivity |
_IF_CONTAMINATION |
0.05 |
Expected outlier fraction for Isolation Forest (~4 per 78-record window) |
_IF_MIN_SAMPLES |
10 |
Minimum records needed to run Isolation Forest |
_SOLAR_HOUR_START |
7 |
UTC hour at which threshold policy becomes active |
_SOLAR_HOUR_END |
21 |
UTC hour at which threshold policy switches off |
| Layer | Library |
|---|---|
| CLI | typer |
| HTTP client | httpx (async, exponential backoff retry) |
| Data contracts | pydantic v2 + pydantic-settings |
| Anomaly detection | scipy (z-score) · scikit-learn (Isolation Forest) |
| Forecasting | prophet (Meta / Facebook) |
| Reporting | openpyxl |
| Alerting | python-telegram-bot v20 (async) |
| Logging | loguru (rotating, gzip-compressed) |
| Scheduling | schedule |
MIT — free to use, modify, and distribute.