← wx.jamestannahill.com

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:

FieldDescription
tempfOutdoor temperature (°F)
feelsLikeFeels-like temperature accounting for humidity and wind
humidityRelative humidity (%)
dewPointDew point temperature (°F)
windspeedmph10-minute average wind speed
windgustmphWind gust (peak 10-min)
winddirWind direction (0–360°)
baromrelinRelative barometric pressure (inHg)
solarradiationSolar radiation (W/m²)
uvUV index (0–11+)
hourlyraininRainfall in the current hour (in)
dailyraininTotal 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:

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.

EndpointDescription
GET /currentLatest 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.