A self-hosted aviation weather learning tool for PPL students. Displays live METARs and TAFs for a configured set of airports, with a full-screen situational awareness map powered by live OpenAIP and aviationweather.gov data.
Built to run on a wall-mounted iPad, served from a Raspberry Pi on the local network.
METAR tab
- Live data from aviationweather.gov — free, no API key
- Raw report alongside decoded fields: wind, visibility, temperature, altimeter (inHg + hPa), clouds, present weather
- Runway Visual Range (RVR) decoded when present
- Cloud type remark decoding (
CU1TCU1AC3AC3→ Cumulus 1/8, Towering Cumulus 1/8, …) - Tap any cloud type tag for a photo and aviation description
- Flight rules badge per airport: VFR / MVFR / IFR / LIFR in standard aviation colours
- Observation time in Zulu and local time for the airport's timezone
TAF tab
- Decoded into individual forecast periods with full DD/HH:MMZ time ranges
- Each period shows its raw source string alongside decoded fields
- Supports all period types: Base, FM, BECMG, TEMPO, INTER, PROB 30%/40%
- Wind shear (WS groups) decoded and highlighted
- Temperature forecast (TX/TN groups) when present (European airports)
Map tab
- Airport dots coloured live by METAR flight category (VFR/MVFR/IFR/LIFR) for aerodromes, airports, and international airports; airports without a current METAR stay gray
- VOR/VORDME/VORTAC/DVOR rendered as an 8-pointed compass rose; NDB as concentric dashed circles
- OpenAIP airspace polygons: Class B/C/D, Class E surface, Restricted, Prohibited, Danger, MOA/Military — loaded per viewport, cached 1 hour
- Click any airspace polygon (or stack of polygons) for a popup: name, altitude range, and TWR frequency for controlled airspace
- Full airport popup: type, elevation, frequencies (TWR/GND/ATIS/APP/UNICOM), runways (designator, length, surface)
- Airport search: enter any ICAO code to fetch its live METAR + TAF
- Live ADS-B aircraft positions from airplanes.live — type-specific icons coloured by altitude, optional callsign/registration filter, click to show flight path trail
- Data layers auto-refresh on map pan/zoom; METAR colours refresh every time new data is loaded
Settings
- All settings are persisted in
localStorage— survive page reloads and container redeployments without any server-side account - Airport List: override the server-configured airport list per device
- Map Default View: save the current map centre and zoom as your personal default
- Base map: Dark Matter (default), Positron, Voyager, OpenStreetMap
- Overlay: FAA VFR Sectional (US only) or Openflightmaps ICAO charts (Europe)
- Layer toggles: show/hide each aviation layer independently
- Airspace opacity: slider to adjust fill opacity
- Airport type filter: 9 types; heliports, military, and ultralight strips hidden by default
- Live Aircraft: toggle on/off; optional callsign/registration whitelist (one per line)
General
- 5-minute auto-refresh with live Zulu/local clock in the header
- Dark theme, touch-friendly, responsive layout
- Canadian (
CY__) and Swiss (LS__) airports out of the box
browser / iPad
│
▼
nginx (frontend) ← serves static HTML/CSS/JS
│ /api/*
▼
FastAPI (backend) ← fetches, parses and caches weather + map data
│
├── aviationweather.gov ← METAR/TAF (free, no key)
├── OpenAIP Core API ← airspaces, airports, navaids (API key required)
└── airplanes.live ← live ADS-B positions (free, no key)
Two Docker containers orchestrated with Docker Compose:
| Container | Stack | Role |
|---|---|---|
backend |
Python 3.12, FastAPI, avwx-engine, httpx | Weather + map data API |
frontend |
nginx | Serves static files, proxies /api/* to backend |
| Variable | Default | Description |
|---|---|---|
OPENAIP_API_KEY |
(required for map) | OpenAIP API key — free account |
AIRPORTS |
See below | Comma-separated ICAO codes for the server-default METAR/TAF list |
CACHE_TTL_SECONDS |
300 |
METAR/TAF cache TTL in seconds |
MAP_CENTER_ICAO |
(first airport) | ICAO code to centre the map on |
MAP_CENTER_LAT / MAP_CENTER_LON |
(derived) | Override map centre coordinates directly |
MAP_ZOOM |
8 |
Initial map zoom level |
AIRCRAFT_REFRESH_SECONDS |
10 |
How often the browser polls for new ADS-B positions (also sets the server-side cache TTL) |
Set these in your docker-compose.yml under backend → environment, or in a .env file referenced via env_file.
The airport list and map view can also be overridden per device from the browser's Settings dialog without touching the server.
| ICAO | Airport |
|---|---|
| CYXX | Abbotsford, BC |
| CYVR | Vancouver, BC |
| CYYJ | Victoria, BC |
| CYLW | Kelowna, BC |
| CYYQ | Campbell River, BC |
| CYHE | Hope / Agassiz, BC |
| LSZB | Bern, Switzerland |
| LSZH | Zurich, Switzerland |
| LSGG | Geneva, Switzerland |
Override with AIRPORTS=CYXX,CYVR,LSZH in your environment, or edit the list in the browser via Settings → Airport List.
| Endpoint | Description |
|---|---|
GET /api/metar/all |
METARs for all configured stations; accepts optional ?airports=CYXX,CYVR |
GET /api/metar/{icao} |
METAR for a single station |
GET /api/metar/map?bounds=W,S,E,N |
Flight categories for all stations in bbox (5-min cache) |
GET /api/taf/all |
TAFs for all configured stations; accepts optional ?airports=CYXX,CYVR |
GET /api/taf/{icao} |
TAF for a single station |
GET /api/map/features?bounds=W,S,E,N |
OpenAIP airspaces, airports, navaids for bbox (1-hour cache) |
GET /api/map-config |
Map centre and zoom from environment |
GET /api/aircraft?bounds=W,S,E,N |
Live ADS-B positions as GeoJSON (cached per AIRCRAFT_REFRESH_SECONDS) |
GET /api/aircraft/config |
Returns {"refresh_seconds": N} for the frontend timer |
GET /health |
Health check |
GET /docs |
Interactive API docs (FastAPI built-in) |
The Pi deployment uses pre-built multi-platform images (amd64 + arm64) from GitHub Container Registry, with Watchtower for automatic updates. After any push to main, GitHub Actions builds new images and Watchtower picks them up within 5 minutes — no SSH needed.
services:
backend:
image: ghcr.io/YOUR_USERNAME/metar-display-backend:latest
restart: unless-stopped
expose:
- "8000"
environment:
- OPENAIP_API_KEY=your_key_here
- AIRPORTS=CYXX,CYVR,LSZH # optional
labels:
- "com.centurylinklabs.watchtower.enable=true"
frontend:
image: ghcr.io/YOUR_USERNAME/metar-display-frontend:latest
restart: unless-stopped
ports:
- "8080:80"
depends_on:
- backend
labels:
- "com.centurylinklabs.watchtower.enable=true"
watchtower:
image: containrrr/watchtower:latest
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_POLL_INTERVAL=300
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_LABEL_ENABLE=true- Open Safari and navigate to the dashboard URL
- Tap Share → Add to Home Screen
- Name it METAR and tap Add
The app opens full-screen with no browser chrome.
Guided Access (recommended for wall-mounted displays):
- Settings → Accessibility → Guided Access → enable
- Open the METAR app
- Triple-click Home/Side button → Start
The screen stays on and locked to the app. Triple-click again + passcode to exit.