Files
magnus919_agent-skills/tempest/references/rest-api-and-auth.md
Magnus Hedemarkandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> a31381bd37 docs(tempest): thicken weather station skill against current API research
Full skill-builder rebuild of tempest per issue #407:

- references/: four dense files replacing the single layouts crib sheet -
  rest-api-and-auth.md (personal-use token via tempestwx.com Settings ->
  Data Authorizations, token-as-query-parameter auth, StationSet wrapper,
  device_type HB/AR/SK/ST enum, observation parameters day_offset vs
  time_start/time_end, better_forecast unit-selection, error signatures),
  udp-broadcast-protocol.md (port 50222 listen-only broadcast, dispatch-
  by-type rule, obs_st 18-position UDP record, rapid_wind ob, evt_precip/
  evt_strike, hub_status/device_status named fields), observation-layouts-
  and-units.md (REST 22-position obs_st vs UDP 18, obs_air 8, obs_sky 17
  vs 14, daily obs_*_ext summaries, metric-native unit tables), and cli-
  worked-recipes.md (six executable pipelines). Every file ends with a
  Sources footer citing live-verified official docs (apidocs.tempestwx.com,
  weatherflow.github.io/Tempest).
- scripts/tempest: fixed researched bugs - rapid_wind handler iterated the
  single ob array element-wise (TypeError on real datagrams), hub_status
  printed undocumented freq field, forecast human display double-converted
  Fahrenheit stations (units_temp=f is documented and honored), SK/AR
  device types now matched alongside SKY/AIR, StationSet unwrap handles
  stations/locations/bare-list shapes, missing ~/.tempest.env fallback
  implemented as documented, dry-run stations plan, handler-owns-flags
  dispatch. Added decode_message()/handle_datagram() type-dispatch layer
  covering all seven UDP message families.
- scripts/test_tempest.py: 42 offline tests (pytest + unittest green,
  proxy-trap clean) - canned UDP datagram bytes fed to the decoder with no
  sockets, mocked REST transport, help/arg-error/dry-run classes, and the
  documented pipelines (stations->current, obs day totals, forecast units).
- SKILL.md: lastfm-model rewrite (275 lines) - Setup, intent-grouped
  commands, UDP family dispatch table, pipeline recipes, jq guidance, ten
  grounded gotchas, when-to-use/when-not-to-use boundaries, reference
  routing table.
- README.md: human-format refresh with hub-on-LAN prerequisite.
- evals/evals.json: 8 schema-v1 cases incl. two negative probes
  (Shakespeare The Tempest, generic city forecast).
- Root README blurb and references/skill-triggers.md row synced to the new
  description; .claude-plugin/marketplace.json and llms.txt regenerated
  (both embed descriptions; check modes exit 0; codex artifact unaffected).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-29 23:04:37 -04:00

12 KiB
Raw Permalink Blame History

Tempest REST API and Authentication

The Tempest REST API is the cloud service at https://swd.weatherflow.com/swd/rest. It is the primary, recommended data source even for programs running on the same LAN as the hub; the local UDP broadcast (see udp-broadcast-protocol.md) is officially positioned as an off-grid backup. Base URL used throughout:

https://swd.weatherflow.com/swd/rest

Authentication: the personal access token

There are exactly two documented authentication methods, and the bundled CLI uses the first:

  1. Personal Access Token — the right choice for scripts and integrations without a graphical interface. Sign in to the Tempest Web App (tempestwx.com), then go to Settings → Data Authorizations → Create Token, and copy the generated token. This is what TEMPEST_TOKEN carries.
  2. OAuth 2.0 (Authorization Code, optionally with PKCE) — the documented choice for production apps with a web UI. Apps are registered from the account's Developers page; authorization and token endpoints are documented separately in the OAuth reference. The CLI does not implement OAuth.

On the wire, the token travels as a query parameter:

GET https://swd.weatherflow.com/swd/rest/stations?token=<YOUR_TOKEN>

The official quick-start examples use token=[your_access_token] and show no Authorization header alternative for this API. Do not send the token as a header or assume bearer syntax is supported. The OpenAPI document describes the scheme as apiKey with in: query, which matches.

