ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
mqtt_client.cpp
Go to the documentation of this file.
1#include "mqtt_client.h"
2
3#ifdef USE_MQTT
4
5#include <utility>
10#include "esphome/core/log.h"
13#ifdef USE_LOGGER
15#endif
16#include "lwip/dns.h"
17#include "lwip/err.h"
18#include "mqtt_component.h"
19#include "mqtt_topic_match.h"
20
21#ifdef USE_API
23#endif
24#ifdef USE_DASHBOARD_IMPORT
26#endif
27
28namespace esphome::mqtt {
29
30ESPHOME_LOG_TAG(TAG, "mqtt");
31
32// Maximum number of MQTT component resends per loop iteration.
33// Limits work to avoid triggering the task watchdog on reconnect.
34static constexpr uint8_t MAX_RESENDS_PER_LOOP = 8;
35
36// Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8)
37PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version",
38 "Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized",
39 "Not Enough Space", "TLS Bad Fingerprint", "DNS Resolve Error", "Unknown");
40
42 global_mqtt_client = this;
43 char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
45 const StringRef &name = App.get_name();
46 char client_id[MAX_NAME_WITH_SUFFIX_SIZE];
47 size_t len = make_name_with_suffix_to(client_id, sizeof(client_id), name.c_str(), name.size(), '-', mac_addr,
48 MAC_ADDRESS_BUFFER_SIZE - 1);
49 this->credentials_.client_id.assign(client_id, len);
50}
51
52// Connection
53// Room for "esphome/<kind>/<name>" with the longest kind, "discover"
54static constexpr size_t ESPHOME_TOPIC_BUFFER_SIZE = sizeof("esphome/discover/") + ESPHOME_DEVICE_NAME_MAX_LEN;
55
56static void format_esphome_topic(char (&topic)[ESPHOME_TOPIC_BUFFER_SIZE], const char *kind) {
57 buf_append_printf(topic, sizeof(topic), 0, "esphome/%s/%s", kind, App.get_name().c_str());
58}
59
62 [this](const char *topic, const char *payload, size_t len, size_t index, size_t total) {
63 if (index == 0) {
64 this->payload_buffer_.clear();
65 this->payload_buffer_.reserve(total);
66 }
67
68 // append new payload, may contain incomplete MQTT message
69 this->payload_buffer_.append(payload, len);
70
71 // MQTT fully received
72 if (len + index == total) {
73 this->on_message(topic, this->payload_buffer_);
74 this->payload_buffer_.clear();
75 }
76 });
78 if (this->state_ == MQTT_CLIENT_DISABLED)
79 return;
81 this->disconnect_reason_ = reason;
82 });
83#ifdef USE_LOGGER
84 if (this->is_log_message_enabled() && logger::global_logger != nullptr) {
86 this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
87 static_cast<MQTTClientComponent *>(self)->on_log(level, tag, message, message_len);
88 });
89#ifdef USE_MQTT_CRASH_LOG
90 // `esphome logs` announces itself here, like an API client's log subscription, and the record stays
91 // loggable until the next boot, so the offer does too. A log topic that drops errors would swallow the
92 // replay, so it is not offered. The topic is rebuilt from the device name when needed, so nothing is
93 // stored for it.
94 this->crash_log_offered_ = logger::crash_log_pending() && this->log_level_ >= ESPHOME_LOG_LEVEL_ERROR;
95#endif
96 }
97#endif
98
99 if (this->is_discovery_ip_enabled()) {
100 this->subscribe(
101 static_topic("esphome/discover"),
102 [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
103
104 static_assert(sizeof("esphome/ping/") + ESPHOME_DEVICE_NAME_MAX_LEN <= MQTT_DEFAULT_TOPIC_MAX_LEN);
105 MQTTTopicSource ping_topic{[](const void *, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) {
106 size_t len = buf_append_printf(buf.data(), buf.size(), 0, "esphome/ping/%s",
107 App.get_name().c_str());
108 return StringRef(buf.data(), len);
109 },
110 nullptr};
111 this->subscribe(
112 ping_topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
113 }
114
115 if (this->enable_on_boot_) {
116 this->enable();
117 }
118}
119
121 if (!this->is_connected() or !this->is_discovery_ip_enabled()) {
122 return;
123 }
124 // Format topic on stack to avoid heap allocation
125 char topic[ESPHOME_TOPIC_BUFFER_SIZE];
126 format_esphome_topic(topic, "discover");
127
128 // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
129 this->publish_json(
130 topic,
131 [](JsonObject root) {
132 uint8_t index = 0;
133 for (auto &ip : network::get_ip_addresses()) {
134 if (ip.is_set()) {
135 char key[8]; // "ip" + up to 3 digits + null
136 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
137 if (index == 0) {
138 key[0] = 'i';
139 key[1] = 'p';
140 key[2] = '\0';
141 } else {
142 buf_append_printf(key, sizeof(key), 0, "ip%u", index);
143 }
144 ip.str_to(ip_buf);
145 root[key] = ip_buf;
146 index++;
147 }
148 }
149 root[ESPHOME_F("name")] = App.get_name();
150 if (!App.get_friendly_name().empty()) {
151 root[ESPHOME_F("friendly_name")] = App.get_friendly_name();
152 }
153#ifdef USE_API
154 root[ESPHOME_F("port")] = api::global_api_server->get_port();
155#endif
156 root[ESPHOME_F("version")] = ESPHOME_VERSION;
157 char mac_buf[MAC_ADDRESS_BUFFER_SIZE];
159 root[ESPHOME_F("mac")] = mac_buf;
160
161#ifdef USE_ESP8266
162 root[ESPHOME_F("platform")] = ESPHOME_F("ESP8266");
163#endif
164#ifdef USE_ESP32
165 root[ESPHOME_F("platform")] = ESPHOME_F("ESP32");
166#endif
167#ifdef USE_LIBRETINY
168 root[ESPHOME_F("platform")] = lt_cpu_get_model_name();
169#endif
170
171 root[ESPHOME_F("board")] = ESPHOME_BOARD;
172#if defined(USE_WIFI)
173 root[ESPHOME_F("network")] = ESPHOME_F("wifi");
174#elif defined(USE_ETHERNET)
175 root[ESPHOME_F("network")] = ESPHOME_F("ethernet");
176#endif
177
178#ifdef ESPHOME_PROJECT_NAME
179 root[ESPHOME_F("project_name")] = ESPHOME_PROJECT_NAME;
180 root[ESPHOME_F("project_version")] = ESPHOME_PROJECT_VERSION;
181#endif // ESPHOME_PROJECT_NAME
182
183#ifdef USE_DASHBOARD_IMPORT
184 root[ESPHOME_F("package_import_url")] = dashboard_import::get_package_import_url();
185#endif
186
187#ifdef USE_API_NOISE
188 root[api::global_api_server->get_noise_ctx().has_psk() ? ESPHOME_F("api_encryption")
189 : ESPHOME_F("api_encryption_supported")] =
190 ESPHOME_F("Noise_NNpsk0_25519_ChaChaPoly_SHA256");
191#endif
192 },
193 2, this->discovery_info_.retain);
194 // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
195}
196
197#ifdef USE_LOGGER
198void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
199 (void) tag;
200 if (level <= this->log_level_ && this->is_connected()) {
201 this->publish(this->log_message_.topic.c_str(), message, message_len, this->log_message_.qos,
202 this->log_message_.retain);
203 }
204}
205#endif
206
208 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
209 // clang-format off
210 ESP_LOGCONFIG(TAG,
211 "MQTT:\n"
212 " Server Address: %s:%u (%s)\n"
213 " Username: " LOG_SECRET("'%s'") "\n"
214 " Client ID: " LOG_SECRET("'%s'") "\n"
215 " Clean Session: %s",
216 this->credentials_.address.c_str(), this->credentials_.port, this->ip_.str_to(ip_buf),
217 this->credentials_.username.c_str(), this->credentials_.client_id.c_str(),
218 YESNO(this->credentials_.clean_session));
219 // clang-format on
220#ifdef USE_ESP32
221 if (const MQTTTransport transport = this->mqtt_backend_.get_transport();
223 ESP_LOGCONFIG(TAG,
224 " Transport: %s\n"
225 " WebSocket Path: '%s'",
226 transport == MQTTTransport::MQTT_TRANSPORT_WSS ? LOG_STR_LITERAL("wss") : LOG_STR_LITERAL("ws"),
227 this->mqtt_backend_.get_ws_path());
228 }
229#endif
230#if defined(USE_MQTT_CERT_BUNDLE)
231 ESP_LOGCONFIG(TAG, " TLS: certificate bundle");
232#elif defined(USE_MQTT_TLS_INSECURE)
233 ESP_LOGCONFIG(TAG, " TLS: no verification");
234#elif defined(USE_MQTT_TLS)
235 ESP_LOGCONFIG(TAG, " TLS: CA certificate");
236#endif
237 if (this->is_discovery_ip_enabled()) {
238 ESP_LOGCONFIG(TAG, " Discovery IP enabled");
239 }
240 if (!this->discovery_info_.prefix.empty()) {
241 ESP_LOGCONFIG(TAG,
242 " Discovery prefix: '%s'\n"
243 " Discovery retain: %s",
244 this->discovery_info_.prefix.c_str(), YESNO(this->discovery_info_.retain));
245 }
246 ESP_LOGCONFIG(TAG, " Topic Prefix: '%s'", this->topic_prefix_.c_str());
247 if (!this->log_message_.topic.empty()) {
248 ESP_LOGCONFIG(TAG, " Log Topic: '%s'", this->log_message_.topic.c_str());
249 }
250 if (!this->availability_.topic.empty()) {
251 ESP_LOGCONFIG(TAG, " Availability: '%s'", this->availability_.topic.c_str());
252 }
253}
255 return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected() ||
257}
258
260 for (auto &subscription : this->subscriptions_) {
261 subscription.subscribed = false;
262 subscription.resubscribe_timeout = 0;
263 }
264
265 this->status_set_warning();
266 this->dns_resolve_error_ = false;
267 this->dns_resolved_ = false;
268 ip_addr_t addr;
269 err_t err;
270 {
271 LwIPLock lock;
272#if USE_NETWORK_IPV6
273 err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
274 this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
275#else
276 err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
277 this, LWIP_DNS_ADDRTYPE_IPV4);
278#endif /* USE_NETWORK_IPV6 */
279 }
280 switch (err) {
281 case ERR_OK: {
282 // Got IP immediately
283 this->dns_resolved_ = true;
284 this->ip_ = network::IPAddress(&addr);
285 this->start_connect_();
286 return;
287 }
288 case ERR_INPROGRESS: {
289 // wait for callback
290 ESP_LOGD(TAG, "Resolving broker IP address");
291 break;
292 }
293 default:
294 case ERR_ARG: {
295 // error
296 ESP_LOGW(TAG, "Error resolving broker IP address: %d", err);
297 break;
298 }
299 }
300
302 this->connect_begin_ = millis();
303}
305 if (!this->dns_resolved_ && millis() - this->connect_begin_ > 20000) {
306 this->dns_resolve_error_ = true;
307 }
308
309 if (this->dns_resolve_error_) {
310 ESP_LOGW(TAG, "Couldn't resolve IP address for '%s'", this->credentials_.address.c_str());
314 return;
315 }
316
317 if (!this->dns_resolved_) {
318 return;
319 }
320
321 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
322 ESP_LOGD(TAG, "Resolved broker IP address to %s", this->ip_.str_to(ip_buf));
323 this->start_connect_();
324}
325#if defined(USE_ESP8266) && LWIP_VERSION_MAJOR == 1
326void MQTTClientComponent::dns_found_callback(const char *name, ip_addr_t *ipaddr, void *callback_arg) {
327#else
328void MQTTClientComponent::dns_found_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg) {
329#endif
330 auto *a_this = (MQTTClientComponent *) callback_arg;
331 if (ipaddr == nullptr) {
332 a_this->dns_resolve_error_ = true;
333 } else {
334 a_this->ip_ = network::IPAddress(ipaddr);
335 a_this->dns_resolved_ = true;
336 }
337}
338
341 return;
342
343 ESP_LOGI(TAG, "Connecting");
344 // Force disconnect first
346
349 const char *username = nullptr;
350 if (!this->credentials_.username.empty())
351 username = this->credentials_.username.c_str();
352 const char *password = nullptr;
353 if (!this->credentials_.password.empty())
354 password = this->credentials_.password.c_str();
355
356 this->mqtt_backend_.set_credentials(username, password);
357
358 this->mqtt_backend_.set_server(this->credentials_.address.c_str(), this->credentials_.port);
359 if (!this->last_will_.topic.empty()) {
360 this->mqtt_backend_.set_will(this->last_will_.topic.c_str(), this->last_will_.qos, this->last_will_.retain,
361 this->last_will_.payload.c_str());
362 }
363
364 this->mqtt_backend_.connect();
366 this->connect_begin_ = millis();
367}
369 return this->state_ == MQTT_CLIENT_CONNECTED && this->mqtt_backend_.connected();
370}
371
373 if (!this->mqtt_backend_.connected()) {
374 if (millis() - this->connect_begin_ > 60000) {
376 this->start_dnslookup_();
377 }
378 return;
379 }
380
382 this->sent_birth_message_ = false;
383 this->status_clear_warning();
384 ESP_LOGI(TAG, "Connected");
385 // MQTT Client needs some time to be fully set up.
386 delay(100); // NOLINT
387
389#ifdef USE_MQTT_CRASH_LOG
390 // Best effort: a refused subscription is tried again at the next connection
391 if (this->crash_log_offered_) {
392 char topic[ESPHOME_TOPIC_BUFFER_SIZE];
393 format_esphome_topic(topic, "logs");
394 this->subscribe_(topic, 0);
395 }
396#endif
397 this->send_device_info_();
398
399 for (MQTTComponent *component : this->children_)
400 component->schedule_resend_state();
401}
402
404 // Call the backend loop first
406
407 if (this->disconnect_reason_.has_value()) {
408 const LogString *reason_s = MQTTDisconnectReasonStrings::get_log_str(
409 static_cast<uint8_t>(*this->disconnect_reason_), MQTTDisconnectReasonStrings::LAST_INDEX);
410 if (!network::is_connected()) {
411 reason_s = LOG_STR("WiFi disconnected");
412 }
413 ESP_LOGW(TAG, "Disconnected: %s", LOG_STR_ARG(reason_s));
414 this->disconnect_reason_.reset();
415 }
416
418
419 switch (this->state_) {
421 return; // Return to avoid a reboot when disabled
423 if (now - this->connect_begin_ > 5000) {
424 this->start_dnslookup_();
425 }
426 break;
428 this->check_dnslookup_();
429 break;
431 this->check_connected();
432 break;
434 if (!this->mqtt_backend_.connected()) {
436 ESP_LOGW(TAG, "Lost client connection");
437 this->start_dnslookup_();
438 } else {
439 if (!this->birth_message_.topic.empty() && !this->sent_birth_message_) {
440 this->sent_birth_message_ = this->publish(this->birth_message_);
441 }
442
443 this->last_connected_ = now;
445
446 // Process pending resends for all MQTT components centrally
447 // Limit work per loop iteration to avoid triggering task WDT on reconnect
448 {
449 uint8_t resend_count = 0;
450 for (MQTTComponent *component : this->children_) {
451 if (component->is_resend_pending()) {
452 component->process_resend();
453 if (++resend_count >= MAX_RESENDS_PER_LOOP)
454 break;
455 }
456 }
457 }
458 }
459 break;
460 }
461
462 if (millis() - this->last_connected_ > this->reboot_timeout_ && this->reboot_timeout_ != 0) {
463 ESP_LOGE(TAG, "Can't connect; restarting");
464 App.reboot();
465 }
466}
468
469// Subscribe
470bool MQTTClientComponent::subscribe_(const char *topic, uint8_t qos) {
471 if (!this->is_connected())
472 return false;
473
474 bool ret = this->mqtt_backend_.subscribe(topic, qos);
475 yield();
476
477 if (ret) {
478 ESP_LOGV(TAG, "subscribe(topic='%s')", topic);
479 } else {
480 delay(5);
481 ESP_LOGV(TAG, "Subscribe failed for topic='%s'. Will retry", topic);
482 this->status_momentary_warning(1000);
483 }
484 return ret != 0;
485}
486// Out of line so the topic buffer is not part of loop()'s stack frame
487__attribute__((noinline)) bool MQTTClientComponent::subscribe_topic_(const MQTTTopicSource &topic, uint8_t qos) {
488 char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
489 return this->subscribe_(topic.build(buf).c_str(), qos);
490}
491
492void MQTTClientComponent::resubscribe_subscription_(MQTTSubscription *sub) {
493 if (sub->subscribed)
494 return;
495
496 const uint32_t now = millis();
497 bool do_resub = sub->resubscribe_timeout == 0 || now - sub->resubscribe_timeout > 1000;
498
499 if (do_resub) {
500 sub->subscribed = this->subscribe_topic_(sub->topic, sub->qos);
501 sub->resubscribe_timeout = now;
502 }
503}
505 for (auto &subscription : this->subscriptions_) {
506 this->resubscribe_subscription_(&subscription);
507 }
508}
509
510#ifdef USE_MQTT_CRASH_LOG
511void MQTTClientComponent::handle_crash_log_request_(const std::string &topic, const std::string &payload) {
512 // Only the CLI's empty request counts: a log_topic pointing at this topic would otherwise replay the crash
513 // log for every line it logs
514 if (!this->crash_log_offered_ || !payload.empty())
515 return;
516 char logs_topic[ESPHOME_TOPIC_BUFFER_SIZE];
517 format_esphome_topic(logs_topic, "logs");
518 if (topic == logs_topic)
520}
521#endif
522
523static StringRef static_topic_builder(const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN>) {
524 return StringRef(static_cast<const char *>(context));
525}
526// Same as static_topic_builder, but a distinct function so its address marks the topics to free
527static StringRef copied_topic_builder(const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN>) {
528 return StringRef(static_cast<const char *>(context));
529}
530
531MQTTTopicSource static_topic(const char *topic) { return {static_topic_builder, topic}; }
532
533static_assert(MQTT_DEFAULT_TOPIC_MAX_LEN == 248, "update MQTT_DEFAULT_TOPIC_MAX_LEN in mqtt/__init__.py");
534
535#ifdef USE_ESP8266
536// mqtt/__init__.py picks this source only for topics that fit
537static StringRef progmem_topic_builder(const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) {
538 size_t len = buf_append_str_p(buf.data(), buf.size(), 0, static_cast<const char *>(context));
539 return StringRef(buf.data(), len);
540}
541
542MQTTTopicSource progmem_topic(ProgmemStr topic) {
543 return {progmem_topic_builder, reinterpret_cast<const char *>(topic)};
544}
545#endif
546
547MQTTTopicSource MQTTClientComponent::copy_topic(StringRef topic) {
548 char *copy = new char[topic.size() + 1]; // NOLINT(cppcoreguidelines-owning-memory)
549 memcpy(copy, topic.c_str(), topic.size());
550 copy[topic.size()] = '\0';
551 return {copied_topic_builder, copy};
552}
553
554void MQTTClientComponent::add_subscription_(MQTTTopicSource topic, mqtt_subscription_callback_t callback, bool boxed,
555 uint8_t qos) {
556 auto &subscription = this->subscriptions_.emplace_back(MQTTSubscription{
557 .topic = topic,
558 .callback = callback,
559 .resubscribe_timeout = 0,
560 .qos = qos,
561 .subscribed = false,
562 .boxed = boxed,
563 .removed = false,
564 });
565 this->resubscribe_subscription_(&subscription);
566}
567
568// Out of line so the topic buffer is not part of unsubscribe()'s stack frame
569__attribute__((noinline)) static bool topic_equals(const MQTTTopicSource &source, const std::string &topic) {
570 char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
571 return source.build(buf) == topic;
572}
573
574void MQTTClientComponent::unsubscribe(const std::string &topic) {
575 bool ret = this->mqtt_backend_.unsubscribe(topic.c_str());
576 yield();
577 if (ret) {
578 ESP_LOGV(TAG, "unsubscribe(topic='%s')", topic.c_str());
579 } else {
580 delay(5);
581 ESP_LOGV(TAG, "Unsubscribe failed for topic='%s'.", topic.c_str());
582 this->status_momentary_warning(1000);
583 }
584
585 for (auto &subscription : this->subscriptions_) {
586 if (topic_equals(subscription.topic, topic))
587 subscription.removed = true;
588 }
589 // A callback may be running; the outermost dispatch erases them once it returns.
590 if (this->dispatching_) {
591 this->removed_while_dispatching_ = true;
592 } else {
594 }
595}
596
598 auto it = this->subscriptions_.begin();
599 while (it != this->subscriptions_.end()) {
600 if (it->removed) {
601 if (it->boxed)
602 it->callback.free_boxed();
603 if (it->topic.builder == copied_topic_builder)
604 delete[] static_cast<const char *>(it->topic.context); // NOLINT(cppcoreguidelines-owning-memory)
605 it = this->subscriptions_.erase(it);
606 } else {
607 ++it;
608 }
609 }
610}
611
612// Publish
613bool MQTTClientComponent::publish(const std::string &topic, const std::string &payload, uint8_t qos, bool retain) {
614 return this->publish(topic, payload.data(), payload.size(), qos, retain);
615}
616
617bool MQTTClientComponent::publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos,
618 bool retain) {
619 return this->publish(topic.c_str(), payload, payload_length, qos, retain);
620}
621
622bool MQTTClientComponent::publish(const MQTTMessage &message) {
623 return this->publish(message.topic.c_str(), message.payload.c_str(), message.payload.length(), message.qos,
624 message.retain);
625}
626bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos,
627 bool retain) {
628 return this->publish_json(topic.c_str(), f, qos, retain);
629}
630
631bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t payload_length, uint8_t qos,
632 bool retain) {
633 if (!this->is_connected()) {
634 return false;
635 }
636 size_t topic_len = strlen(topic);
637 bool logging_topic = (topic_len == this->log_message_.topic.size()) &&
638 (memcmp(this->log_message_.topic.c_str(), topic, topic_len) == 0);
639 bool ret = this->mqtt_backend_.publish(topic, payload, payload_length, qos, retain);
640 delay(0);
641 if (!ret && !logging_topic && this->is_connected()) {
642 delay(0);
643 ret = this->mqtt_backend_.publish(topic, payload, payload_length, qos, retain);
644 delay(0);
645 }
646
647 if (!logging_topic) {
648 if (ret) {
649 ESP_LOGV(TAG, "Publish(topic='%s' retain=%d qos=%d)", topic, retain, qos);
650 ESP_LOGVV(TAG, "Publish payload (len=%u): '%.*s'", payload_length, static_cast<int>(payload_length), payload);
651 } else {
652 ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). Will retry", topic, payload_length);
653 this->status_momentary_warning(1000);
654 }
655 }
656 return ret != 0;
657}
658
659bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) {
660 auto message = json::build_json(f);
661 return this->publish(topic, message.c_str(), message.size(), qos, retain);
662}
663
665 if (this->state_ != MQTT_CLIENT_DISABLED)
666 return;
667 ESP_LOGD(TAG, "Enabling");
669 this->last_connected_ = millis();
670 this->start_dnslookup_();
671}
672
674 if (this->state_ == MQTT_CLIENT_DISABLED)
675 return;
676 ESP_LOGD(TAG, "Disabling");
678 this->on_shutdown();
679}
680
681// Out of line so the topic buffer is off the stack while the matched callback runs
682__attribute__((noinline)) static bool topic_matches(const char *message, const MQTTTopicSource &topic) {
683 char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
684 return topic_match(message, topic.build(buf).c_str());
685}
686
687void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
688#ifdef USE_ESP8266
689 // IMPORTANT: This defer is REQUIRED to prevent stack overflow crashes on ESP8266.
690 //
691 // On ESP8266, this callback is invoked directly from the lwIP/AsyncTCP network stack
692 // which runs in the "sys" context with a very limited stack (~4KB). By the time we
693 // reach this function, the stack is already partially consumed by the network
694 // processing chain: tcp_input -> AsyncClient::_recv -> AsyncMqttClient::_onMessage -> here.
695 //
696 // MQTT subscription callbacks can trigger arbitrary user actions (automations, HTTP
697 // requests, sensor updates, etc.) which may have deep call stacks of their own.
698 // For example, an HTTP request action requires: DNS lookup -> TCP connect -> TLS
699 // handshake (if HTTPS) -> request formatting. This easily overflows the remaining
700 // system stack space, causing a LoadStoreAlignmentCause exception or silent corruption.
701 //
702 // By deferring to the main loop, we ensure callbacks execute with a fresh, full-size
703 // stack in the normal application context rather than the constrained network task.
704 //
705 // DO NOT REMOVE THIS DEFER without understanding the above. It may appear to work
706 // in simple tests but will cause crashes with complex automations.
707 this->defer([this, topic, payload]() {
708#endif
709#ifdef USE_MQTT_CRASH_LOG
710 this->handle_crash_log_request_(topic, payload);
711#endif
712 // Indexed with the count taken up front: a callback may subscribe, which can reallocate the
713 // vector, and the new entry waits for the next message.
714 // Restored rather than cleared, so only the outermost dispatch erases what was unsubscribed meanwhile.
715 const bool was_dispatching = this->dispatching_;
716 this->dispatching_ = true;
717 const size_t count = this->subscriptions_.size();
718 for (size_t i = 0; i < count; i++) {
719 const auto &subscription = this->subscriptions_[i];
720 if (!subscription.removed && topic_matches(topic.c_str(), subscription.topic))
721 subscription.callback.call(topic, payload);
722 }
723 this->dispatching_ = was_dispatching;
724 if (!was_dispatching && this->removed_while_dispatching_) {
725 this->removed_while_dispatching_ = false;
727 }
728#ifdef USE_ESP8266
729 });
730#endif
731}
732
733// Setters
735bool MQTTClientComponent::is_log_message_enabled() const { return !this->log_message_.topic.empty(); }
737void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); }
738void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); }
739const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
740void MQTTClientComponent::set_topic_prefix(std::string topic_prefix) { this->topic_prefix_ = std::move(topic_prefix); }
741const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
743 this->birth_message_.topic = "";
745}
747 this->shutdown_message_.topic = "";
749}
750bool MQTTClientComponent::is_discovery_enabled() const { return !this->discovery_info_.prefix.empty(); }
752const Availability &MQTTClientComponent::get_availability() { return this->availability_; }
754 if (this->birth_message_.topic.empty() || this->birth_message_.topic != this->last_will_.topic) {
755 this->availability_.topic = "";
756 return;
757 }
761}
762
764 this->last_will_ = std::move(message);
766}
767
769 this->birth_message_ = std::move(message);
771}
772
773void MQTTClientComponent::set_shutdown_message(MQTTMessage &&message) { this->shutdown_message_ = std::move(message); }
774
775void MQTTClientComponent::set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator,
776 MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain,
777 bool discover_ip, bool clean) {
778 this->discovery_info_.prefix = std::move(prefix);
779 this->discovery_info_.discover_ip = discover_ip;
780 this->discovery_info_.unique_id_generator = unique_id_generator;
781 this->discovery_info_.object_id_generator = object_id_generator;
782 this->discovery_info_.retain = retain;
783 this->discovery_info_.clean = clean;
784}
785
787
789 this->discovery_info_ = MQTTDiscoveryInfo{
790 .prefix = "",
791 .retain = false,
792 .discover_ip = false,
793 .clean = false,
794 .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR,
795 .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR,
796 };
797}
799 if (!this->shutdown_message_.topic.empty()) {
800 yield();
801 this->publish(this->shutdown_message_);
802 yield();
803 }
805}
806
808 this->mqtt_backend_.set_on_connect(std::forward<mqtt_on_connect_callback_t>(callback));
809}
810
812 auto callback_copy = callback;
813 this->mqtt_backend_.set_on_disconnect(std::forward<mqtt_on_disconnect_callback_t>(callback));
814 this->on_disconnect_.add(std::move(callback_copy));
815}
816
817MQTTClientComponent *global_mqtt_client = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
818
819} // namespace esphome::mqtt
820
821#endif // USE_MQTT
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
const StringRef & get_friendly_name() const
Get the friendly name of this Application set by pre_setup().
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void status_momentary_warning(uint32_t length=5000)
Set warning status flag and clear it after length ms.
void defer(const char *name, std::function< void()> &&f)
Defer a callback to the next loop() call with a const char* name.
void status_clear_warning()
Definition component.h:296
constexpr const char * c_str() const
Definition string_ref.h:78
constexpr bool empty() const
Definition string_ref.h:81
uint16_t get_port() const
Definition api_server.h:62
noise::NoiseContext & get_noise_ctx()
Definition api_server.h:92
void add_log_callback(void *instance, void(*fn)(void *, uint8_t, const char *, const char *, size_t))
Register a log callback to receive log messages.
Definition logger.h:194
void set_keep_alive(uint16_t keep_alive) final
void set_on_message(std::function< on_message_callback_t > &&callback) final
void set_client_id(const char *client_id) final
void set_on_connect(std::function< on_connect_callback_t > &&callback) final
void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final
bool subscribe(const char *topic, uint8_t qos) final
void set_server(network::IPAddress ip, uint16_t port) final
bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final
void set_clean_session(bool clean_session) final
bool unsubscribe(const char *topic) final
void set_on_disconnect(std::function< on_disconnect_callback_t > &&callback) final
void set_credentials(const char *username, const char *password) final
void set_birth_message(MQTTMessage &&message)
Set the birth message.
void start_connect_()
Reconnect to the MQTT broker if not already connected.
void setup() override
Setup the MQTT client, registering a bunch of callbacks and attempting to connect.
void disable_discovery()
Globally disable Home Assistant discovery.
void recalculate_availability_()
Re-calculate the availability property.
void subscribe(MQTTTopicSource topic, F &&callback, uint8_t qos=0)
Subscribe to an MQTT topic and call callback when a message is received.
void set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator, MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, bool discover_ip, bool clean=false)
Set the Home Assistant discovery info.
float get_setup_priority() const override
MQTT client setup priority.
void disable_log_message()
Get the topic used for logging. Defaults to "<topic_prefix>/debug" and the value is cached for speed.
const std::string & get_topic_prefix() const
Get the topic prefix of this device, using default if necessary.
void register_mqtt_component(MQTTComponent *component)
void set_last_will(MQTTMessage &&message)
Set the last will testament message.
bool publish(const MQTTMessage &message)
Publish a MQTTMessage.
void set_topic_prefix(std::string topic_prefix)
Set the topic prefix that will be prepended to all topics together with "/".
const MQTTDiscoveryInfo & get_discovery_info() const
Get Home Assistant discovery info.
void disable_birth_message()
Remove the birth message.
static void dns_found_callback(const char *name, ip_addr_t *ipaddr, void *callback_arg)
MQTTMessage last_will_
The last will message.
void set_shutdown_message(MQTTMessage &&message)
MQTTMessage birth_message_
The birth message (e.g.
std::vector< MQTTComponent * > children_
void add_subscription_(MQTTTopicSource topic, mqtt_subscription_callback_t callback, bool boxed, uint8_t qos)
void set_on_connect(mqtt_on_connect_callback_t &&callback)
optional< MQTTClientDisconnectReason > disconnect_reason_
static MQTTTopicSource copy_topic(StringRef topic)
A heap copy of topic, owned by the one subscription it is passed to and freed when that is erased.
bool subscribe_topic_(const MQTTTopicSource &topic, uint8_t qos)
void unsubscribe(const std::string &topic)
Unsubscribe from an MQTT topic.
void on_message(const std::string &topic, const std::string &payload)
void set_log_message_template(MQTTMessage &&message)
Manually set the topic used for logging.
void resubscribe_subscription_(MQTTSubscription *sub)
void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback)
bool subscribe_(const char *topic, uint8_t qos)
const Availability & get_availability()
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len)
std::vector< MQTTSubscription > subscriptions_
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.
void disable_last_will()
Remove the last will testament message.
void set_keep_alive(uint16_t keep_alive_s)
Set the keep alive time in seconds, every 0.7*keep_alive a ping will be sent.
void loop() override
Reconnect if required.
MQTTDiscoveryInfo discovery_info_
The discovery info options for Home Assistant.
CallbackManager< MQTTBackend::on_disconnect_callback_t > on_disconnect_
void handle_crash_log_request_(const std::string &topic, const std::string &payload)
Availability availability_
Caches availability.
const Component * component
Definition component.cpp:34
const LogString * message
Definition component.cpp:35
void yield(void)
const char * key
unsigned count
Definition gif.cpp:51
int ret
PROGMEM_STRING_TABLE(AlarmControlPanelStateStrings, "DISARMED", "ARMED_HOME", "ARMED_AWAY", "ARMED_NIGHT", "ARMED_VACATION", "ARMED_CUSTOM_BYPASS", "PENDING", "ARMING", "DISARMING", "TRIGGERED", "UNKNOWN")
ESPHOME_LOG_TAG(TAG, "alarm_control_panel")
APIServer * global_api_server
esphome::climate::Climate __attribute__
std::function< void(JsonObject)> json_build_t
Callback function typedef for building JsonObjects.
Definition json_util.h:164
SerializationBuffer build_json(const json_build_t &f)
Build a JSON string with the provided json build function.
Definition json_util.cpp:19
bool crash_log_pending()
Whether a crash record from before this boot is waiting to be reported.
Logger * global_logger
Definition logger.cpp:276
void log_crash_to_client()
Log the previous crash for a client that just subscribed to the logs.
std::function< MQTTBackend::on_disconnect_callback_t > mqtt_on_disconnect_callback_t
Definition mqtt_client.h:53
MQTTDiscoveryObjectIdGenerator
available discovery object_id generators
@ MQTT_NONE_OBJECT_ID_GENERATOR
MQTTTopicSource progmem_topic(ProgmemStr topic)
A topic in flash, copied into the buffer when needed, so at most MQTT_DEFAULT_TOPIC_MAX_LEN - 1 chara...
bool topic_match(const char *message, const char *subscription)
True if the message topic matches the subscription, with + and # wildcards.
MQTTDiscoveryUniqueIdGenerator
available discovery unique_id generators
@ MQTT_LEGACY_UNIQUE_ID_GENERATOR
MQTTTransport
Transport for the broker connection; only the ESP-IDF backend supports WebSocket.
Callback< void(const std::string &, const std::string &)> mqtt_subscription_callback_t
Stored callback of an MQTT subscription: the topic, then the payload.
Definition mqtt_client.h:56
MQTTClientComponent * global_mqtt_client
@ MQTT_CLIENT_RESOLVING_ADDRESS
MQTTTopicSource static_topic(const char *topic)
A topic string that outlives the subscription, such as a literal; it is referenced,...
std::function< MQTTBackend::on_connect_callback_t > mqtt_on_connect_callback_t
Callback for MQTT events.
Definition mqtt_client.h:52
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:36
network::IPAddresses get_ip_addresses()
Definition util.cpp:98
bool is_disabled()
Return whether the network is disabled: every configured interface with a disable() lifecycle (modem,...
Definition util.cpp:42
constexpr float AFTER_WIFI
For components that should be initialized after WiFi is connected.
Definition component.h:55
size_t buf_append_str_p(char *buf, size_t size, size_t pos, PGM_P str)
Safely append a PROGMEM string to buffer, returning new position (capped at size).
Definition helpers.h:1281
size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *name, size_t name_len, char sep, const char *suffix_ptr, size_t suffix_len)
Format name + separator + suffix directly into buffer without heap allocation.
Definition helpers.cpp:277
const void size_t len
Definition hal.h:68
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:848
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
const __FlashStringHelper * ProgmemStr
Definition progmem.h:31
std::string topic
Empty means disabled.
std::string address
The address of the server without port number.
Definition mqtt_client.h:96
bool clean_session
Whether the session will be cleaned or remembered between connects.
std::string client_id
The client ID. Will automatically be truncated to 23 characters.
MQTTDiscoveryUniqueIdGenerator unique_id_generator
bool discover_ip
Enable the Home Assistant device discovery.
std::string prefix
The Home Assistant discovery prefix. Empty means disabled.
MQTTDiscoveryObjectIdGenerator object_id_generator
bool retain
Whether to retain discovery messages.
char * str_to(char *buf) const
Definition ip_address.h:153
SemaphoreHandle_t lock