I quite struggled with defining a device, too - so I thought sharing my current solution for a device containing multiple entities and autodetection could be helpful to someone.
Currently my sensor contains a LDR and the BME280 (temperature, humidity, pressure)
Here just the relevant code:
/************* MQTT TOPICS **************************/
const string SENSOR_ID {"sn01"};
const string state_topic{"homeassistant/sensor/" + SENSOR_ID + "/state"};
const string set_topic{"homeassistant/sensor/" + SENSOR_ID + "/set"};
// {<entityId>, <entityUnit>, <entityClass>}
const vector<vector<string>> ENTITIES{
{"ldr", "%", "illuminance"},
{"temp", "°C", "temperature"},
{"hum", "%", "humidity"},
{"pres", "hPa", "pressure"},
};
// ...
void sendState()
{
// Save the last time a new reading was published
lastPublishMillis = millis();
DynamicJsonDocument configPayload(1024);
//////////////////////////////////////////////////////
///////// Shared config for every entity /////////////
//////////////////////////////////////////////////////
configPayload["stat_t"] = state_topic.c_str();
configPayload["expire_after"] = EXPIRE_AFTER;
configPayload["dev"]["name"] = SENSOR_ID.c_str();
configPayload["dev"]["model"] = "NodeMCU";
configPayload["dev"]["manufacturer"] = "Manufacturer";
JsonArray identifiers{configPayload["dev"].createNestedArray("ids")};
identifiers.add(SENSOR_ID.c_str());
//////////////////////////////////////////////////////
///////// Setting the config for each entity /////////
//////////////////////////////////////////////////////
for (auto entity : ENTITIES)
{
const string unique_id {SENSOR_ID + "_" + entity.at(0)};
const string value_template {"{{value_json['" + entity.at(0)+ "']}}"};
configPayload["name"] = unique_id.c_str();
configPayload["uniq_id"] = unique_id.c_str();
configPayload["val_tpl"] = value_template.c_str();
configPayload["unit_of_meas"] = entity.at(1).c_str();
configPayload["device_class"] = entity.at(2).c_str();
const string config_topic_entity{"homeassistant/sensor/" + SENSOR_ID + "_" + entity.at(0) + "/config"};
char configPayloadSerialized[1024]{};
serializeJson(configPayload, configPayloadSerialized);
Serial.println(config_topic_entity.c_str());
Serial.println(configPayloadSerialized);
// Publishing
bool publishSuccessful {0};
if (client.publish(config_topic_entity.c_str(), configPayloadSerialized, true))
publishSuccessful = 1;
const string publishMessage {
"config " + entity[0] + (publishSuccessful ? " " : " NOT ") + "published\n"
};
Serial.println(publishMessage.c_str());
}
////////////////////////////////////
//////// updating the state ////////
////////////////////////////////////
// Inverting value to receive bright = high numbers + converting to %.
const unsigned short LDRPIN_MAX{1024};
const float PERCENTAGE_DIVIDER{10.24};
LDR = (LDRPIN_MAX - analogRead(LDRPIN)) / PERCENTAGE_DIVIDER;
// New BME280 sensor readings
// Celsius:
temp = bme.readTemperature();
// Fahrenheit:
//temp = 1.8*bme.readTemperature() + 32;
hum = bme.readHumidity();
pres = bme.readPressure() / 100.0F;
DynamicJsonDocument sensorData(1024);
sensorData["ldr"] = round(LDR*10)/10;
sensorData["temp"] = round(temp*10)/10;
sensorData["hum"] = round(hum*10)/10;
sensorData["pres"] = round(pres*10)/10;
char buffer[1024]{""};
serializeJson(sensorData, buffer);
Serial.println(buffer);
if (client.publish(state_topic.c_str(), buffer, true))
Serial.println("state published");
else
Serial.println("state NOT published");
Serial.println();
}
Important for a device including multiple entities is that each entity has to be configured independently to the config_topic. Something like
homeassistant/sensor/<sensorId>_<entityName>/config
E.g. for the ldr:
homeassistant/sensor/sensornode1_ldr/config
Sending the state is then handled to the same stateTopic that’s defined in the entity’s json (in my example within “shared config”):
homeassistant/sensor/sensornode1/state