Skip to content

Instantly share code, notes, and snippets.

@Resinchem
Last active February 29, 2024 23:42
Show Gist options
  • Save Resinchem/ecd86dfb52bd699c79acfa80cd348d7b to your computer and use it in GitHub Desktop.
Save Resinchem/ecd86dfb52bd699c79acfa80cd348d7b to your computer and use it in GitHub Desktop.
Sample Arduino code for publishing devices using Home Assistant MQTT Discovery. See the following for use case: https://youtu.be/VHiCtZqllU8 or https://resinchemtech.blogspot.com/2023/12/mqtt-auto-discovery.html
/* ===================================================================
SAMPLE CODE Segments for Home Assistant MQTT Discovery
This is NOT complete code but shows an example of generating a unique ID
for topic/entity publishing based on the device MAC address.
It also includes sample code for creating a Home Assistant device with four
entities via MQTT Discovery, as shown here:
Video: https://youtu.be/VHiCtZqllU8
Blog: https://resinchemtech.blogspot.com/2023/12/mqtt-auto-discovery.html
It is highly recommended that you include some sort of end-user accessible setting to
enable/disable Home Assistant discovery as some users may not want your device auto-discovered
and Home Assistant currently doesn't provide any prompting... devices/entities are immediately
added as soon as a valid topic/payload are received.
THIS IS AN INCOMPLETE EXAMPLE ONLY!!!!
===================================================================== */
//Partial list of libaries - also need Wifi, ESP8266/ESP32 libraries, etc.
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <ESP8266WebServer.h> //Used for HTTP callback to enable/disable discovery
//Auto-discover enable/disable option
bool auto_discovery = false; //default to false and provide end-user interface to allow toggling
//Variables for creating unique entity IDs and topics
byte macAddr[6]; //Device MAC address
char uidPrefix[] = "rctdev"; //Prefix for unique ID generation (limit to 20 chars)
char devUniqueID[30]; //Generated Unique ID for this device (uidPrefix + last 6 MAC characters)
// =====================================
// Create Unique ID for topics/entities
// =====================================
void createDiscoveryUniqueID() {
//Generate UniqueID from uidPrefix + last 6 chars of device MAC address
//This should insure that even multiple devices installed in same HA instance are unique
strcpy(devUniqueID, uidPrefix);
int preSizeBytes = sizeof(uidPrefix);
int preSizeElements = (sizeof(uidPrefix) / sizeof(uidPrefix[0]));
//Now add last 6 chars from MAC address (these are 'reversed' in the array)
int j = 0;
for (int i = 2; i >= 0; i--) {
sprintf(&devUniqueID[(preSizeBytes - 1) + (j)], "%02X", macAddr[i]); //preSizeBytes indicates num of bytes in prefix - null terminator, plus 2 (j) bytes for each hex segment of MAC
j = j + 2;
}
// End result is a unique ID for this device (e.g. rctdevE350CA)
Serial.print("Unique ID: ");
Serial.println(devUniqueID);
}
// ===============================
// Main HA MQTT Discover Function
// This creates a single fictional device with four entities:
// - A dimmer switch with light level, temperature and IP address
// ===============================
void haDiscovery() {
char topic[128];
if (auto_discovery) {
char buffer1[512];
char buffer2[512];
char buffer3[512];
char buffer4[512];
char uid[128];
DynamicJsonDocument doc(512);
doc.clear();
Serial.println("Discovering new devices...");
Serial.println("Adding light switch...");
//Create unique topic based on devUniqueID
strcpy(topic, "homeassistant/light/");
strcat(topic, devUniqueID);
strcat(topic, "S/config");
//Create unique_id based on devUniqueID
strcpy(uid, devUniqueID);
strcat(uid, "S");
//Create JSON payload per HA documentation
doc["name"] = "My MQTT Light";
doc["obj_id"] = "mqtt_light";
doc["uniq_id"] = uid;
doc["stat_t"] = "stat/mydevice/switch";
doc["cmd_t"] = "cmnd/mydevice/switch";
doc["brightness"] = "true";
doc["bri_scl"] = "100";
doc["bri_stat_t"] = "stat/mydevice/brightness";
doc["bri_cmd_t"] = "cmnd/mydevice/brightness";
JsonObject device = doc.createNestedObject("device");
device["ids"] = "mymqttdevice01";
device["name"] = "My MQTT Device";
device["mf"] = "Resinchem Tech";
device["mdl"] = "ESP8266";
device["sw"] = "1.24";
device["hw"] = "0.45";
device["cu"] = "http://192.168.1.226/config"; //web interface for device, with discovery toggle
serializeJson(doc, buffer1);
//Publish discovery topic and payload (with retained flag)
client.publish(topic, buffer1, true);
//Lux Sensor
Serial.println("Adding light sensor...");
//Create unique Topic based on devUniqueID
strcpy(topic, "homeassistant/sensor/");
strcat(topic, devUniqueID);
strcat(topic, "L/config");
//Create unique_id based on decUniqueID
strcpy(uid, devUniqueID);
strcat(uid, "L");
//Create JSON payload per HA documentation
doc.clear();
doc["name"] = "Light Level";
doc["obj_id"] = "mqtt_light_level";
doc["dev_cla"] = "illuminance";
doc["uniq_id"] = uid;
doc["stat_t"] = "stat/mydevice/lightlevel";
doc["unit_of_meas"] = "lx";
JsonObject deviceS = doc.createNestedObject("device");
deviceS["ids"] = "mymqttdevice01";
deviceS["name"] = "My MQTT Device";
serializeJson(doc, buffer2);
//Publish discovery topic and payload (with retained flag)
client.publish(topic, buffer2, true);
//Temperature Sensor
Serial.println("Adding Temp Sensor...");
//Create unique Topic based on devUniqueID
strcpy(topic, "homeassistant/sensor/");
strcat(topic, devUniqueID);
strcat(topic, "T/config");
//Create unique_id based on decUniqueID
strcpy(uid, devUniqueID);
strcat(uid, "T");
//Create JSON payload per HA documentation
doc.clear();
doc["name"] = "Temperature";
doc["obj_id"] = "mqtt_temperature";
doc["deve_cla"] = "temperature";
doc["uniq_id"] = uid;
doc["stat_t"] = "stat/mydevice/temperature";
doc["unit_of_meas"] = "°F";
JsonObject deviceT = doc.createNestedObject("device");
deviceT["ids"] = "mymqttdevice01";
deviceT["name"] = "My MQTT Device";
serializeJson(doc, buffer3);
//Publish discovery topic and payload (with retained flag)
client.publish(topic, buffer3, true);
//IP Address Diagnostic
Serial.println("Adding IP Diagnostic Sensor...");
//Create unique Topic based on devUniqueID
strcpy(topic, "homeassistant/sensor/");
strcat(topic, devUniqueID);
strcat(topic, "I/config");
//Create unique_id based on decUniqueID
strcpy(uid, devUniqueID);
strcat(uid, "I");
//Create JSON payload per HA documentation
doc.clear();
doc["name"] = "IP Address";
doc["uniq_id"] = uid;
doc["ent_cat"] = "diagnostic";
doc["stat_t"] = "stat/mydevice/ipaddress";
JsonObject deviceI = doc.createNestedObject("device");
deviceI = doc.createNestedObject("device");
deviceI["ids"] = "mymqttdevice01";
deviceI["name"] = "My MQTT Device";
serializeJson(doc, buffer4);
//Publish discovery topic and payload (with retained flag)
client.publish(topic, buffer4, true);
Serial.println("All devices added!");
} else {
//Remove all entities by publishing empty payloads
//Must use original topic, so recreate from original Unique ID
//This will immediately remove/delete the device/entities from HA
Serial.println("Removing discovered devices...");
//Lux Sensor
strcpy(topic, "homeassistant/sensor/");
strcat(topic, devUniqueID);
strcat(topic, "L/config");
client.publish(topic, "");
//Temperature Sensor
strcpy(topic, "homeassistant/sensor/");
strcat(topic, devUniqueID);
strcat(topic, "T/config");
client.publish(topic, "");
//IP Address Sensor
strcpy(topic, "homeassistant/sensor/");
strcat(topic, devUniqueID);
strcat(topic, "I/config");
client.publish(topic, "");
//Light (switch)
strcpy(topic, "homeassistant/light/");
strcat(topic, devUniqueID);
strcat(topic, "S/config");
client.publish(topic, "");
Serial.println("Devices Removed...");
}
}
// =====================
// MAIN SETUP
// =====================
void Setup() {
// The following should be included in (or called from) the main setup routine
//Get MAC address when joining wifi and place into char array
WiFi.macAddress(macAddr);
//Call routing (or embed here) to create initial Unique ID
createDiscoveryUniqueID();
//Handle web callbacks for enabling or disabling discovery (using this method is just one of many ways to do this)
server.on("/discovery_on",[]() {
server.send(200, "text/html", "<h1>Discovery ON...<h1><h3>Home Assistant MQTT Discovery enabled</h3>");
delay(200);
auto_discovery = true;
haDiscovery();
});
server.on("/discovery_off",[]() {
server.send(200, "text/html", "<h1>Discovery OFF...<h1><h3>Home Assistant MQTT Discovery disabled. Previous entities removed.</h3>");
delay(200);
auto_discovery = false;
haDiscovery();
});
server.begin();
}
@ylnest2018
Copy link

