ESPHome 2026.1.4
Loading...
Searching...
No Matches
mqtt_component.cpp
Go to the documentation of this file.
1#include "mqtt_component.h"
2
3#ifdef USE_MQTT
4
7#include "esphome/core/log.h"
9
10#include "mqtt_const.h"
11
12namespace esphome::mqtt {
13
14static const char *const TAG = "mqtt.component";
15
16// Helper functions for building topic strings on stack
17inline char *append_str(char *p, const char *s, size_t len) {
18 memcpy(p, s, len);
19 return p + len;
20}
21
22inline char *append_char(char *p, char c) {
23 *p = c;
24 return p + 1;
25}
26
27// Max lengths for stack-based topic building.
28// These limits are enforced at Python config validation time in mqtt/__init__.py
29// using cv.Length() validators for topic_prefix and discovery_prefix.
30// MQTT_COMPONENT_TYPE_MAX_LEN and MQTT_SUFFIX_MAX_LEN are defined in mqtt_component.h.
31// ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h.
32// This ensures the stack buffers below are always large enough.
33static constexpr size_t TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
34static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
35
36// Stack buffer sizes - safe because all inputs are length-validated at config time
37// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
38static constexpr size_t DEFAULT_TOPIC_MAX_LEN =
39 TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
40// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
41static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 +
42 ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
43
44void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; }
45
46void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; }
47
48void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; }
49
50std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const {
51 std::string sanitized_name = str_sanitize(App.get_name());
52 const char *comp_type = this->component_type();
53 char object_id_buf[OBJECT_ID_MAX_LEN];
54 StringRef object_id = this->get_default_object_id_to_(object_id_buf);
55
56 char buf[DISCOVERY_TOPIC_MAX_LEN];
57 char *p = buf;
58
59 p = append_str(p, discovery_info.prefix.data(), discovery_info.prefix.size());
60 p = append_char(p, '/');
61 p = append_str(p, comp_type, strlen(comp_type));
62 p = append_char(p, '/');
63 p = append_str(p, sanitized_name.data(), sanitized_name.size());
64 p = append_char(p, '/');
65 p = append_str(p, object_id.c_str(), object_id.size());
66 p = append_str(p, "/config", 7);
67
68 return std::string(buf, p - buf);
69}
70
71std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
72 const std::string &topic_prefix = global_mqtt_client->get_topic_prefix();
73 if (topic_prefix.empty()) {
74 // If the topic_prefix is null, the default topic should be null
75 return "";
76 }
77
78 const char *comp_type = this->component_type();
79 char object_id_buf[OBJECT_ID_MAX_LEN];
80 StringRef object_id = this->get_default_object_id_to_(object_id_buf);
81
82 char buf[DEFAULT_TOPIC_MAX_LEN];
83 char *p = buf;
84
85 p = append_str(p, topic_prefix.data(), topic_prefix.size());
86 p = append_char(p, '/');
87 p = append_str(p, comp_type, strlen(comp_type));
88 p = append_char(p, '/');
89 p = append_str(p, object_id.c_str(), object_id.size());
90 p = append_char(p, '/');
91 p = append_str(p, suffix.data(), suffix.size());
92
93 return std::string(buf, p - buf);
94}
95
98 return this->custom_state_topic_.value();
99 return this->get_default_topic_for_("state");
100}
101
104 return this->custom_command_topic_.value();
105 return this->get_default_topic_for_("command");
106}
107
108bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
109 return this->publish(topic, payload.data(), payload.size());
110}
111
112bool MQTTComponent::publish(const std::string &topic, const char *payload, size_t payload_length) {
113 if (topic.empty())
114 return false;
115 return global_mqtt_client->publish(topic, payload, payload_length, this->qos_, this->retain_);
116}
117
118bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) {
119 if (topic.empty())
120 return false;
121 return global_mqtt_client->publish_json(topic, f, this->qos_, this->retain_);
122}
123
126
127 if (discovery_info.clean) {
128 ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str());
129 return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true);
130 }
131
132 ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str());
133
134 // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
136 this->get_discovery_topic_(discovery_info),
137 [this](JsonObject root) {
138 SendDiscoveryConfig config;
139 config.state_topic = true;
140 config.command_topic = true;
141
142 this->send_discovery(root, config);
143 // Set subscription QoS (default is 0)
144 if (this->subscribe_qos_ != 0) {
145 root[MQTT_QOS] = this->subscribe_qos_;
146 }
147
148 // Fields from EntityBase
149 root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : "";
150
151 if (this->is_disabled_by_default_())
152 root[MQTT_ENABLED_BY_DEFAULT] = false;
153 // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
154 const auto icon_ref = this->get_icon_ref_();
155 if (!icon_ref.empty()) {
156 root[MQTT_ICON] = icon_ref;
157 }
158 // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
159
160 const auto entity_category = this->get_entity()->get_entity_category();
161 switch (entity_category) {
163 break;
166 root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic";
167 break;
168 }
169
170 if (config.state_topic)
171 root[MQTT_STATE_TOPIC] = this->get_state_topic_();
172 if (config.command_topic)
173 root[MQTT_COMMAND_TOPIC] = this->get_command_topic_();
174 if (this->command_retain_)
175 root[MQTT_COMMAND_RETAIN] = true;
176
177 const Availability &avail =
178 this->availability_ == nullptr ? global_mqtt_client->get_availability() : *this->availability_;
179 if (!avail.topic.empty()) {
180 root[MQTT_AVAILABILITY_TOPIC] = avail.topic;
181 if (avail.payload_available != "online")
182 root[MQTT_PAYLOAD_AVAILABLE] = avail.payload_available;
183 if (avail.payload_not_available != "offline")
184 root[MQTT_PAYLOAD_NOT_AVAILABLE] = avail.payload_not_available;
185 }
186
188 char object_id_buf[OBJECT_ID_MAX_LEN];
189 StringRef object_id = this->get_default_object_id_to_(object_id_buf);
191 char friendly_name_hash[9];
192 sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_()));
193 friendly_name_hash[8] = 0; // ensure the hash-string ends with null
194 // Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678")
195 // MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43
196 char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11];
197 char mac_buf[MAC_ADDRESS_BUFFER_SIZE];
199 snprintf(unique_id, sizeof(unique_id), "%s-%s-%s", mac_buf, this->component_type(), friendly_name_hash);
200 root[MQTT_UNIQUE_ID] = unique_id;
201 } else {
202 // default to almost-unique ID. It's a hack but the only way to get that
203 // gorgeous device registry view.
204 root[MQTT_UNIQUE_ID] = "ESP" + std::string(this->component_type()) + object_id.c_str();
205 }
206
207 const std::string &node_name = App.get_name();
209 root[MQTT_OBJECT_ID] = node_name + "_" + object_id.c_str();
210
211 const std::string &friendly_name_ref = App.get_friendly_name();
212 const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref;
213 std::string node_area = App.get_area();
214
215 JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
216 char mac[MAC_ADDRESS_BUFFER_SIZE];
218 device_info[MQTT_DEVICE_IDENTIFIERS] = mac;
219 device_info[MQTT_DEVICE_NAME] = node_friendly_name;
220#ifdef ESPHOME_PROJECT_NAME
221 device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")";
222 const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.');
223 device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1;
224 device_info[MQTT_DEVICE_MANUFACTURER] =
225 model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
226#else
227 static const char ver_fmt[] PROGMEM = ESPHOME_VERSION " (config hash 0x%08" PRIx32 ")";
228#ifdef USE_ESP8266
229 char fmt_buf[sizeof(ver_fmt)];
230 strcpy_P(fmt_buf, ver_fmt);
231 const char *fmt = fmt_buf;
232#else
233 const char *fmt = ver_fmt;
234#endif
235 device_info[MQTT_DEVICE_SW_VERSION] = str_sprintf(fmt, App.get_config_hash());
236 device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
237#if defined(USE_ESP8266) || defined(USE_ESP32)
238 device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif";
239#elif defined(USE_RP2040)
240 device_info[MQTT_DEVICE_MANUFACTURER] = "Raspberry Pi";
241#elif defined(USE_BK72XX)
242 device_info[MQTT_DEVICE_MANUFACTURER] = "Beken";
243#elif defined(USE_RTL87XX)
244 device_info[MQTT_DEVICE_MANUFACTURER] = "Realtek";
245#elif defined(USE_HOST)
246 device_info[MQTT_DEVICE_MANUFACTURER] = "Host";
247#endif
248#endif
249 if (!node_area.empty()) {
250 device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area;
251 }
252
253 device_info[MQTT_DEVICE_CONNECTIONS][0][0] = "mac";
254 device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac;
255 },
256 this->qos_, discovery_info.retain);
257 // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
258}
259
260uint8_t MQTTComponent::get_qos() const { return this->qos_; }
261
262bool MQTTComponent::get_retain() const { return this->retain_; }
263
267
268void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) {
269 global_mqtt_client->subscribe(topic, std::move(callback), qos);
270}
271
272void MQTTComponent::subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos) {
273 global_mqtt_client->subscribe_json(topic, callback, qos);
274}
275
277
280void MQTTComponent::set_command_retain(bool command_retain) { this->command_retain_ = command_retain; }
281
282void MQTTComponent::set_availability(std::string topic, std::string payload_available,
283 std::string payload_not_available) {
284 this->availability_ = make_unique<Availability>();
285 this->availability_->topic = std::move(topic);
286 this->availability_->payload_available = std::move(payload_available);
287 this->availability_->payload_not_available = std::move(payload_not_available);
288}
291 if (this->is_internal())
292 return;
293
294 this->setup();
295
297
298 if (!this->is_connected_())
299 return;
300
301 if (this->is_discovery_enabled()) {
302 if (!this->send_discovery_()) {
303 this->schedule_resend_state();
304 }
305 }
306 if (!this->send_initial_state()) {
307 this->schedule_resend_state();
308 }
309}
310
312 if (this->is_internal())
313 return;
314
315 this->loop();
316
317 if (!this->resend_state_ || !this->is_connected_()) {
318 return;
319 }
320
321 this->resend_state_ = false;
322 if (this->is_discovery_enabled()) {
323 if (!this->send_discovery_()) {
324 this->schedule_resend_state();
325 }
326 }
327 if (!this->send_initial_state()) {
328 this->schedule_resend_state();
329 }
330}
332 if (this->is_internal())
333 return;
334
335 this->dump_config();
336}
339
340// Pull these properties from EntityBase if not overridden
341std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); }
342StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX_LEN> buf) const {
343 return this->get_entity()->get_object_id_to(buf);
344}
348 if (this->custom_state_topic_.has_value()) {
349 // If the custom state_topic is null, return true as it is internal and should not publish
350 // else, return false, as it is explicitly set to a topic, so it is not internal and should publish
351 return this->get_state_topic_().empty();
352 }
353
354 if (this->custom_command_topic_.has_value()) {
355 // If the custom command_topic is null, return true as it is internal and should not publish
356 // else, return false, as it is explicitly set to a topic, so it is not internal and should publish
357 return this->get_command_topic_().empty();
358 }
359
360 // No custom topics have been set
361 if (this->get_default_topic_for_("").empty()) {
362 // If the default topic prefix is null, then the component, by default, is internal and should not publish
363 return true;
364 }
365
366 // Use ESPHome's component internal state if topic_prefix is not null with no custom state_topic or command_topic
367 return this->get_entity()->is_internal();
368}
369
370} // namespace esphome::mqtt
371
372#endif // USE_MQTT
const std::string & get_friendly_name() const
Get the friendly name of this Application set by pre_setup().
constexpr uint32_t get_config_hash()
Get the config hash as a 32-bit integer.
const char * get_area() const
Get the area of this Application set by pre_setup().
const std::string & get_name() const
Get the name of this Application set by pre_setup().
virtual void setup()
Where the component's initialization should happen.
virtual void dump_config()
virtual void loop()
This method will be called repeatedly.
bool has_own_name() const
Definition entity_base.h:49
bool is_internal() const
Definition entity_base.h:76
const StringRef & get_name() const
StringRef get_icon_ref() const
Definition entity_base.h:97
bool is_disabled_by_default() const
Definition entity_base.h:82
EntityCategory get_entity_category() const
Definition entity_base.h:86
StringRef get_object_id_to(std::span< char, OBJECT_ID_MAX_LEN > buf) const
Get object_id with zero heap allocation For static case: returns StringRef to internal storage (buffe...
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
constexpr const char * c_str() const
Definition string_ref.h:73
constexpr size_type size() const
Definition string_ref.h:74
T value(X... x) const
Definition automation.h:148
const std::string & get_topic_prefix() const
Get the topic prefix of this device, using default if necessary.
void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos=0)
Subscribe to a MQTT topic and automatically parse JSON payload.
void register_mqtt_component(MQTTComponent *component)
bool publish(const MQTTMessage &message)
Publish a MQTTMessage.
const MQTTDiscoveryInfo & get_discovery_info() const
Get Home Assistant discovery info.
void subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos=0)
Subscribe to an MQTT topic and call callback when a message is received.
const Availability & get_availability()
bool publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos=0, bool retain=false)
Construct and send a JSON MQTT message.
TemplatableValue< std::string > custom_state_topic_
TemplatableValue< std::string > custom_command_topic_
std::unique_ptr< Availability > availability_
bool is_disabled_by_default_() const
Get whether the underlying Entity is disabled by default.
MQTTComponent()
Constructs a MQTTComponent.
void set_qos(uint8_t qos)
Set QOS for state messages.
void schedule_resend_state()
Internal method for the MQTT client base to schedule a resend of the state on reconnect.
bool publish(const std::string &topic, const std::string &payload)
Send a MQTT message.
void set_command_retain(bool command_retain)
Set whether command message should be retained.
bool send_discovery_()
Internal method to start sending discovery info, this will call send_discovery().
void set_subscribe_qos(uint8_t qos)
Set the QOS for subscribe messages (used in discovery).
void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos=0)
Subscribe to a MQTT topic and automatically parse JSON payload.
void call_setup() override
Override setup_ so that we can call send_discovery() when needed.
bool publish_json(const std::string &topic, const json::json_build_t &f)
Construct and send a JSON MQTT message.
std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const
Helper method to get the discovery topic for this component.
std::string get_default_topic_for_(const std::string &suffix) const
Get this components state/command/... topic.
StringRef get_default_object_id_to_(std::span< char, OBJECT_ID_MAX_LEN > buf) const
Get the object ID for this MQTT component, writing to the provided buffer.
void set_retain(bool retain)
Set whether state message should be retained.
virtual const EntityBase * get_entity() const =0
Gets the Entity served by this MQTT component.
std::string get_state_topic_() const
Get the MQTT topic that new states will be shared to.
virtual void send_discovery(JsonObject root, SendDiscoveryConfig &config)=0
Send discovery info the Home Assistant, override this.
std::string friendly_name_() const
Get the friendly name of this MQTT component.
virtual bool send_initial_state()=0
void disable_discovery()
Disable discovery. Sets friendly name to "".
float get_setup_priority() const override
MQTT_COMPONENT setup priority.
void set_availability(std::string topic, std::string payload_available, std::string payload_not_available)
Set the Home Assistant availability data.
std::string get_command_topic_() const
Get the MQTT topic for listening to commands.
void subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos=0)
Subscribe to a MQTT topic.
virtual const char * component_type() const =0
Override this method to return the component type (e.g. "light", "sensor", ...)
StringRef get_icon_ref_() const
Get the icon field of this component as StringRef.
std::function< void(JsonObject)> json_build_t
Callback function typedef for building JsonObjects.
Definition json_util.h:46
@ MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR
Definition mqtt_client.h:76
@ MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR
Definition mqtt_client.h:70
std::function< void(const std::string &, JsonObject)> mqtt_json_callback_t
Definition mqtt_client.h:39
char * append_char(char *p, char c)
char * append_str(char *p, const char *s, size_t len)
std::function< void(const std::string &, const std::string &)> mqtt_callback_t
Callback for MQTT subscriptions.
Definition mqtt_client.h:38
MQTTClientComponent * global_mqtt_client
const float AFTER_CONNECTION
For components that should be initialized after a data connection (API/MQTT) is connected.
Definition component.cpp:89
std::string str_sanitize(const std::string &str)
Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores.
Definition helpers.cpp:202
std::string size_t len
Definition helpers.h:595
uint32_t fnv1_hash(const char *str)
Calculate a FNV-1 hash of str.
Definition helpers.cpp:147
@ ENTITY_CATEGORY_NONE
Definition entity_base.h:33
@ ENTITY_CATEGORY_CONFIG
Definition entity_base.h:34
@ ENTITY_CATEGORY_DIAGNOSTIC
Definition entity_base.h:35
void get_mac_address_into_buffer(std::span< char, MAC_ADDRESS_BUFFER_SIZE > buf)
Get the device MAC address into the given buffer, in lowercase hex notation.
Definition helpers.cpp:731
std::string str_sprintf(const char *fmt,...)
Definition helpers.cpp:223
Application App
Global storage of Application pointer - only one Application can exist.
Simple data struct for Home Assistant component availability.
Definition mqtt_client.h:61
std::string payload_not_available
Definition mqtt_client.h:64
std::string topic
Empty means disabled.
Definition mqtt_client.h:62
Internal struct for MQTT Home Assistant discovery.
Definition mqtt_client.h:83
MQTTDiscoveryUniqueIdGenerator unique_id_generator
Definition mqtt_client.h:88
std::string prefix
The Home Assistant discovery prefix. Empty means disabled.
Definition mqtt_client.h:84
MQTTDiscoveryObjectIdGenerator object_id_generator
Definition mqtt_client.h:89
bool retain
Whether to retain discovery messages.
Definition mqtt_client.h:85
Simple Helper struct used for Home Assistant MQTT send_discovery().
bool command_topic
If the command topic should be included. Default to true.
bool state_topic
If the state topic should be included. Defaults to true.
const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM
Definition web_server.h:27