In pt 1 the goal was just to see the spectrum. Plug things in, fix permissions, log a band, stop being intimidated by waterfalls. This one is about turning that into a map of what actually lives in the air around the house.

Every place has two RF layers. Stuff that’s always there and stuff that fires when something happens. Once you can tell them apart, the spectrum stops looking like static and starts looking like an inventory.

The premise

A house broadcasts more than people realise. Weather sensors that ping every 30 seconds. A smart meter that wakes up on its own schedule. The garage door, which is silent until it isn’t. Door contacts. A neighbour’s tyre pressure sensors driving past. Half of it is licensed ISM band traffic, the rest is whatever the manufacturer thought they could get away with.

I wanted to know, concretely:

  • What devices in my own environment are emitting, and on what frequencies
  • Which of them are predictable (heartbeats, polling) and which are reactive
  • What’s mine vs what’s leaking in from outside

No reversing protocols yet. Just identification.

Tooling shift: rtl_433 does most of the work

rtl_power from pt 1 is fine for surveying energy. For identification, rtl_433 is what you want. It ships with decoders for hundreds of consumer devices on the common ISM bands and will happily print everything it recognises as JSON.

rtl_433 -F json -M time:iso -M level

That’s the entire starting command. No frequency argument means it sits on 433.92 MHz. Once it’s running, walk around the house and trigger things. Press the doorbell, open a window with a contact sensor, lock and unlock the car from inside. Anything decoded will print a line.

To save it:

rtl_433 -F json -M time:iso -M level 2>/dev/null \
  | tee -a ~/rf/rtl_433_$(date +%F).jsonl

JSONL is the right format here. One event per line, append-only, trivial to grep through later.

For other bands you tell it explicitly:

rtl_433 -f 315M -F json   # North American garage doors, TPMS, some fobs
rtl_433 -f 868M -F json   # European alarm sensors, smart meters
rtl_433 -f 915M -F json   # NA ISM, weather stations, some utility meters

If you have a HackRF, hackrf_sweep is the wider-net version for finding where activity is in the first place:

hackrf_sweep -f 300:960 -w 100000 -1 > sweep.csv

One pass from 300 to 960 MHz, 100 kHz bins, single sweep. Open it, see where the energy is, then point rtl_433 at the live bands.

Pass 1: the always-on layer

I left rtl_433 running on 433 MHz for a few hours with nobody touching anything in the house. The point was to see what fires on its own.

What showed up in my environment:

  • A weather station outside that transmits temperature and humidity every 47 seconds, like clockwork. Same device ID every time.
  • Soil moisture sensors from the garden, less frequent, but every device announces itself with the model name in the decoder output.
  • A smart meter reading that wasn’t mine, coming from the direction of the neighbours’ wall.
  • An LPG tank level sensor I’d forgotten was wireless.

The pattern is heartbeats. Always-on devices want to be polled, so they shout into the void on a fixed interval, and the interval itself is fingerprintable. If a sensor pings every 47 seconds with the same ID, you can plot it on a timeline and the gaps tell you when the device was off or out of range.

A useful question to ask of every constant emitter: do I own this? If not, why can I hear it, and what does that say about how far my receiver is reaching.

Pass 2: the event-driven layer

This is where you walk around the house being a nuisance. Press every button, open every door, trigger every motion sensor you know about. Have rtl_433 logging the whole time.

What I found:

  • The garage door remote at 433.92 MHz, rolling code, decoder didn’t name it but the bit pattern is consistent per press.
  • A wireless doorbell on 433 that the decoder identified by model.
  • Two PIR sensors from the alarm panel that wake up on motion and stay quiet otherwise.
  • Three door contacts that send a short burst on open and another on close.
  • The TV remote, which is IR not RF, and reminded me that not everything you press is going to show up on an SDR.

The car key fob didn’t decode cleanly on 433. That one was on 868 in this region, which I only confirmed by re-running rtl_433 on the other band while pressing the unlock button.

Event-driven traffic is the more interesting half because it’s tied to physical state changes. A door contact that hasn’t transmitted in 12 hours either means the door hasn’t moved or the sensor is dead. Both are useful pieces of information if you’re the one who owns the network.