Using this code I am presented with the following error

2024-02-28 12:35:05.730 ERROR (MainThread) [homeassistant.components.sensor] Platform mqtt does not generate unique IDs. ID TDSMonF9D108tapTDS is already used by sensor.mqtt_tdsmonitor_taptds - ignoring sensor.mqtt_tdsmonitor_taptds_2
2024-02-28 12:35:05.734 ERROR (MainThread) [homeassistant.components.sensor] Platform mqtt does not generate unique IDs. ID TDSMonF9D108tapTDS is already used by sensor.mqtt_tdsmonitor_taptds - ignoring sensor.mqtt_tdsmonitor_taptds_2
2024-02-28 12:35:05.735 ERROR (MainThread) [homeassistant.components.sensor] Platform mqtt does not generate unique IDs. ID TDSMonF9D108tapTDS is already used by sensor.mqtt_tdsmonitor_taptds - ignoring sensor.mqtt_tdsmonitor_taptds_2

my equivalent code to yours is
void haDiscovery () {
char topic[128];
if (auto_discovery) {
char buffer1[512];
char buffer2[512];
char buffer3[512];
char buffer4[512];
char uid[128];
DynamicJsonDocument doc(512);
doc.clear();
Serial.println("Discovering new devices...");

//tapTDS Sensor
Serial.println("Adding TDS Sensor...Tap TDS");
//Create unique Topic based on devUniqueID
strcpy(topic, "homeassistant/sensor/");
strcat(topic, "TDSMonitor");
strcat(topic, "tapTDS/config");
//Create unique_id based on decUniqueID
strcpy(uid, "TDSMonitor");
strcat(uid, "tapTDS");
//Create JSON payload per HA documentation
doc["name"] = "Tap TDS";
doc["obj_id"] = "mqtt_TDSmonitor_tapTDS";
doc["dev_cla"] = "volatile_organic_compounds_parts";
doc["uniq_id"] = uid;
doc["stat_t"] = "stat/TDSMonitor/tapTDS";
doc["unit_of_meas"] = "ppm";
doc["ic"] = "mdi:water-pump";
JsonObject devicetapTDS = doc.createNestedObject("device");
devicetapTDS["ids"] = "tdsmonitor";
devicetapTDS["name"] = "TDS Monitor";
devicetapTDS["mf"] = "DeezNutzInc";
devicetapTDS["mdl"] = "TDS Monitor Elite";
devicetapTDS["sw"] = "1.89";
devicetapTDS["hw"] = "1.0";
devicetapTDS["cu"] = "http://10.0.0.14/config";  //web interface for device, with discovery toggle
serializeJson(doc, buffer1);
//Publish discovery topic and payload (with retained flag)
client.publish(topic, buffer1, true);


//prediTDS Sensor
Serial.println("Adding TDS Sensor...Pre-DI TDS");
//Create unique Topic based on devUniqueID
strcpy(topic, "homeassistant/sensor/");
strcat(topic, "TDSMonitor");
strcat(topic, "prediTDS/config");
//Create unique_id based on decUniqueID
strcpy(uid, "TDSMonitor");
strcat(uid, "prediTDS");
//Create JSON payload per HA documentation
doc.clear();
doc["name"] = "Pre-DI TDS";
doc["obj_id"] = "mqtt_TDSmonitor_prediTDS";
doc["dev_cla"] = "volatile_organic_compounds_parts";
doc["uniq_id"] = uid;
doc["stat_t"] = "stat/TDSMonitor/prediTDS";
doc["unit_of_meas"] = "ppm";
doc["ic"] = "mdi:toilet";
JsonObject deviceprediTDS = doc.createNestedObject("device");
deviceprediTDS["ids"] = "tdsmonitor";
deviceprediTDS["name"] = "TDS Monitor";
serializeJson(doc, buffer2);
//Publish discovery topic and payload (with retained flag)
client.publish(topic, buffer1, true);

//outTDS Sensor
Serial.println("Adding TDS Sensor...Output TDS");
//Create unique Topic based on devUniqueID
strcpy(topic, "homeassistant/sensor/");
strcat(topic, "TDSMonitor");
strcat(topic, "outTDS/config");
//Create unique_id based on decUniqueID
strcpy(uid, "TDSMonitor");
strcat(uid, "outTDS");
//Create JSON payload per HA documentation
doc.clear();
doc["name"] = "Output TDS";
doc["obj_id"] = "mqtt_TDSmonitor_outTDS";
doc["dev_cla"] = "volatile_organic_compounds_parts";
doc["uniq_id"] = uid;
doc["stat_t"] = "stat/TDSMonitor/outTDS";
doc["unit_of_meas"] = "ppm";
doc["ic"] = "mdi:water-check";
JsonObject deviceoutTDS = doc.createNestedObject("device");
deviceoutTDS["ids"] = "tdsmonitor";
deviceoutTDS["name"] = "TDS Monitor";
serializeJson(doc, buffer3);
//Publish discovery topic and payload (with retained flag)
client.publish(topic, buffer1, true);

//IP Address Diagnostic
Serial.println("Adding IP Diagnostic Sensor...");
//Create unique Topic based on devUniqueID
strcpy(topic, "homeassistant/sensor/");
strcat(topic, "TDSMonitor");
strcat(topic, "IP/config");
//Create unique_id based on decUniqueID
strcpy(uid, "TDSMonitor");
strcat(uid, "IP");
//Create JSON payload per HA documentation
doc.clear();
doc["name"] = "IP Address";
doc["uniq_id"] = uid;
doc["ent_cat"] = "diagnostic";
doc["stat_t"] = "stat/TDSMonitor/ipaddress";
JsonObject deviceIP = doc.createNestedObject("device");
deviceIP = doc.createNestedObject("device");
deviceIP["ids"] = "tdsmonitor";
deviceIP["name"] = "TDS Monitor";
serializeJson(doc, buffer4);
//Publish discovery topic and payload (with retained flag)
client.publish(topic, buffer1, true);

Serial.println("All Devices Added!");

} else {

//Remove all entities by publishing empty payloads
//Must use original topic, so recreate from original Unique ID
//This will immediately remove/delete the device/entities from HA
Serial.println("Removing discovered devices...");

//tapTDS Sensor
strcpy(topic, "homeassistant/sensor/");
strcat(topic, "TDSMonitor");
strcat(topic, "tapTDS/config");
client.publish(topic, "");

//prediTDS Sensor
strcpy(topic, "homeassistant/sensor/");
strcat(topic, "TDSMonitor");
strcat(topic, "prediTDS/config");
client.publish(topic, "");

//outTDS Sensor
strcpy(topic, "homeassistant/sensor/");
strcat(topic, "TDSMonitor");
strcat(topic, "outTDS/config");
client.publish(topic, "");

//IP Diragnostics Sensor
strcpy(topic, "homeassistant/sensor/");
strcat(topic, "TDSMonitor");
strcat(topic, "IP/config");
client.publish(topic, "");

Serial.println("All Devices Removed...");
}

}

