256 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Lightning Report (n8n)
Automated lightning activity reports for wind farms. An **n8n** workflow detects strikes via the [iklim.co](https://iklim.co) API, generates a Word report through a **Python report service**, routes it through **Slack** for human approval, and emails the approved report to customers via **SendGrid**.
## Architecture
```text
┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ n8n workflow│────▶│ iklim.co API │ │ Report service │
│ (scheduled) │ │ lightnings + │ │ FastAPI /generate │
│ │ │ thunderstorms │ │ → DOCX │
└──────┬──────┘ └──────────────────┘ └──────────▲──────────┘
│ │
│ Slack upload + approve/reject buttons │
▼ │
┌─────────────┐ storm_logs datatable (pending report) │
│ Slack │───────────────────────────────────────────┘
└──────┬──────┘
│ on approve
┌─────────────┐
│ SendGrid │──▶ customer contact_email
└─────────────┘
```
## Repository layout
| Path | Description |
|------|-------------|
| `Lightning_Report_Automatic.json` | n8n workflow export (import in n8n) |
| `report_service/` | FastAPI wrapper around report generation |
| `report_service/adapter.py` | Maps n8n JSON payload → pandas DataFrames |
| `src/reporting/docx.py` | Main DOCX builder |
| `src/analysis/` | Risk, histogram, geospatial, statistics |
| `src/visualization/` | Maps and storm-cell plots |
| `src/config.py` | Default analysis parameters |
## Prerequisites
- **Python 3.12+** (local or Docker)
- **n8n** (self-hosted, with Data Tables enabled)
- **iklim.co API** credentials (test: `https://api-test.iklim.co`)
- **Slack** app/bot for uploads and approval buttons
- **SendGrid** API key with Mail Send permission
- Optional: **Gemini API key** if commentary is generated inside the report service (otherwise pass `gemini_text` from n8n)
## Report service (local)
### 1. Install dependencies
```bash
cd Lightning_Report_n8n
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt -r report_service/requirements.txt
```
### 2. Environment variables
Create a `.env` file in the project root (never commit it):
```env
REPORT_SERVICE_TOKEN=your-long-random-secret
GEMINI_API_KEY=optional-if-not-supplied-by-n8n
GEMINI_MODEL=gemini-2.5-flash-lite
LOG_LEVEL=INFO
```
`REPORT_SERVICE_TOKEN` is required. The service refuses `/generate` if it is unset.
### 3. Run the server
```bash
export REPORT_SERVICE_TOKEN="your-long-random-secret"
uvicorn report_service.main:app --host 0.0.0.0 --port 8000
```
Health check: `GET http://localhost:8000/health`
### 4. Expose to n8n (development)
If n8n runs elsewhere, tunnel the service:
```bash
ngrok http 8000
```
Point the n8n **Generate Report** HTTP Request node at `https://<your-ngrok-host>/generate` and set header `X-Report-Token` to the same value as `REPORT_SERVICE_TOKEN`.
## Report service (Docker)
```bash
cd Lightning_Report_n8n
export REPORT_SERVICE_TOKEN=your-long-random-secret
docker compose -f report_service/docker-compose.yml up --build -d
```
From n8n on the same Docker network: `http://report-service:8000/generate`
## API
### `GET /health`
Liveness probe. Returns `{"ok": true, ...}`.
### `POST /generate`
| Header | Required | Description |
|--------|----------|-------------|
| `X-Report-Token` | Yes | Must match `REPORT_SERVICE_TOKEN` |
| `Content-Type` | Yes | `application/json` |
**Body** (built by n8n **Build Report Payload** node):
| Field | Description |
|-------|-------------|
| `customer_name` | Wind farm / customer label |
| `t_start`, `t_end` | Storm window (epoch ms) |
| `centroid_lat`, `centroid_lon` | Farm centroid |
| `boundary_m` | Monitoring radius (m) |
| `rings` | `{ r1, r2, r3, r4 }` distance rings |
| `timezone` | e.g. `Europe/Istanbul` |
| `turbines` | Array of turbine objects |
| `strikes` | Lightning strike records (`captured` ISO time preferred) |
| `storm_records` | Thunderstorm polygons from `/v1/thunderstorms/within` |
| `gemini_text` | Optional pre-generated commentary |
| `language` | Optional report language: `en` (default) or `tr` |
**Response:** DOCX file (`application/vnd.openxmlformats-officedocument.wordprocessingml.document`).
## n8n workflow
Import `Lightning_Report_Automatic.json` into n8n and configure:
### Credentials
| Integration | Used for |
|-------------|----------|
| iklim.co login | `Login to iklim.co` / token refresh |
| Slack (`n8n_lightning_report_bot`) | Report upload, approval buttons |
| Slack (`Tarla Slack Account`) | Error / email-sent notifications |
| SendGrid | Customer email after approval |
### Data tables
**`customers`**
| Column | Purpose |
|--------|---------|
| `customer_name` | Display name |
| `contact_email` | SendGrid recipient |
| `language` | Report language: `en` or `tr` (also accepts `turkish`, `Türkçe`) |
| `id` | Linked from wind turbines |
**`wind_turbine_farm`**
| Column | Purpose |
|--------|---------|
| `customer_id` | FK to customers |
| `latitude`, `longitude` | Turbine positions |
| `name` | Turbine label |
**`storm_logs`**
| Column | Purpose |
|--------|---------|
| `customer_name`, `storm_key`, `storm_start`, `storm_end` | Storm metadata |
| `total_strikes`, `status` | Count; `waiting for confirmation``confirmed` / `rejected` |
| `pending_contact_email` | Email snapshot at report time |
| `pending_email_file_name` | DOCX filename |
| `pending_report_base64` | Queued report (survives n8n restarts) |
| `pending_report_mime_type` | MIME type for attachment |
### Main flow (simplified)
1. Daily trigger → authenticate → loop customers.
2. Query lightnings within farm boundary; detect storm window.
3. Insert/update `storm_logs`, fetch thunderstorms, build payload (include `language` from the customer row).
4. **Generate Report** → upload DOCX to Slack → approval buttons.
5. Persist pending report fields on `storm_logs`.
6. On Slack **Approve** → load queued report → **SendGrid** email → clear pending fields.
Email is sent only after approval, not when the report is first generated.
## Troubleshooting
### `channel_not_found` (Slack)
The Slack channel ID in the node does not exist or the bot is not invited. Re-select the channel in n8n and run `/invite @YourBot` in that channel.
### `Account Is not Found For User …` (`/v1/thunderstorms/within`)
Login succeeded but the iklim user has no **account** on the API environment (e.g. test). Ask iklim.co to provision the user, or use a service account that has thunderstorm access. Lightning-only reports can continue if the workflow skips failed thunderstorm calls (optional branch).
### ngrok `503` / `ERR_NGROK_3004` on **Generate Report**
ngrok closes long-lived HTTP connections when the report service takes several minutes to respond (Kaleido chart export). The HTML `503` page with `ERR_NGROK_3004` means ngrok never received a complete response from uvicorn.
**Fix:** Re-import `Lightning_Report_Automatic.json`. Report generation uses separate n8n nodes (not one long Code node):
1. **Start Async Report**`POST /generate/async`
2. **Wait for Report** — 15s pause
3. **Poll Report Status**`GET /generate/async/{job_id}` (loops until complete)
4. **Download Report**`GET /generate/async/{job_id}/download`
Restart uvicorn after pulling report-service changes. Update the ngrok URL in the three HTTP nodes when your tunnel URL changes.
### `Task execution timed out after 60 seconds` on report generation
n8n Code nodes are capped at **60 seconds**. Do not put polling loops inside a single Code node. Use the multi-node async flow above instead.
**Alternative:** Skip ngrok and call the service directly (`http://host.docker.internal:8000` from Docker n8n, or `http://127.0.0.1:8000` on the same host).
### `timeout of 300000ms exceeded` on **Generate Report**
The report service can take several minutes when many lightning strikes produce multiple Kaleido chart exports (maps, histograms, heatmaps). n8ns HTTP Request node defaults to **5 minutes (300000 ms)**.
**Fixes (use together):**
1. Re-import `Lightning_Report_Automatic.json` — the **Generate Report** node sets `timeout: 900000` (15 min).
2. On self-hosted n8n, raise the global ceiling if needed:
```bash
N8N_DEFAULT_REQUEST_TIMEOUT=900000
EXECUTIONS_TIMEOUT=3600
EXECUTIONS_TIMEOUT_MAX=3600
```
3. Prefer **Docker network** (`http://report-service:8000/generate`) over ngrok when n8n and the report service run on the same host — fewer proxy timeouts.
4. Watch report-service logs: each `kaleido` export spawns Chrome briefly; very large datasets are capped by `histogram_params.max_periods` (default 8 activity periods).
### Histogram shows `01-01-1970` / wrong period
Strike timestamps were parsed incorrectly when `timestamp` (epoch ms) was preferred over `captured` (ISO). Fixed in `report_service/adapter.py` — restart the report service after pulling updates.
### SendGrid: attachment must be base64 encoded
Use the **Prepare SendGrid Email Payload** node path; ensure `pending_report_base64` in `storm_logs` is populated and not truncated by datatable size limits.
### Binary missing at **Upload Report to Slack**
The datatable persist step must run **after** Slack upload, not before (persist nodes drop binary data).
### Slack `invalid_payload` on approval follow-up
Do not double-`JSON.stringify` the body when posting to `response_url`. Slack response URLs expire after ~30 minutes (optional confirmation message only).
## Development notes
- Per-farm settings (`rings`, centroid, dates, `language`) come from the n8n payload via `apply_farm_config()`, not hardcoded in `src/config.py`.
- Set `customers.language` to `tr` for Turkish DOCX output; omit or use `en` for English.
- Prefer strike field **`captured`** (ISO) for `local_time`; numeric **`timestamp`** is supported as epoch milliseconds.
- HMAC signing for iklim API calls is implemented in n8n Code nodes (`Calculate Lightning Headers`, `Calculate Thunderstorm Headers`).
## License
Internal use — iklim.co lightning reporting pipeline.