Skip to content

Commit

Permalink
Add RTL_433 discovery with Home Assistant convention inspired from rt…
Browse files Browse the repository at this point in the history
…l_433_mqtt_hass.py

This also change the topic structure when using the macro valueAsATopic=true, it adds as sub topics the type, subtype, and channel when they exist.
The id is the last subtopic to facilitate filtering as it may change when replacing the batteries.

Co-authored-by: 19808920+NorthernMan54@users.noreply.github.com
  • Loading branch information
1technophile committed Jan 8, 2023
1 parent ba70d36 commit 55bd69d
Show file tree
Hide file tree
Showing 9 changed files with 375 additions and 89 deletions.
39 changes: 36 additions & 3 deletions docs/integrate/home_assistant.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,48 @@ OMG will use the auto discovery functionality of home assistant to create gatewa

![](../img/OpenMQTTGateway_Home_Assistant_MQTT_discovery.png)

::: info
The Bluetooth and the RTL_433 gateway will create automatically devices and entities, the RF gateway will create DeviceTrigger.
The OpenMQTTGateway will also be available as a device to monitor its parameters and control it. The sensors (DHT for example) and actuators (relays) are attached to the gateway.
:::

## MQTT Device Trigger and RF
## RTL_433 auto discovery specificity

With OpenMQTTGateway [configured to receive RF signals](./setitup/rf.html) the messages are transmitted as indicated by [RCSwitch based gateway](./use/rf.html#rcswitch-based-gateway), so it is possible to receive a pulse every time the sensor discover a signal.
Even if the RTL_433 gateway will create automatically the devices and entities, you may loose the link to them when you change the batteries. This is proper to the RF devices. In this case new device and entities will be created. You may bypass this by creating entities through manual configuration that filter following the device model and other parameters and don't take into account the id.
Example:
```yaml
mqtt:
sensor:
- state_topic: "+/+/RTL_433toMQTT/WS2032/+"
```
instead of
```yaml
mqtt:
sensor:
- state_topic: "+/+/RTL_433toMQTT/WS2032/47998"
```
Note also that the sensor may leverage channels, types or subtypes, they can be used in the filtering
Example:
In the example below 9 is the `subtype` and 1 is the `channel`
```yaml
mqtt:
sensor:
- state_topic: "+/+/RTL_433toMQTT/Prologue-TH/9/1/+"
```
instead of
```yaml
mqtt:
sensor:
- state_topic: "+/+/RTL_433toMQTT/Prologue-TH/9/1/215"
```

With autodiscovery enabled, HomeAssistant will discover a [MQTT Device Trigger](https://www.home-assistant.io/integrations/device_trigger.mqtt/) identified by the value field given in the mqtt argument.
Alternatively the rssi signal could be used also.

## MQTT Device Trigger and RF

With OpenMQTTGateway [configured to receive RF signals](./setitup/rf.html) the messages are transmitted as indicated by [RCSwitch based gateway](./use/rf.html#rcswitch-based-gateway), so it is possible to receive a pulse every time the sensor discover a signal.

With autodiscovery enabled, HomeAssistant will discover a [MQTT Device Trigger](https://www.home-assistant.io/integrations/device_trigger.mqtt/) identified by the value field given in the mqtt argument.

## Manual integration examples
From @123, @finity, @denniz03, @jrockstad, @anarchking, @dkluivingh
Expand Down
3 changes: 1 addition & 2 deletions docs/use/rf.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ This feature is only available on a ESP32 based device with a supported transcei
### Supported hardware combinations

- ESP32 based device with a CC1101 transceiver
- Heltec WiFi LoRa 32 (V2.1)
- Heltec WiFi LoRa 32 (V2.1) and LilyGo Lora 32 V2.1

### Supported Decoders

Expand Down Expand Up @@ -349,7 +349,6 @@ Delta applied to RSSI floor noise level to determine start and end of signal, de

`home/OpenMQTTGateway/commands/MQTTtoRTL_433 {"rssi": 9}`


### Retrieve current status of receiver

`home/OpenMQTTGateway/commands/MQTTtoRTL_433 {"status":1}`
Expand Down
11 changes: 0 additions & 11 deletions main/ZgatewayBT.ino
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ QueueHandle_t BLEQueue;
# include <esp_wifi.h>

# include <atomic>
# include <vector>

# include "ZgatewayBLEConnect.h"
# include "soc/timer_group_reg.h"
Expand All @@ -58,12 +57,6 @@ using namespace std;
// Global struct to store live BT configuration data
BTConfig_s BTConfig;

# define device_flags_init 0 << 0
# define device_flags_isDisc 1 << 0
# define device_flags_isWhiteL 1 << 1
# define device_flags_isBlackL 1 << 2
# define device_flags_connect 1 << 3

TheengsDecoder decoder;

struct decompose {
Expand Down Expand Up @@ -412,10 +405,6 @@ void createOrUpdateDevice(const char* mac, uint8_t flags, int model, int mac_typ
xSemaphoreGive(semaphoreCreateOrUpdateDevice);
}

# define isWhite(device) device->isWhtL
# define isBlack(device) device->isBlkL
# define isDiscovered(device) device->isDisc

void dumpDevices() {
for (vector<BLEdevice*>::iterator it = devices.begin(); it != devices.end(); ++it) {
BLEdevice* p = *it;
Expand Down
203 changes: 194 additions & 9 deletions main/ZgatewayRTL_433.ino
Original file line number Diff line number Diff line change
Expand Up @@ -34,34 +34,215 @@

# include "ArduinoLog.h"
# include "User_config.h"
# ifdef ZmqttDiscovery
# include "config_mqttDiscovery.h"
# endif

char messageBuffer[JSON_MSG_BUFFER];

rtl_433_ESP rtl_433(-1);

# ifdef ZmqttDiscovery
SemaphoreHandle_t semaphorecreateOrUpdateDeviceRTL_433;
std::vector<RTL_433device*> RTL_433devices;
int newRTL_433Devices = 0;

static RTL_433device NO_DEVICE_FOUND = {{0},
0,
false};

RTL_433device* getDeviceById(const char* id); // Declared here to avoid pre-compilation issue (misplaced auto declaration by pio)
RTL_433device* getDeviceById(const char* id) {
Log.trace(F("getDeviceById %s" CR), id);

for (std::vector<RTL_433device*>::iterator it = RTL_433devices.begin(); it != RTL_433devices.end(); ++it) {
if ((strcmp((*it)->uniqueId, id) == 0)) {
return *it;
}
}
return &NO_DEVICE_FOUND;
}

void dumpRTL_433Devices() {
for (std::vector<RTL_433device*>::iterator it = RTL_433devices.begin(); it != RTL_433devices.end(); ++it) {
RTL_433device* p = *it;
Log.trace(F("uniqueId %s" CR), p->uniqueId);
Log.trace(F("modelName %s" CR), p->modelName);
Log.trace(F("isDisc %d" CR), p->isDisc);
}
}

void createOrUpdateDeviceRTL_433(const char* id, const char* model, uint8_t flags) {
if (xSemaphoreTake(semaphorecreateOrUpdateDeviceRTL_433, pdMS_TO_TICKS(30000)) == pdFALSE) {
Log.error(F("[rtl_433] semaphorecreateOrUpdateDeviceRTL_433 Semaphore NOT taken" CR));
return;
}

RTL_433device* device = getDeviceById(id);
if (device == &NO_DEVICE_FOUND) {
Log.trace(F("add %s" CR), id);
//new device
device = new RTL_433device();
if (strlcpy(device->uniqueId, id, uniqueIdSize) > uniqueIdSize) {
Log.warning(F("[rtl_433] Device id %s exceeds available space" CR), id); // Remove from production release ?
};
if (strlcpy(device->modelName, model, modelNameSize) > modelNameSize) {
Log.warning(F("[rtl_433] Device model %s exceeds available space" CR), id); // Remove from production release ?
};
device->isDisc = flags & device_flags_isDisc;
RTL_433devices.push_back(device);
newRTL_433Devices++;
} else {
Log.trace(F("update %s" CR), id);

if (flags & device_flags_isDisc) {
device->isDisc = true;
}
}

xSemaphoreGive(semaphorecreateOrUpdateDeviceRTL_433);
}

// This function always should be called from the main core as it generates direct mqtt messages
// When overrideDiscovery=true, we publish discovery messages of known RTL_433devices (even if no new)
void launchRTL_433Discovery(bool overrideDiscovery) {
if (!overrideDiscovery && newRTL_433Devices == 0)
return;
if (xSemaphoreTake(semaphorecreateOrUpdateDeviceRTL_433, pdMS_TO_TICKS(1000)) == pdFALSE) {
Log.error(F("[rtl_433] semaphorecreateOrUpdateDeviceRTL_433 Semaphore NOT taken" CR));
return;
}
newRTL_433Devices = 0;
std::vector<RTL_433device*> localDevices = RTL_433devices;
xSemaphoreGive(semaphorecreateOrUpdateDeviceRTL_433);
for (std::vector<RTL_433device*>::iterator it = localDevices.begin(); it != localDevices.end(); ++it) {
RTL_433device* pdevice = *it;
Log.trace(F("Device id %s" CR), pdevice->uniqueId);
// Do not launch discovery for the RTL_433devices already discovered (unless we have overrideDiscovery) or that are not unique by their MAC Address (Ibeacon, GAEN and Microsoft Cdp)
if (overrideDiscovery || !isDiscovered(pdevice)) {
size_t numRows = sizeof(parameters) / sizeof(parameters[0]);
for (int i = 0; i < numRows; i++) {
if (strstr(pdevice->uniqueId, parameters[i][0]) != 0) {
// Remove the key from the unique id to extract the device id
String idWoKey = pdevice->uniqueId;
idWoKey.remove(idWoKey.length() - (strlen(parameters[i][0]) + 1));
Log.trace(F("idWoKey %s" CR), idWoKey.c_str());
# if OpenHABDiscovery
String value_template = "{{ value_json." + String(parameters[i][0]) + "}}";
# else
String value_template = "{{ value_json." + String(parameters[i][0]) + " | is_defined }}";
# endif
String topic = subjectRTL_433toMQTT;
# if valueAsATopic
// Remove the key from the unique id to extract the device id
String idWoKeyAndModel = idWoKey;
idWoKeyAndModel.remove(0, strlen(pdevice->modelName));
idWoKeyAndModel.replace("-", "/");
Log.trace(F("idWoKeyAndModel %s" CR), idWoKeyAndModel.c_str());
topic = topic + "/" + String(pdevice->modelName) + idWoKeyAndModel;
# endif
if (strcmp(parameters[i][0], "tamper") == 0 || strcmp(parameters[i][0], "alarm") == 0 || strcmp(parameters[i][0], "motion") == 0) {
createDiscovery("binary_sensor", //set Type
(char*)topic.c_str(), parameters[i][1], pdevice->uniqueId, //set state_topic,name,uniqueId
"", "", (char*)value_template.c_str(), //set availability_topic,device_class,value_template,
"1", "0", parameters[i][2], //set,payload_on,payload_off,unit_of_meas,
0, //set off_delay
"", "", false, "", //set,payload_avalaible,payload_not avalaible ,is a gateway entity, command topic
(char*)idWoKey.c_str(), "", pdevice->modelName, (char*)idWoKey.c_str(), false, // device name, device manufacturer, device model, device ID, retain
"" //State Class
);
} else if (strcmp(parameters[i][0], "strike_count") == 0) {
createDiscovery("sensor", //set Type
(char*)topic.c_str(), parameters[i][1], pdevice->uniqueId, //set state_topic,name,uniqueId
"", "", (char*)value_template.c_str(), //set availability_topic,device_class,value_template,
"1", "0", parameters[i][2], //set,payload_on,payload_off,unit_of_meas,
0, //set off_delay
"", "", false, "", //set,payload_avalaible,payload_not avalaible ,is a gateway entity, command topic
(char*)idWoKey.c_str(), "", pdevice->modelName, (char*)idWoKey.c_str(), false, // device name, device manufacturer, device model, device ID, retain
stateClassTotalIncreasing //State Class
);
} else {
createDiscovery("sensor", //set Type
(char*)topic.c_str(), parameters[i][1], pdevice->uniqueId, //set state_topic,name,uniqueId
"", "", (char*)value_template.c_str(), //set availability_topic,device_class,value_template,
"", "", parameters[i][2], //set,payload_on,payload_off,unit_of_meas,
0, //set off_delay
"", "", false, "", //set,payload_avalaible,payload_not avalaible ,is a gateway entity, command topic
(char*)idWoKey.c_str(), "", pdevice->modelName, (char*)idWoKey.c_str(), false, // device name, device manufacturer, device model, device ID, retain
stateClassMeasurement //State Class
);
}
pdevice->isDisc = true; // we don't need the semaphore and all the search magic via createOrUpdateDevice
dumpRTL_433Devices();
break;
}
}
if (!pdevice->isDisc) {
Log.trace(F("Device id %s was not discovered" CR), pdevice->uniqueId); // Remove from production release ?
}
} else {
Log.trace(F("Device already discovered or that doesn't require discovery %s" CR), pdevice->uniqueId);
}
}
}

void storeRTL_433Discovery(JsonObject& RFrtl_433_ESPdata, const char* model, const char* uniqueid) {
//Sanitize model name
String modelSanitized = model;
modelSanitized.replace(" ", "_");
modelSanitized.replace("/", "_");
modelSanitized.replace(".", "_");
modelSanitized.replace("&", "");

//Sensors translation matrix for sensors that requires statistics by using stateClassMeasurement
size_t numRows = sizeof(parameters) / sizeof(parameters[0]);

for (int i = 0; i < numRows; i++) {
if (RFrtl_433_ESPdata.containsKey(parameters[i][0])) {
String key_id = String(uniqueid) + "-" + String(parameters[i][0]);
createOrUpdateDeviceRTL_433((char*)key_id.c_str(), (char*)modelSanitized.c_str(), device_flags_init);
}
}
}
# else
void storeRTL_433Discovery(JsonObject& RFrtl_433_ESPdata, const char* model, const char* uniqueid) {}
# endif

void rtl_433_Callback(char* message) {
DynamicJsonDocument jsonBuffer2(JSON_MSG_BUFFER);
JsonObject RFrtl_433_ESPdata = jsonBuffer2.to<JsonObject>();
auto error = deserializeJson(jsonBuffer2, message);
if (error) {
Log.error(F("deserializeJson() failed: %s" CR), error.c_str());
Log.error(F("[rtl_433] deserializeJson() failed: %s" CR), error.c_str());
return;
}

unsigned long MQTTvalue = (int)RFrtl_433_ESPdata["id"] + round((float)RFrtl_433_ESPdata["temperature_C"]);
String topic = String(subjectRTL_433toMQTT);
# if valueAsATopic
String topic = subjectRTL_433toMQTT;
String model = RFrtl_433_ESPdata["model"];
String id = RFrtl_433_ESPdata["id"];
if (model != 0) {
topic = topic + "/" + model;
if (id != 0) {
topic = topic + "/" + id;
String uniqueid;

const char naming_keys[5][8] = {"type", "model", "subtype", "channel", "id"}; // from rtl_433_mqtt_hass.py
size_t numRows = sizeof(naming_keys) / sizeof(naming_keys[0]);
for (int i = 0; i < numRows; i++) {
if (RFrtl_433_ESPdata.containsKey(naming_keys[i])) {
if (uniqueid == 0) {
uniqueid = RFrtl_433_ESPdata[naming_keys[i]].as<String>(); // Start of the unique id with the first key
} else {
uniqueid = uniqueid + "/" + RFrtl_433_ESPdata[naming_keys[i]].as<String>(); // Following keys
}
}
}

# if valueAsATopic
topic = topic + "/" + uniqueid;
# endif

uniqueid.replace("/", "-");

Log.notice(F("uniqueid: %s" CR), uniqueid.c_str());
if (!isAduplicateSignal(MQTTvalue)) {
storeRTL_433Discovery(RFrtl_433_ESPdata, (char*)model.c_str(), (char*)uniqueid.c_str());
pub((char*)topic.c_str(), RFrtl_433_ESPdata);
storeSignalValue(MQTTvalue);
}
Expand All @@ -72,6 +253,10 @@ void rtl_433_Callback(char* message) {

void setupRTL_433() {
rtl_433.setCallback(rtl_433_Callback, messageBuffer, JSON_MSG_BUFFER);
# ifdef ZmqttDiscovery
semaphorecreateOrUpdateDeviceRTL_433 = xSemaphoreCreateBinary();
xSemaphoreGive(semaphorecreateOrUpdateDeviceRTL_433);
# endif
Log.trace(F("ZgatewayRTL_433 command topic: %s%s%s" CR), mqtt_topic, gateway_name, subjectMQTTtoRTL_433);
Log.notice(F("ZgatewayRTL_433 setup done " CR));
}
Expand Down Expand Up @@ -124,7 +309,7 @@ extern void MQTTtoRTL_433(char* topicOri, JsonObject& RTLdata) {
pub(subjectRTL_433toMQTT, RTLdata);
} else {
pub(subjectRTL_433toMQTT, "{\"Status\": \"Error\"}"); // Fail feedback
Log.error(F("MQTTtoRTL_433 Fail json" CR));
Log.error(F("[rtl_433] MQTTtoRTL_433 Fail json" CR));
}
enableActiveReceiver(false);
}
Expand Down
Loading

0 comments on commit 55bd69d

Please sign in to comment.