Skip to content

Tamooj/SDR_NoiseMon

Repository files navigation

SDR_NoiseMon

RF noise floor monitor for RTL-SDR, with Home Assistant integration via MQTT.

Periodically samples configurable frequency bands, estimates the noise floor using Welch's PSD method, stores readings in SQLite, and publishes them to Home Assistant via MQTT autodiscovery. Runs as a systemd timer on Debian/Ubuntu Linux.


Architecture

RTL-SDR v3/v4 (USB)
        │
SDR++ headless server  ──── SoapyRemote TCP (default :55123)
        │
noise_floor_monitor.py
        │
        ├── SoapySDR Python bindings  (driver=remote)
        │       └── Capture IQ samples per band
        │
        ├── DSP: Welch PSD → Nth-percentile bin → dBFS
        │
        ├── SQLite  ~/.noise_monitor/noise_floor.db
        │
        ├── MQTT → Home Assistant
        │       ├── Autodiscovery:  homeassistant/sensor/rf_conditions/…/config
        │       └── Data:           noise_monitor/{band_label}/noise_dbfs
        │
        └── matplotlib PNG  ~/.noise_monitor/plots/noise_floor.png

What "noise floor" means here

Metric: 10th-percentile power bin from Welch's averaged PSD, in dBFS (dB relative to ADC full scale).

Welch's method averages many overlapping FFT segments, producing a stable power spectral density estimate even from a short capture. Taking the 10th-percentile bin ignores narrowband signals (carriers, birdies, broadcast stations) and tracks only the background noise floor of the band.

A rising noise floor typically indicates new local RFI — switching power supplies, solar inverters, motor drives, LED dimmers, or nearby electronics. By comparing multiple bands simultaneously you can distinguish broadband RFI (all bands rise together) from interference on a specific frequency range.

Because dBFS is relative to full ADC scale, readings are only comparable across runs at the same gain setting. Keep sdr.gain fixed.


Hardware requirements

Item Notes
RTL-SDR v3 or v4 v4 recommended; direct sampling mod required for HF
SDR++ (headless) Must have SoapyRemote server enabled
Linux host (LXC, VM, or bare metal) Debian 12 or Ubuntu 22.04+
MQTT broker Mosquitto, EMQX, etc.
Home Assistant Optional — MQTT integration required for autodiscovery

RTL-SDR tuning range

Mode Range Notes
Normal (via tuner) ~24 MHz – 1766 MHz Default for VHF/UHF bands
Direct Q-branch sampling ~500 kHz – 14 MHz RTL-SDR v3/v4 with mod; auto-enabled by this script

Bands below sdr.direct_sampling_threshold_hz (default 24 MHz) automatically switch the device to Q-branch direct sampling mode. No manual configuration is needed — just add the band to config.yaml.

Note: Direct sampling quality degrades above ~14 MHz on most dongles. The 20m HF band (14.2 MHz) is at the practical upper limit. Results may vary depending on your specific hardware revision.


SDR++ server setup

SDR++ must be running in headless/server mode with SoapyRemote enabled on the host machine that has the RTL-SDR dongle attached.

  1. Install SDR++ and enable the Remote Source module
  2. Start SDR++ — no GUI client connection is needed
  3. Confirm the SoapyRemote server is listening:
    SoapySDRUtil --probe="driver=remote,remote=YOUR_SDR_HOST:55123"
    You should see device info (manufacturer, tuner type, sample rate range, etc.)

The noise monitor opens and closes the SDR device for each band capture. Do not run it while an SDR++ client is actively streaming the same device — only one SoapySDR client can hold the device at a time.


Installation

1. Clone the repo

git clone https://github.com/your-username/SDR_NoiseMon.git
cd SDR_NoiseMon

Note: Git does not preserve execute permissions across all platforms. After cloning on Linux, make setup.sh executable manually:

chmod +x setup.sh

2. Configure

cp config.example.yaml config.yaml
cp .env.example .env

Edit config.yaml — at minimum, verify the bands and MQTT device name. Edit .env — set your SDR++ server host, MQTT broker address, and credentials.

3. Run setup

chmod +x setup.sh
./setup.sh

