├── Dockerfile ├── README.md ├── entry.sh ├── rtl.blacklist.conf └── rtl_433_mqtt_hass.py /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 AS intermediate 2 | 3 | # 4 | # First install software packages needed to compile RTL-SDR and rtl_433 5 | # 6 | RUN apt-get update && apt-get install --no-install-recommends -y \ 7 | git \ 8 | libtool \ 9 | libusb-1.0.0-dev \ 10 | librtlsdr-dev \ 11 | rtl-sdr \ 12 | build-essential \ 13 | autoconf \ 14 | cmake \ 15 | pkg-config \ 16 | && rm -rf /var/lib/apt/lists/* 17 | 18 | # 19 | # Pull RTL_433 source code from GIT, compile it and install it 20 | # 21 | WORKDIR /rtl_433 22 | RUN git clone https://github.com/merbanan/rtl_433.git . \ 23 | && mkdir build \ 24 | && cd build \ 25 | && cmake ../ \ 26 | && make \ 27 | && make install 28 | 29 | 30 | 31 | # Final image build 32 | FROM python:3 AS final 33 | 34 | # 35 | # Define environment variables 36 | # 37 | # Use this variable when creating a container to specify the MQTT broker host. 38 | ENV MQTT_HOST "" 39 | ENV MQTT_PORT 1883 40 | ENV MQTT_USERNAME "" 41 | ENV MQTT_PASSWORD "" 42 | ENV MQTT_TOPIC rtl_433 43 | ENV DISCOVERY_PREFIX homeassistant 44 | ENV DISCOVERY_INTERVAL 600 45 | 46 | RUN apt-get update && apt-get install --no-install-recommends -y \ 47 | libtool \ 48 | libusb-1.0.0-dev \ 49 | librtlsdr-dev \ 50 | rtl-sdr \ 51 | && rm -rf /var/lib/apt/lists/* 52 | 53 | COPY --from=intermediate /usr/local/include/rtl_433.h /usr/local/include/rtl_433.h 54 | COPY --from=intermediate /usr/local/include/rtl_433_devices.h /usr/local/include/rtl_433_devices.h 55 | COPY --from=intermediate /usr/local/bin/rtl_433 /usr/local/bin/rtl_433 56 | COPY --from=intermediate /usr/local/etc/rtl_433 /usr/local/etc/rtl_433 57 | 58 | # 59 | # Install Paho-MQTT client 60 | # 61 | RUN pip3 install paho-mqtt 62 | 63 | # 64 | # Blacklist kernel modules for RTL devices 65 | # 66 | COPY rtl.blacklist.conf /etc/modprobe.d/rtl.blacklist.conf 67 | 68 | # 69 | # Copy scripts, make executable 70 | # 71 | COPY entry.sh rtl_433_mqtt_hass.py /scripts/ 72 | RUN chmod +x /scripts/entry.sh 73 | 74 | # 75 | # Execute entry script 76 | # 77 | ENTRYPOINT [ "/scripts/entry.sh" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rtl2hass 2 | 3 | This is source code for a Docker image that will receive 433.92 MHz sensor data (Acurite, etc) and pass it to Home Assitant using MQTT. Setup to work with Home Assistant's MQTT Discovery module. 4 | 5 | Pre-built Docker image built at https://hub.docker.com/r/jochocki/rtl2hass 6 | 7 | rtl_433 project can be found here: https://github.com/merbanan/rtl_433 8 | 9 | `rtl_433_mqtt_hass.py` sourced from example script: https://github.com/merbanan/rtl_433/blob/master/examples/rtl_433_mqtt_hass.py 10 | 11 | ## Requirements 12 | 13 | ### DVB-T Receiver 14 | A USB DVB-T dongle is required to use this container. 15 | 16 | You must pass your USB DVB-T dongle to the container, as well as blacklist the kernel modules from your host. 17 | 18 | #### To find the device location of your dongle, run: 19 | ``` 20 | lsusb 21 | ``` 22 | 23 | Sample output: 24 | ``` 25 | Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub 26 | Bus 001 Device 003: ID 8087:0a2b Intel Corp. 27 | Bus 001 Device 004: ID 0bda:2838 Realtek Semiconductor Corp. RTL2838 DVB-T # This is your DVB-T dongle 28 | Bus 001 Device 002: ID 0658:0200 Sigma Designs, Inc. 29 | Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 30 | ``` 31 | 32 | Example device location would be `/dev/bus/usb/001/004` 33 | 34 | #### To blacklist the kernel modules on your host, run the following: 35 | ``` 36 | wget -O rtl.blacklist.conf https://raw.githubusercontent.com/jochocki/rtl2hass/master/rtl.blacklist.conf 37 | sudo cp rtl.blacklist.conf /etc/modprobe.d/rtl.blacklist.conf 38 | ``` 39 | ### Home Assistant configuration 40 | 41 | See https://www.home-assistant.io/docs/mqtt/discovery/ 42 | 43 | ## Environment variables: 44 | ``` 45 | MQTT_HOST 46 | MQTT_PORT (default value: 1883) 47 | MQTT_USERNAME (if required) 48 | MQTT_PASSWORD (if required) 49 | MQTT_TOPIC (default value: rtl_433/+/events) 50 | DISCOVERY_PREFIX (default value: homeassistant) 51 | DISCOVERY_INTERVAL (default value: 600) 52 | ``` 53 | 54 | * MQTT_HOST has no default value - supply the hostname or IP of your MQTT broker 55 | * DISCOVERY_PREFIX should match the `discovery_prefix:` setting in your Home Assistant MQTT config 56 | * DISCOVERY_INTERVAL is how often (in seconds) events are sent to Home Assistant 57 | 58 | ## Sample docker run command: 59 | ``` 60 | docker run -d --name=rtl2hass --device=/dev/bus/usb/001/004 --env MQTT_HOST=mqtt.example.com jochocki/rtl2hass 61 | ``` 62 | 63 | ## Sample docker compose file: 64 | ``` 65 | version: '2' 66 | 67 | services: 68 | rtl2hass: 69 | container_name: rtl2hass 70 | image: jochocki/rtl2hass 71 | devices: 72 | - "/dev/bus/usb/001/004" 73 | environment: 74 | - MQTT_HOST=mqtt.example.com 75 | ``` 76 | -------------------------------------------------------------------------------- /entry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rtl_433 -F mqtt://$MQTT_HOST:$MQTT_PORT,user=$MQTT_USERNAME,pass=$MQTT_PASSWORD,events=$MQTT_TOPIC/events,states=$MQTT_TOPIC/states,devices=$MQTT_TOPIC[/model][/id][/channel:0] -M time -M protocol -M level | python3 /scripts/rtl_433_mqtt_hass.py -------------------------------------------------------------------------------- /rtl.blacklist.conf: -------------------------------------------------------------------------------- 1 | blacklist rtl2832 2 | blacklist r820t 3 | blacklist rtl12830 4 | blacklist dvb_usb_rtl128xxu -------------------------------------------------------------------------------- /rtl_433_mqtt_hass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | """MQTT Home Assistant auto discovery for rtl_433 events.""" 5 | 6 | # It is strongly recommended to run rtl_433 with "-C si" and "-M newmodel". 7 | 8 | # Needs Paho-MQTT https://pypi.python.org/pypi/paho-mqtt 9 | 10 | # Option: PEP 3143 - Standard daemon process library 11 | # (use Python 3.x or pip install python-daemon) 12 | # import daemon 13 | 14 | from __future__ import print_function 15 | from __future__ import with_statement 16 | 17 | import os 18 | import socket 19 | import time 20 | import json 21 | import paho.mqtt.client as mqtt 22 | 23 | MQTT_HOST = os.environ['MQTT_HOST'] 24 | MQTT_PORT = os.environ['MQTT_PORT'] 25 | MQTT_USERNAME = os.environ['MQTT_USERNAME'] 26 | MQTT_PASSWORD = os.environ['MQTT_PASSWORD'] 27 | MQTT_TOPIC = os.environ['MQTT_TOPIC'] 28 | DISCOVERY_PREFIX = os.environ['DISCOVERY_PREFIX'] 29 | DISCOVERY_INTERVAL = os.environ['DISCOVERY_INTERVAL'] 30 | 31 | # Convert number environment variables to int 32 | MQTT_PORT = int(MQTT_PORT) 33 | DISCOVERY_INTERVAL = int(DISCOVERY_INTERVAL) 34 | 35 | discovery_timeouts = {} 36 | 37 | mappings = { 38 | "protocol": { 39 | "device_type": "sensor", 40 | "object_suffix": "Protocol", 41 | "config": { 42 | "name": "Protocol", 43 | # "value_template": "{{ value_json.protocol }}" 44 | } 45 | }, 46 | "rssi": { 47 | "device_type": "sensor", 48 | "object_suffix": "RSSI", 49 | "config": { 50 | "name": "RSSI", 51 | "unit_of_measurement": "dB", 52 | # "value_template": "{{ value_json.rssi }}" 53 | } 54 | }, 55 | "temperature_C": { 56 | "device_type": "sensor", 57 | "object_suffix": "Temperature", 58 | "config": { 59 | "device_class": "temperature", 60 | "name": "Temperature", 61 | "unit_of_measurement": "°C", 62 | # "value_template": "{{ value_json.temperature_C }}" 63 | } 64 | }, 65 | "temperature_1_C": { 66 | "device_type": "sensor", 67 | "object_suffix": "Temperature 1", 68 | "config": { 69 | "device_class": "temperature", 70 | "name": "Temperature 1", 71 | "unit_of_measurement": "°C", 72 | "value_template": "{{ value_json.temperature_1_C }}" 73 | } 74 | }, 75 | "temperature_2_C": { 76 | "device_type": "sensor", 77 | "object_suffix": "Temperature 2", 78 | "config": { 79 | "device_class": "temperature", 80 | "name": "Temperature 2", 81 | "unit_of_measurement": "°C", 82 | "value_template": "{{ value_json.temperature_2_C }}" 83 | } 84 | }, 85 | "temperature_F": { 86 | "device_type": "sensor", 87 | "object_suffix": "Temperature", 88 | "config": { 89 | "device_class": "temperature", 90 | "name": "Temperature", 91 | "unit_of_measurement": "°F", 92 | "value_template": "{{ value_json.temperature_F }}" 93 | } 94 | }, 95 | 96 | "battery_ok": { 97 | "device_type": "sensor", 98 | "object_suffix": "Battery", 99 | "config": { 100 | "device_class": "battery", 101 | "name": "Battery", 102 | "unit_of_measurement": "%", 103 | "value_template": "{{ float(value_json.battery_ok) * 99 + 1 }}" 104 | } 105 | }, 106 | 107 | "humidity": { 108 | "device_type": "sensor", 109 | "object_suffix": "Humidity", 110 | "config": { 111 | "device_class": "humidity", 112 | "name": "Humidity", 113 | "unit_of_measurement": "%", 114 | # "value_template": "{{ value_json.humidity }}" 115 | } 116 | }, 117 | 118 | "moisture": { 119 | "device_type": "sensor", 120 | "object_suffix": "Moisture", 121 | "config": { 122 | "device_class": "moisture", 123 | "name": "Moisture", 124 | "unit_of_measurement": "%", 125 | "value_template": "{{ value_json.moisture }}" 126 | } 127 | }, 128 | 129 | "pressure_hPa": { 130 | "device_type": "sensor", 131 | "object_suffix": "Pressure", 132 | "config": { 133 | "device_class": "pressure", 134 | "name": "Pressure", 135 | "unit_of_measurement": "hPa", 136 | "value_template": "{{ value_json.pressure_hPa }}" 137 | } 138 | }, 139 | 140 | "wind_speed_km_h": { 141 | "device_type": "sensor", 142 | "object_suffix": "WS", 143 | "config": { 144 | "device_class": "weather", 145 | "name": "Wind Speed", 146 | "unit_of_measurement": "km/h", 147 | "value_template": "{{ value_json.wind_speed_km_h }}" 148 | } 149 | }, 150 | 151 | "wind_speed_m_s": { 152 | "device_type": "sensor", 153 | "object_suffix": "WS", 154 | "config": { 155 | "device_class": "weather", 156 | "name": "Wind Speed", 157 | "unit_of_measurement": "km/h", 158 | "value_template": "{{ float(value_json.wind_speed_m_s) * 3.6 }}" 159 | } 160 | }, 161 | 162 | "gust_speed_km_h": { 163 | "device_type": "sensor", 164 | "object_suffix": "GS", 165 | "config": { 166 | "device_class": "weather", 167 | "name": "Gust Speed", 168 | "unit_of_measurement": "km/h", 169 | "value_template": "{{ value_json.gust_speed_km_h }}" 170 | } 171 | }, 172 | 173 | "gust_speed_m_s": { 174 | "device_type": "sensor", 175 | "object_suffix": "GS", 176 | "config": { 177 | "device_class": "weather", 178 | "name": "Gust Speed", 179 | "unit_of_measurement": "km/h", 180 | "value_template": "{{ float(value_json.gust_speed_m_s) * 3.6 }}" 181 | } 182 | }, 183 | 184 | "wind_dir_deg": { 185 | "device_type": "sensor", 186 | "object_suffix": "WD", 187 | "config": { 188 | "device_class": "weather", 189 | "name": "Wind Direction", 190 | "unit_of_measurement": "°", 191 | "value_template": "{{ value_json.wind_dir_deg }}" 192 | } 193 | }, 194 | 195 | "rain_mm": { 196 | "device_type": "sensor", 197 | "object_suffix": "RT", 198 | "config": { 199 | "device_class": "weather", 200 | "name": "Rain Total", 201 | "unit_of_measurement": "mm", 202 | "value_template": "{{ value_json.rain_mm }}" 203 | } 204 | }, 205 | 206 | "rain_mm_h": { 207 | "device_type": "sensor", 208 | "object_suffix": "RR", 209 | "config": { 210 | "device_class": "weather", 211 | "name": "Rain Rate", 212 | "unit_of_measurement": "mm/h", 213 | "value_template": "{{ value_json.rain_mm_h }}" 214 | } 215 | }, 216 | 217 | # motion... 218 | 219 | # switches... 220 | 221 | "depth_cm": { 222 | "device_type": "sensor", 223 | "object_suffix": "D", 224 | "config": { 225 | "device_class": "depth", 226 | "name": "Depth", 227 | "unit_of_measurement": "cm", 228 | "value_template": "{{ value_json.depth_cm }}" 229 | } 230 | }, 231 | } 232 | 233 | 234 | def mqtt_connect(client, userdata, flags, rc): 235 | """Callback for MQTT connects.""" 236 | print("MQTT connected: " + mqtt.connack_string(rc)) 237 | client.publish("/".join([MQTT_TOPIC, "status"]), payload="online", qos=0, retain=True) 238 | if rc != 0: 239 | print("Could not connect. Error: " + str(rc)) 240 | else: 241 | client.subscribe("/".join([MQTT_TOPIC, "events"])) 242 | 243 | 244 | def mqtt_disconnect(client, userdata, rc): 245 | """Callback for MQTT disconnects.""" 246 | print("MQTT disconnected: " + mqtt.connack_string(rc)) 247 | 248 | 249 | def mqtt_message(client, userdata, msg): 250 | """Callback for MQTT message PUBLISH.""" 251 | try: 252 | # Decode JSON payload 253 | data = json.loads(msg.payload.decode()) 254 | bridge_event_to_hass(client, msg.topic, data) 255 | 256 | except json.decoder.JSONDecodeError: 257 | print("JSON decode error: " + msg.payload.decode()) 258 | return 259 | 260 | 261 | def sanitize(text): 262 | """Sanitize a name for Graphite/MQTT use.""" 263 | return (text 264 | .replace(" ", "_") 265 | .replace("/", "_") 266 | .replace(".", "_") 267 | .replace("&", "")) 268 | 269 | 270 | def publish_config(mqttc, topic, manmodel, instance, channel, mapping): 271 | """Publish Home Assistant auto discovery data.""" 272 | global discovery_timeouts 273 | 274 | device_type = mapping["device_type"] 275 | object_id = "_".join([manmodel.replace("-", "_"), instance]) 276 | object_suffix = mapping["object_suffix"] 277 | 278 | path = "/".join([DISCOVERY_PREFIX, device_type, object_id, object_suffix, "config"]) 279 | 280 | # check timeout 281 | now = time.time() 282 | if path in discovery_timeouts: 283 | if discovery_timeouts[path] > now: 284 | return 285 | 286 | discovery_timeouts[path] = now + DISCOVERY_INTERVAL 287 | 288 | config = mapping["config"].copy() 289 | config["state_topic"] = "/".join([MQTT_TOPIC, manmodel, instance, channel, topic]) 290 | config["name"] = " ".join([manmodel.replace("-", " "), instance, object_suffix]) 291 | config["unique_id"] = "".join(["rtl433", device_type, instance, object_suffix]) 292 | config["availability_topic"] = "/".join([MQTT_TOPIC, "status"]) 293 | 294 | # add Home Assistant device info 295 | 296 | manufacturer,model = manmodel.split("-", 1) 297 | 298 | device = {} 299 | device["identifiers"] = instance 300 | device["name"] = instance 301 | device["model"] = model 302 | device["manufacturer"] = manufacturer 303 | config["device"] = device 304 | 305 | mqttc.publish(path, json.dumps(config)) 306 | print(path, " : ", json.dumps(config)) 307 | 308 | 309 | def bridge_event_to_hass(mqttc, topic, data): 310 | """Translate some rtl_433 sensor data to Home Assistant auto discovery.""" 311 | 312 | if "model" not in data: 313 | # not a device event 314 | return 315 | manmodel = sanitize(data["model"]) 316 | 317 | if "id" in data: 318 | instance = str(data["id"]) 319 | if not instance: 320 | # no unique device identifier 321 | return 322 | 323 | if "channel" in data: 324 | channel = str(data["channel"]) 325 | 326 | # detect known attributes 327 | for key in data.keys(): 328 | if key in mappings: 329 | publish_config(mqttc, key, manmodel, instance, channel, mappings[key]) 330 | 331 | 332 | def rtl_433_bridge(): 333 | """Run a MQTT Home Assistant auto discovery bridge for rtl_433.""" 334 | mqttc = mqtt.Client() 335 | mqttc.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) 336 | mqttc.on_connect = mqtt_connect 337 | mqttc.on_disconnect = mqtt_disconnect 338 | mqttc.on_message = mqtt_message 339 | mqttc.will_set("/".join([MQTT_TOPIC, "status"]), payload="offline", qos=0, retain=True) 340 | mqttc.connect_async(MQTT_HOST, MQTT_PORT, 60) 341 | mqttc.loop_start() 342 | 343 | while True: 344 | time.sleep(1) 345 | 346 | 347 | def run(): 348 | """Run main or daemon.""" 349 | # with daemon.DaemonContext(files_preserve=[sock]): 350 | # detach_process=True 351 | # uid 352 | # gid 353 | # working_directory 354 | rtl_433_bridge() 355 | 356 | 357 | if __name__ == "__main__": 358 | run() --------------------------------------------------------------------------------