A self-hosted system for historizing ADS-B flight data from a FlightRadar24 feeder station, with Telegram digest broadcasts.
| Component | Location | Purpose |
|---|---|---|
| Squawk | squawk/ |
Polls Pi every 5s, writes sightings to TimescaleDB, Telegram digest |
| Feeder | feeder/ |
readsb + tar1090 + fr24feed on the Pi |
┌──────────────────────────────────────────────┐
│ squawk service │
│ │
tar1090 ──────────────► run_pipeline() │
│ ├─ record sightings (SightingRepository) │
│ └─ enrich_batch() (EnrichmentRepository) │
│ │
│ Scheduler ──► generate_digest() │
│ /debug ──┘ │ │
│ ├─ DigestQuery │
│ ├─ DigestRepository │
│ ├─ ChartQuery │
│ └─ Broadcaster ──► Telegram│
│ │
│ TelegramBot: /start /digest /stats /debug │
└──────────────────────────────────────────────┘
│
TimescaleDB
Two async tasks run in an asyncio.TaskGroup:
- Pipeline (
run_pipeline): continuous loop — polls tar1090, records sightings, batches new/expired aircraft, enriches them via Gemini, stores results. - Bot (
TelegramBot.run): handles/start,/digest,/stats,/debug. The scheduler triggersgenerate_digest(cron);/debugtriggers it on demand.
squawk/
__main__.py ← wiring only: construct everything, start TaskGroup
config.py ← all env vars in one frozen dataclass
db.py ← asyncpg pool creation
scheduler.py ← Scheduler protocol + APSchedulerBackend
pipeline.py ← run_pipeline(): polling loop + enrichment batching
enrichment.py ← ScoringClient protocol, enrich_batch(), ADK impl
digest.py ← DigestClient protocol, generate_digest(), ADK impl
charts.py ← matplotlib traffic charts for digests
tags.py ← StoryTag enum + TAG_DESCRIPTIONS
clients/ ← typed HTTP clients, each behind a Protocol
adsbdb.py ← AircraftInfo, AircraftLookupClient
hexdb.py ← HexdbClient (hexdb.io lookups)
planespotters.py ← PhotoInfo, PhotoClient
routes.py ← RouteInfo, RouteClient
mictronics.py ← bulk aircraft DB download + ingest
repositories/ ← write repositories, one per table-owner
sightings.py ← SightingRepository (aircraft, sightings, position_updates)
enrichment.py ← EnrichmentRepository (enriched_aircraft, callsign_routes)
bulk_aircraft.py ← BulkAircraftRepository (bulk_aircraft)
digest.py ← DigestRepository (digests)
queries/ ← read-only, cross-table
digest.py ← DigestQuery (joins sightings + enriched_aircraft)
charts.py ← ChartQuery (daily/hourly traffic counts)
bot/
app.py ← TelegramBot: PTB wiring, command registration, run()
handlers.py ← /start /digest /stats /debug command handlers
broadcaster.py ← Broadcaster protocol + TelegramBroadcaster
libs/
tar1090/ ← pure package: polls tar1090 HTTP API
Each repository owns specific tables for writes. Cross-table reads go through query objects.
Table Writer Read by
──────────────────────────────────────────────────────────────────────
aircraft SightingRepository DigestQuery
sightings SightingRepository DigestQuery, ChartQuery
position_updates SightingRepository DigestQuery
enriched_aircraft EnrichmentRepository DigestQuery, run_pipeline (expiry)
callsign_routes EnrichmentRepository DigestQuery
bulk_aircraft BulkAircraftRepository enrich_batch
digests DigestRepository —
- Pi:
tracker@flighttracker.local— runs the feeder stack - NAS / server:
coolify.local— runs squawk via Coolify (auto-deploys from master) - Data endpoint:
http://<pi-ip>/data/aircraft.json - Database: TimescaleDB (shared between squawk and feeder)
Squawk polls the Pi's tar1090 endpoint:
http://<pi-ip>/data/aircraft.json
Key fields per aircraft:
| Field | Description |
|---|---|
hex |
ICAO 24-bit address — stable aircraft identifier |
flight |
Callsign |
alt_baro |
Barometric altitude (feet), or "ground" |
gs |
Ground speed (knots) |
lat, lon |
Position |
r_dst |
Distance from receiver (nautical miles) |
rssi |
Signal strength (dBFS) |
seen |
Seconds since last message received |
Seven tables in TimescaleDB:
aircraft— registry, one row per unique ICAO hexsightings— one row per continuous observation session (start/end time, altitude/distance aggregates, callsign)position_updates— high-frequency position samples (hypertable, 1-day chunks, compressed after 7 days, retained 90 days)enriched_aircraft— AI scores, annotations, registration data per aircraft (TTL-based expiry)callsign_routes— origin/destination per flight callsignbulk_aircraft— bulk aircraft registry from mictronics.dedigests— cached digests
Deployed via Coolify on coolify.local. Pushes to master auto-deploy.
| Variable | Default | Description |
|---|---|---|
ADSB_URL |
— | tar1090 aircraft.json URL |
DATABASE_URL |
— | PostgreSQL connection string |
BOT_TOKEN |
— | Telegram bot token from @BotFather |
CHANNEL_ID |
— | Telegram channel ID for digest broadcast |
GEMINI_API_KEY |
— | Google Gemini API key |
ADMIN_CHAT_ID |
— | Telegram chat ID allowed to use /debug |
POLL_INTERVAL |
5 |
Seconds between polls |
SESSION_TIMEOUT |
300 |
Seconds of silence before a sighting session ends |
DIGEST_SCHEDULE |
15 7 * * * |
Cron schedule for digest generation |
ENRICHMENT_TTL_DAYS |
30 |
Days before re-enriching a known aircraft |
ENRICHMENT_BATCH_SIZE |
20 |
Max aircraft per Gemini scoring call |
ENRICHMENT_FLUSH_SECS |
30 |
Max seconds to wait before flushing batch |
CLIENT_MAX_RETRIES |
3 |
Max retries for 429/5xx from external APIs |
nix develop # provides Python 3.13, uv, ruff, mypy, psql, pre-commit, dbmateSingle pyproject.toml and uv.lock at repo root. Run tools from repo root:
uv run pytest
uv run ruff check .
uv run mypy squawk