How It Works
wx.jamestannahill.com publishes live hyperlocal weather from a private station at 560 W 43rd St, Midtown Manhattan. Here's the full data pipeline.
DATA COLLECTION — AMBIENT WEATHER
The station is an Ambient Weather WS-2902 (or equivalent) mounted at the property. It transmits readings every 5 minutes to Ambient Weather's cloud via a local gateway.
A scheduled AWS Lambda (wx-poller) fires every 5 minutes via EventBridge and calls the Ambient Weather REST API:
GET https://api.ambientweather.net/v1/devices/{mac}
?apiKey=...&applicationKey=...&limit=1
Each reading includes:
| Field | Description |
|---|---|
tempf | Outdoor temperature (°F) |
feelsLike | Feels-like temperature accounting for humidity and wind |
humidity | Relative humidity (%) |
dewPoint | Dew point temperature (°F) |
windspeedmph | 10-minute average wind speed |
windgustmph | Wind gust (peak 10-min) |
winddir | Wind direction (0–360°) |
baromrelin | Relative barometric pressure (inHg) |
solarradiation | Solar radiation (W/m²) |
uv | UV index (0–11+) |
hourlyrainin | Rainfall in the current hour (in) |
dailyrainin | Total rainfall since midnight (in) |
Raw readings are stored in DynamoDB (wx-readings) with a 90-day TTL. On every write, the poller also updates a rolling 30-day average for that station, month, and hour of day in wx-daily-stats.
BASELINES — APPLE WEATHERKIT
To compute anomalies ("8.2°F above average for 9am in April"), the system needs historical climate normals for this exact location. These come from Apple WeatherKit, which provides multi-decade historical averages via the historicalComparisons dataset.
A one-time bootstrap Lambda calls the WeatherKit REST API for all 12 months × 24 hours = 288 time buckets and writes them to wx-baselines. The JWT auth uses an ES256 private key issued from the Apple Developer portal:
GET https://weatherkit.apple.com/api/v1/weather/en/{lat}/{lon}
?dataSets=historicalComparisons&timezone=America/New_York
Authorization: Bearer <ES256 JWT>
The JWT header carries kid (key ID) and id ({teamID}.{serviceID}). Once the station accumulates 30 days of its own readings (8,640 samples), the system transitions from WeatherKit baselines to station-derived averages — which are hyperlocal by definition.
ANOMALY SCORING
On every /current request, the API looks up the baseline for the current month and hour, then computes a simple delta:
delta = current_value - avg_value
label = "8.2°F above average for 9am in April"
Anomalies are computed for temperature, humidity, wind speed, and UV index. The dashboard surfaces the most notable one as the lede.
DOWNSAMPLING FOR LONGER RANGES
The /history endpoint accepts up to 720 hours (30 days). To keep payloads manageable, readings are automatically bucketed before returning:
- ≤24h — raw 5-minute readings (~288 points)
- 25–168h (7d) — hourly averages (~168 points)
- >168h (30d) — daily averages (~30 points)
Numeric fields are averaged within each bucket. The timestamp in each bucket is the floor of the bucket start (e.g., 14:00 UTC for the 2pm hour).
API
The API is public and read-only. No auth required.
| Endpoint | Description |
|---|---|
GET /current | Latest reading with anomalies, condition label, and pressure trend |
GET /history?hours=N |
Last N hours of readings (default 24, max 720). Downsampled for longer ranges. |
Responses are JSON. CloudFront caches /current for 5 minutes and /history responses (keyed by the hours parameter) for 5 minutes. CORS is open.
INFRASTRUCTURE
Everything runs on AWS in us-east-1. Estimated cost: ~$3–5/month.
- DynamoDB (on-demand) —
wx-readings,wx-daily-stats,wx-baselines - Lambda (Python 3.12, arm64) — poller, API, bootstrap
- API Gateway HTTP API — routes to the API Lambda
- CloudFront — in front of both the API and the S3 dashboard bucket
- EventBridge — triggers the poller every 5 minutes
- Secrets Manager — Ambient API keys, WeatherKit private key, station config