Policy notes (remote-developer-policy): personal-use access covers station metadata, observations, and forecasts with "rate/volume limits (enough for personal use)". No numeric quota is published, and no 429 response behavior is documented. Higher-volume or network-wide access requires a commercial agreement (TempestONE). Keep personal integrations to your own stations.

Endpoint catalog (personal-use surface)

GET /stations — your stations with devices

Parameters: limit (int64, default 10000), next_cursor (string; present when more than 10,000 stations are provisioned), optional geographic filters (lat_min/lon_min/lat_max/lon_max bounding box, or center_lat/center_lon/radius in meters).

Response is a StationSet wrapper, not a bare list:

{
  "status": { "status_code": 0, "status_message": "SUCCESS" },
  "stations": [
    {
      "station_id": 12799,
      "location_id": 12799,
      "name": "Home",
      "public_name": "Home",
      "latitude": 42.37,
      "longitude": -71.06,
      "timezone": "America/New_York",
      "timezone_offset_minutes": -300,
      "station_meta": { "elevation": 1567.65, "share_with_wf": true, "share_with_wu": true },
      "is_local_mode": false,
      "devices": [
        {
          "device_id": 60526,
          "serial_number": "ST-00012345",
          "device_type": "ST",
          "hardware_revision": "3",
          "firmware_revision": "165",
          "device_meta": { "agl": 2.2, "name": "Backyard", "environment": "outdoor" },
          "device_settings": { "show_precip_final": false },
          "notes": ""
        }
      ],
      "station_items": [ { "item": "air_temperature_humidity", "device_id": 60526, "sort": 0 } ]
    }
  ]
}

device_type values: HB (hub — has no observation data), ST (Tempest all-in-one), AR (Air sensor), SK (Sky sensor). The OpenAPI enum lists exactly these four. Note that AR/SK are metadata codes for the Air/Sky hardware; the observation type discriminator for the same hardware is obs_air/obs_sky. Always filter HB out before auto-selecting a device for observation calls — the hub has no /observations/device/{id} data. A null or missing serial_number on a device means inactive hardware per the legacy docs.

GET /stations/{station_id} — one station

Same Station model; documented responses are 200 and 404 ("Station not found"). Per the legacy Swagger the body still arrives in the {stations: [...]}-style wrapper shape with the selected station inside, so unwrap defensively rather than assuming a bare station object.

GET /observations/device/{device_id} — device observations

Query parameters (mutually exclusive modes):

