mirror of https://github.com/merbanan/rtl_433.git
1128 lines
34 KiB
Python
Executable File
1128 lines
34 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# coding=utf-8
|
|
|
|
from __future__ import print_function
|
|
from __future__ import with_statement
|
|
|
|
AP_DESCRIPTION="""
|
|
Publish Home Assistant MQTT auto discovery topics for rtl_433 devices.
|
|
|
|
rtl_433_mqtt_hass.py connects to MQTT and subscribes to the rtl_433
|
|
event stream that is published to MQTT by rtl_433. The script publishes
|
|
additional MQTT topics that can be used by Home Assistant to automatically
|
|
discover and minimally configure new devices.
|
|
|
|
The configuration topics published by this script tell Home Assistant
|
|
what MQTT topics to subscribe to in order to receive the data published
|
|
as device topics by MQTT.
|
|
"""
|
|
|
|
AP_EPILOG="""
|
|
It is strongly recommended to run rtl_433 with "-C si".
|
|
This script requires rtl_433 to publish both event messages and device
|
|
messages. If you've changed the device topic in rtl_433, use the same device
|
|
topic with the "-T" parameter.
|
|
|
|
MQTT Username and Password can be set via the cmdline or passed in the
|
|
environment: MQTT_USERNAME and MQTT_PASSWORD.
|
|
|
|
Prerequisites:
|
|
|
|
1. rtl_433 running separately publishing events and devices messages to MQTT.
|
|
|
|
2. Python installation
|
|
* Python 3.x preferred.
|
|
* Needs Paho-MQTT https://pypi.python.org/pypi/paho-mqtt
|
|
|
|
Debian/raspbian: apt install python3-paho-mqtt
|
|
Or
|
|
pip install paho-mqtt
|
|
* Optional for running as a daemon see PEP 3143 - Standard daemon process library
|
|
(use Python 3.x or pip install python-daemon)
|
|
|
|
|
|
Running:
|
|
|
|
This script can run continually as a daemon, where it will publish
|
|
a configuration topic for the device events sent to MQTT by rtl_433
|
|
every 10 minutes.
|
|
|
|
Alternatively if the rtl_433 devices in your environment change infrequently
|
|
this script can use the MQTT retain flag to make the configuration topics
|
|
persistent. The script will only need to be run when things change or if
|
|
the MQTT server loses its retained messages.
|
|
|
|
Getting rtl_433 devices back after Home Assistant restarts will happen
|
|
more quickly if MQTT retain is enabled. Note however that definitions
|
|
for any transitient devices/false positives will retained indefinitely.
|
|
|
|
If your sensor values change infrequently and you prefer to write the most
|
|
recent value even if not changed set -f to append "force_update = true" to
|
|
all configs. This is useful if you're graphing the sensor data or want to
|
|
alert on missing data.
|
|
|
|
If you have changed the topic structure from the default topics in the rtl433
|
|
configuration use the -T parameter to set the same topic structure here.
|
|
|
|
Suggestions:
|
|
|
|
Running this script will cause a number of Home Assistant entities (sensors
|
|
and binary sensors) to be created. These entities can linger for a while unless
|
|
the topic is republished with an empty config string. To avoid having to
|
|
do a lot of clean up When running this initially or debugging, set this
|
|
script to publish to a topic other than the one Home Assistant users (homeassistant).
|
|
|
|
MQTT Explorer (http://mqtt-explorer.com/) is a very nice GUI for
|
|
working with MQTT. It is free, cross platform, and OSS. The structured
|
|
hierarchical view makes it easier to understand what rtl_433 is publishing
|
|
and how this script works with Home Assistant.
|
|
|
|
MQTT Explorer also makes it easy to publish an empty config topic to delete an
|
|
entity from Home Assistant.
|
|
|
|
|
|
As of 2020-10, Home Assistant MQTT auto discovery doesn't currently support
|
|
supplying "friendly name", and "area" key, so some configuration must be
|
|
done in Home Assistant.
|
|
|
|
There is a single global set of field mappings to Home Assistant meta data.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# import daemon
|
|
|
|
|
|
import os
|
|
import argparse
|
|
import logging
|
|
import time
|
|
import json
|
|
import paho.mqtt.client as mqtt
|
|
import re
|
|
|
|
|
|
discovery_timeouts = {}
|
|
|
|
# Fields that get ignored when publishing to Home Assistant
|
|
# (reduces noise to help spot missing field mappings)
|
|
SKIP_KEYS = [ "type", "model", "subtype", "channel", "id", "mic", "mod",
|
|
"freq", "sequence_num", "message_type", "exception", "raw_msg" ]
|
|
|
|
|
|
# Global mapping of rtl_433 field names to Home Assistant metadata.
|
|
# @todo - should probably externalize to a config file
|
|
# @todo - Model specific definitions might be needed
|
|
|
|
mappings = {
|
|
"temperature_C": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "T",
|
|
"config": {
|
|
"device_class": "temperature",
|
|
"name": "Temperature",
|
|
"unit_of_measurement": "°C",
|
|
"value_template": "{{ value|float|round(1) }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
"temperature_1_C": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "T1",
|
|
"config": {
|
|
"device_class": "temperature",
|
|
"name": "Temperature 1",
|
|
"unit_of_measurement": "°C",
|
|
"value_template": "{{ value|float|round(1) }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
"temperature_2_C": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "T2",
|
|
"config": {
|
|
"device_class": "temperature",
|
|
"name": "Temperature 2",
|
|
"unit_of_measurement": "°C",
|
|
"value_template": "{{ value|float|round(1) }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
"temperature_3_C": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "T3",
|
|
"config": {
|
|
"device_class": "temperature",
|
|
"name": "Temperature 3",
|
|
"unit_of_measurement": "°C",
|
|
"value_template": "{{ value|float|round(1) }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
"temperature_4_C": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "T4",
|
|
"config": {
|
|
"device_class": "temperature",
|
|
"name": "Temperature 4",
|
|
"unit_of_measurement": "°C",
|
|
"value_template": "{{ value|float|round(1) }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
"temperature_F": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "F",
|
|
"config": {
|
|
"device_class": "temperature",
|
|
"name": "Temperature",
|
|
"unit_of_measurement": "°F",
|
|
"value_template": "{{ value|float|round(1) }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
# This diagnostic sensor is useful to see when a device last sent a value,
|
|
# even if the value didn't change.
|
|
# https://community.home-assistant.io/t/send-metrics-to-influxdb-at-regular-intervals/9096
|
|
# https://github.com/home-assistant/frontend/discussions/13687
|
|
"time": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "UTC",
|
|
"config": {
|
|
"device_class": "timestamp",
|
|
"name": "Timestamp",
|
|
"entity_category": "diagnostic",
|
|
"enabled_by_default": False,
|
|
"icon": "mdi:clock-in"
|
|
}
|
|
},
|
|
|
|
"battery_ok": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "B",
|
|
"config": {
|
|
"device_class": "battery",
|
|
"name": "Battery",
|
|
"unit_of_measurement": "%",
|
|
"value_template": "{{ ((float(value) * 99)|round(0)) + 1 }}",
|
|
"state_class": "measurement",
|
|
"entity_category": "diagnostic"
|
|
}
|
|
},
|
|
|
|
"battery_mV": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "mV",
|
|
"config": {
|
|
"device_class": "voltage",
|
|
"name": "Battery mV",
|
|
"unit_of_measurement": "mV",
|
|
"value_template": "{{ float(value) }}",
|
|
"state_class": "measurement",
|
|
"entity_category": "diagnostic"
|
|
}
|
|
},
|
|
|
|
"supercap_V": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "V",
|
|
"config": {
|
|
"device_class": "voltage",
|
|
"name": "Supercap V",
|
|
"unit_of_measurement": "V",
|
|
"value_template": "{{ float(value) }}",
|
|
"state_class": "measurement",
|
|
"entity_category": "diagnostic"
|
|
}
|
|
},
|
|
|
|
"humidity": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "H",
|
|
"config": {
|
|
"device_class": "humidity",
|
|
"name": "Humidity",
|
|
"unit_of_measurement": "%",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
"humidity_1": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "H1",
|
|
"config": {
|
|
"device_class": "humidity",
|
|
"name": "Humidity 1",
|
|
"unit_of_measurement": "%",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
"humidity_2": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "H2",
|
|
"config": {
|
|
"device_class": "humidity",
|
|
"name": "Humidity 2",
|
|
"unit_of_measurement": "%",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"moisture": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "M",
|
|
"config": {
|
|
"device_class": "moisture",
|
|
"name": "Moisture",
|
|
"unit_of_measurement": "%",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"detect_wet": {
|
|
"device_type": "binary_sensor",
|
|
"object_suffix": "moisture",
|
|
"config": {
|
|
"name": "Water Sensor",
|
|
"device_class": "moisture",
|
|
"force_update": "true",
|
|
"payload_on": "1",
|
|
"payload_off": "0"
|
|
}
|
|
},
|
|
|
|
"pressure_hPa": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "P",
|
|
"config": {
|
|
"device_class": "pressure",
|
|
"name": "Pressure",
|
|
"unit_of_measurement": "hPa",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"pressure_kPa": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "P",
|
|
"config": {
|
|
"device_class": "pressure",
|
|
"name": "Pressure",
|
|
"unit_of_measurement": "kPa",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"wind_speed_km_h": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "WS",
|
|
"config": {
|
|
"device_class": "wind_speed",
|
|
"name": "Wind Speed",
|
|
"unit_of_measurement": "km/h",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"wind_avg_km_h": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "WS",
|
|
"config": {
|
|
"device_class": "wind_speed",
|
|
"name": "Wind Speed",
|
|
"unit_of_measurement": "km/h",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"wind_avg_mi_h": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "WS",
|
|
"config": {
|
|
"device_class": "wind_speed",
|
|
"name": "Wind Speed",
|
|
"unit_of_measurement": "mi/h",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"wind_avg_m_s": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "WS",
|
|
"config": {
|
|
"device_class": "wind_speed",
|
|
"name": "Wind Average",
|
|
"unit_of_measurement": "km/h",
|
|
"value_template": "{{ (float(value|float) * 3.6) | round(2) }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"wind_speed_m_s": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "WS",
|
|
"config": {
|
|
"device_class": "wind_speed",
|
|
"name": "Wind Speed",
|
|
"unit_of_measurement": "km/h",
|
|
"value_template": "{{ float(value|float) * 3.6 }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"gust_speed_km_h": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "GS",
|
|
"config": {
|
|
"device_class": "wind_speed",
|
|
"name": "Gust Speed",
|
|
"unit_of_measurement": "km/h",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"wind_max_km_h": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "GS",
|
|
"config": {
|
|
"device_class": "wind_speed",
|
|
"name": "Wind max speed",
|
|
"unit_of_measurement": "km/h",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"wind_max_m_s": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "GS",
|
|
"config": {
|
|
"device_class": "wind_speed",
|
|
"name": "Wind max",
|
|
"unit_of_measurement": "km/h",
|
|
"value_template": "{{ (float(value|float) * 3.6) | round(2) }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"gust_speed_m_s": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "GS",
|
|
"config": {
|
|
"device_class": "wind_speed",
|
|
"name": "Gust Speed",
|
|
"unit_of_measurement": "km/h",
|
|
"value_template": "{{ float(value|float) * 3.6 }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"wind_dir_deg": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "WD",
|
|
"config": {
|
|
"name": "Wind Direction",
|
|
"unit_of_measurement": "°",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"rain_mm": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "RT",
|
|
"config": {
|
|
"device_class": "precipitation",
|
|
"name": "Rain Total",
|
|
"unit_of_measurement": "mm",
|
|
"value_template": "{{ value|float|round(2) }}",
|
|
"state_class": "total_increasing"
|
|
}
|
|
},
|
|
|
|
"rain_rate_mm_h": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "RR",
|
|
"config": {
|
|
"device_class": "precipitation_intensity",
|
|
"name": "Rain Rate",
|
|
"unit_of_measurement": "mm/h",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"rain_in": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "RT",
|
|
"config": {
|
|
"device_class": "precipitation",
|
|
"name": "Rain Total",
|
|
"unit_of_measurement": "in",
|
|
"value_template": "{{ value|float|round(2) }}",
|
|
"state_class": "total_increasing"
|
|
}
|
|
},
|
|
|
|
"rain_rate_in_h": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "RR",
|
|
"config": {
|
|
"device_class": "precipitation_intensity",
|
|
"name": "Rain Rate",
|
|
"unit_of_measurement": "in/h",
|
|
"value_template": "{{ value|float|round(2) }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"reed_open": {
|
|
"device_type": "binary_sensor",
|
|
"object_suffix": "reed_open",
|
|
"config": {
|
|
"device_class": "safety",
|
|
"force_update": "true",
|
|
"payload_on": "1",
|
|
"payload_off": "0",
|
|
"entity_category": "diagnostic"
|
|
}
|
|
},
|
|
|
|
"contact_open": {
|
|
"device_type": "binary_sensor",
|
|
"object_suffix": "contact_open",
|
|
"config": {
|
|
"device_class": "safety",
|
|
"force_update": "true",
|
|
"payload_on": "1",
|
|
"payload_off": "0",
|
|
"entity_category": "diagnostic"
|
|
}
|
|
},
|
|
|
|
"tamper": {
|
|
"device_type": "binary_sensor",
|
|
"object_suffix": "tamper",
|
|
"config": {
|
|
"device_class": "safety",
|
|
"force_update": "true",
|
|
"payload_on": "1",
|
|
"payload_off": "0",
|
|
"entity_category": "diagnostic"
|
|
}
|
|
},
|
|
|
|
"alarm": {
|
|
"device_type": "binary_sensor",
|
|
"object_suffix": "alarm",
|
|
"config": {
|
|
"device_class": "safety",
|
|
"force_update": "true",
|
|
"payload_on": "1",
|
|
"payload_off": "0",
|
|
"entity_category": "diagnostic"
|
|
}
|
|
},
|
|
|
|
"rssi": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "rssi",
|
|
"config": {
|
|
"device_class": "signal_strength",
|
|
"unit_of_measurement": "dB",
|
|
"value_template": "{{ value|float|round(2) }}",
|
|
"state_class": "measurement",
|
|
"entity_category": "diagnostic"
|
|
}
|
|
},
|
|
|
|
"snr": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "snr",
|
|
"config": {
|
|
"device_class": "signal_strength",
|
|
"unit_of_measurement": "dB",
|
|
"value_template": "{{ value|float|round(2) }}",
|
|
"state_class": "measurement",
|
|
"entity_category": "diagnostic"
|
|
}
|
|
},
|
|
|
|
"noise": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "noise",
|
|
"config": {
|
|
"device_class": "signal_strength",
|
|
"unit_of_measurement": "dB",
|
|
"value_template": "{{ value|float|round(2) }}",
|
|
"state_class": "measurement",
|
|
"entity_category": "diagnostic"
|
|
}
|
|
},
|
|
|
|
"depth_cm": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "D",
|
|
"config": {
|
|
"name": "Depth",
|
|
"unit_of_measurement": "cm",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"power_W": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "watts",
|
|
"config": {
|
|
"device_class": "power",
|
|
"name": "Power",
|
|
"unit_of_measurement": "W",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"energy_kWh": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "kwh",
|
|
"config": {
|
|
"device_class": "energy",
|
|
"name": "Energy",
|
|
"unit_of_measurement": "kWh",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"current_A": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "A",
|
|
"config": {
|
|
"device_class": "current",
|
|
"name": "Current",
|
|
"unit_of_measurement": "A",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"voltage_V": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "V",
|
|
"config": {
|
|
"device_class": "voltage",
|
|
"name": "Voltage",
|
|
"unit_of_measurement": "V",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"light_lux": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "lux",
|
|
"config": {
|
|
"device_class": "illuminance",
|
|
"name": "Outside Luminance",
|
|
"unit_of_measurement": "lx",
|
|
"value_template": "{{ value|int }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
"lux": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "lux",
|
|
"config": {
|
|
"device_class": "illuminance",
|
|
"name": "Outside Luminance",
|
|
"unit_of_measurement": "lx",
|
|
"value_template": "{{ value|int }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"uv": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "uv",
|
|
"config": {
|
|
"name": "UV Index",
|
|
"unit_of_measurement": "UV Index",
|
|
"value_template": "{{ value|float|round(1) }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
"uvi": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "uvi",
|
|
"config": {
|
|
"name": "UV Index",
|
|
"unit_of_measurement": "UV Index",
|
|
"value_template": "{{ value|float|round(1) }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"storm_dist_km": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "stdist",
|
|
"config": {
|
|
"name": "Lightning Distance",
|
|
"unit_of_measurement": "km",
|
|
"value_template": "{{ value|int }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"storm_dist": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "stdist",
|
|
"config": {
|
|
"name": "Lightning Distance",
|
|
"unit_of_measurement": "mi",
|
|
"value_template": "{{ value|int }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"strike_distance": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "stdist",
|
|
"config": {
|
|
"name": "Lightning Distance",
|
|
"unit_of_measurement": "mi",
|
|
"value_template": "{{ value|int }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"strike_count": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "strcnt",
|
|
"config": {
|
|
"name": "Lightning Strike Count",
|
|
"value_template": "{{ value|int }}",
|
|
"state_class": "total_increasing"
|
|
}
|
|
},
|
|
|
|
"consumption_data": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "consumption",
|
|
"config": {
|
|
"name": "SCM Consumption Value",
|
|
"value_template": "{{ value|int }}",
|
|
"state_class": "total_increasing",
|
|
}
|
|
},
|
|
|
|
"consumption": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "consumption",
|
|
"config": {
|
|
"name": "SCMplus Consumption Value",
|
|
"value_template": "{{ value|int }}",
|
|
"state_class": "total_increasing",
|
|
}
|
|
},
|
|
|
|
"channel": {
|
|
"device_type": "device_automation",
|
|
"object_suffix": "CH",
|
|
"config": {
|
|
"automation_type": "trigger",
|
|
"type": "button_short_release",
|
|
"subtype": "button_1",
|
|
}
|
|
},
|
|
|
|
"button": {
|
|
"device_type": "device_automation",
|
|
"object_suffix": "BTN",
|
|
"config": {
|
|
"automation_type": "trigger",
|
|
"type": "button_short_release",
|
|
"subtype": "button_2",
|
|
}
|
|
},
|
|
|
|
# WH45, WH290
|
|
"pm2_5_ug_m3": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "PM25",
|
|
"config": {
|
|
"device_class": "pm25",
|
|
"name": "PM 2.5 Concentration",
|
|
"unit_of_measurement": "µg/m³",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
# WH45
|
|
"pm10_ug_m3": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "PM10",
|
|
"config": {
|
|
"device_class": "pm10",
|
|
"name": "PM 10 Concentration",
|
|
"unit_of_measurement": "µg/m³",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
# WH290
|
|
"estimated_pm10_0_ug_m3": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "PM10",
|
|
"config": {
|
|
"device_class": "pm10",
|
|
"name": "Estimated PM 10 Concentration",
|
|
"unit_of_measurement": "µg/m³",
|
|
"value_template": "{{ value|float }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
# WH45
|
|
"co2_ppm": {
|
|
"device_type": "sensor",
|
|
"object_suffix": "CO2",
|
|
"config": {
|
|
"device_class": "carbon_dioxide",
|
|
"name": "CO2 Concentration",
|
|
"unit_of_measurement": "ppm",
|
|
"value_template": "{{ value|int }}",
|
|
"state_class": "measurement"
|
|
}
|
|
},
|
|
|
|
"ext_power": {
|
|
"device_type": "binary_sensor",
|
|
"object_suffix": "extpwr",
|
|
"config": {
|
|
"device_class": "power",
|
|
"name": "External Power",
|
|
"payload_on": "1",
|
|
"payload_off": "0",
|
|
"entity_category": "diagnostic"
|
|
}
|
|
},
|
|
|
|
}
|
|
|
|
# Use secret_knock to trigger device automations for Honeywell ActivLink
|
|
# doorbells. We have this outside of mappings as we need to configure two
|
|
# different configuration topics.
|
|
secret_knock_mappings = [
|
|
|
|
{
|
|
"device_type": "device_automation",
|
|
"object_suffix": "Knock",
|
|
"config": {
|
|
"automation_type": "trigger",
|
|
"type": "button_short_release",
|
|
"subtype": "button_1",
|
|
"payload": 0,
|
|
}
|
|
},
|
|
|
|
{
|
|
"device_type": "device_automation",
|
|
"object_suffix": "Secret-Knock",
|
|
"config": {
|
|
"automation_type": "trigger",
|
|
"type": "button_triple_press",
|
|
"subtype": "button_1",
|
|
"payload": 1,
|
|
}
|
|
},
|
|
|
|
]
|
|
|
|
TOPIC_PARSE_RE = re.compile(r'\[(?P<slash>/?)(?P<token>[^\]:]+):?(?P<default>[^\]:]*)\]')
|
|
|
|
def mqtt_connect(client, userdata, flags, rc):
|
|
"""Callback for MQTT connects."""
|
|
|
|
logging.info("MQTT connected: " + mqtt.connack_string(rc))
|
|
if rc != 0:
|
|
logging.error("Could not connect. Error: " + str(rc))
|
|
else:
|
|
logging.info("Subscribing to: " + args.rtl_topic)
|
|
client.subscribe(args.rtl_topic)
|
|
|
|
|
|
def mqtt_disconnect(client, userdata, rc):
|
|
"""Callback for MQTT disconnects."""
|
|
logging.info("MQTT disconnected: " + mqtt.connack_string(rc))
|
|
|
|
|
|
def mqtt_message(client, userdata, msg):
|
|
"""Callback for MQTT message PUBLISH."""
|
|
logging.debug("MQTT message: " + json.dumps(msg.payload.decode()))
|
|
|
|
try:
|
|
# Decode JSON payload
|
|
data = json.loads(msg.payload.decode())
|
|
|
|
except json.decoder.JSONDecodeError:
|
|
logging.error("JSON decode error: " + msg.payload.decode())
|
|
return
|
|
|
|
topicprefix = "/".join(msg.topic.split("/", 2)[:2])
|
|
bridge_event_to_hass(client, topicprefix, data)
|
|
|
|
|
|
def sanitize(text):
|
|
"""Sanitize a name for Graphite/MQTT use."""
|
|
return (text
|
|
.replace(" ", "_")
|
|
.replace("/", "_")
|
|
.replace(".", "_")
|
|
.replace("&", ""))
|
|
|
|
def rtl_433_device_info(data, topic_prefix):
|
|
"""Return rtl_433 device topic to subscribe to for a data element, based on the
|
|
rtl_433 device topic argument, as well as the device identifier"""
|
|
|
|
path_elements = []
|
|
id_elements = []
|
|
last_match_end = 0
|
|
# The default for args.device_topic_suffix is the same topic structure
|
|
# as set by default in rtl433 config
|
|
for match in re.finditer(TOPIC_PARSE_RE, args.device_topic_suffix):
|
|
path_elements.append(args.device_topic_suffix[last_match_end:match.start()])
|
|
key = match.group(2)
|
|
if key in data:
|
|
# If we have this key, prepend a slash if needed
|
|
if match.group(1):
|
|
path_elements.append('/')
|
|
element = sanitize(str(data[key]))
|
|
path_elements.append(element)
|
|
id_elements.append(element)
|
|
elif match.group(3):
|
|
path_elements.append(match.group(3))
|
|
last_match_end = match.end()
|
|
|
|
path = ''.join(list(filter(lambda item: item, path_elements)))
|
|
id = '-'.join(id_elements)
|
|
return (f"{topic_prefix}/{path}", id)
|
|
|
|
|
|
def publish_config(mqttc, topic, model, object_id, mapping, key=None):
|
|
"""Publish Home Assistant auto discovery data."""
|
|
global discovery_timeouts
|
|
|
|
device_type = mapping["device_type"]
|
|
object_suffix = mapping["object_suffix"]
|
|
object_name = "-".join([object_id, object_suffix])
|
|
|
|
path = "/".join([args.discovery_prefix, device_type, object_id, object_name, "config"])
|
|
|
|
# check timeout
|
|
now = time.time()
|
|
if path in discovery_timeouts:
|
|
if discovery_timeouts[path] > now:
|
|
logging.debug("Discovery timeout in the future for: " + path)
|
|
return False
|
|
|
|
discovery_timeouts[path] = now + args.discovery_interval
|
|
|
|
config = mapping["config"].copy()
|
|
|
|
# Device Automation configuration is in a different structure compared to
|
|
# all other mqtt discovery types.
|
|
# https://www.home-assistant.io/integrations/device_trigger.mqtt/
|
|
if device_type == 'device_automation':
|
|
config["topic"] = topic
|
|
config["platform"] = 'mqtt'
|
|
else:
|
|
readable_name = mapping["config"]["name"] if "name" in mapping["config"] else key
|
|
config["state_topic"] = topic
|
|
config["unique_id"] = object_name
|
|
config["name"] = readable_name
|
|
config["device"] = { "identifiers": [object_id], "name": object_id, "model": model, "manufacturer": "rtl_433" }
|
|
|
|
if args.force_update:
|
|
config["force_update"] = "true"
|
|
|
|
if args.expire_after:
|
|
config["expire_after"] = args.expire_after
|
|
|
|
logging.debug(path + ":" + json.dumps(config))
|
|
|
|
mqttc.publish(path, json.dumps(config), retain=args.retain)
|
|
|
|
return True
|
|
|
|
def bridge_event_to_hass(mqttc, topic_prefix, data):
|
|
"""Translate some rtl_433 sensor data to Home Assistant auto discovery."""
|
|
|
|
if "model" not in data:
|
|
# not a device event
|
|
logging.debug("Model is not defined. Not sending event to Home Assistant.")
|
|
return
|
|
|
|
model = sanitize(data["model"])
|
|
|
|
skipped_keys = []
|
|
published_keys = []
|
|
|
|
base_topic, device_id = rtl_433_device_info(data, topic_prefix)
|
|
if not device_id:
|
|
# no unique device identifier
|
|
logging.warning("No suitable identifier found for model: %s", model)
|
|
return
|
|
|
|
if args.ids and "id" in data and data.get("id") not in args.ids:
|
|
# not in the safe list
|
|
logging.debug("Device (%s) is not in the desired list of device ids: [%s]" % (data["id"], ids))
|
|
return
|
|
|
|
# detect known attributes
|
|
for key in data.keys():
|
|
if key in mappings:
|
|
# topic = "/".join([topicprefix,"devices",model,instance,key])
|
|
topic = "/".join([base_topic, key])
|
|
if publish_config(mqttc, topic, model, device_id, mappings[key], key):
|
|
published_keys.append(key)
|
|
else:
|
|
if key not in SKIP_KEYS:
|
|
skipped_keys.append(key)
|
|
|
|
if "secret_knock" in data.keys():
|
|
for m in secret_knock_mappings:
|
|
topic = "/".join([base_topic, "secret_knock"])
|
|
if publish_config(mqttc, topic, model, device_id, m, "secret_knock"):
|
|
published_keys.append("secret_knock")
|
|
|
|
if published_keys:
|
|
logging.info("Published %s: %s" % (device_id, ", ".join(published_keys)))
|
|
|
|
if skipped_keys:
|
|
logging.info("Skipped %s: %s" % (device_id, ", ".join(skipped_keys)))
|
|
|
|
|
|
def rtl_433_bridge():
|
|
"""Run a MQTT Home Assistant auto discovery bridge for rtl_433."""
|
|
|
|
if hasattr(mqtt, 'CallbackAPIVersion'): # paho >= 2.0.0
|
|
mqttc = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION1)
|
|
else:
|
|
mqttc = mqtt.Client()
|
|
|
|
if args.debug:
|
|
mqttc.enable_logger()
|
|
|
|
if args.user is not None:
|
|
mqttc.username_pw_set(args.user, args.password)
|
|
|
|
if args.ca_cert is not None:
|
|
mqttc.tls_set(ca_certs=args.ca_cert)
|
|
|
|
mqttc.on_connect = mqtt_connect
|
|
mqttc.on_disconnect = mqtt_disconnect
|
|
mqttc.on_message = mqtt_message
|
|
mqttc.connect_async(args.host, args.port, 60)
|
|
logging.debug("MQTT Client: Starting Loop")
|
|
mqttc.loop_start()
|
|
|
|
while True:
|
|
time.sleep(1)
|
|
|
|
|
|
def run():
|
|
"""Run main or daemon."""
|
|
# with daemon.DaemonContext(files_preserve=[sock]):
|
|
# detach_process=True
|
|
# uid
|
|
# gid
|
|
# working_directory
|
|
rtl_433_bridge()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logging.basicConfig(format='[%(asctime)s] %(levelname)s:%(name)s:%(message)s',datefmt='%Y-%m-%dT%H:%M:%S%z')
|
|
logging.getLogger().setLevel(logging.INFO)
|
|
|
|
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
description=AP_DESCRIPTION,
|
|
epilog=AP_EPILOG)
|
|
|
|
parser.add_argument("-d", "--debug", action="store_true")
|
|
parser.add_argument("-q", "--quiet", action="store_true")
|
|
parser.add_argument("-u", "--user", type=str, help="MQTT username")
|
|
parser.add_argument("-P", "--password", type=str, help="MQTT password")
|
|
parser.add_argument("-H", "--host", type=str, default="127.0.0.1",
|
|
help="MQTT hostname to connect to (default: %(default)s)")
|
|
parser.add_argument("-p", "--port", type=int, default=1883,
|
|
help="MQTT port (default: %(default)s)")
|
|
parser.add_argument("-c", "--ca_cert", type=str, help="MQTT TLS CA certificate path")
|
|
parser.add_argument("-r", "--retain", action="store_true")
|
|
parser.add_argument("-f", "--force_update", action="store_true",
|
|
help="Append 'force_update = true' to all configs.")
|
|
parser.add_argument("-R", "--rtl-topic", type=str,
|
|
default="rtl_433/+/events",
|
|
dest="rtl_topic",
|
|
help="rtl_433 MQTT event topic to subscribe to (default: %(default)s)")
|
|
parser.add_argument("-D", "--discovery-prefix", type=str,
|
|
dest="discovery_prefix",
|
|
default="homeassistant",
|
|
help="Home Assistant MQTT topic prefix (default: %(default)s)")
|
|
# This defaults to the rtl433 config default, so we assemble the same topic structure
|
|
parser.add_argument("-T", "--device-topic_suffix", type=str,
|
|
dest="device_topic_suffix",
|
|
default="devices[/type][/model][/subtype][/channel][/id]",
|
|
help="rtl_433 device topic suffix (default: %(default)s)")
|
|
parser.add_argument("-i", "--interval", type=int,
|
|
dest="discovery_interval",
|
|
default=600,
|
|
help="Interval to republish config topics in seconds (default: %(default)d)")
|
|
parser.add_argument("-x", "--expire-after", type=int,
|
|
dest="expire_after",
|
|
help="Number of seconds with no updates after which the sensor becomes unavailable")
|
|
parser.add_argument("-I", "--ids", type=int, nargs="+",
|
|
help="ID's of devices that will be discovered (omit for all)")
|
|
args = parser.parse_args()
|
|
|
|
if args.debug and args.quiet:
|
|
logging.critical("Debug and quiet can not be specified at the same time")
|
|
exit(1)
|
|
|
|
if args.debug:
|
|
logging.info("Enabling debug logging")
|
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
if args.quiet:
|
|
logging.getLogger().setLevel(logging.ERROR)
|
|
|
|
# allow setting MQTT username and password via environment variables
|
|
if not args.user and 'MQTT_USERNAME' in os.environ:
|
|
args.user = os.environ['MQTT_USERNAME']
|
|
|
|
if not args.password and 'MQTT_PASSWORD' in os.environ:
|
|
args.password = os.environ['MQTT_PASSWORD']
|
|
|
|
if not args.user or not args.password:
|
|
logging.warning("User or password is not set. Check credentials if subscriptions do not return messages.")
|
|
|
|
if args.ids:
|
|
ids = ', '.join(str(id) for id in args.ids)
|
|
logging.info("Only discovering devices with ids: [%s]" % ids)
|
|
else:
|
|
logging.info("Discovering all devices")
|
|
|
|
run()
|