Read the weather sensors on 433 MHz, and let them be given names

A consumer weather station is two things.  The display on the kitchen wall is
one of them; the other is a plastic box on a fence post that says what it can
see every sixteen seconds, in the clear, to anyone who happens to be
listening.  This reads the box.

A section of its own, like the aircraft one, and for the same reason: it does
not fit through the scanner.  A sensor message is a burst of a carrier
switched on and off, a fifth of a second long, and the scan path is a squelch
and a recorder -- it would record the bursts as clicks in a WAV file and
decode nothing.  `bandsaunter weather` listens, `bandsaunter readings` reads
a log back, `bandsaunter sensors` says what is out there.  Item 6 in the main
menu is the same thing without a command line.

Five families: the Tower 592TXR, the 5-in-1, the 6045M lightning detector,
the 609TXC and the 606TX.  Temperature, humidity, wind speed and direction,
rainfall, strike counts, how far off the storm is, and battery state from all
of them.  Every one is implemented from its published description and checked
against frames built from the same description, which proves the framing, the
parity, the checksums and the arithmetic and is not the same as having held
one of each.

The naming is the point.  A sensor broadcasts an identity, and that identity
is a number that came out of a hat in a factory; it tells one sensor from
another and is no use at all for telling which is which.  So press n while
listening: the display comes down, the sensors are listed, you name one, and
it goes back up, with the receiver running throughout.  That is the moment it
is possible -- the sensor is on the screen saying 3.1 degrees, and the person
watching is the one who knows that the cold one is the shed.  An hour later it
is a list of hexadecimal again.  Names are written the instant they are given
rather than at exit, to a neighbouring file renamed over the old one, and one
given before a sensor has ever been heard waits under its identity and moves
across when the first message says which model it is.

Four things keep the neighbours' doorbells off the display.  The checks the
message carries; a second copy, for the two models that carry only one byte
of check between them; a plausibility range, because a checksum can be
satisfied by a message the hardware could not send; and where in the burst
the message sits.  That last one is the one that is easy to miss: a seven-byte
message read out of the front of a real eight-byte one is made of that
message's own payload bytes, whose parity is already correct, so the parity
bits contribute nothing and one byte of sum is all that is left -- and
corroboration cannot help, the three copies being identical.  What gives that
window away every time is that it ends a whole byte before the burst does.

The Atlas is nine bytes like the lightning detector and lays its payload out
differently, so every decoder insists on a message type it knows.  Anything
else that frames correctly is reported with its identity and no weather,
because wrong weather under somebody's sensor name is a worse answer than
none.

ism.py now delegates to this rather than keeping a second implementation of
the tower sensor, which fixes the channel letters -- A is 3, B is 2, C is 0,
and there is no D -- and the battery bit, which is set while the battery is
good.  The two thinly-checked models are not reported from a scan at all: a
scan hears one burst, and they need two.

The option menus are now handed the module that owns the options rather than
importing the aircraft one, so one set of screens drives both sections and
will drive a third.

169 new tests, checked against nineteen deliberately broken builds; two of the
tests were too weak to notice their own mutation and were rewritten.  Full
suite 2252 passed.  Built as 2026-09-07_01.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
The Dust Council 2026-09-07 13:40:33 -07:00
parent f01de4117f
commit 65cc03b78d
18 changed files with 6006 additions and 123 deletions

309
README.md
View file

@ -7,6 +7,11 @@ built-in US band plan — and it sweeps them, stops on anything above the noise
floor, records it, and works out what kind of signal it was. CW/Morse is
decoded to text.
Two things get sections of their own, because neither fits through a scan:
[the aircraft overhead](#aircraft) on 1090 MHz, drawn on a moving map, and
[the weather sensors](#weather-sensors-on-433-mhz) on 433 MHz, which you can
give names to as they arrive.
Two programs: `bandsaunter` scans, and
[`saunterbrowse`](#browsing-what-you-recorded) reads back what it collected —
transcripts, identifications and playback, in one screen.
@ -240,6 +245,8 @@ bandsaunter # the menus: set up and scan
bandsaunter scan -b 2m -b marine-vhf # band-plan presets
bandsaunter scan -r 144M-148M -r 420M-450M # your own ranges
bandsaunter scan -b 2m --simulate # try it without hardware
bandsaunter adsb --window # aircraft overhead, on a map
bandsaunter weather # the weather sensors on 433 MHz
```
## Two ways to drive it
@ -253,11 +260,18 @@ table of settings, so neither can offer something the other cannot.
2 Band plan 107 US presets
3 Settings record no limit, hang 6s, squelch +12 dB, keep voice, cw
4 Saved settings and profiles
5 Aircraft (ADS-B) listen on 1090 MHz, draw where they went
6 Weather sensors listen on 433 MHz, name what is out there
h Help
s Start scanning
q Quit
```
Items 5 and 6 are sections of their own, with their own options and their own
menus, because neither fits through a scan: ADS-B is a megabit a second and a
scan channel is 12.5 kHz wide, and a weather sensor message is a burst of a
carrier switched on and off that a scan would record as clicks.
Settings are grouped, show their current value against the built-in default,
and carry their own help:
@ -1784,30 +1798,299 @@ form of every one.
copy between machines. API keys are deliberately **not** kept there — see
[Knowing the leg for certain](#knowing-the-leg-for-certain).
## Meters and weather sensors
## Weather sensors on 433 MHz
Two things on the ISM bands are worth naming rather than reporting as hex.
A consumer weather station is two things. The display on the kitchen wall is
one of them; the other is a plastic box on a fence post that says what it can
see every sixteen seconds, in the clear, on 433.92 MHz, to anyone who happens
to be listening. `bandsaunter weather` reads the box.
```
bandsaunter weather listen, and name things as they arrive
bandsaunter weather --simulate a garden of sensors that are not there
bandsaunter readings --csv turn a log into something plottable
bandsaunter sensors what is out there, and what it is called
```
It is a section of its own, like the aircraft one, and for the same reason:
it does not fit through the scanner. A sensor message is a burst of a carrier
switched on and off, a fifth of a second long, and the scan path is a squelch
and a recorder — it would record the bursts as clicks in a WAV file and decode
nothing.
```
╭──────────────────────────────────────────────────────────────────────────────────────╮
│ 433.92 MHz 6 sensors 148 messages 41:02 weather_2026-09-07_11_31_04.jsonl │
│ n to name (2 unnamed) control-C to stop │
╰──────────────────────────────────────────────────────────────────────────────────────╯
# name id model readings batt msgs ago
1 back fence 1A2B Tower 592TXR temperature 8.4 C humidity 88% ok 41 0:07
2 mast 0777 5-in-1 06014RM wind 14.2 km/h wind from 248° WSW ok 36 0:11
rain 41.4 mm temperature 8.1 C
humidity 90%
3 greenhouse 0C41 Tower 592TXR temperature 15.9 C humidity 61% ok 39 0:04
4 unnamed 0311 Lightning 6045M temperature 8.6 C humidity 87% ok 24 0:19
strikes 4 storm 19 km
5 shed 5C 609TXC temperature 3.1 C humidity 94% low 21 0:12
6 unnamed 93 606TX temperature -18.2 C ok 19 0:22
```
### Naming things while they arrive
A sensor broadcasts an identity, and that identity is a number that came out
of a hat in a factory — or a different number out of the same hat the next
time the batteries were changed. It is enough to tell one sensor from another
and it is no use at all for telling which is which. `1A2B` is not a place.
So **press `n` while listening**. The display comes down, the sensors are
listed with numbers, you pick one and type a name, and it goes back up. The
receiver keeps running throughout: a slow typist loses a few seconds of
weather and nothing else.
That is the moment it is possible to do. The sensor is on the screen saying
3.1 degrees, and the person watching is the one who knows that the cold one is
the shed. An hour later it is a list of hexadecimal again.
Names can also be given from the command line, before or after anything has
been heard:
```
bandsaunter weather --name 1A2B="back fence" --name 5C=shed
bandsaunter sensors --name 0311="lightning detector" --note 0311="south gable"
bandsaunter sensors --forget 93
```
A name given before the sensor has ever been received waits under its
identity alone — nothing yet knows which model it is, because that is in the
message and there has not been one — and moves across the moment the first
message arrives. So naming the garden from a list and then listening works,
and the display says `shed` from the first reception rather than listing the
shed and the sensor on it as two separate things.
They live in `sensors.yaml` beside the settings, one entry per sensor, meant
to be opened and edited by hand — it is a list of things in a garden, and
typing them is often quicker than tagging them one at a time off the air.
Names are written the moment they are given rather than when the program
exits, because it exits on control-C; and they are written to a neighbouring
file which is then renamed over the old one, so a machine losing power halfway
through leaves either the old names or the new ones and never half of each.
Nothing is ever dropped for being stale. A sensor whose battery ran out two
winters ago keeps its name, because the alternative is that putting a battery
back in loses it.
### What it reads
Five families, each with its own framing and its own check:
| model | bytes | what it says |
|---|---|---|
| Tower 592TXR / 06002RM | 7 | temperature, humidity |
| 5-in-1 06014RM / VN1TXC | 8 | wind speed, wind direction, rainfall / wind speed, temperature, humidity |
| Lightning 6045M | 9 | temperature, humidity, strike count, how far off the storm is |
| 609TXC | 5 | temperature, humidity |
| 606TX | 4 | temperature |
Battery state comes from all of them. The 5-in-1 has more to say than fits in
one message and alternates two of them, so its last message is always either
the wind and the rain or the temperature and the humidity, never both — the
display keeps the newest value of each quantity rather than the newest
message, so the line is the whole sensor rather than half of it flickering.
**What is not read.** The Atlas, the 986 and 515 fridge thermometers, the
00275rm room monitor and the 899 standalone rain gauge. They are on the same
band and are not decoded. A message from one of them whose framing happens to
match is reported as an unknown message type with its identity and nothing
else, rather than guessed at — because knowing that something is out there
transmitting, with an identity that stays the same, is worth a line on its
own. `--no-unknown` leaves them off.
These formats are implemented from their published descriptions and are
checked against frames built from the same descriptions. That proves the
framing, the parity, the checksums and the arithmetic; it is not the same as
having held every one of these sensors.
### Why nothing false gets through
433 MHz is a crowded band. Doorbells, car keys, tyre-pressure sensors, garage
doors and the neighbours' weather station are all on it, and a decoder that
looks at every bit offset of every burst will find a message in noise if it is
allowed to. Four things stop it.
**The checks the message carries.** The three newer models have an eight-bit
sum plus odd parity in the top bit of every payload byte — twelve to fourteen
bits of check on a message of seven to nine bytes.
**Arriving twice.** The two older models carry one byte of check between them,
which is one false message in two hundred and fifty-six, and that is not a
rate a display can be trusted at. So those two are only believed when the same
message arrives twice. It costs nothing: these sensors send everything three
times in a row, for exactly this reason.
**A plausibility range.** A checksum can be satisfied by a message the
hardware could not have sent. Nothing outside −40 to 70 °C, 0 to 100% or a
wind the anemometer cannot physically report is accepted.
**Where the message sits.** This is the one that is easy to get wrong. A
seven-byte message read out of the front of a real eight-byte one is made of
that message's own payload bytes, whose parity is *already correct* — so the
parity bits contribute nothing at all and what is left is one byte of sum.
Corroboration does not help either, because the copies of a message are
identical, so a coincidence in one copy is a coincidence in all three. What
gives that window away every time is that it ends a whole byte before the
burst does. A real message begins after the sync and runs to the end.
### Off the air
Three things happen between the aerial and a bit, in this order.
The receiver is tuned a little to one side of 433.92 MHz, because every
RTL-SDR puts a spike of its own at whatever it is tuned to, and a spike
sitting on top of a signal that works by being switched on and off is the one
thing that stops it being off. The sensors are shifted back to the middle in
software, which puts the spike out at the edge instead. `--offset 0` tunes
straight at them, which is worth trying once to see what the spike was costing.
Then a running average of the complex samples, long enough that its first null
lands on the spike. It is a crude filter and a deliberately crude one: what it
has to reject is one tone at a frequency this end chose.
Only then is the magnitude taken. Filtering before detection rather than after
is what keeps the neighbours — a doorbell, a tyre sensor, a car key — from
adding themselves to the envelope of the sensor.
Slicing the envelope into bits never measures anything against a clock. The
newer sensors vary the length of the pulse and keep the gaps even; the two
older ones keep the pulse even and vary the gap. Both readings of the same
pulses are tried and the checksums say which it was — rather than deciding
from the timings, which is guessing, and wrong on a weak burst where the edges
have moved. A transmitter running ten per cent fast is read correctly and
never noticed, which matters: these transmitters are unlocked and drift tens
of kilohertz and a few per cent of rate with the temperature. An outdoor
sensor in January is not the one that was on the fence in July.
### Afterwards
```
sensors heard
name id model ch msgs every battery last heard
back fence 1A2B Tower 592TXR A 148 16 s ok 23:14:07
mast 0777 5-in-1 06014RM A 131 18 s ok 23:14:02
greenhouse 0C41 Tower 592TXR B 140 17 s ok 23:14:05
shed 5C 609TXC 76 30 s low 23:13:58
what they said
sensor reading first last lowest highest
back fence temperature 8.4 C 6.1 C 5.9 C 9.2 C
humidity 88% 94% 86% 94%
mast wind 14.2 km/h 3.1 km/h 0.0 km/h 38.6 km/h
wind from 248° WSW 202° SSW
rain 41.4 mm 43.9 mm 41.4 mm 43.9 mm
```
The two tables are split on purpose. The first is about reception — who, how
often, how well — and is the one to look at when something is missing; `every`
is the honest measure of an aerial, because these transmit on a fixed cycle,
so thirty seconds from a sensor that sends every sixteen means half of them
are being missed. The second is about the weather, and is the one to look at
when nothing is.
There is no average. These arrive every sixteen seconds when the sensor is in
range and not at all when it is not, and rain and cold both shorten the range
of a 433 MHz transmitter — so the mean of what was received is the mean of a
sample whose gaps are themselves the weather, and it would read like a number
when it is not one. A bearing gets no lowest or highest either: north is 0 and
also 360.
`--csv`, or `bandsaunter readings --csv` afterwards, writes a column per
quantity and a row per reading, with the sensor's name in the second column
and the unit in the heading rather than beside every number — a column of
"21.5 C" is text, and a column of 21.5 is a temperature.
The log keeps the raw bytes of every message underneath whatever was made of
them, because the message is the evidence and the rest of the line is an
opinion about it. A later version of this program that reads a model this one
cannot will be able to go back through old logs and read them properly.
Readings are converted once, on the way in, to degrees Celsius, kilometres an
hour, millimetres and kilometres — different models report in different units,
and the tower sends Celsius while the 5-in-1 and the lightning detector send
Fahrenheit. The log holds the converted value, so `--units imperial` changes
only what is shown and can be changed afterwards on an old log.
### Every weather option
| option | flag | default | what it does |
|---|---|---|---|
| Receiver | `--device` | 0 | which receiver, when more than one is plugged in |
| Gain | `--gain` | auto | tuner gain in dB, or automatic |
| Sample rate | `--rate` | 1.024 MS/s | how fast to sample; 250 kS/s is the minimum |
| Sensors on | `--frequency`, `--freq` | 433.92 MHz | where the sensors transmit |
| Tuning offset | `--offset` | 250 kHz | how far to one side of them to tune |
| Invent a garden | `--simulate` / `--no-simulate` | no | six sensors that are not there |
| Listen for | `--seconds` | until stopped | how long before stopping |
| Write a log | `--log` / `--no-log` | yes | one line of JSON per message |
| Print every message | `--messages` / `--no-messages` | no | a stream of lines instead of a table |
| Keep on screen for | `--hold` | 1800 s | how long a sensor stays after its last message |
| Show readings in | `--units` | metric | metric or imperial, for the display and the export |
| Only named sensors | `--only-named` / `--all-sensors` | no | ignore anything without a name |
| Show unreadable models | `--unknown` / `--no-unknown` | yes | list sensors whose model cannot be read |
| Report at the end | `--report` / `--no-report` | yes | print what each sensor said |
| Also write a spreadsheet | `--csv` / `--no-csv` | no | CSV beside the log |
`--name ID=NAME` is not a setting: it names a sensor and is repeatable.
Saved in `weather.yaml` beside the other settings, from the menu's **s** or by
hand. `bandsaunter readings` also takes `--sensor NAME` to narrow a log to one
sensor, and `--no-report` to write the spreadsheet and say nothing.
### Without a sensor
`--simulate` puts six sensors on a fence that does not exist — one of every
model — transmitting real messages with real checksums, keyed on and off as a
real one does, through the real filter, the real slicer and the real decoders.
Nothing touches the receiver. The naming, the log, the report and the export
can all be tried before an aerial exists.
The weather moves at roughly a real afternoon's pace, which is slower than it
sounds like it should be: about a degree an hour of drift, wind gusting around
a mean, rain falling a tenth of a millimetre at a time. A garden that swung
six degrees a minute would exercise everything downstream perfectly well and
would look, to anyone running `--simulate` to see what the program does, like
a program that cannot read a thermometer.
### If nothing is heard
These are a few milliwatts. A quarter-wave whip for 433.92 MHz is 17 cm of
wire, and the stock telescopic aerial set to about that length works well.
Indoors, behind a wall, with the dongle in the back of a machine, is usually
the problem. Try `--gain 40` if the automatic gain control is not finding
them, and `--messages` to watch individual receptions arrive while moving the
aerial about.
## Meters on 900 MHz
A scan of 902–928 MHz that turns up a burst gets it named rather than reported
as hex.
```
915.0 MHz decoded: electricity meter 12345678 reading 987654
433.92 MHz decoded: AcuRite sensor 1234 temperature 21.5 C humidity 48% channel A
433.92 MHz decoded: Tower 592TXR 1A2B temperature 21.5 C humidity 48% channel A
```
**Utility meters.** The Itron ERT modules fitted to electricity, gas and water
meters across North America broadcast their reading every thirty seconds or so
on 902–928 MHz, in the clear, so a van can drive past and read a street. The
message says which meter, what kind, what the register reads, and whether the
tamper switches have been tripped.
tamper switches have been tripped. It is not guessed at: the message carries a
16-bit BCH check and nothing is reported that has not satisfied it.
**AcuRite sensors.** The 433.92 MHz outdoor sensors sold with every consumer
weather station send temperature, humidity, battery state and a channel letter
every sixteen seconds.
Neither is guessed at: a meter message carries a 16-bit BCH check and a sensor
message a checksum and four parity bits, and nothing is reported that has not
satisfied them. Both are implemented from their published descriptions and
checked against frames built from the same descriptions — which proves the
framing and the arithmetic, and is not the same as having held a meter.
**Weather sensors.** A scan of 433 MHz that catches one of these names it the
same way, using the same decoders as
[the weather section](#weather-sensors-on-433-mhz) — which is where they
belong, because a scan catches one burst and the weather is a night of them.
The two thinly-checked models are not reported from a scan at all: they need
the same message twice before they are believed, and a scan hears one.
## Hex into words