@ylnest2018
Copy link

ylnest2018 commented Feb 29, 2024

ah figured it out

Your code has:

    serializeJson(doc, buffer1);
    //Publish discovery topic and payload (with retained flag)
    client.publish(topic, buffer1, true);
    serializeJson(doc, buffer2);
    //Publish discovery topic and payload (with retained flag)
    client.publish(topic, buffer1, true);
    serializeJson(doc, buffer3);
    //Publish discovery topic and payload (with retained flag)
    client.publish(topic, buffer1, true);
    serializeJson(doc, buffer4);
    //Publish discovery topic and payload (with retained flag)
    client.publish(topic, buffer1, true);

I assumed this means the buffer1 was important given it needed to be used as it was nested, appears this is just a typo from a copy paste potentially and should be

    serializeJson(doc, buffer1);
    //Publish discovery topic and payload (with retained flag)
    client.publish(topic, buffer1, true);
    serializeJson(doc, buffer2);
    //Publish discovery topic and payload (with retained flag)
    client.publish(topic, buffer2, true);
    serializeJson(doc, buffer3);
    //Publish discovery topic and payload (with retained flag)
    client.publish(topic, buffer3, true);
    serializeJson(doc, buffer4);
    //Publish discovery topic and payload (with retained flag)
    client.publish(topic, buffer4, true);