setup.sh will:

  • Install system packages via apt (SoapySDR, numpy, scipy, matplotlib)
  • Install paho-mqtt and python-dotenv via pip
  • Set up RTL-SDR udev rules for non-root USB access
  • Probe the SoapySDR remote server to verify connectivity
  • Install and enable the systemd service + timer

4. Verify

# Check the timer is active
systemctl status noise-monitor.timer

# Run a sample pass immediately and watch the output
systemctl start noise-monitor.service
journalctl -u noise-monitor.service -f

Successful output looks like:

2026-03-21T14:00:03 INFO Config: config.yaml | SDR: driver=remote,...
2026-03-21T14:00:03 INFO DB: /home/user/.noise_monitor/noise_floor.db
2026-03-21T14:00:03 INFO MQTT connected → 192.168.1.12:1883
2026-03-21T14:00:04 INFO MQTT autodiscovery published: 5 band sensor(s) + timestamp
2026-03-21T14:00:04 INFO Sampling MW_AM @ 1.000 MHz ...
2026-03-21T14:00:05 INFO   MW_AM: -58.34 dBFS
2026-03-21T14:00:05 INFO Sampling 40m_HF @ 7.100 MHz ...
2026-03-21T14:00:06 INFO   40m_HF: -62.11 dBFS
...
2026-03-21T14:00:12 INFO Sample pass complete: 5/5 band(s) OK.
2026-03-21T14:00:12 INFO Plot → /home/user/.noise_monitor/plots/noise_floor.png

Home Assistant integration

After the first successful run, entities appear automatically in HA under Settings → Devices & Services → MQTT → RF Conditions:

Entity Topic Unit
sensor.mw_am_noise_floor noise_monitor/MW_AM/noise_dbfs dBFS
sensor.40m_hf_noise_floor noise_monitor/40m_HF/noise_dbfs dBFS
sensor.20m_hf_noise_floor noise_monitor/20m_HF/noise_dbfs dBFS
sensor.vhf_wx_noise_floor noise_monitor/VHF_WX/noise_dbfs dBFS
sensor.2m_ham_noise_floor noise_monitor/2m_HAM/noise_dbfs dBFS
sensor.rf_monitor_last_sample noise_monitor/last_sample timestamp

Entity names and topic labels are derived from the label field in config.yaml. Add or rename bands and re-run --discover to update HA.

Re-publishing discovery after HA restart

MQTT discovery payloads are retained (retain=True), so HA normally picks them up automatically on restart. If entities go missing, re-publish manually:

python3 noise_floor_monitor.py --discover

Dashboard example

Use an HA history graph card or ApexCharts card with the sensor entities to plot noise floor trends over time. Example Lovelace YAML:

type: history-graph
title: RF Noise Floor
hours_to_show: 48
entities:
  - entity: sensor.mw_am_noise_floor
  - entity: sensor.40m_hf_noise_floor
  - entity: sensor.20m_hf_noise_floor
  - entity: sensor.vhf_wx_noise_floor
  - entity: sensor.2m_ham_noise_floor

Configuration reference

config.yaml

Key Default Description
sdr.remote_host (required) IP/hostname of SDR++ server
sdr.remote_port 55123 SoapyRemote TCP port
sdr.soapy_args (built from host/port) Override with a raw SoapySDR driver string
sdr.gain 30 RF gain in dB; "auto" for AGC (not recommended)
sdr.num_samples 256000 IQ samples captured per band
sdr.nperseg 1024 Welch segment length (power of 2)
sdr.noise_percentile 10 PSD percentile used as noise floor estimate
sdr.direct_sampling_threshold_hz 24000000 Bands below this auto-enable Q-branch mode
mqtt.broker (required) MQTT broker IP/hostname
mqtt.port 1883 MQTT broker port
mqtt.topic_prefix noise_monitor Prefix for all data topics
mqtt.discovery_prefix homeassistant HA MQTT discovery prefix
mqtt.device_name RF Conditions Device name shown in HA
mqtt.device_id rf_conditions Unique device identifier
storage.db_path ~/.noise_monitor/noise_floor.db SQLite database location
storage.plot_dir ~/.noise_monitor/plots PNG and CSV output directory
storage.plot_hours 48 Rolling window for plots

.env