Building the inventory

After a couple of sessions I had enough to start a flat inventory. Mine looks like a TSV with these columns:

freq_mhz    type            label                   cadence         protocol_or_model
433.92      heartbeat       weather_station_outside ~47s            Acurate-986
433.92      heartbeat       soil_sensor_bed_1       ~5min           Generic-FS20
433.92      event           garage_door_remote      on_press        unknown_rolling
433.92      event           doorbell_front          on_press        Honeywell-RCWL
433.92      event           door_contact_kitchen    on_state_change unknown
868.30      heartbeat       smart_meter             ~10s            wmbus
868.30      event           car_fob                 on_press        unknown
915.00      heartbeat       (neighbour) utility     ~30s            unknown

That’s enough to do real things with. If something new shows up that isn’t on the list, it’s either a new device I just installed, a neighbour got something, or somebody’s nearby holding a transmitter. Any of those is worth a glance.

A small script for the boring part

The bit I wanted automated was “tell me what’s new since yesterday”. This is a 20-line script, not a project:

#!/usr/bin/env python3
import json, sys, collections

seen = collections.defaultdict(int)
for line in sys.stdin:
    try:
        d = json.loads(line)
    except json.JSONDecodeError:
        continue
    key = (d.get("model","?"), d.get("id","?"), round(d.get("freq",0), 2))
    seen[key] += 1

for (model, dev_id, freq), count in sorted(seen.items(), key=lambda x: -x[1]):
    print(f"{count:6d}  {freq:7.2f} MHz  {model:30s}  id={dev_id}")

Pipe a day of JSONL through it:

cat ~/rf/rtl_433_2026-06-01.jsonl | ./rf_summary.py

The output is a leaderboard of which device IDs were most active. New entries are obvious. Missing entries are obvious. That’s most of what you need for daily checks.

What surprised me

A few things from this round.

The smart meter wasn’t mine. It’s loud enough to clear the wall. That’s not a vulnerability exactly, but consumption telemetry leaving the building in the clear is a thing worth knowing about.

Cars driving past show up. TPMS sensors transmit at 315 or 433 depending on region and you can see them passing if you’re logging long enough. None of it is identifying on its own, but the pattern of passes is.

A device I thought was wired was wireless. The LPG tank sensor was installed by a contractor years ago and I had no documentation on it. The decoder gave me the model in one line of JSON.

Most “smart” devices announce themselves by manufacturer in the protocol header. There’s no obscurity here. If it’s on a hobbyist decoder list, it’s identifiable.

What I’m not doing

Not replaying anything. Not transmitting on the HackRF. Rolling code captures are interesting from a “how does this work” point of view, but the moment you press the button on a transmitter aimed at someone else’s gate you’ve crossed a line that isn’t worth crossing. The HackRF stays on receive.

Not trying to demodulate anything rtl_433 doesn’t already know about. URH (Universal Radio Hacker) is the next step for that and it’s its own rabbit hole.

Lessons

A few short ones.

Heartbeats are gold. Anything with a fixed cadence is easy to spot, easy to baseline, easy to alarm on when it goes quiet.

The decoder is more useful than the waterfall once you know roughly where things live. Waterfalls are for finding signals. Decoders are for identifying them.

Region matters. ISM bands are different in EU, US, and AU. If a fob doesn’t show on 433, it’s probably on 315 or 868. Try the other ones before assuming the hardware is broken.

Your inventory will outlive any single script. Keep it in a flat file you can read in ten years.

What’s next

A 30-day version of the inventory, with cadence drift tracking. If the weather station’s 47-second heartbeat starts arriving every 52 seconds, I want to know about it.

Some long captures on 868 MHz. The smart meter and the car fob are both there and I haven’t given that band the time it deserves.

A small dashboard. Probably a static page that reads the JSONL and renders last-seen times per device. Not a project, just a page.

Same as before. Receiving on bands I’m allowed to receive on, in an environment I own, on equipment I own. No transmitting, no replaying, no decoding anything that isn’t mine to look at. RF is the kind of hobby where the line between curious and a problem is short and worth respecting.