RTLAMR2MQTT
AI Disclosure: This version of rtlamr2mqtt was developed with the assistance of AI (Claude by Anthropic). The architecture, code, tests, and documentation were produced collaboratively between a human developer and an AI assistant. The code has been reviewed, tested, and validated by the maintainer.
Supported Platforms
RTLAMR2MQTT reads utility meters (water, gas, energy) using an inexpensive USB RTL-SDR device and publishes the readings to an MQTT broker. It integrates with Home Assistant through MQTT auto-discovery, automatically creating sensor entities for each configured meter.
It works by running rtl_tcp to interface with the SDR hardware and rtlamr to decode the meter transmissions. Readings are parsed and forwarded to MQTT in real time.
What's New in This Version
[!CAUTION] Major code rewrite \ This version is a complete rewrite of the application internals. \ Your old entities should be cleaned manually from your MQTT broker.
Changes from the previous version
- Async subprocess management -- Replaced blocking
subprocess.Popenwithasyncio.create_subprocess_exec. The application no longer hangs when rtl_tcp or rtlamr stall, which was a recurring issue on resource-constrained devices like Raspberry Pi. - Separated architecture -- The monolithic main loop has been split into independent async tasks:
MeterReader(reads and parses rtlamr output),MQTTPublisher(handles all MQTT communication), andManagedProcess(generic subprocess lifecycle manager). They communicate through anasyncio.Queue. - Native async MQTT -- Switched from paho-mqtt with threading to aiomqtt for native asyncio MQTT support. Includes automatic reconnection with backoff.
- Process retry with backoff -- When rtl_tcp or rtlamr fail to start, the application retries up to 5 times with increasing delays (2, 5, 10, 20, 30 seconds) before giving up. This handles transient USB issues common on Raspberry Pi.
- Stable device identification --
device_idnow uses an integer index (0, 1, 2...) matching the order devices are found by librtlsdr, instead of the oldbus:addressformat which changed across reboots and in VMs. - Standard Python logging -- Replaced the custom numeric log level system with Python's built-in
loggingmodule. No moreif LOG_LEVEL >= 3:checks scattered throughout the code. - Replaced
unbufferwithstdbuf-- Removed the dependency on theexpectpackage. Line-buffered subprocess output now usesstdbuf -oLfrom coreutils. - HA discovery re-publish -- When Home Assistant restarts, discovery messages are automatically re-published. This was previously broken (gated behind a log level check).
- Listen mode -- New
general.listen_mode: trueoption. Runs without any meter filter and logs every meter it hears, deduplicated per session. No MQTT connection is made. Useful for discovering your meter ID before configuring the add-on. - Periodic HA discovery re-publish -- Discovery payloads are re-sent on a configurable interval (
mqtt.discovery_interval, default 300 s) in addition to on HA restart, so entities survive MQTT broker restarts even withretain=false. - Test coverage -- Added 90 unit tests using pytest and pytest-asyncio.
- Dual-purpose Dockerfile -- Works both as a Home Assistant add-on (with
BUILD_FROM) and as a standalone Docker container (defaults topython:3.13-slim). - Bug fixes -- Fixed: non-deterministic meter ID key lookup, undefined variable in mock mode, deprecated paho-mqtt API usage, USB file descriptor leak in reset, hardcoded TLS 1.2 protocol.
Requirements
- A compatible smart meter -- Check the list of supported meters
- A USB RTL-SDR device -- For example: NooElec NESDR Mini USB
- An MQTT broker -- Such as Mosquitto
- Home Assistant (optional but recommended)
Installation
Home Assistant Add-On
Or manually:
- Go to Settings > Add-ons > Add-on Store
- Click the three dots in the top-right corner, then Repositories
- Add
https://github.com/allangood/rtlamr2mqttand click Add - The rtlamr2mqtt add-on will appear in the store. Click to install and configure.
When running as an add-on, MQTT broker settings are automatically obtained from the Home Assistant Supervisor API. You only need to configure the meters section.
Docker (Standalone)
docker run --name rtlamr2mqtt \
-v /path/to/rtlamr2mqtt.yaml:/etc/rtlamr2mqtt.yaml:ro \
--device /dev/bus/usb:/dev/bus/usb \
--restart unless-stopped \
allangood/rtlamr2mqtt
Docker Compose (Standalone)
services:
rtlamr2mqtt:
image: allangood/rtlamr2mqtt
container_name: rtlamr2mqtt
restart: unless-stopped
devices:
- /dev/bus/usb:/dev/bus/usb
volumes:
- /path/to/rtlamr2mqtt.yaml:/etc/rtlamr2mqtt.yaml:ro
Finding Your Meter ID (Listen Mode)
If you don't know your meter ID, use listen mode to discover it. In this mode rtlamr2mqtt runs without any meter filter, logs every meter it receives, and makes no MQTT connection.
Home Assistant Add-On
In the add-on configuration, set:
general:
listen_mode: true
Leave the meters list empty (or remove it entirely). Start the add-on and open the Log tab. You will see a warning followed by one log line per discovered meter:
WARNING: LISTEN MODE ACTIVE — no meter filtering, no MQTT publishing. Check logs for "New meter" lines to discover your meter ID, then configure it and disable listen_mode.
INFO: New meter | ID: 12345678 | Type: SCM | Consumption: 1978226
INFO: New meter | ID: 87654321 | Type: R900 | Consumption: 4555831
Each meter is logged once per session regardless of how many times it broadcasts. Note the ID and Type for the meter you want to track, then configure it normally and set listen_mode: false.
Docker / Standalone
Create a minimal config file:
general:
listen_mode: true
verbosity: info
mqtt:
host: 127.0.0.1 # still required for standalone, but no connection is made
Run the container and watch the logs:
docker run --rm \
-v /path/to/listen.yaml:/etc/rtlamr2mqtt.yaml:ro \
--device /dev/bus/usb:/dev/bus/usb \
allangood/rtlamr2mqtt
Once you have found your meter ID, update your config with the full meter definition and set listen_mode: false (or remove the line — false is the default).
Configuration
When running standalone (not as an HA add-on), create a rtlamr2mqtt.yaml file. Below is a complete example with all available options:
general:
# Seconds to sleep after all meters are read. 0 = continuous reading.
sleep_for: 60
# Log verbosity: debug, info, warning, error, critical, none
verbosity: info
# RTL-SDR device index (0 = first device). Use if you have multiple SDR dongles.
# device_id: 0
# RTL_TCP server address. Default: local server at 127.0.0.1:1234
# Set to a remote address to use an external rtl_tcp instance.
# rtltcp_host: "192.168.1.100:1234"
# Enable to log all received meters without MQTT. Useful for finding your meter ID.
# listen_mode: false
mqtt:
# MQTT broker connection (not needed when running as HA add-on)
host: 127.0.0.1
port: 1883
# user: myuser
# password: mypassword
# TLS settings
tls_enabled: false
# tls_insecure: false
# tls_ca: "/path/to/ca.crt"
# tls_cert: "/path/to/client.crt"
# tls_keyfile: "/path/to/client.key"
# MQTT topics
ha_autodiscovery_topic: homeassistant
ha_status_topic: homeassistant/status
base_topic: rtlamr
# How often (seconds) to re-publish HA discovery payloads. Keeps entities alive after broker restarts.
# discovery_interval: 300
# Optional: pass extra arguments to rtl_tcp or rtlamr
# custom_parameters:
# rtltcp: "-s 2048000"
# rtlamr: "-unique=true"
# Meter definitions (required)
meters:
- id: 12345678
protocol: scm+ # scm, scm+, idm, netidm, r900, r900bcd
name: my_water_meter
format: "######.###" # Each '#' is a digit
unit_of_measurement: "m3"
icon: mdi:water
device_class: water # water, gas, energy, power, current, none
state_class: total_increasing # measurement, total, total_increasing
# expire_after: 3600 # Seconds before sensor becomes unavailable
# force_update: true # Send updates even if value unchanged
# manufacturer: "Badger Meter" # Shown in HA device info
# model: "ORION SE" # Shown in HA device info
Configuration Reference
general
| Parameter | Type | Default | Description |
|---|---|---|---|
sleep_for |
int | 0 |
Seconds to sleep after all meters are read. 0 = continuous. |
verbosity |
string | info |
Log level: debug, info, warning, error, critical, none |
device_id |
int | 0 |
RTL-SDR device index. 0 = first device found. |
rtltcp_host |
string | 127.0.0.1:1234 |
RTL_TCP server address. Set to remote host to skip local rtl_tcp. |
listen_mode |
bool | false |
Log all received meters without filtering. No MQTT connection. See Finding Your Meter ID. |
mqtt
| Parameter | Type | Default | Description |
|---|---|---|---|
host |
string | (from Supervisor) | MQTT broker hostname. Auto-detected in HA add-on mode. |
port |
int | 1883 |
MQTT broker port. |
user |
string | none | MQTT username. |
password |
string | none | MQTT password. |
tls_enabled |
bool | false |
Enable TLS for MQTT connection. |
tls_insecure |
bool | false |
Skip TLS certificate verification (for self-signed certs). |
tls_ca |
string | none | Path to CA certificate file. |
tls_cert |
string | none | Path to client certificate file. |
tls_keyfile |
string | none | Path to client key file. |
ha_autodiscovery_topic |
string | homeassistant |
HA MQTT auto-discovery prefix. |
ha_status_topic |
string | homeassistant/status |
Topic to monitor HA restarts. |
base_topic |
string | rtlamr |
Base topic for status and readings. |
discovery_interval |
int | 300 |
Seconds between periodic HA discovery re-publishes. |
meters[]
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
int | yes | Meter ID number. |
protocol |
string | yes | Protocol: scm, scm+, idm, netidm, r900, r900bcd |
name |
string | yes | Sensor name in Home Assistant. |
format |
string | no | Number format. Each # is a digit (e.g., ######.###). |
unit_of_measurement |
string | no | Unit shown in HA (e.g., m3, ft3, KWh). |
icon |
string | no | MDI icon (e.g., mdi:water, mdi:gauge). |
device_class |
string | no | HA device class: water, gas, energy, power, current, none |
state_class |
string | no | HA state class: measurement, total, total_increasing (default). |
expire_after |
int | no | Seconds before sensor becomes unavailable if not updated. |
force_update |
bool | no | Send update events even if value hasn't changed. |
manufacturer |
string | no | Manufacturer name shown in HA device info. |
model |
string | no | Model name shown in HA device info. |
MQTT Topics
For each configured meter, the following topics are published:
| Topic | Description |
|---|---|
rtlamr/status |
online / offline (LWT) |
rtlamr/{meter_id}/state |
JSON: {"reading": "001234.567", "lastseen": "2025-01-15T10:30:00-05:00"} |
rtlamr/{meter_id}/attributes |
JSON with protocol-specific fields |
homeassistant/device/{meter_id}/config |
HA auto-discovery payload |
Multiple RTL-SDR Devices
If you have more than one RTL-SDR dongle, set device_id to the index of the one you want to use. The index corresponds to the order devices are detected by librtlsdr (starting from 0).
Home Assistant Utility Meter
To track usage over time, add a utility meter in your Home Assistant configuration:
utility_meter:
daily_water:
source: sensor.my_water_meter_reading
cycle: daily
monthly_water:
source: sensor.my_water_meter_reading
cycle: monthly
Water Leak Detection
The cumulative reading published by this add-on lets you detect two common leak patterns: overnight consumption (running toilets, dripping fixtures) and continuous flow (slow pipe leaks, irrigation left on).
First, add an hourly utility meter and a derivative sensor that exposes the instantaneous flow rate:
# configuration.yaml
utility_meter:
hourly_water:
source: sensor.my_water_meter_reading
cycle: hourly
sensor:
- platform: derivative
source: sensor.my_water_meter_reading
name: Water flow rate
unit_time: h
time_window: "00:30:00" # 30-minute rolling window smooths out meter transmission gaps
round: 2
Then create two automations — the first catches leaks by time-of-day, the second catches any continuous flow regardless of hour:
# automations.yaml
- alias: Water leak — overnight consumption
description: Alert if any water is consumed between 02:00 and 05:00
trigger:
- platform: numeric_state
entity_id: sensor.hourly_water
above: 0
for:
minutes: 30
condition:
- condition: time
after: "02:00:00"
before: "05:00:00"
action:
- service: notify.notify
data:
title: "⚠️ Possible water leak"
message: >
{{ states('sensor.hourly_water') }}
{{ state_attr('sensor.hourly_water', 'unit_of_measurement') }}
used overnight. Check toilets and fixtures.
- alias: Water leak — continuous flow
description: Alert when the flow rate stays above zero for 2+ hours straight
trigger:
- platform: numeric_state
entity_id: sensor.water_flow_rate
above: 0
for:
hours: 2
action:
- service: notify.notify
data:
title: "⚠️ Continuous water flow detected"
message: >
Water has been flowing for over 2 hours at
{{ states('sensor.water_flow_rate') }}
{{ state_attr('sensor.my_water_meter_reading', 'unit_of_measurement') }}/h.
Check for leaks or irrigation left on.
Tuning tips:
above: 0on the continuous-flow automation is the most sensitive setting. If you get false positives from a fridge icemaker or humidifier, raise it slightly (e.g.above: 0.1).- The overnight window (
02:00–05:00) is a good starting point. Adjust for your household's schedule. - Replace
notify.notifywith your specific mobile app service (e.g.notify.mobile_app_my_phone) to target a single device. - The same pattern works for gas meters — swap
sensor.my_water_meter_readingfor your gas meter entity.
Development
Running Tests
cd rtlamr2mqtt-addon
python3 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt
.venv/bin/pytest tests/ -v
Testing with Docker Compose
A docker-compose setup with Mosquitto is included for integration testing using mock rtl_tcp/rtlamr scripts:
cd rtlamr2mqtt-addon
docker compose up --build
This starts Mosquitto and rtlamr2mqtt with mock data. You can subscribe to see the messages:
docker exec rtlamr2mqtt-addon-mosquitto-1 mosquitto_sub -t 'rtlamr/#' -t 'homeassistant/#' -v
Credits
- rtlamr by bemasher
- rtl-sdr by Osmocom
- aiomqtt by Frederik Aalund
- Icon by Plastic Donut - Flaticon
Thank you to all contributors!
License
See LICENSE.