Compare commits

..

11 Commits

Author SHA1 Message Date
J. Nick Koston
d66d05dbfc [mqtt] Use stack buffers for publish_state() topic building 2026-01-21 11:08:06 -10:00
J. Nick Koston
bba447e656 Merge branch 'dev' into mqtt_formatting 2026-01-21 11:06:26 -10:00
J. Nick Koston
77b6720a25 Merge branch 'dev' into mqtt_formatting 2026-01-19 17:40:37 -10:00
J. Nick Koston
2f8f052f43 Merge branch 'dev' into mqtt_formatting 2026-01-18 18:44:11 -10:00
J. Nick Koston
86e70c7e76 more 2026-01-17 07:38:51 -10:00
J. Nick Koston
40025bb277 tweaks to reduce RAM 2026-01-17 07:34:22 -10:00
J. Nick Koston
438bb96687 tweaks to reduce RAM 2026-01-17 07:28:44 -10:00
J. Nick Koston
c1e1325af2 Merge branch 'dev' into mqtt_formatting 2026-01-17 07:24:11 -10:00
pre-commit-ci-lite[bot]
944194e04e [pre-commit.ci lite] apply automatic fixes 2026-01-15 00:02:35 +00:00
J. Nick Koston
d27d6d64da Update esphome/components/mqtt/mqtt_component.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-14 14:00:57 -10:00
J. Nick Koston
2182d1e9f0 [mqtt] Use stack buffers for discovery message formatting 2026-01-14 13:57:45 -10:00
28 changed files with 228 additions and 350 deletions

View File

