Skip to content

Instantly share code, notes, and snippets.

@Resinchem
Last active August 11, 2024 07:48
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();
}
@DozoG
Copy link

DozoG commented Aug 7, 2024

If I may, I'd like to ask a question about your EXAMPLE code.

You create a "devUniqueID" , which gets used in the topic (with a single letter appended so that it is also unique for all the entities in that device) and it also gets used in the uniq_id -field of the payload (describing the entity)

Your example:
rctdev1AFF0CS for the switch (where 1AFF0C is my sample mac, and the S is the single letter appended)
rctdev1AFF0CL for the lux sensor.

I think I got that.
But then why, in the state_topic : stat/mydevice/switch
And in the command_topic: cmd/mydevice/switch

"mydevice" seems so not unique.
Should that in the actual code also be rctdev1AFF0CS ?

And why doesn't the ids -field in the device object that is nested wihtin the entity object, also use the "devUniqueID" (but mymqttdevice01)
It seems the 01 at the end, is a way to make it unique among other Devices.

So my questions are: Are the names "mydevice" and "mymqttdevice01" the result of copying parts of code when you created this example,
or is there a specific reason those names do not appear as unique as the devUniqueID+S ?

Edit.. Oh, I undertstand that the ["ids"] field in the nested object CANNOT be devUniqueID+S , because it MUST be the same for all entities within the device. (So it can not be devUniqueID+S for the switch and then devUniqueID+L for the sensor)

@Resinchem
Copy link
Author

The state and command topics are what are used by the device to publish and subscribe to topics after the device is discovered. The unique ID is needed for the actual discovery topic/payload itself. I think you are getting the two confused. For discovery, Home Assistant needs a unique ID in the topic so it knows to create a new entity... otherwise, a duplicated object ID will result in an update to the existing entity... and not creation of the new one.

Then within the entity definition, a unique ID is needed by Home Assistant if you want to be able to edit that entity within the Home Assistant UI. Those are the only two places that a unique ID is needed. The rest, state topics, device info, etc. are just the definitions for the entity itself.

Think of the unique ID in the topic and the entity as primary keys for a database table. Every entity must be unique from the others. But the rest of the fields are specific to that record. For example, each topic/entity must have a unique "email address" in the database, but multiple entities can all have the same "city". In this case, the unique ID must be used for the discovery topic... and a unique ID for the entity to enable editing in the UI. All other fields are distinct to that entity and do not need to be unique. That's why the state/cmnd topics and device ID do not use the generated unique ID. They can theoretically be anything you want them to be for that device. You could use the unique ID if you really wanted to, but it's not necessary and probably not the most readable/user friendly within MQTT on the Home Assistant side.

@DozoG
Copy link

DozoG commented Aug 8, 2024

Thank you so much for your elaborate response.
I have MQTT running in HA and node-red, but am very new to MQTT.

I have about 4 homebrew devices (Infrared remotes and LED drivers with body sensors) that I had before I used HomeAssistant.
Then when I started using HA, I was looking for a way to integrate those into HA.
So far, I have done that by creating HA-entities in node-red and send text messages to a node-red TCP-node, then filtering based on keywords, and send them to the correct HA-entity. (and letting node-red send text messages back to my device.)
But I was looking for a more standardized and manageable alternative.

I think I totally missed the part that the discovery is just the first step, to create entitiet/devices in HA.
So If I understand you correctly: While creating a HA-entity with a unique_id, you include/specify the separate MQTT topics that control the entity through the state_topic and the command_topic (and br-_stat_t ... etc, depending on the device type)

And within the "cmd/mydevice/switch" , the cmd part and the switch part are HA reserved keywords , but the mydevice is free to name ?
What would happen if my device has two swithes (entities)? They would both need a unique state_topic and command_topic, right?
Would you use:
"cmd/mydevice/switch1" and "cmd/mydevice/switch2"
or
"cmd/mydevice_1/switch" and "cmd/mydevice2/switch"

I would think the second option, if my understanding that "switch" is a HA reserved keyword?

Sorry for the wordy follow up.

Edit: I may be confused by "mydevice" because it is in the main part of the payload, (describing the entity), not in the nested object (describing the device that the entity is part of)

@Resinchem
Copy link
Author

Just remember there are really two parts here... the discovery topic and the discovery payload. The discovery topic is what MUST contain that unique object ID. Technically that's the only place you need to have a unique ID.

The payload are the specifics for creating the entity or device in Home Assistant. While it is good practice to include a unique ID so that the entity can be edited in the Home Assistant UI, it technically isn't required. You can use the same unique ID as you used in the discovery topic, but even that isn't necessary. It just need to be unique across all other entities in Home Assistant.

As far as the state and command topics within the payload, these are the specific MQTT commands that the device or entity will use after it is discovered and integrated into Home Assistant. They have absolutely nothing to do with the discovery topic and can be anything you want them to be. Your state topic could be something as simple as /abc and the command topic of /xyz. This would be the same as if you defined it in YAML. In fact, for something like a sensor, there won't even be a command topic, as it only sends data and doesn't receive commands. Assure you understand how MQTT works and how Home Assistant entities and automations utilize MQTT. Just remember that the state and command topics used within the payload for the device are just the topics that the device and Home Assistant are going to subscribe and publish to for exchanging data. They have nothing to do with the discovery topic and payload... or the unique ID as used for discovery.

