Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rtl433-scanner

An inventory and band-survey web UI for an rtl_433 receiver: it subscribes to the receiver's MQTT event stream, records every transmitter that decodes, and serves the result as a browsable table. It can also retune the same dongle for a time-boxed survey of other bands (315 / 868 / 915 MHz) to answer "is there anything over there worth a second receiver?", then always puts it back.

The device table: every transmitter heard, with model, id, signal and the fields it sends

(Both screenshots are of the service running against invented devices, not a real receiver's inventory — see the privacy note below.)

flowchart LR
    RX["rtl_433 receiver<br/>-M level -M protocol<br/>-F mqtt -F http://0.0.0.0:8433"]
    MQ["MQTT broker"]
    SC["rtl433-scanner<br/>(this service)"]
    DB[("SQLite /data/scanner.db<br/>devices · packets · bands · scans")]
    UI["Web UI :8434<br/>devices · device detail · band scan"]
    HA["Home Assistant<br/>(optional, MQTT discovery)"]

    RX -- "events JSON" --> MQ
    MQ -- "rtl_433/events" --> SC
    SC -- "GET /cmd?cmd=center_frequency&val=…" --> RX
    SC --> DB
    DB --> UI
    SC -- "rollup entities + scan state" --> MQ
    MQ --> HA
    HA -- "rtl433scan/cmd/start_scan" --> MQ
Loading

Two connections to the broker, one thread each: a subscriber for the event stream and the command topic, a publisher for discovery, state and acknowledgements. The only channel to the receiver is rtl_433's own HTTP command API.

A privacy note, before anything else

A 433 MHz receiver is indiscriminate. This service records everything it decodes — your own sensors, the neighbours' weather stations and doorbells, the tyre-pressure sensors of cars driving past — with an identifier, a first- and last-seen time, packet counts, signal strength and the last decoded payload. That is the point of the tool: you cannot decide what to adopt without first seeing what is out there. It is also a database about people who did not ask to be in it.

So: keep it on a private network, do not expose port 8434 to the internet, and think about retention (PACKETS_DAYS / PACKETS_MAX) before you widen it. Nothing here is transmitted; the receiver only listens. Check what your local rules say about logging radio traffic before running it anywhere but your own home.

What it does

  • Inventory. Every decoded event updates one row keyed model|id|channel: first and last seen, packet count, every field name the device has ever sent, its last payload, the protocol number, and running RSSI / SNR / noise statistics. Devices first heard in the last 24 h are highlighted, so a new arrival is obvious.
  • Device detail. The full field set, an SNR sparkline over recent packets, which bands it has been heard on, and the most recent raw payloads.
  • Labels and adoption. Give a device a role name and a note. The page renders the JSON line you would paste into the allowlist of whatever rtl_433 → Home Assistant bridge you run, and you can then tick it as adopted so it drops out of the "still to triage" view. This service deliberately never creates a per-device entity itself — that would put the whole neighbourhood into your home automation system.
  • Band survey. A named preset retunes the receiver, optionally changes the sample rate and enables extra decoders, listens for N minutes per step, and reports how many devices and packets each band produced. Presets ship for 315 MHz (US TPMS and remotes), 868.3 MHz (EU ISM), 915 MHz (US ISM utility meters) and a combined survey of all three.
  • Home Assistant surface (optional). MQTT discovery publishes rollups only — devices heard in 24 h, new in 24 h, packets in 24 h, awaiting adoption, receiver frequency, health, last publish, and a scanning binary sensor carrying the preset, step and end time as attributes. Availability is a retained LWT plus expire_after. Scans can be started and stopped over MQTT, by preset name only — never a free-form frequency.
  • Health. /api/state reports receiver_ok (the /cmd API answered on the last poll) and mqtt_ok (the subscriber is connected); the container healthcheck asserts both, so a listener that came up with no receiver behind it is unhealthy rather than quietly idle.

The band scan, and why it is safe to leave running

The band-scan control: choose a preset, start it, and the receiver returns to 433.92 MHz

With one dongle, a scan silences the 433 MHz sensors for its duration — anything that depends on them will read unavailable until it ends. The design is therefore time-boxed, auto-restoring, and visible:

  • a scan is a preset from presets.json, never a caller-supplied frequency;
  • SCAN_MAX_MINUTES is a hard ceiling and truncates any longer plan;
  • restore means home frequency + home sample rate + disabling every decoder the scan enabled, then reading the frequency back to confirm. Five retries, then health goes to restore_failed and it retries every minute forever — a receiver parked on 315 MHz is the one failure this must never be quiet about;
  • if the process dies mid-scan, /data/scan-active.json records that, and start-up restores when it finds the marker or the receiver off-frequency. SIGTERM restores within the 30 s stop grace period;
  • rtl_433's setters are not persisted, so restarting the receiver is itself a restore.

What it runs on

Python 3.12 in a python:3.12-slim-bookworm container, zero pip dependencies: the standard library's http.server and sqlite3, threads, and a small vendored MQTT 5.0 client (app/mqtt_min.py). The image is a COPY and nothing else, so a build cannot fail on a wheel — it builds on a Raspberry Pi as happily as on a laptop.

It needs an rtl_433 (25.12 or newer) started with -M level -M protocol — so events carry rssi/snr/noise and the decoder number — publishing to MQTT, and with -F http://0.0.0.0:8433 so the scan engine can retune it. That HTTP command port is unauthenticated: keep it on a trusted network.

How to run it

cp .env.example .env && chmod 600 .env      # then edit: receiver, broker, credentials
docker compose up -d --build

Then open http://<host>:8434/. data/ is created next to the compose file and holds scanner.db, an editable copy of presets.json, audit.jsonl (every scan start/stop and label change) and the scan marker.

Variable Default What it is
RTL433_HTTP http://rtl433-receiver:8433 rtl_433's HTTP command API. Required.
MQTT_HOST / MQTT_PORT mqtt-broker / 1883 The broker. Host required.
MQTT_USER / MQTT_PASS rtl433scan / — Its own broker account: read the events topic, read/write rtl433scan/# and homeassistant/+/rtl433scan/#.
EVENTS_TOPIC rtl_433/events The receiver's JSON event stream.
SLUG rtl433scan Prefix for this service's own MQTT topics and entity ids.
DISCOVERY_PREFIX homeassistant Point it elsewhere to exercise discovery without Home Assistant seeing it.
HOME_FREQ / HOME_SAMPLE_RATE 433920000 / 250000 What a scan restores to. Must match the receiver's own CLI defaults.
SCAN_MAX_MINUTES 60 Hard ceiling on any scan.
PACKETS_MAX / PACKETS_DAYS 200000 / 7 Packet history retention.
PUBLISH_INTERVAL 300 Home Assistant publish cadence, seconds; entities expire at 3x.
PORT 8434 HTTP port inside the container.

Without Docker: pip-free, so RTL433_HTTP=… MQTT_HOST=… DATA_DIR=./data python3 app/scanner.py works anywhere with Python 3.12.

The HTTP API

GET /api/state (health), GET /api/devices?since=SECS&sort=…, GET|POST /api/device/<key> (label, note, adopted), GET /api/scan (current, history, presets), POST /api/scan {"preset": name}, DELETE /api/scan, GET /api/receiver (read-only passthrough of rtl_433's own getters), GET /api/audit.

Tests

python3 -m unittest tests.test_db      # from the project root

Six offline tests covering the rollups, the retention cap, new-since, scan attribution and labels. No radio and no broker needed.

Layout

Path What it is
app/scanner.py Entry point: config, shared state, threads, signal handling.
app/ingest.py MQTT subscriber — the event stream and the command topic.
app/db.py SQLite inventory: devices, packets, bands, scans, retention.
app/scan.py The band-scan engine and the restore path.
app/receiver.py Thin client for rtl_433's /cmd HTTP API.
app/hapub.py MQTT publisher: Home Assistant discovery and state.
app/web.py, app/ui.py The HTTP server, and the pages as module strings.
app/mqtt_min.py Vendored dependency-free MQTT 5.0 client.
seed/presets.json Band-scan presets, copied to /data on first start and editable there.
tests/ Offline unit tests.

About

Inventory and band-scan UI for a 433 MHz rtl_433 receiver

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages