Files
magnus919_agent-skills/tempest/references/observation-layouts-and-units.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

8.1 KiB
Raw Permalink Blame History

Observation Layouts and Units (obs_st, obs_air, obs_sky)

Observations arrive as positional arrays: a list of values whose meaning depends on the array index. The type field on the containing object selects the layout (obs_st = Tempest all-in-one, obs_air = Air, obs_sky = Sky). There are no field names on the wire — any decoder is a table like the ones below, and reading the wrong index silently yields a wrong value (e.g. treating index 6 pressure as index 7 temperature).

Two different record lengths exist for obs_st: REST returns 22 positions and the UDP broadcast returns 18 (the four Nearcast/analysis fields are REST-only). obs_air is 8 positions in both transports; obs_sky is 17 over REST and 14 over UDP.

obs_st — Tempest all-in-one (REST record, 22 positions)

Index Field Units Notes
0 timestamp epoch seconds, UTC
1 wind lull m/s minimum 3-second sample
2 wind average 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 (millibars) ≡ hPa; raw sensor pressure, not sea-level
7 air temperature °C
8 relative humidity %
9 illuminance lux
10 UV index
11 solar radiation W/m²
12 rain accumulation mm during the reporting interval
13 precipitation type enum 0 none, 1 rain, 2 hail, 3 rain + hail (experimental)
14 lightning strike average distance km
15 lightning strike count count during the reporting interval
16 battery volts ≈2.4 nominal; below ≈2.3 plan service
17 report interval minutes
18 local day rain accumulation mm midnight-to-midnight, station timezone
19 Nearcast rain accumulation mm REST only
20 local day Nearcast rain accumulation mm REST only
21 precipitation analysis type enum 0 none, 1 Nearcast display on, 2 off — REST only

UDP obs_st datagrams end at index 17 (see udp-broadcast-protocol.md).

obs_air — Air sensor (8 positions, both transports)

Index Field Units Notes
0 timestamp epoch seconds, UTC
1 station pressure MB (millibars) ≡ hPa
2 air temperature °C
3 relative humidity %
4 lightning strike count count during the reporting interval
5 lightning strike average distance km
6 battery volts
7 report interval minutes

obs_sky — Sky sensor (REST record, 17 positions)

Index Field Units Notes
0 timestamp epoch seconds, UTC
1 illuminance lux
2 UV index
3 rain accumulation mm during the reporting interval
4 wind lull m/s
5 wind average m/s
6 wind gust m/s
7 wind direction degrees
8 battery volts
9 report interval minutes
10 solar radiation W/m²
11 local day rain accumulation mm always null over UDP — REST supplies it
12 precipitation type enum 0 none, 1 rain, 2 hail, 3 rain + hail
13 wind sample interval seconds
14 Nearcast rain accumulation mm REST only
15 local day Nearcast rain accumulation mm REST only
16 precipitation analysis type enum 0 none, 1 Nearcast display on, 2 off — REST only

UDP obs_sky datagrams end at index 13 and always carry null at index 11.

Daily summary records (obs_*_ext)

The API also emits midnight-to-midnight daily summaries with their own discriminators: obs_st_ext (34 positions — avg/high/low pressure, temperature, humidity, illuminance, UV, solar, wind stats, strikes, battery, day rain, precipitation minutes), obs_air_ext (14), and obs_sky_ext (22). They appear in stats/history contexts, not in the minute firehose. Decode them only from their own type — never with the minute-record tables.

The units story: metric-native, caller converts

Every raw value is metric: wind m/s, rain mm, temperature °C, pressure MB (millibars — numerically identical to hPa, not kPa), distance km, illuminance lux, solar radiation W/m², battery volts. Nothing on the wire is imperial; conversions are the consumer's job:

Wire unit Imperial Formula
°C °F c * 9/5 + 32
m/s mph mps * 2.23694 (≈ ×2.237)
m/s km/h mps * 3.6
m/s knots mps * 1.94384
MB (hPa) inHg mb * 0.02953
mm inches mm / 25.4
km miles km / 1.60934

Two traps:

  1. /better_forecast is unit-selectable, not Celsius-locked. It defaults to metric (units_temp=c), honors overrides (units_temp=f, units_wind=mph, units_pressure=inhg, units_precip=in, units_distance=mi), and reports what it used in response.units. Read units before converting anything, or a Fahrenheit response gets double-converted into absurd values.
  2. Station vs sea-level pressure. Index 6 / index 1 pressure is the raw station pressure. The Tempest app's "relative pressure" adds an elevation adjustment — don't compare your raw value against the app and conclude the sensor is broken.

The bundled CLI keeps --json output in metric-native wire units (raw, lossless — convert with your own jq) and converts only in human display. Decode positionally with jq like:

tempest current --json \
  | jq '{temp_c: .observation.air_temperature,
         temp_f: (.observation.air_temperature * 9 / 5 + 32),
         wind_mps: .observation.wind_avg,
         wind_mph: (.observation.wind_avg * 2.237),
         pressure_mb: .observation.station_pressure}'

Field type traps in /better_forecast

The forecast endpoint uses epoch integers where you'd expect date strings, and field names that differ from what common sense suggests:

Field Actual type Common mistake Fix
daily[].day_start_local epoch int (e.g. 1778385600) assumed ISO string datetime.fromtimestamp(ts).strftime(...)
hourly[].local_hour int (023) assumed timestamp string format directly {h:02d}:00
hourly[].local_day int (day of month) N/A use alongside local_hour
hourly[].local_time does not exist commonly assumed field use local_hour instead

Code looking for local_time silently falls back to its default/"?" branch — no error is raised.

Decoding recipe (jq, no script needed)

Latest REST observation, positionally decoded to named fields:

curl -s "https://swd.weatherflow.com/swd/rest/observations/device/$DEVICE_ID?token=$TEMPEST_TOKEN" \
  | jq --argjson layout '["timestamp","wind_lull","wind_avg","wind_gust","wind_direction",
      "wind_sample_interval","station_pressure","air_temperature","relative_humidity",
      "illuminance","uv","solar_radiation","rain_accumulation","precipitation_type",
      "avg_strike_distance","strike_count","battery","report_interval",
      "local_day_rain","nc_rain","local_day_nc_rain","precip_analysis_type"]' '
      {type: .type,
       obs: (.obs[-1] | [$layout, .] | transpose | map({(.[0]): .[1]}) | add)}'

The bundled CLI does the same in Python (decode_obs in scripts/tempest, driven by the OBS_ST_FIELDS/OBS_AIR_FIELDS/OBS_SKY_FIELDS tables) and tolerates both UDP-length and REST-length rows.

Sources