@Resinchem
Copy link
Author

You are 100% correct and it was my mistake with my copy and paste from my actual code. I've corrected the gist to show the proper buffers for the publish statements. Sorry for the error, but I appreciate you pointing it out.

@ylnest2018
Copy link

Don’t apologise! I learnt a lot from your video and this code, and so much so I didn’t think your code was wrong in any way but after 3 days of wrecking my brain I just analysed step by step and went on a whim.

I know you’re probably less likely to share your codes from projects as they may be very specific, but would be great to see some more of your examples or a write up on a project you’ve done as it’d help bridge the gap between the knowledge gap.

I hope none of this comes across ungrateful, super thankful for your videos and posts, and your prompt response!

@ylnest2018
Copy link

Reason I ask is, I seem to be having trouble publishing values to the sensors I created, sometimes seeing a larger code example in full helps identify where we go wrong, you're extremely helpful and knowledgeable and some of your projects you have done are just amazing.

image

I assumed I needed to do something like:

const char* mqtt_pub_topic_IP = "homeassistant/sensor/TDSMonitorIP";

//IP Address Diagnostic
    Serial.println("Adding IP Diagnostic Sensor...");
    //Create unique Topic based on devUniqueID
    strcpy(topic, "homeassistant/sensor/");
    strcat(topic, "TDSMonitor");
    strcat(topic, "IP/config");
    //Create unique_id based on decUniqueID
    strcpy(uid, "TDSMonitor");
    strcat(uid, "IP");
    //Create JSON payload per HA documentation
    doc.clear();
    doc["name"] = "IP Address";
    doc["uniq_id"] = uid;
    doc["ent_cat"] = "diagnostic";
    doc["stat_t"] = "stat/TDSMonitor/ipaddress";
    JsonObject deviceIP = doc.createNestedObject("device");
    deviceIP = doc.createNestedObject("device");
    deviceIP["ids"] = "tdsmonitor";
    deviceIP["name"] = "TDS Monitor";
    serializeJson(doc, buffer4);
    //Publish discovery topic and payload (with retained flag)
    client.publish(topic, buffer4, true);

 client.publish(mqtt_pub_topic_IP, "10.0.0.14");