@@ -11,7 +11,6 @@
#include <esp_ota_ops.h>
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
#include "esphome/components/http_request/http_request.h"
#include "esphome/components/json/json_util.h"
#include "esphome/components/network/util.h"
#endif
@@ -185,23 +184,15 @@ bool Esp32HostedUpdate::fetch_manifest_() {
}
// Read manifest JSON into string (manifest is small, ~1KB max)
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
std::string json_str;
json_str.reserve(container->content_length);
uint8_t buf[256];
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
while (container->get_bytes_read() < container->content_length) {
int read_or_error = container->read(buf, sizeof(buf));
App.feed_wdt();
int read = container->read(buf, sizeof(buf));
if (read > 0) {
json_str.append(reinterpret_cast<char *>(buf), read);
}
yield();
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
if (result != http_request::HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
}
container->end();
@@ -306,38 +297,32 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
}
// Stream firmware to coprocessor while computing SHA256
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
sha256::SHA256 hasher;
hasher.init();
uint8_t buffer[CHUNK_SIZE];
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
while (container->get_bytes_read() < total_size) {
int read_or_error = container->read(buffer, sizeof(buffer));
int read = container->read(buffer, sizeof(buffer));
// Feed watchdog and give other tasks a chance to run
App.feed_wdt();
yield();
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
if (result != http_request::HttpReadLoopResult::DATA) {
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading firmware data");
} else {
ESP_LOGE(TAG, "Error reading firmware data: %d", read_or_error);
// Exit loop if no data available (stream closed or end of data)
if (read <= 0) {
if (read < 0) {
ESP_LOGE(TAG, "Stream closed with error");
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
}
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
// read == 0: no more data available, exit loop
break;
}
hasher.add(buffer, read_or_error);
err = esp_hosted_slave_ota_write(buffer, read_or_error); // NOLINT
hasher.add(buffer, read);
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT

View File

@@ -79,80 +79,6 @@ inline bool is_redirect(int const status) {
*/
inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; }
/*
* HTTP Container Read Semantics
* =============================
*
* IMPORTANT: These semantics differ from standard BSD sockets!
*
* BSD socket read() returns:
* > 0: bytes read
* == 0: connection closed (EOF)
* < 0: error (check errno)
*
* HttpContainer::read() returns:
* > 0: bytes read successfully
* == 0: no data available yet (non-blocking, caller should RETRY)
* < 0: error or connection closed (caller should EXIT)
* HTTP_ERROR_CONNECTION_CLOSED (-1) = connection closed prematurely
* other negative values = platform-specific errors
*
* This non-blocking design allows consistent behavior across:
* - ESP-IDF (async mode with EAGAIN handling)
* - Arduino (available() + connected() checks)
*
* Use the helper functions below instead of checking return values directly:
* - http_read_loop_result(): for manual loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes into buffer" operations
*/
/// Error code returned by HttpContainer::read() when connection closed prematurely
/// NOTE: Unlike BSD sockets where 0 means EOF, here 0 means "no data yet, retry"
static constexpr int HTTP_ERROR_CONNECTION_CLOSED = -1;
/// Status of a read operation
enum class HttpReadStatus : uint8_t {
OK, ///< Read completed successfully
ERROR, ///< Read error occurred
TIMEOUT, ///< Timeout waiting for data
};
/// Result of an HTTP read operation
struct HttpReadResult {
HttpReadStatus status; ///< Status of the read operation
int error_code; ///< Error code from read() on failure, 0 on success
};
/// Result of processing a non-blocking read with timeout (for manual loops)
enum class HttpReadLoopResult : uint8_t {
DATA, ///< Data was read, process it
RETRY, ///< No data yet, already delayed, caller should continue loop
ERROR, ///< Read error, caller should exit loop
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
};
/// Process a read result with timeout tracking and delay handling
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
/// @param last_data_time Time of last successful read, updated when data received
/// @param timeout_ms Maximum time to wait for data
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
uint32_t timeout_ms) {
if (bytes_read_or_error > 0) {
last_data_time = millis();
return HttpReadLoopResult::DATA;
}
if (bytes_read_or_error < 0) {
return HttpReadLoopResult::ERROR;
}
// bytes_read_or_error == 0: no data available yet
if (millis() - last_data_time >= timeout_ms) {
return HttpReadLoopResult::TIMEOUT;
}
delay(1); // Small delay to prevent tight spinning
return HttpReadLoopResult::RETRY;
}
class HttpRequestComponent;
class HttpContainer : public Parented<HttpRequestComponent> {
@@ -162,28 +88,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
int status_code;
uint32_t duration_ms;
/**
* @brief Read data from the HTTP response body (non-blocking).
*
* WARNING: These semantics differ from BSD sockets!
* BSD sockets: 0 = EOF (connection closed)
* This method: 0 = no data yet (retry), negative = error/closed
*
* @param buf Buffer to read data into
* @param max_len Maximum number of bytes to read
* @return
* - > 0: Number of bytes read successfully
* - 0: No data available yet (NOT EOF!), caller should retry
* - HTTP_ERROR_CONNECTION_CLOSED (-1): Connection closed prematurely
* - < -1: Other error (platform-specific error code)
*
* Use get_bytes_read() and content_length to track progress.
* When get_bytes_read() >= content_length, all data has been received.
*
* IMPORTANT: Do not use raw return values directly. Use these helpers:
* - http_read_loop_result(): for loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes" operations
*/
virtual int read(uint8_t *buf, size_t max_len) = 0;
virtual void end() = 0;
@@ -206,38 +110,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
std::map<std::string, std::list<std::string>> response_headers_{};
};
/// Read data from HTTP container into buffer with timeout handling
/// Handles feed_wdt, yield, and timeout checking internally
/// @param container The HTTP container to read from
/// @param buffer Buffer to read into
/// @param total_size Total bytes to read
/// @param chunk_size Maximum bytes per read call
/// @param timeout_ms Read timeout in milliseconds
/// @return HttpReadResult with status and error_code on failure
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
uint32_t timeout_ms) {
size_t read_index = 0;
uint32_t last_data_time = millis();
while (read_index < total_size) {
int read_bytes_or_error = container->read(buffer + read_index, std::min(chunk_size, total_size - read_index));
App.feed_wdt();
yield();
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result == HttpReadLoopResult::ERROR)
return {HttpReadStatus::ERROR, read_bytes_or_error};
if (result == HttpReadLoopResult::TIMEOUT)
return {HttpReadStatus::TIMEOUT, 0};
read_index += read_bytes_or_error;
}
return {HttpReadStatus::OK, 0};
}
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
public:
void process(const std::shared_ptr<HttpContainer> &container, std::string &response_body) {
@@ -252,7 +124,6 @@ class HttpRequestComponent : public Component {
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
uint32_t get_timeout() const { return this->timeout_; }
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
@@ -378,21 +249,15 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
RAMAllocator<uint8_t> allocator;
uint8_t *buf = allocator.allocate(max_length);
if (buf != nullptr) {
// NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
// Use http_read_loop_result() helper instead of checking return values directly
size_t read_index = 0;
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->parent_->get_timeout();
while (container->get_bytes_read() < max_length) {
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
if (read <= 0) {
break;
}
App.feed_wdt();
yield();
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result != HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
read_index += read_or_error;
read_index += read;
}
response_body.reserve(read_index);
response_body.assign((char *) buf, read_index);

View File

@@ -139,23 +139,6 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
return container;
}
// Arduino HTTP read implementation
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// Arduino's WiFiClient is inherently non-blocking - available() returns 0 when
// no data is ready. We use connected() to distinguish "no data yet" from
// "connection closed".
//
// WiFiClient behavior:
// available() > 0: data ready to read
// available() == 0 && connected(): no data yet, still connected
// available() == 0 && !connected(): connection closed
//
// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!):
// > 0: bytes read
// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
// < 0: error/connection closed <-- connection closed returns -1, not 0
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
@@ -163,7 +146,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
WiFiClient *stream_ptr = this->client_.getStreamPtr();
if (stream_ptr == nullptr) {
ESP_LOGE(TAG, "Stream pointer vanished!");
return HTTP_ERROR_CONNECTION_CLOSED;
return -1;
}
int available_data = stream_ptr->available();
@@ -171,15 +154,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
if (bufsize == 0) {
this->duration_ms += (millis() - start);
// Check if we've read all expected content
if (this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully
}
// No data available - check if connection is still open
if (!stream_ptr->connected()) {
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely
}
return 0; // No data yet, caller should retry
return 0;
}
App.feed_wdt();

View File

@@ -100,7 +100,6 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
config.buffer_size = this->buffer_size_rx_;
config.buffer_size_tx = this->buffer_size_tx_;
config.is_async = true; // Enable non-blocking mode
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
@@ -210,67 +209,26 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
return container;
}
// ESP-IDF HTTP read implementation
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// Uses non-blocking mode (config.is_async = true) for consistent behavior with Arduino.
// esp_http_client_read() in async mode returns:
// > 0: bytes read
// 0: connection closed (end of stream) <-- BSD socket EOF semantics
// -ESP_ERR_HTTP_EAGAIN: no data available yet (would block)
// other negative: error
//
// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!):
// > 0: bytes read
// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
// < 0: error/connection closed <-- connection closed returns -1, not 0
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
// Check if we've already read all expected content
if (this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully
this->feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, max_len);
this->feed_wdt();
if (read_len > 0) {
this->bytes_read_ += read_len;
}
this->feed_wdt();
int read_len_or_error = esp_http_client_read(this->client_, (char *) buf, max_len);
this->feed_wdt();
this->duration_ms += (millis() - start);
if (read_len_or_error > 0) {
this->bytes_read_ += read_len_or_error;
return read_len_or_error;
}
// No data available yet in non-blocking mode
// ESP_ERR_HTTP_EAGAIN is returned as a negative error code
if (read_len_or_error == -ESP_ERR_HTTP_EAGAIN) {
return 0; // No data yet, caller should retry
}
// Connection closed by server
if (read_len_or_error == 0) {
// We haven't read all content yet (early check handles success case)
// Return error so caller exits immediately instead of waiting for timeout
return HTTP_ERROR_CONNECTION_CLOSED;
}
// Other negative value - real error, return the actual error code for debugging
return read_len_or_error;
return read_len;
}
void HttpContainerIDF::end() {
if (this->client_ == nullptr) {
return; // Already cleaned up
}
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
esp_http_client_close(this->client_);
esp_http_client_cleanup(this->client_);
this->client_ = nullptr;
}
void HttpContainerIDF::feed_wdt() {

View File

@@ -115,47 +115,39 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
return error_code;
}
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->parent_->get_timeout();
while (container->get_bytes_read() < container->content_length) {
// read a maximum of chunk_size bytes into buf. (real read size returned, or negative error code)
int bufsize_or_error = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize_or_error = %i", container->get_bytes_read(),
container->content_length, bufsize_or_error);
// read a maximum of chunk_size bytes into buf. (real read size returned)
int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(),
container->content_length, bufsize);
// feed watchdog and give other tasks a chance to run
App.feed_wdt();
yield();
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result != HttpReadLoopResult::DATA) {
if (result == HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading data");
} else {
ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
// Exit loop if no data available (stream closed or end of data)
if (bufsize <= 0) {
if (bufsize < 0) {
ESP_LOGE(TAG, "Stream closed with error");
this->cleanup_(std::move(backend), container);
return OTA_CONNECTION_ERROR;
}
this->cleanup_(std::move(backend), container);
return OTA_CONNECTION_ERROR;
// bufsize == 0: no more data available, exit loop
break;
}
// At this point bufsize_or_error > 0, so it's a valid size
if (bufsize_or_error <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
if (bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
// add read bytes to MD5
md5_receive.add(buf, bufsize_or_error);
md5_receive.add(buf, bufsize);
// write bytes to OTA backend
this->update_started_ = true;
error_code = backend->write(buf, bufsize_or_error);
error_code = backend->write(buf, bufsize);
if (error_code != ota::OTA_RESPONSE_OK) {
// error code explanation available at
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
container->get_bytes_read() - bufsize_or_error, container->content_length);
container->get_bytes_read() - bufsize, container->content_length);
this->cleanup_(std::move(backend), container);
return error_code;
}
@@ -252,19 +244,19 @@ bool OtaHttpRequestComponent::http_get_md5_() {
}
this->md5_expected_.resize(MD5_SIZE);
auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
this->parent_->get_timeout());
int read_len = 0;
while (container->get_bytes_read() < MD5_SIZE) {
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
if (read_len <= 0) {
break;
}
App.feed_wdt();
yield();
}
container->end();
if (result.status != HttpReadStatus::OK) {
if (result.status == HttpReadStatus::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading MD5");
} else {
ESP_LOGE(TAG, "Error reading MD5: %d", result.error_code);
}
return false;
}
return true;
ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE);
return read_len == MD5_SIZE;
}
bool OtaHttpRequestComponent::validate_url_(const std::string &url) {

View File

@@ -11,12 +11,7 @@ namespace http_request {
// The update function runs in a task only on ESP32s.
#ifdef USE_ESP32
// vTaskDelete doesn't return, but clang-tidy doesn't know that
#define UPDATE_RETURN \
do { \
vTaskDelete(nullptr); \
__builtin_unreachable(); \
} while (0)
#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task
#else
#define UPDATE_RETURN return
#endif
@@ -75,21 +70,19 @@ void HttpRequestUpdate::update_task(void *params) {
UPDATE_RETURN;
}
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
this_update->request_parent_->get_timeout());
if (read_result.status != HttpReadStatus::OK) {
if (read_result.status == HttpReadStatus::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading manifest");
} else {
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
size_t read_index = 0;
while (container->get_bytes_read() < container->content_length) {
int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
yield();
if (read_bytes <= 0) {
// Network error or connection closed - break to avoid infinite loop
break;
}
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
allocator.deallocate(data, container->content_length);
container->end();
UPDATE_RETURN;
read_index += read_bytes;
}
size_t read_index = container->get_bytes_read();
bool valid = false;
{ // Ensures the response string falls out of scope and deallocates before the task ends

View File

@@ -18,7 +18,7 @@ bool CustomMQTTDevice::publish(const std::string &topic, float value, int8_t num
}
bool CustomMQTTDevice::publish(const std::string &topic, int value) {
char buffer[24];
int len = snprintf(buffer, sizeof(buffer), "%d", value);
size_t len = buf_append_printf(buffer, sizeof(buffer), 0, "%d", value);
return global_mqtt_client->publish(topic, buffer, len);
}
bool CustomMQTTDevice::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, bool retain) {

View File

@@ -119,7 +119,8 @@ bool MQTTAlarmControlPanelComponent::publish_state() {
default:
state_s = "unknown";
}
return this->publish(this->get_state_topic_(), state_s);
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
}
} // namespace esphome::mqtt

View File

@@ -52,8 +52,9 @@ bool MQTTBinarySensorComponent::publish_state(bool state) {
if (this->binary_sensor_->is_status_binary_sensor())
return true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
const char *state_s = state ? "ON" : "OFF";
return this->publish(this->get_state_topic_(), state_s);
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
}
} // namespace esphome::mqtt

View File

@@ -98,7 +98,17 @@ void MQTTClientComponent::send_device_info_() {
uint8_t index = 0;
for (auto &ip : network::get_ip_addresses()) {
if (ip.is_set()) {
root["ip" + (index == 0 ? "" : esphome::to_string(index))] = ip.str();
char key[8]; // "ip" + up to 3 digits + null
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
if (index == 0) {
key[0] = 'i';
key[1] = 'p';
key[2] = '\0';
} else {
buf_append_printf(key, sizeof(key), 0, "ip%u", index);
}
ip.str_to(ip_buf);
root[key] = ip_buf;
index++;
}
}

View File

@@ -107,17 +107,29 @@ std::string MQTTComponent::get_command_topic_() const {
}
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
return this->publish(topic, payload.data(), payload.size());
return this->publish(topic.c_str(), payload.data(), payload.size());
}
bool MQTTComponent::publish(const std::string &topic, const char *payload, size_t payload_length) {
if (topic.empty())
return this->publish(topic.c_str(), payload, payload_length);
}
bool MQTTComponent::publish(const char *topic, const char *payload, size_t payload_length) {
if (topic[0] == '\0')
return false;
return global_mqtt_client->publish(topic, payload, payload_length, this->qos_, this->retain_);
}
bool MQTTComponent::publish(const char *topic, const char *payload) {
return this->publish(topic, payload, strlen(payload));
}
bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) {
if (topic.empty())
return this->publish_json(topic.c_str(), f);
}
bool MQTTComponent::publish_json(const char *topic, const json::json_build_t &f) {
if (topic[0] == '\0')
return false;
return global_mqtt_client->publish_json(topic, f, this->qos_, this->retain_);
}
@@ -190,27 +202,37 @@ bool MQTTComponent::send_discovery_() {
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
char friendly_name_hash[9];
snprintf(friendly_name_hash, sizeof(friendly_name_hash), "%08" PRIx32, fnv1_hash(this->friendly_name_()));
buf_append_printf(friendly_name_hash, sizeof(friendly_name_hash), 0, "%08" PRIx32,
fnv1_hash(this->friendly_name_()));
// Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678")
// MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43
char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11];
char mac_buf[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac_buf);
snprintf(unique_id, sizeof(unique_id), "%s-%s-%s", mac_buf, this->component_type(), friendly_name_hash);
buf_append_printf(unique_id, sizeof(unique_id), 0, "%s-%s-%s", mac_buf, this->component_type(),
friendly_name_hash);
root[MQTT_UNIQUE_ID] = unique_id;
} else {
// default to almost-unique ID. It's a hack but the only way to get that
// gorgeous device registry view.
root[MQTT_UNIQUE_ID] = "ESP" + std::string(this->component_type()) + object_id.c_str();
// "ESP" (3) + component_type (max 20) + object_id (max 128) + null
char unique_id_buf[3 + MQTT_COMPONENT_TYPE_MAX_LEN + OBJECT_ID_MAX_LEN + 1];
buf_append_printf(unique_id_buf, sizeof(unique_id_buf), 0, "ESP%s%s", this->component_type(),
object_id.c_str());
root[MQTT_UNIQUE_ID] = unique_id_buf;
}
const std::string &node_name = App.get_name();
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR)
root[MQTT_OBJECT_ID] = node_name + "_" + object_id.c_str();
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) {
// node_name (max 31) + "_" (1) + object_id (max 128) + null
char object_id_full[ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1];
buf_append_printf(object_id_full, sizeof(object_id_full), 0, "%s_%s", node_name.c_str(), object_id.c_str());
root[MQTT_OBJECT_ID] = object_id_full;
}
const std::string &friendly_name_ref = App.get_friendly_name();
const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref;
std::string node_area = App.get_area();
const char *node_area = App.get_area();
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
char mac[MAC_ADDRESS_BUFFER_SIZE];
@@ -221,18 +243,29 @@ bool MQTTComponent::send_discovery_() {
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")";
const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.');
device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1;
device_info[MQTT_DEVICE_MANUFACTURER] =
model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
if (model == nullptr) {
device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME;
} else {
// Extract manufacturer (part before '.') using stack buffer to avoid heap allocation
// memcpy is used instead of strncpy since we know the exact length and strncpy
// would still require manual null-termination
char manufacturer[sizeof(ESPHOME_PROJECT_NAME)];
size_t len = model - ESPHOME_PROJECT_NAME;
memcpy(manufacturer, ESPHOME_PROJECT_NAME, len);
manufacturer[len] = '\0';
device_info[MQTT_DEVICE_MANUFACTURER] = manufacturer;
}
#else
static const char ver_fmt[] PROGMEM = ESPHOME_VERSION " (config hash 0x%08" PRIx32 ")";
// Buffer sized for format string expansion: ~4 bytes net growth from format specifier to 8 hex digits, plus
// safety margin
char version_buf[sizeof(ver_fmt) + 8];
#ifdef USE_ESP8266
char fmt_buf[sizeof(ver_fmt)];
strcpy_P(fmt_buf, ver_fmt);
const char *fmt = fmt_buf;
snprintf_P(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
#else
const char *fmt = ver_fmt;
snprintf(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
#endif
device_info[MQTT_DEVICE_SW_VERSION] = str_sprintf(fmt, App.get_config_hash());
device_info[MQTT_DEVICE_SW_VERSION] = version_buf;
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
#if defined(USE_ESP8266) || defined(USE_ESP32)
device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif";
@@ -246,7 +279,7 @@ bool MQTTComponent::send_discovery_() {
device_info[MQTT_DEVICE_MANUFACTURER] = "Host";
#endif
#endif
if (!node_area.empty()) {
if (node_area[0] != '\0') {
device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area;
}

View File

@@ -149,6 +149,38 @@ class MQTTComponent : public Component {
*/
bool publish(const std::string &topic, const char *payload, size_t payload_length);
/** Send a MQTT message (no heap allocation for topic).
*
* @param topic The topic as C string.
* @param payload The payload buffer.
* @param payload_length The length of the payload.
*/
bool publish(const char *topic, const char *payload, size_t payload_length);
/** Send a MQTT message (no heap allocation for topic).
*
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
* @param payload The payload buffer.
* @param payload_length The length of the payload.
*/
bool publish(StringRef topic, const char *payload, size_t payload_length) {
return this->publish(topic.c_str(), payload, payload_length);
}
/** Send a MQTT message (no heap allocation for topic).
*
* @param topic The topic as C string.
* @param payload The null-terminated payload.
*/
bool publish(const char *topic, const char *payload);
/** Send a MQTT message (no heap allocation for topic).
*
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
* @param payload The null-terminated payload.
*/
bool publish(StringRef topic, const char *payload) { return this->publish(topic.c_str(), payload); }
/** Construct and send a JSON MQTT message.
*
* @param topic The topic.
@@ -156,6 +188,20 @@ class MQTTComponent : public Component {
*/
bool publish_json(const std::string &topic, const json::json_build_t &f);
/** Construct and send a JSON MQTT message (no heap allocation for topic).
*
* @param topic The topic as C string.
* @param f The Json Message builder.
*/
bool publish_json(const char *topic, const json::json_build_t &f);
/** Construct and send a JSON MQTT message (no heap allocation for topic).
*
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
* @param f The Json Message builder.
*/
bool publish_json(StringRef topic, const json::json_build_t &f) { return this->publish_json(topic.c_str(), f); }
/** Subscribe to a MQTT topic.
*
* @param topic The topic. Wildcards are currently not supported.

View File

@@ -115,7 +115,8 @@ bool MQTTCoverComponent::publish_state() {
: this->cover_->position == COVER_OPEN ? "open"
: traits.get_supports_position() ? "open"
: "unknown";
if (!this->publish(this->get_state_topic_(), state_s))
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf), state_s))
success = false;
return success;
}

View File

@@ -53,7 +53,8 @@ bool MQTTDateComponent::send_initial_state() {
}
}
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [year, month, day](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("year")] = year;
root[ESPHOME_F("month")] = month;

View File

@@ -66,15 +66,17 @@ bool MQTTDateTimeComponent::send_initial_state() {
}
bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
uint8_t second) {
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("year")] = year;
root[ESPHOME_F("month")] = month;
root[ESPHOME_F("day")] = day;
root[ESPHOME_F("hour")] = hour;
root[ESPHOME_F("minute")] = minute;
root[ESPHOME_F("second")] = second;
});
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf),
[year, month, day, hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("year")] = year;
root[ESPHOME_F("month")] = month;
root[ESPHOME_F("day")] = day;
root[ESPHOME_F("hour")] = hour;
root[ESPHOME_F("minute")] = minute;
root[ESPHOME_F("second")] = second;
});
}
} // namespace esphome::mqtt

View File

@@ -44,7 +44,8 @@ void MQTTEventComponent::dump_config() {
}
bool MQTTEventComponent::publish_event_(const std::string &event_type) {
return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [event_type](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[MQTT_EVENT_TYPE] = event_type;
});

View File

@@ -158,9 +158,10 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig
}
}
bool MQTTFanComponent::publish_state() {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
const char *state_s = this->state_->state ? "ON" : "OFF";
ESP_LOGD(TAG, "'%s' Sending state %s.", this->state_->get_name().c_str(), state_s);
this->publish(this->get_state_topic_(), state_s);
this->publish(this->get_state_topic_to_(topic_buf), state_s);
bool failed = false;
if (this->state_->get_traits().supports_direction()) {
bool success = this->publish(this->get_direction_state_topic(),
@@ -175,7 +176,7 @@ bool MQTTFanComponent::publish_state() {
auto traits = this->state_->get_traits();
if (traits.supports_speed()) {
char buf[12];
int len = snprintf(buf, sizeof(buf), "%d", this->state_->speed);
size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed);
bool success = this->publish(this->get_speed_level_state_topic(), buf, len);
failed = failed || !success;
}

View File

@@ -34,7 +34,8 @@ void MQTTJSONLightComponent::on_light_remote_values_update() {
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
bool MQTTJSONLightComponent::publish_state_() {
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [this](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
LightJSONSchema::dump_json(*this->state_, root);
});

View File

@@ -47,13 +47,14 @@ void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfi
bool MQTTLockComponent::send_initial_state() { return this->publish_state(); }
bool MQTTLockComponent::publish_state() {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
#ifdef USE_STORE_LOG_STR_IN_FLASH
char buf[LOCK_STATE_STR_SIZE];
strncpy_P(buf, (PGM_P) lock_state_to_string(this->lock_->state), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
return this->publish(this->get_state_topic_(), buf);
return this->publish(this->get_state_topic_to_(topic_buf), buf);
#else
return this->publish(this->get_state_topic_(), LOG_STR_ARG(lock_state_to_string(this->lock_->state)));
return this->publish(this->get_state_topic_to_(topic_buf), LOG_STR_ARG(lock_state_to_string(this->lock_->state)));
#endif
}

View File

@@ -74,9 +74,10 @@ bool MQTTNumberComponent::send_initial_state() {
}
}
bool MQTTNumberComponent::publish_state(float value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
char buffer[64];
snprintf(buffer, sizeof(buffer), "%f", value);
return this->publish(this->get_state_topic_(), buffer);
size_t len = buf_append_printf(buffer, sizeof(buffer), 0, "%f", value);
return this->publish(this->get_state_topic_to_(topic_buf), buffer, len);
}
} // namespace esphome::mqtt

View File

@@ -50,7 +50,8 @@ bool MQTTSelectComponent::send_initial_state() {
}
}
bool MQTTSelectComponent::publish_state(const std::string &value) {
return this->publish(this->get_state_topic_(), value);
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size());
}
} // namespace esphome::mqtt

View File

@@ -79,12 +79,13 @@ bool MQTTSensorComponent::send_initial_state() {
}
}
bool MQTTSensorComponent::publish_state(float value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (mqtt::global_mqtt_client->is_publish_nan_as_none() && std::isnan(value))
return this->publish(this->get_state_topic_(), "None", 4);
return this->publish(this->get_state_topic_to_(topic_buf), "None", 4);
int8_t accuracy = this->sensor_->get_accuracy_decimals();
char buf[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(buf, value, accuracy);
return this->publish(this->get_state_topic_(), buf, len);
return this->publish(this->get_state_topic_to_(topic_buf), buf, len);
}
} // namespace esphome::mqtt

View File

@@ -52,8 +52,9 @@ void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); }
bool MQTTSwitchComponent::publish_state(bool state) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
const char *state_s = state ? "ON" : "OFF";
return this->publish(this->get_state_topic_(), state_s);
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
}
} // namespace esphome::mqtt

View File

@@ -53,7 +53,8 @@ bool MQTTTextComponent::send_initial_state() {
}
}
bool MQTTTextComponent::publish_state(const std::string &value) {
return this->publish(this->get_state_topic_(), value);
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size());
}
} // namespace esphome::mqtt

View File

@@ -31,7 +31,10 @@ void MQTTTextSensor::dump_config() {
LOG_MQTT_COMPONENT(true, false);
}
bool MQTTTextSensor::publish_state(const std::string &value) { return this->publish(this->get_state_topic_(), value); }
bool MQTTTextSensor::publish_state(const std::string &value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size());
}
bool MQTTTextSensor::send_initial_state() {
if (this->sensor_->has_state()) {
return this->publish_state(this->sensor_->state);

View File

@@ -53,7 +53,8 @@ bool MQTTTimeComponent::send_initial_state() {
}
}
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("hour")] = hour;
root[ESPHOME_F("minute")] = minute;

View File

@@ -28,7 +28,8 @@ void MQTTUpdateComponent::setup() {
}
bool MQTTUpdateComponent::publish_state() {
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [this](JsonObject root) {
root[ESPHOME_F("installed_version")] = this->update_->update_info.current_version;
root[ESPHOME_F("latest_version")] = this->update_->update_info.latest_version;
root[ESPHOME_F("title")] = this->update_->update_info.title;

View File

@@ -84,7 +84,8 @@ bool MQTTValveComponent::publish_state() {
: this->valve_->position == VALVE_OPEN ? "open"
: traits.get_supports_position() ? "open"
: "unknown";
if (!this->publish(this->get_state_topic_(), state_s))
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf), state_s))
success = false;
return success;
}