@DozoG
Copy link

DozoG commented Aug 8, 2024

:-) Thank you

@DozoG
Copy link

DozoG commented Aug 8, 2024

Can add and remove entities after:

  • I added my mosquito user and password to the client.connect("arduinoClient", "me" , "mypass")
  • I studied the sprintf(&devUniqueID[(preSizeBytes - 1) + (j)], "%02X", macAddr[i]); line and realized it won't work. (write outside buffer)

I know its just Sample code. :-)

Thank you. I think I got it now.

@DozoG
Copy link

DozoG commented Aug 10, 2024

Cleaned up some code, trying to implement the light only.

I cannot get the brightness to work. Checked and double checked the state_topic and brightness topic with my code and MQTT-Explorer.
The broker gets all commands and states correctly.
But then I found this in the homeassistant log

2024-08-10 13:49:22.686 WARNING (MainThread) [homeassistant.components.light] light.office_light_my_mqtt_light (<class 'homeassistant.components.mqtt.light.schema_basic.MqttLight'>) set to unsupported color mode onoff, expected one of {<ColorMode.BRIGHTNESS: 'brightness'>}, this will stop working in Home Assistant Core 2025.3, please create a bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+mqtt%22

Home assistant seems to get the on/off and brightness states correctly from my code too.

Then checking with the Developer tools in HA:
The brightness attribute always gets set to null.
(when I touch the control for either the switch or the brightness slider)
And there is a color_mode attribute that I did not set in my arduino code.

mqtt_light

Are you using any mqtt discovered lights yourself? And do they still work, inlcuding brightness setting?

I have already reported to home-assistant too. But perhaps you have some workaround?

Thanks in advance.

@Resinchem
Copy link
Author

Well, every month a new version of Home Assistant is released, with breaking changes. And it seems that every month, MQTT is listed in the breaking changes. In this case, I believe the "brightness: true" is no longer valid. You can always check the latest valid parameters and format by searching for "Home Assistant MQTT entitytype". For lights, see: MQTT Light. Also note that if you are using abbreviations, you should also check the main Home Assistant MQTT Documentation page for valid abbreviations, as these may change with other parameter changes.

As mentioned before, the code I include here is really just a guideline and, as you found, may not always be 100% correct with later versions of Home Assistant. I actually do not even have an MQTT light (although I do have many switches, sensors. etc.) so the code above was used to create a discovered light, but wasn't actually installed on or connected to a 'real' light. Due to the rapid changing nature of Home Assistant, it is almost always necessary to take an sample code and then compare that to the latest official documentation (although sometimes even the official documentation lags behind the changes as well). But it looks like you are very close with your device. Probably just a small tweak related to the brightness attribute changes in MQTT.

@DozoG
Copy link

DozoG commented Aug 10, 2024

LoL.. Yeah, I got that. The MQTT-integration is frequently part of the breaking changes. It's frustrating.
I read the Home assistant documentation for mqtt discovery. I tried using the full name instead of the abbreviations.. etc etc.

I read some topic named "Do not allow mqtt lights to set brightness to zero #91296"
But thats on the core... thats above my paygrade. (if it is relevant)

The example for an mqtt device with light on the HA website, is 6 years old. Its almost criminal to have it on the website. :-)

There is a library: https://github.com/dawidchyrzynski/arduino-home-assistant/tree/main
I may try that out (I am scanning it to see if it does things significantly different from the simple code I have so far.)
But it adds an extra level of abstraction. Once the HA implementation changes, I would depend on that library to do its maintenance.
It's a little similar to the reason I hessitate using ESP-Home. Very frequent updates and it's basically an abstraction layer on top of arduino code.
I love how mqtt-discovery creates device on HA, but if it breaks every two months..

So far I create quite some HomeAssistant entities in node-red. And then sending regular text commands to TCP or UDP node in node-red.
It works, but it is a task to manage them.
Anyway, we got options.

Hey, thank you man for at least introducing me to mqtt discovery and getting me this far.

@DozoG
Copy link

DozoG commented Aug 11, 2024

Dear Resinchem,

I thought it fair (and possibly helpfull to others that read your blog or watch your video) to report back that I figured it out.
And its simple but deceptive trick in the MQTT-Callback function:

I previously responded to receiving a switch command, by publishing the new switch_state.
And responded to receiving a brightness command, by publishing the new brightness_state.
(I had tried to simply respond by publishing all states upon receiving any command, but that did not work)

I now have included code that when receiving a brightness command: when it is >0 , ALSO publish switch_state = on
And when the brightness command has a value == 0, then ALSO publish the switch_state = off

And it magically works.
When checking the brightness value in the Developer_tools, it also updates.
When the switch is off, the brightness value is null. when I toggle the switch, it gets the (original) value through mqtt.
When I drag the slider, the light goes brighter/less bright, and goes off when slider set to lowest position.

Thanks again.

Edit.... OR..... it got solved in the core_2024.8.1 , because it now behaves quite a bit diffferent
Anyway.. will figure it out.

P.S. It works better if you set the [bri_scl] = "255" .. because then the attribite "color_mode" will switch between "brightnes" (switch is on) and "null" (switch is off)
And the attribute "brightness" will get a value depending on the mqtt state value .. or null when the switch is off.
OK, I will stop bothering you with updates.
Thanks for the last time.

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