Imported from magnus919/agent-skills (
tempest/SKILL.md). Install upstream withnpx skills add magnus919/agent-skills --skill tempest. Copyright stays with the author (MIT).
tempest — Hyper-local weather from your Tempest station
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
- 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.)
- Export it:
export TEMPEST_TOKEN="<YOUR_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
stations — discover your stations and devices
tempest stations # names, station ids, device types, serials
tempest stations --json | jq '.stations[] | {station_id, name,
devices: [.devices[] | {device_id, device_type, serial_number}]}'
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.
current — latest conditions
tempest current # human-readable, converted
tempest current --json # metric-native, jq-ready
tempest current --station-id 12799 --device-id 60526 # pin exact hardware
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
tempest forecast # current + 5-day daily + next 12 hours
tempest forecast --days 7 --json
tempest forecast --station-id 12799 --days 3
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
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
--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)
tempest udp listen # live stream until Ctrl-C
tempest udp listen --timeout 30 # auto-stop after 30s
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 (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:
| 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 |
Multi-step pipeline recipes
Discover, then observe
# 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
Rain watch: yesterday's total, then live rain events
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")'
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.
Unit-aware forecast slice
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]}'
The jq math here is safe only because it checks units_temp first — see
gotcha 2.
JSON output and jq processing
--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:
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)}'
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.
Known Gotchas
- Observations are positional arrays, not objects. Raw
obsrows 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. /better_forecastis unit-selectable, not Celsius-locked. It defaults to metric but honorsunits_temp=f,units_wind=mph,units_pressure=inhg,units_precip=in. It reports what it used inresponse.units. Converting an already-Fahrenheit response doubles it (25.4 °C → 77.7 °F → 172 "°F"). Always readunitsbefore converting; the CLI does this for you.- UDP message families differ structurally — dispatch on
typefirst. obs families nest rows underobs;rapid_windcarries one array underob;evt_precip/evt_strikecarry one array underevt;hub_status/device_statuscarry named fields with no payload array. Iteratingrapid_wind'sobelement-wise is the classic TypeError; the bundleddecode_message()shows the correct dispatch. - 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
Nonefor missing tails. - 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.
- Forecast timestamps are epoch integers, never ISO strings.
day_start_local,sunrise,sunset, hourlytimeare epoch seconds; hourly objects carrylocal_hour(0–23) andlocal_day(day of month). There is nolocal_timeortime_stringfield — code expecting one silently falls back to its default branch. - The forecast nests under a
forecastwrapper key.data["daily"]is always empty; readdata["forecast"]["daily"]anddata["forecast"]["hourly"](the CLI's--jsonpreserves the full response, wrapper and all). - Hubs (
HB) have no observations. They only relay. Auto-selection skips them; if you call the API directly, filterdevice_type == "HB"out before hitting/observations/device/{id}(documented 404 otherwise). - 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.
obs_skyUDP day-rain is always null. Local-day rain accumulation (index 11) isnullin UDP SKY broadcasts; REST supplies the real value. Don't build day-rain totals from UDP SKY rows.
When to use
- 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.
When not to use
- 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).
Reference Files
| File | Read when |
|---|---|
| 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 | Parsing raw UDP datagrams: port 50222 transport, every message family's layout, the type-dispatch rule |
| 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 | Copy-paste multi-step CLI recipes with jq stages, dry-run plans, and expected error paths |
Available Scripts
- scripts/tempest — the CLI:
stations,current,obs,forecast,udp listen; global--json,--dry-run,--quiet,--verboseaccepted in any position; offline dry-run plans for every command. - scripts/test_tempest.py — offline suite: canned UDP datagram bytes fed to the decoder (no sockets), mocked REST transport, both pytest and unittest runners.
Prerequisites
- Python 3.8+ with
requests(the only dependency). TEMPEST_TOKENfor 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.