tempest · diff
git:20260826.2619ba3 to git:20260830.a31381b
211 added, 96 removed. Audit A to A.
---
name: tempest
- description: 'Query hyper-local weather from a WeatherFlow Tempest station: current
- conditions, 7-day forecast, historical observations, and real-time UDP broadcasts.
- Use when the user asks about the weather, temperature, rain, wind, humidity, forecast,
- or wants conditions from their own station rather than a generic weather service.'
+ description: >-
+ Query hyper-local weather from a WeatherFlow Tempest station over its REST
+ API and the hub's local UDP broadcast: current conditions, forecast,
+ historical observations, and real-time decoded datagrams (obs_st,
+ rapid_wind, evt_precip, evt_strike, hub_status). Use when the user asks
+ about weather, temperature, rain, wind, humidity, or forecast data from
+ their own Tempest/WeatherFlow station, or wants to parse the hub's UDP port
+ 50222 broadcast. Do not use this skill for generic or city forecasts
+ without a Tempest station (public weather services serve those), for
+ Shakespeare's play The Tempest or other literature questions, or for
+ weather hardware from other vendors - the REST endpoints require a
+ personal-use token and the UDP broadcast only exists on a Tempest hub's
+ LAN.
license: MIT
- compatibility: Requires TEMPEST_TOKEN env var (free from weatherflow.com), Python
- 3.8+, and the `requests` library.
+ compatibility: >-
+ Requires TEMPEST_TOKEN env var for REST (create it in the Tempest web app
+ under Settings -> Data Authorizations), Python 3.8+, and `requests`. UDP
+ listening needs a Tempest hub on the LAN and no token. `--help` and
+ `--dry-run` work without credentials.
metadata:
- tags: weather, tempest, forecast, weatherflow, station, hyper-local
- sources: https://weatherflow.com, https://swd.weatherflow.com/swd/rest
+ tags: weather, tempest, weatherflow, forecast, station, udp, hyper-local
+ sources: https://apidocs.tempestwx.com/reference/quick-start, https://weatherflow.github.io/Tempest/api/udp/v171/
---
- # tempest — Hyper-Local Weather from Your Tempest Station
+ # tempest — Hyper-local weather from your Tempest station
- Query live weather data from a WeatherFlow Tempest station. Supports REST API access to current conditions, forecasts, and history via the cloud, plus local UDP broadcast reception from your hub on the same LAN.
+ Drive a WeatherFlow Tempest station from the terminal. Two transports, both
+ first-class: the documented REST API (`swd.weatherflow.com/swd/rest`,
+ personal-use token) for conditions, forecast, and history — officially the
+ primary data source — and the hub's unauthenticated UDP broadcast on port
+ 50222 for real-time, lowest-latency readings on your LAN. The bundled CLI
+ decodes the positional observation arrays and every UDP message family, keeps
+ `--json` output metric-native, and converts units only for human display.
## Setup
- 1. Get a personal access token at [weatherflow.com](https://weatherflow.com) (Account → API Tokens)
- 2. Set it in your environment:
+ 1. Create a personal access token: sign in to the Tempest web app
+ (tempestwx.com), then **Settings → Data Authorizations → Create Token**.
+ (This is the documented non-graphical auth method; OAuth exists for web
+ apps but is not what a CLI uses.)
+ 2. Export it:
```bash
- export TEMPEST_TOKEN="your-token-here"
+ export TEMPEST_TOKEN="<YOUR_TOKEN>"
```
- The CLI reads `TEMPEST_TOKEN` from the environment. It also falls back to reading `~/.tempest.env` if the env var is not set (for agent subprocesses that don't inherit env vars). `--help` and `--dry-run` work without a token.
+ The token travels to the API as a **query parameter** (`?token=...`) per the
+ official docs — the CLI handles this. If the env var is not set, the CLI
+ falls back to reading `TEMPEST_TOKEN=` from `~/.tempest.env` (handy for agent
+ subprocesses that skip shell profiles). `--help` and `--dry-run` never need a
+ token. UDP listening never needs one either — the hub broadcast is
+ unauthenticated and LAN-only.
## Essential Commands
- ### current — Current conditions
+ ### stations — discover your stations and devices
```bash
- tempest current # human-readable
- tempest current --station-id 12345 --device-id 67890 # specific hardware
- tempest current --json # machine-readable
+ tempest stations # names, station ids, device types, serials
+ tempest stations --json | jq '.stations[] | {station_id, name,
+ devices: [.devices[] | {device_id, device_type, serial_number}]}'
```
- If you have one station, it auto-selects it and picks the best sensor (ST > SKY > AIR, skips the HB hub). Pass `--station-id` or `--device-id` to override.
+ Every station response nests a `devices` array: `device_type` is `ST` (the
+ Tempest all-in-one), `AR`/`AIR`, `SK`/`SKY`, or `HB` (the hub — it has **no**
+ observations; always filter it out before querying observations). Run this
+ first when you don't know your ids.
- ### forecast — Multi-day forecast (+ current conditions + hourly)
+ ### current — latest conditions
```bash
- tempest forecast # current + 5-day daily + 12-hour hourly
- tempest forecast --days 3 # fewer days
- tempest forecast --station-id 12345 # specific station
- tempest forecast --json # machine-readable
+ tempest current # human-readable, converted
+ tempest current --json # metric-native, jq-ready
+ tempest current --station-id 12799 --device-id 60526 # pin exact hardware
```
- ### stations — List your stations and devices
+ With one station it auto-selects and picks the best sensor (`ST`, then
+ `SKY`/`SK`, then `AIR`/`AR`, skipping `HB`). Output `.observation` carries the
+ decoded positional array as named fields with `_unit` companions.
+ ### forecast — current conditions + daily + hourly
+
```bash
- tempest stations # shows station names, IDs, device types, serials
- tempest stations --json # full device inventory
+ tempest forecast # current + 5-day daily + next 12 hours
+ tempest forecast --days 7 --json
+ tempest forecast --station-id 12799 --days 3
```
- Use this first if you don't know your station ID or want to see what sensors are online.
+ The `better_forecast` response nests daily/hourly under a `forecast` wrapper
+ key, and it is unit-selectable (`units_temp=c|f` and friends, default metric)
+ — the CLI reads the response's `units` before converting anything.
- ### obs — Historical observations
+ ### obs — historical observations
```bash
- tempest obs --device-id 67890 --days 1 # last 24 hours
- tempest obs --device-id 67890 --days 7 # last week
- tempest obs --device-id 67890 --json # machine-readable
+ tempest obs --device-id 60526 --days 1 # last UTC day (day_offset)
+ tempest obs --device-id 60526 --days 7
+ tempest obs --device-id 60526 --json
```
- ### udp listen — Real-time broadcasts from the hub
+ `--days N` maps to the API's `day_offset` (whole UTC days). The underlying
+ endpoint also accepts `time_start`/`time_end` epoch ranges (one-minute
+ resolution guaranteed up to 5 days) — use raw calls for those; see
+ references/rest-api-and-auth.md.
+ ## UDP broadcasts from your hub (port 50222, listen-only)
+
```bash
- tempest udp listen # listen indefinitely (Ctrl-C to stop)
+ tempest udp listen # live stream until Ctrl-C
tempest udp listen --timeout 30 # auto-stop after 30s
- tempest udp listen --show-all # include hub_status messages
+ tempest udp listen --timeout 60 --json # one JSON object per datagram
+ tempest udp listen --show-all # include hub_status/device_status
```
- Requires being on the same LAN as the hub (port 50222 UDP broadcast). Receives observations, rapid wind updates, lightning strike events, and precipitation start events in real time.
+ Requires being on the same LAN as the hub (routed connectivity is not enough
+ — broadcasts don't cross routers). No token involved. The listener decodes
+ every message family, dispatching on `type` before touching array positions:
- ## Data Reference
+ | Family | Payload shape | Decoded fields |
+ |---|---|---|
+ | `obs_st` / `obs_air` / `obs_sky` | list of report rows under `obs` | named observation fields |
+ | `rapid_wind` | ONE 3-element array under `ob` | wind_speed_mps, wind_direction |
+ | `evt_precip` | ONE array under `evt` | timestamp (rain started) |
+ | `evt_strike` | ONE array under `evt` | distance_km, energy |
+ | `hub_status`, `device_status` | named fields, no array | uptime, rssi, seq, voltage, sensor_status |
- ### obs_st field layout (Tempest all-in-one)
+ ## Multi-step pipeline recipes
- Observations from the Tempest sensor arrive as positional arrays. The CLI decodes them, but if you're reading raw JSON output, this map tells you what each index means:
+ ### Discover, then observe
- | Index | Field | Units | Notes |
- |-------|-------|-------|-------|
- | 0 | epoch | seconds UTC | |
- | 1 | wind_lull | m/s | Minimum 3-second sample |
- | 2 | wind_avg | m/s | Average over report interval |
- | 3 | wind_gust | m/s | Maximum 3-second sample |
- | 4 | wind_direction | degrees | 0=N |
- | 5 | wind_sample_interval | seconds | |
- | 6 | station_pressure | MB | |
- | 7 | air_temperature | C | CLI converts to °F |
- | 8 | relative_humidity | % | |
- | 9 | illuminance | lux | |
- | 10 | uv | index | |
- | 11 | solar_radiation | W/m² | |
- | 12 | rain_accumulation | mm | Over last interval |
- | 13 | precipitation_type | enum | 0=none 1=rain 2=hail |
- | 14 | avg_strike_distance | km | Lightning |
- | 15 | strike_count | count | Lightning |
- | 16 | battery | volts | ~2.6V normal, ~2.5V low |
- | 17 | report_interval | minutes | |
- | 18 | local_day_rain_accumulation | mm | |
+ ```bash
+ # Stage 1 -> stage 2: stations --json emits integer ids that current consumes
+ tempest stations --json | jq -r '.stations[].devices[]
+ | select(.device_type == "ST") | .device_id' | head -1
+ tempest current --device-id <DEVICE_ID> --json
+ ```
- See [references/tempest-api-field-layouts.md](references/tempest-api-field-layouts.md) for the full obs_air and obs_sky field layouts.
+ ### Rain watch: yesterday's total, then live rain events
- ### Unit conversions the CLI applies
+ ```bash
+ tempest obs --device-id 60526 --days 1 --json \
+ | jq '{samples: (.observations | length),
+ day_rain_mm: .observations[-1].local_day_rain_accumulation}'
+ tempest udp listen --timeout 600 --json | jq 'select(.type == "evt_precip")'
+ ```
- | Input | Output | Conversion |
- |-------|--------|------------|
- | °C | °F | `c * 9/5 + 32` |
- | m/s | mph | `mps * 2.237` |
- | MB | inHg | `mb * 0.02953` |
- | mm | in | `mm / 25.4` |
- | degrees | cardinal | N, NNE, NE, ..., NNW |
+ `obs --json` ends with decoded observations carrying
+ `local_day_rain_accumulation` (mm, number); `evt_precip` datagrams decode to
+ `{type, serial_number, timestamp}` — both stages emit typed fields the next
+ stage can consume.
- ## Known Gotchas
+ ### Unit-aware forecast slice
- ### The API is metric-native
+ ```bash
+ tempest forecast --days 7 --json \
+ | jq '{units_temp: .forecast.units.units_temp,
+ highs_f: [.forecast.forecast.daily[] | .air_temp_high * 9 / 5 + 32],
+ rain_hours: [.forecast.forecast.hourly[]
+ | select(.precip_probability > 30) | .local_hour]}'
+ ```
- All raw observation data comes in metric (Celsius, m/s, MB, mm). The CLI converts for human display. If you're parsing raw `--json` output, expect metric values. The `better_forecast` endpoint returns unit-converted values based on station preferences — check the `units` key in the response — but **temperatures are always in Celsius** regardless.
+ The jq math here is safe **only because** it checks `units_temp` first — see
+ gotcha 2.
- ### Timestamps are epoch integers, not strings
+ ## JSON output and jq processing
- The API returns Unix epoch timestamps, not ISO 8601 strings. The `daily[].day_start_local` field is an `int`, not `"2026-05-10T00:00:00"`. Hourly objects have `local_hour` (int 0-23) and `local_day` (int) — there is **no** `local_time` or `time_string` field. The CLI handles this, but raw JSON consumers need to convert with `datetime.fromtimestamp(ts)`.
+ `--json` output is **metric-native** — the raw wire units (m/s wind, mm rain,
+ °C temperature, MB pressure) with `_unit` companion fields naming each.
+ Convert at the consumption edge:
- ### Nested forecast response
+ ```bash
+ tempest current --json | jq '{temp_c: .observation.air_temperature,
+ temp_f: (.observation.air_temperature * 9 / 5 + 32),
+ wind_mph: (.observation.wind_avg * 2.237),
+ rain_in: (.observation.rain_accumulation / 25.4)}'
+ ```
- The `better_forecast` endpoint nests daily and hourly arrays under a `forecast` wrapper key, not at the top level:
+ Global flags work in any position: `tempest --json current --device-id 60526`
+ and `tempest current --device-id 60526 --json` are identical. `--quiet`
+ silences the progress logs (data on stdout, logs on stderr).
+ `--dry-run` prints a plan object and exits 0 without touching the network.
- ```python
- # Correct path:
- fc = data.get("forecast", {})
- days = fc.get("daily", [])
- hours = fc.get("hourly", [])
- ```
+ ## Known Gotchas
- The top-level keys are: `current_conditions` (dict), `forecast` (dict with `daily` + `hourly`), `station` (metadata), `units`, `status`, `timezone`.
+ 1. **Observations are positional arrays, not objects.** Raw `obs` rows have
+ no field names; meaning comes from the index (obs_st: 0 epoch, 2 wind avg
+ m/s, 4 wind direction, 6 pressure MB, 7 temperature °C, 12 rain mm, 16
+ battery V, 17 report interval). Reading index 6 as temperature gives you a
+ plausible-looking wrong number — decode with the CLI or the layout tables
+ in references/observation-layouts-and-units.md.
+ 2. **`/better_forecast` is unit-selectable, not Celsius-locked.** It defaults
+ to metric but honors `units_temp=f`, `units_wind=mph`, `units_pressure=inhg`,
+ `units_precip=in`. It reports what it used in `response.units`. Converting
+ an already-Fahrenheit response doubles it (25.4 °C → 77.7 °F → 172 "°F").
+ Always read `units` before converting; the CLI does this for you.
+ 3. **UDP message families differ structurally — dispatch on `type` first.**
+ obs families nest rows under `obs`; `rapid_wind` carries one array under
+ `ob`; `evt_precip`/`evt_strike` carry one array under `evt`;
+ `hub_status`/`device_status` carry named fields with no payload array.
+ Iterating `rapid_wind`'s `ob` element-wise is the classic TypeError; the
+ bundled `decode_message()` shows the correct dispatch.
+ 4. **UDP obs_st rows stop at index 17; REST rows run to 21.** The four
+ Nearcast/analysis fields (18–21) exist only in REST responses. Decoders
+ must tolerate both lengths — the CLI emits `None` for missing tails.
+ 5. **Pressure is MB (millibars), numerically hPa — not kPa.** It is also
+ *station* pressure (raw sensor). The Tempest app's "relative pressure"
+ adds an elevation adjustment; don't compare raw station pressure against
+ the app and conclude the sensor drifted.
+ 6. **Forecast timestamps are epoch integers, never ISO strings.**
+ `day_start_local`, `sunrise`, `sunset`, hourly `time` are epoch seconds;
+ hourly objects carry `local_hour` (0–23) and `local_day` (day of month).
+ There is **no** `local_time` or `time_string` field — code expecting one
+ silently falls back to its default branch.
+ 7. **The forecast nests under a `forecast` wrapper key.** `data["daily"]` is
+ always empty; read `data["forecast"]["daily"]` and
+ `data["forecast"]["hourly"]` (the CLI's `--json` preserves the full
+ response, wrapper and all).
+ 8. **Hubs (`HB`) have no observations.** They only relay. Auto-selection
+ skips them; if you call the API directly, filter `device_type == "HB"`
+ out before hitting `/observations/device/{id}` (documented 404 otherwise).
+ 9. **UDP is LAN-only and unauthenticated.** Broadcasts don't cross routers
+ and can't be token-gated — anyone on the network can read your station.
+ WeatherFlow officially positions REST/WebSocket as primary and UDP as the
+ off-grid/backup interface.
+ 10. **`obs_sky` UDP day-rain is always null.** Local-day rain accumulation
+ (index 11) is `null` in UDP SKY broadcasts; REST supplies the real value.
+ Don't build day-rain totals from UDP SKY rows.
- ### Device type filtering
+ ## When to use
- A station returns all devices including the hub (device_type `HB`). The hub cannot serve observations. The CLI auto-filters HB devices and prefers ST > SKY > AIR. If you're bypassing the CLI and calling the API directly, always filter out device_type `HB` before querying observation endpoints.
+ - The user owns or manages a WeatherFlow Tempest / Air / Sky station and asks
+ about its readings, forecast, or history.
+ - Parsing or integrating with the hub's local UDP broadcast (port 50222).
+ - Rain/wind/lightning monitoring scripts, dashboards, or home-automation
+ hooks fed from the station.
- ### Global flags in any position
+ ## When not to use
- `--json`, `--dry-run`, `--quiet`, and `--verbose` work anywhere in the command:
+ - **Generic city forecasts or users without a station** — every endpoint
+ requires the user's own Tempest station and a personal-use token; use a
+ public weather service instead.
+ - **Shakespeare's play *The Tempest*, or any literary/meteorological-theory
+ question** — this is a station-data CLI, not an encyclopedia.
+ - **Other vendors' hardware** (Netatmo, Ecowitt, Davis, Ambient) — different
+ APIs entirely; no endpoint here will accept their devices.
+ - **Commercial/network-wide data products** — those need WeatherFlow's
+ TempestONE agreements, not a personal token (see the remote developer
+ policy).
- ```bash
- tempest --json current --device-id 67890 # flag before subcommand
- tempest current --device-id 67890 --json # flag after subcommand
- ```
+ ## Reference Files
- ## References
+ | File | Read when |
+ |---|---|
+ | [references/rest-api-and-auth.md](references/rest-api-and-auth.md) | Working with REST endpoints directly: token auth, StationSet shapes, observation parameters, forecast units, error signatures |
+ | [references/udp-broadcast-protocol.md](references/udp-broadcast-protocol.md) | Parsing raw UDP datagrams: port 50222 transport, every message family's layout, the type-dispatch rule |
+ | [references/observation-layouts-and-units.md](references/observation-layouts-and-units.md) | Decoding positional observation arrays by index (obs_st/obs_air/obs_sky, UDP vs REST lengths) and unit conversion tables |
+ | [references/cli-worked-recipes.md](references/cli-worked-recipes.md) | Copy-paste multi-step CLI recipes with jq stages, dry-run plans, and expected error paths |
- - [references/tempest-api-field-layouts.md](references/tempest-api-field-layouts.md) — Full field index maps for obs_st, obs_air, and obs_sky observation arrays. Read when decoding raw JSON output or building on top of the Tempest API.
- - [scripts/tempest](scripts/tempest) — The CLI binary itself. Designed following the cli-builder patterns: non-interactive, `--json`, `--dry-run`, `--quiet`, `--verbose`, idempotent, dual-output via `emit()`, and structured logging.
+ ## Available Scripts
- ## When not to use
+ - [scripts/tempest](scripts/tempest) — the CLI: `stations`, `current`, `obs`,
+ `forecast`, `udp listen`; global `--json`, `--dry-run`, `--quiet`,
+ `--verbose` accepted in any position; offline dry-run plans for every
+ command.
+ - [scripts/test_tempest.py](scripts/test_tempest.py) — offline suite: canned
+ UDP datagram bytes fed to the decoder (no sockets), mocked REST transport,
+ both pytest and unittest runners.
- Do not use this skill for weather questions that do not involve a personal WeatherFlow station (a public forecast service serves those better), for aviation METAR/TAF data, or for hardware from other vendors — every endpoint here requires a Tempest account token and talks to WeatherFlow's consumer API.
+ ## Prerequisites
+
+ - Python 3.8+ with `requests` (the only dependency).
+ - `TEMPEST_TOKEN` for REST commands (free, personal use; created in the
+ Tempest web app). UDP listening needs no token, only line-of-sight to the
+ hub's LAN.