WaffleWeather exposes a REST API and a WebSocket endpoint. All REST endpoints live under /api/v1. The canonical OpenAPI spec is at openapi/waffleweather.yaml — set WW_ENABLE_DOCS=true to get interactive Swagger UI at /docs.
When WW_API_KEY is set, all requests must include an X-API-Key header. When unset, auth is disabled (suitable for LAN-only use). The deploy setup script generates a key automatically and injects it via Nginx.
Weather Station → ecowitt2mqtt → Mosquitto (MQTT)
↓
FastAPI Backend
↙ ↓ ↘
WebSocket REST API TimescaleDB
(live obs) (queries) (persistence)
- The weather station gateway pushes readings to ecowitt2mqtt (which supports Ecowitt, Ambient Weather, and other Fine Offset-based brands), which publishes normalized JSON to MQTT.
- The backend's MQTT listener parses each message, stores it in
weather_observations, detects lightning deltas (→lightning_events), computes derived values, and broadcasts the enriched observation to all WebSocket clients. - REST endpoints query the database for historical, aggregated, and current data.
- The frontend merges REST and WebSocket data: REST provides the baseline (including fields like
zambretti_forecastthat require DB lookback), and WebSocket overlays real-time updates.
| Name | Type | Description |
|---|---|---|
weather_observations |
Hypertable | Raw observations, one row per MQTT message (~16s interval). Chunked by day, compressed after 14 days, retained for 1 year. |
lightning_events |
Hypertable | Detected strike events with delta counts and distance. Created when the MQTT listener detects a change in lightning_count or lightning_time. |
stations |
Table | Station metadata (id, name, model, firmware, location). Upserted on first observation. |
observations_hourly |
Continuous aggregate | Hourly rollups from raw data. Refreshes every hour. |
observations_daily |
Continuous aggregate | Daily rollups from hourly. Refreshes daily. |
observations_monthly |
Continuous aggregate | Monthly rollups from daily. Refreshes daily. |
Aggregated columns (all three views share the same schema):
temp_outdoor_avg, temp_outdoor_min, temp_outdoor_max, dewpoint_avg, dewpoint_min, dewpoint_max, humidity_outdoor_avg, humidity_outdoor_min, humidity_outdoor_max, pressure_rel_avg, wind_speed_avg, wind_gust_max, rain_daily_max, solar_radiation_avg, uv_index_max
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/stations |
List all registered stations |
| GET | /api/v1/stations/{station_id} |
Get a single station's metadata |
Source: stations table. Station records are created/updated automatically when the first MQTT observation arrives.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/observations/latest |
Most recent observation (optionally filtered by station_id) |
| GET | /api/v1/observations |
Paginated raw observations with start, end, limit, offset filters |
Source: weather_observations hypertable.
The latest endpoint also computes the Zambretti barometric forecast on-the-fly by looking up pressure from 3 hours ago.
Response fields (40+ weather metrics): temp_outdoor, temp_indoor, dewpoint, feels_like, heat_index, wind_chill, frost_point, humidity_outdoor, humidity_indoor, pressure_abs, pressure_rel, wind_speed, wind_gust, wind_dir, rain_rate, rain_daily, rain_weekly, rain_monthly, rain_yearly, rain_event, solar_radiation, uv_index, pm25, pm10, co2, soil_moisture_1, soil_moisture_2, lightning_count, lightning_distance, lightning_time, utci, zambretti_forecast
Derived fields (dewpoint, feels_like, heat_index, wind_chill, utci) are computed at query time from raw inputs, never stored.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/observations/hourly |
Hourly aggregated observations (start, end required) |
| GET | /api/v1/observations/daily |
Daily aggregated observations |
| GET | /api/v1/observations/monthly |
Monthly aggregated observations |
Source: observations_hourly, observations_daily, observations_monthly continuous aggregate views respectively.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/observations/calendar |
Daily metric values for a given year |
Parameters: metric (required), year (optional, defaults to current year), station_id (optional)
Allowed metrics: temp_outdoor_max, rain_daily_max, solar_radiation_avg, wind_gust_max, humidity_outdoor_avg, lightning_strikes
Source: observations_daily view for weather metrics, lightning_events table for lightning_strikes.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/observations/wind-rose |
Wind direction/speed distribution binned into 16 compass sectors and 5 speed bands |
Parameters: start, end (required), station_id (optional)
Speed bands (km/h): 0-5, 5-15, 15-25, 25-40, 40+
Source: Raw weather_observations (wind_dir + wind_speed columns).
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/reports/monthly |
Monthly climate report with daily rows |
| GET | /api/v1/reports/yearly |
Yearly climate report with monthly rows |
| GET | /api/v1/reports/monthly/txt |
Monthly report as NOAA-format plain text |
| GET | /api/v1/reports/yearly/txt |
Yearly report as NOAA-format plain text |
Parameters (monthly): year (required), month (required), station_id (optional)
Parameters (yearly): year (required), station_id (optional)
TXT endpoints accept an additional units parameter (metric or imperial, default metric). Returns Content-Type: text/plain with a Content-Disposition header for file download.
Source: observations_daily continuous aggregate for temperature, dewpoint, humidity, pressure, wind speed/gust, and rain. Raw weather_observations for prevailing wind direction (16-point compass mode). Heating/cooling degree days computed from daily average temperature with base 18.3°C (65°F).
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/observations/lightning/events |
Paginated list of detected lightning events |
| GET | /api/v1/observations/lightning/summary |
Aggregated strike statistics with hourly and daily breakdowns |
Source: lightning_events table.
Both endpoints accept an include_filtered query parameter (default false). When false, events flagged as likely false positives (ghost strikes) are excluded from results and aggregations.
The summary endpoint returns: total_strikes, event_count, filtered_count, closest_distance, plus hourly[] (strikes per hour, min distance) and daily[] (strikes per day) breakdowns.
Ghost strike filtering: The WH57 sensor (AS3935) is prone to EMI-triggered false positives — isolated single-strike events at fixed distances. When WW_LIGHTNING_FILTER_ENABLED=true, the MQTT listener flags events matching the configured distance blocklist (WW_LIGHTNING_FILTER_DISTANCES) with filtered=true. Events with new_strikes exceeding WW_LIGHTNING_FILTER_MAX_STRIKES (default 1) bypass the filter, since real storms produce multi-strike bursts. The filter is disabled by default; configure it in .env with distances specific to your environment.
Endpoint: ws://<host>/ws/live
The WebSocket broadcasts every observation as it arrives from MQTT. There is no request/subscribe protocol — connect and you receive all updates.
{
"type": "observation",
"timestamp": "2026-04-09T15:30:00Z",
"station_id": "GW3000B",
"temp_outdoor": 12.6,
"humidity_outdoor": 45,
"wind_speed": 3.2,
"...all observation fields...",
"zambretti_forecast": "Fine, becoming less settled",
"diagnostics": {
"batteries": {
"wh65batt": { "label": "Outdoor Sensor Array", "type": "boolean", "value": "OK" }
},
"gateway": {
"runtime": 123456,
"heap": 65536,
"interval": 30
}
}
}- Diagnostics (battery levels, gateway metrics) are broadcast via WebSocket only and are never stored in the database.
- Zambretti forecast is computed in the MQTT listener using an in-memory 4-hour pressure deque. The REST
/latestendpoint computes it separately via a DB lookback query.
Pages that use both REST and WebSocket (Observatory, Console, Lightning) merge them as:
const data = wsData ? { ...apiData, ...wsData } : apiData;WebSocket fields overwrite REST fields for real-time feel, but REST-only fields (like zambretti_forecast from the DB lookback) persist from the initial fetch until overwritten by a WS message that includes them.
| Page | REST Endpoints | WebSocket | Update Pattern |
|---|---|---|---|
| Observatory | latest, observations (trends), hourly (today extremes) |
Live observation | REST baseline + WS live updates; trends refresh every 60s |
| Console | latest, stations, hourly (barometer chart) |
Live observation | WS for live obs; hourly chart refetches every 60s |
| Lightning | latest, stations, lightning/summary, lightning/events |
Live observation | WS for live strike count; REST on time range change |
| History | observations (24h) or hourly/daily/monthly (7d/30d/1y) |
Not used | One-shot fetch per time range selection |
| Wind Rose | wind-rose |
Not used | One-shot fetch per time range selection |
| Settings | stations |
Diagnostics only | Station info once; diagnostics update on each WS message |
| Reports | reports/monthly, reports/yearly |
Not used | One-shot fetch per period selection |
The History page auto-selects data granularity based on time range:
| Range | Endpoint | Resolution | Typical Row Count |
|---|---|---|---|
| 24 hours | /observations (raw) |
~16 seconds | ~5,000 |
| 7 days | /observations/hourly |
1 hour | ~168 |
| 30 days | /observations/daily |
1 day | ~30 |
| 1 year | /observations/monthly |
1 month | ~12 |