Note, I tried WiFi.localIP instead of a string with the IP, that didnt work either but I imagine that is because I am doing something that the language doesnt allow, so used a string to keep it simple but when I do it breaks the auto-discovery (as i write this, it could be because the publish above is in the loop or in a step prior to discovery being turned on, will continue to test but hoping you might have something you can share.

@Resinchem
Copy link
Author

First, I believe you are publishing the IP address to the wrong topic. The topic you need to post to is the state topic you defined for the IP address entity. According to the above, that topic should be "stat/TDSMonitor/ipaddress"... not the one with the sensor in it. Don't confuse the discovery topics with the topics you will use to publish values to those entities. So your publish statement to actually publish the IP address should be:

client.publish("stat/TDSMonitor/ipaddress", "10.0.0.14");

If you want to see a full example of the implementation of discovery, plus that device publishing its states to those sensors to update the values, you can take a peek at my car parking project. That can be found here: ESP Parking Assistant. Take a look at the .ino in the /src folder and you will see a full implementation. Some of the info in the wiki may also be helpful.

@ylnest2018
Copy link

You're a wizard...thank you!

I will look at that project, im still stumbling my way through arduino and mqtt so there are some things where I look very silly and then you point it out and it's obvious 😅

Thank you again, please keep up the great content!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment