_ring_upper_km function and update sorting of ring items based on lower bounds, improving clarity and accuracy in report outputs.
Lightning Report (n8n)
Automated lightning activity reports for wind farms. An n8n workflow detects strikes via the 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
┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ 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_textfrom n8n)
Report service (local)
1. Install dependencies
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):
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
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:
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)
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)
- Daily trigger → authenticate → loop customers.
- Query lightnings within farm boundary; detect storm window.
- Insert/update
storm_logs, fetch thunderstorms, build payload (includelanguagefrom the customer row). - Generate Report → upload DOCX to Slack → approval buttons.
- Persist pending report fields on
storm_logs. - 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):
- Start Async Report —
POST /generate/async - Wait for Report — 15s pause
- Poll Report Status —
GET /generate/async/{job_id}(loops until complete) - 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). n8n’s HTTP Request node defaults to 5 minutes (300000 ms).
Fixes (use together):
- Re-import
Lightning_Report_Automatic.json— the Generate Report node setstimeout: 900000(15 min). - On self-hosted n8n, raise the global ceiling if needed:
N8N_DEFAULT_REQUEST_TIMEOUT=900000 EXECUTIONS_TIMEOUT=3600 EXECUTIONS_TIMEOUT_MAX=3600 - Prefer Docker network (
http://report-service:8000/generate) over ngrok when n8n and the report service run on the same host — fewer proxy timeouts. - Watch report-service logs: each
kaleidoexport spawns Chrome briefly; very large datasets are capped byhistogram_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 viaapply_farm_config(), not hardcoded insrc/config.py. - Set
customers.languagetotrfor Turkish DOCX output; omit or useenfor English. - Prefer strike field
captured(ISO) forlocal_time; numerictimestampis 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.