Parameter Meaning
day_offset Whole UTC day: 0 = current UTC day, 1 = yesterday UTC
time_start + time_end UTC epoch-seconds range; one-minute resolution guaranteed for ranges ≤ 5 days
latest=true Latest single observation (the CLI's current default)
format=csv CSV instead of JSON

Response is an observation set: obs (array of positional arrays, oldest to newest), type (obs_st | obs_air | obs_sky — the layout discriminator), plus device identity/status fields. Field layouts are in observation-layouts.md. Documented errors: 404 "Device not found". Passing a hub HB device id yields no observation data.

GET /observations/stn/{station_id} — station observations

Note the segment is stn, not stations. Optional parameters: time_start/time_end, bucket (1 | 5 | 30 | 180 minutes; mapped to 1 day / 5 days / 30 days / 180 days of history, and the docs mention 1440 ≈ 4 years), ob_fields selection, and the standard unit parameters. Station observations are federated from the station's designated primary sensors; device observations are one physical device's raw data. Use station observations when you want "the station's" reading, device observations when you care about a specific unit.

GET /better_forecast — conditions + daily + hourly

Parameters: station_id (or lat/lon with optional snap_to_nearest_owned_station=true for within-5 km snapping), plus unit overrides: units_temp (c|f), units_wind (mph|kph|kts|mps|bft| lfm), units_pressure (mb|inhg|mmhg|hpa), units_precip (mm|cm|in), units_distance (km|mi).

Response top level:

{
  "status": { "status_code": 0, "status_message": "SUCCESS" },
  "current_conditions": { "air_temperature": 18.2, "conditions": "Mostly Clear", "icon": "partly-cloudy-day", "relative_humidity": 61, "station_pressure": 1015.4, "wind_avg": 2.1, "wind_direction": 225, "feels_like": 18.2 },
  "forecast": {
    "daily": [ { "day_start_local": 1778385600, "air_temp_high": 25.4, "air_temp_low": 15.1, "conditions": "Partly cloudy", "precip_probability": 10, "precip_type": "rain", "sunrise": 1778378400, "sunset": 1778425200 } ],
    "hourly": [ { "time": 1778388000, "local_hour": 10, "local_day": 10, "air_temperature": 19.8, "precip_probability": 5, "conditions": "Sunny" } ]
  },
  "units": { "units_temp": "c", "units_wind": "mps", "units_precip": "mm", "units_pressure": "mb", "units_distance": "km" },
  "latitude": 42.37, "longitude": -71.06,
  "timezone": "America/New_York", "timezone_offset_minutes": -300
}

The critical structural fact: daily and hourly live under the forecast wrapper key, not at top level. Reading data["daily"] returns nothing.

Unit behavior: the response honors the requested units and reports what it used in units. Default is Celsius/m/s/mm/mb, but the endpoint is unit-selectable — not Celsius-locked. units_temp=f is documented and honored. Any consumer that hard-codes Celsius conversion must first read units.units_temp, or it will double-convert Fahrenheit responses (see units-and-conversions.md).

Timestamps: day_start_local, sunrise, sunset, and hourly time are integer epoch seconds. Hourly objects carry local_hour (int 023) and local_day (int day-of-month); there is no local_time or time_string field — code expecting one silently falls back to its default branch.

Other documented endpoints

  • GET /diagnostics/{station_id} — latest station status; 200/401/404.
  • GET /stats/station/{station_id} — daily/weekly/monthly/annual/all-time high-low-average statistics; 200/401.
  • GET /metadata/network/stations and GET /observations/network/stations — network-wide access governed by the remote data policy (not part of the personal single-station flow).
  • Lightning endpoints exist but documented access is for paid subscribers.
  • The current docs index does not document /user/devices for the consumer surface — use /stations and its nested devices array. There is no /better_forecast/hourly route; hourly data is forecast.hourly inside the standard /better_forecast response.

Error signatures

Status Documented meaning Practical symptom
401 Unauthorized (documented on forecast/diagnostics/stats) Missing, revoked, or mistyped token — regenerate at tempestwx.com Settings → Data Authorizations
403 Not documented for this API Treat as access-denied to that station/device; verify the token belongs to the station owner
404 "Station not found" / "Device not found" (documented) Wrong station/device id, or an HB hub id passed to an observation endpoint

No JSON error-body schema is published, so parse defensively. No numeric rate limit or 429 behavior is documented; the policy only promises personal-use volume is acceptable. The CLI maps 401/403/404 to targeted messages and dumps the response body for anything else.

Worked recipes

Recipe A: stations → pick sensor → latest observation

# 1. List stations (StationSet wrapper)
curl -s "https://swd.weatherflow.com/swd/rest/stations?token=$TEMPEST_TOKEN"
# 2. Choose a device: devices[].device_type must not be "HB"; prefer ST
# 3. Latest observation for that device
curl -s "https://swd.weatherflow.com/swd/rest/observations/device/$DEVICE_ID?token=$TEMPEST_TOKEN"

The observation response's type field selects the positional layout (obs_st: temperature is index 7, epoch is index 0). One command does all three steps: tempest current --json.

Recipe B: station forecast with explicit units

curl -s "https://swd.weatherflow.com/swd/rest/better_forecast?station_id=$STATION_ID&units_temp=c&units_wind=mps&units_pressure=mb&units_precip=mm&token=$TEMPEST_TOKEN" \
  | jq '{current: .current_conditions.air_temperature,
         days: [.forecast.daily[] | {day_start_local, air_temp_high, air_temp_low}],
         units: .units.units_temp}'

Read units instead of assuming units. Extract daily/hourly from .forecast.daily / .forecast.hourly.

Recipe C: a UTC day of device history

# day_offset=1 is yesterday UTC; day_offset=0 is today
curl -s "https://swd.weatherflow.com/swd/rest/observations/device/$DEVICE_ID?day_offset=1&token=$TEMPEST_TOKEN" \
  | jq '{type, count: (.obs | length), first: .obs[0], last: .obs[-1]}'

For a custom range, send both time_start and time_end as epoch seconds and keep the span ≤ 5 days to guarantee one-minute resolution. Do not mix day_offset with time_start/time_end in one call.

Sources