Variable Description
SDR_HOST IP/hostname of SDR++ server
SDR_PORT SoapyRemote port (default 55123)
MQTT_BROKER Broker IP/hostname
NOISE_MQTT_USER MQTT username
NOISE_MQTT_PASSWORD MQTT password

Usage

# Normal operation (sample + plot + publish) — what the timer runs:
python3 noise_floor_monitor.py

# Override SDR++ host for testing without editing config:
python3 noise_floor_monitor.py --sdr-host 192.168.1.50

# Verbose output:
python3 noise_floor_monitor.py --debug

# Regenerate plots from existing DB without sampling:
python3 noise_floor_monitor.py --plot

# Publish MQTT autodiscovery only (no sampling):
python3 noise_floor_monitor.py --discover

# Explicit config file location:
python3 noise_floor_monitor.py --config /etc/noise_monitor/config.yaml

Systemd service management

# Timer status (next scheduled run):
systemctl status noise-monitor.timer

# Last service run status and exit code:
systemctl status noise-monitor.service

# Trigger an immediate run:
systemctl start noise-monitor.service

# Follow logs live:
journalctl -u noise-monitor.service -f

# Recent logs:
journalctl -u noise-monitor.service -n 100

# Disable the timer:
systemctl disable --now noise-monitor.timer

Database

Data is stored in SQLite at the path configured in storage.db_path.

Schema

CREATE TABLE noise_floor (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    ts          TEXT    NOT NULL,   -- ISO-8601 UTC (e.g. 2026-03-21T14:00:04Z)
    band        TEXT    NOT NULL,   -- label from config.yaml
    freq_hz     INTEGER NOT NULL,
    noise_dbfs  REAL    NOT NULL,   -- 10th-pctile Welch PSD bin in dBFS
    gain_db     REAL    NOT NULL,   -- -1.0 if gain was "auto"
    sample_rate INTEGER NOT NULL
);

Queries

# Most recent reading per band:
sqlite3 ~/.noise_monitor/noise_floor.db \
  "SELECT ts, band, printf('%.2f', noise_dbfs) dBFS
   FROM noise_floor
   GROUP BY band
   HAVING ts = MAX(ts);"

# Last 24 hours for a specific band:
sqlite3 ~/.noise_monitor/noise_floor.db \
  "SELECT ts, printf('%.2f', noise_dbfs) dBFS
   FROM noise_floor
   WHERE band = '2m_HAM'
     AND ts >= datetime('now', '-24 hours')
   ORDER BY ts;"

# Daily average per band:
sqlite3 ~/.noise_monitor/noise_floor.db \
  "SELECT date(ts) day, band, printf('%.2f', avg(noise_dbfs)) avg_dBFS
   FROM noise_floor
   GROUP BY day, band
   ORDER BY day DESC, band;"

Troubleshooting

SoapySDR probe fails

WARNING: SoapySDR probe failed
  1. Confirm SDR++ is running on the remote host with SoapyRemote enabled
  2. Check firewall — port 55123 must be reachable from the monitor host
  3. Test manually: SoapySDRUtil --probe="driver=remote,remote=HOST:PORT"
  4. Verify only one client is connected at a time

HF bands return unexpected values

Direct sampling output can be affected by:

  • USB noise on the dongle itself (use a USB extension cable)
  • Lack of an HF antenna (a simple wire helps significantly)
  • DC offset at exactly 0 Hz (normal — the 10th-percentile estimator avoids this)

MQTT entities don't appear in HA

  1. Verify MQTT integration is enabled in HA with the correct discovery prefix (homeassistant by default)
  2. Run python3 noise_floor_monitor.py --discover to re-publish retained config payloads
  3. Check broker connectivity: mosquitto_pub -h BROKER -u USER -P PASS -t test/ping -m hello

"No bands configured" error

config.yaml is missing a bands: list. Copy config.example.yaml and edit it.


Contributing

Pull requests welcome. Useful additions:

  • Support for other SoapySDR-compatible devices (HackRF, SDRplay, etc.)
  • Additional noise floor estimators (e.g. minimum statistics)
  • InfluxDB / Prometheus export alongside SQLite
  • Docker container definition
  • Web UI for the SQLite data

Please open an issue before starting a large feature to discuss the approach.


License

MIT License. See LICENSE for details.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors