diff --git a/esphome/components/a01nyub/a01nyub.cpp b/esphome/components/a01nyub/a01nyub.cpp index d0bc89a0c9..210c3557b3 100644 --- a/esphome/components/a01nyub/a01nyub.cpp +++ b/esphome/components/a01nyub/a01nyub.cpp @@ -30,7 +30,9 @@ void A01nyubComponent::check_buffer_() { ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters); this->publish_state(meters); } else { - ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); + char hex_buf[format_hex_pretty_size(4)]; + ESP_LOGW(TAG, "Invalid data read from sensor: %s", + format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_.size())); } } else { ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); diff --git a/esphome/components/a02yyuw/a02yyuw.cpp b/esphome/components/a02yyuw/a02yyuw.cpp index ee378c3283..a2aad0cef1 100644 --- a/esphome/components/a02yyuw/a02yyuw.cpp +++ b/esphome/components/a02yyuw/a02yyuw.cpp @@ -29,7 +29,9 @@ void A02yyuwComponent::check_buffer_() { ESP_LOGV(TAG, "Distance from sensor: %f mm", distance); this->publish_state(distance); } else { - ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); + char hex_buf[format_hex_pretty_size(4)]; + ESP_LOGW(TAG, "Invalid data read from sensor: %s", + format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_.size())); } } else { ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h index f47389fd05..53f8604b7d 100644 --- a/esphome/components/addressable_light/addressable_light_display.h +++ b/esphome/components/addressable_light/addressable_light_display.h @@ -25,11 +25,13 @@ class AddressableLightDisplay : public display::DisplayBuffer { if (enabled_ && !enabled) { // enabled -> disabled // - Tell the parent light to refresh, effectively wiping the display. Also // restores the previous effect (if any). - light_state_->make_call().set_effect(this->last_effect_).perform(); + if (this->last_effect_index_.has_value()) { + light_state_->make_call().set_effect(*this->last_effect_index_).perform(); + } } else if (!enabled_ && enabled) { // disabled -> enabled - // - Save the current effect. - this->last_effect_ = light_state_->get_effect_name(); + // - Save the current effect index. + this->last_effect_index_ = light_state_->get_current_effect_index(); // - Disable any current effect. light_state_->make_call().set_effect(0).perform(); } @@ -56,7 +58,7 @@ class AddressableLightDisplay : public display::DisplayBuffer { int32_t width_; int32_t height_; std::vector addressable_light_buffer_; - optional last_effect_; + optional last_effect_index_; optional> pixel_mapper_f_; }; } // namespace addressable_light diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 26ddb16e9a..2ecd54bb00 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1530,7 +1530,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 13; + resp.api_version_minor = 14; // Send only the version string - the client only logs this for debugging and doesn't use it otherwise resp.set_server_info(ESPHOME_VERSION_REF); resp.set_name(StringRef(App.get_name())); @@ -1749,20 +1749,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { // the action list. This ensures async actions (delays, waits) complete first. } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES -void APIConnection::send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message) { +void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message) { ExecuteServiceResponse resp; resp.call_id = call_id; resp.success = success; - resp.set_error_message(StringRef(error_message)); + resp.set_error_message(error_message); this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON -void APIConnection::send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message, +void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message, const uint8_t *response_data, size_t response_data_len) { ExecuteServiceResponse resp; resp.call_id = call_id; resp.success = success; - resp.set_error_message(StringRef(error_message)); + resp.set_error_message(error_message); resp.response_data = response_data; resp.response_data_len = response_data_len; this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 6363116900..59c42aa033 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -24,9 +24,10 @@ struct ClientInfo { // Keepalive timeout in milliseconds static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; // Maximum number of entities to process in a single batch during initial state/info sending -// This was increased from 20 to 24 after removing the unique_id field from entity info messages, -// which reduced message sizes allowing more entities per batch without exceeding packet limits -static constexpr size_t MAX_INITIAL_PER_BATCH = 24; +// API 1.14+ clients compute object_id client-side, so messages are smaller and we can fit more per batch +// TODO: Remove MAX_INITIAL_PER_BATCH_LEGACY before 2026.7.0 - all clients should support API 1.14 by then +static constexpr size_t MAX_INITIAL_PER_BATCH_LEGACY = 24; // For clients < API 1.14 (includes object_id) +static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= API 1.14 (no object_id) // Maximum number of packets to process in a single batch (platform-dependent) // This limit exists to prevent stack overflow from the PacketInfo array in process_batch_ // Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes @@ -233,9 +234,9 @@ class APIConnection final : public APIServerConnection { #ifdef USE_API_USER_DEFINED_ACTIONS void execute_service(const ExecuteServiceRequest &msg) override; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - void send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message); + void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - void send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message, + void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message, const uint8_t *response_data, size_t response_data_len); #endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #endif // USE_API_USER_DEFINED_ACTION_RESPONSES @@ -323,10 +324,16 @@ class APIConnection final : public APIServerConnection { APIConnection *conn, uint32_t remaining_size, bool is_single) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); - // Get object_id with zero heap allocation - // Static case returns direct reference, dynamic case uses buffer + + // API 1.14+ clients compute object_id client-side from the entity name + // For older clients, we must send object_id for backward compatibility + // See: https://github.com/esphome/backlog/issues/76 + // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then + // Buffer must remain in scope until encode_message_to_buffer is called char object_id_buf[OBJECT_ID_MAX_LEN]; - msg.set_object_id(entity->get_object_id_to(object_id_buf)); + if (!conn->client_supports_api_version(1, 14)) { + msg.set_object_id(entity->get_object_id_to(object_id_buf)); + } if (entity->has_own_name()) { msg.set_name(entity->get_name()); @@ -349,16 +356,24 @@ class APIConnection final : public APIServerConnection { inline bool check_voice_assistant_api_connection_() const; #endif + // Get the max batch size based on client API version + // API 1.14+ clients don't receive object_id, so messages are smaller and more fit per batch + // TODO: Remove this method before 2026.7.0 and use MAX_INITIAL_PER_BATCH directly + size_t get_max_batch_size_() const { + return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY; + } + // Helper method to process multiple entities from an iterator in a batch template void process_iterator_batch_(Iterator &iterator) { size_t initial_size = this->deferred_batch_.size(); - while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) { + size_t max_batch = this->get_max_batch_size_(); + while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) { iterator.advance(); } // If the batch is full, process it immediately // Note: iterator.advance() already calls schedule_batch_() via schedule_message_() - if (this->deferred_batch_.size() >= MAX_INITIAL_PER_BATCH) { + if (this->deferred_batch_.size() >= max_batch) { this->process_batch_(); } } diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 20f8fcaf61..420f42a90a 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -13,12 +13,26 @@ namespace esphome::api { static const char *const TAG = "api.frame_helper"; +// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) +static constexpr size_t API_MAX_LOG_BYTES = 168; + #define HELPER_LOG(msg, ...) \ ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) #ifdef HELPER_LOG_PACKETS -#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) -#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) +#define LOG_PACKET_RECEIVED(buffer) \ + do { \ + char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ + ESP_LOGVV(TAG, "Received frame: %s", \ + format_hex_pretty_to(hex_buf_, (buffer).data(), \ + (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \ + } while (0) +#define LOG_PACKET_SENDING(data, len) \ + do { \ + char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ + ESP_LOGVV(TAG, "Sending raw: %s", \ + format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \ + } while (0) #else #define LOG_PACKET_RECEIVED(buffer) ((void) 0) #define LOG_PACKET_SENDING(data, len) ((void) 0) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 1d6f32ee9d..37b497e2a1 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -24,12 +24,26 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit"; #endif static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") +// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) +static constexpr size_t API_MAX_LOG_BYTES = 168; + #define HELPER_LOG(msg, ...) \ ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) #ifdef HELPER_LOG_PACKETS -#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) -#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) +#define LOG_PACKET_RECEIVED(buffer) \ + do { \ + char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ + ESP_LOGVV(TAG, "Received frame: %s", \ + format_hex_pretty_to(hex_buf_, (buffer).data(), \ + (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \ + } while (0) +#define LOG_PACKET_SENDING(data, len) \ + do { \ + char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ + ESP_LOGVV(TAG, "Sending raw: %s", \ + format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \ + } while (0) #else #define LOG_PACKET_RECEIVED(buffer) ((void) 0) #define LOG_PACKET_SENDING(data, len) ((void) 0) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index b5d90b2429..8b7d002d7c 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -18,12 +18,26 @@ namespace esphome::api { static const char *const TAG = "api.plaintext"; +// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) +static constexpr size_t API_MAX_LOG_BYTES = 168; + #define HELPER_LOG(msg, ...) \ ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) #ifdef HELPER_LOG_PACKETS -#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) -#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) +#define LOG_PACKET_RECEIVED(buffer) \ + do { \ + char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ + ESP_LOGVV(TAG, "Received frame: %s", \ + format_hex_pretty_to(hex_buf_, (buffer).data(), \ + (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \ + } while (0) +#define LOG_PACKET_SENDING(data, len) \ + do { \ + char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ + ESP_LOGVV(TAG, "Sending raw: %s", \ + format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \ + } while (0) #else #define LOG_PACKET_RECEIVED(buffer) ((void) 0) #define LOG_PACKET_SENDING(data, len) ((void) 0) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 23cecd2663..8b76740e4d 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -678,7 +678,7 @@ void APIServer::unregister_active_action_calls_for_connection(APIConnection *con } } -void APIServer::send_action_response(uint32_t action_call_id, bool success, const std::string &error_message) { +void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message) { for (auto &call : this->active_action_calls_) { if (call.action_call_id == action_call_id) { call.connection->send_execute_service_response(call.client_call_id, success, error_message); @@ -688,7 +688,7 @@ void APIServer::send_action_response(uint32_t action_call_id, bool success, cons ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id); } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON -void APIServer::send_action_response(uint32_t action_call_id, bool success, const std::string &error_message, +void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message, const uint8_t *response_data, size_t response_data_len) { for (auto &call : this->active_action_calls_) { if (call.action_call_id == action_call_id) { diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 11d726a40a..b855d2cce0 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -165,9 +165,9 @@ class APIServer : public Component, void unregister_active_action_call(uint32_t action_call_id); void unregister_active_action_calls_for_connection(APIConnection *conn); // Send response for a specific action call (uses action_call_id, sends client_call_id in response) - void send_action_response(uint32_t action_call_id, bool success, const std::string &error_message); + void send_action_response(uint32_t action_call_id, bool success, StringRef error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - void send_action_response(uint32_t action_call_id, bool success, const std::string &error_message, + void send_action_response(uint32_t action_call_id, bool success, StringRef error_message, const uint8_t *response_data, size_t response_data_len); #endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #endif // USE_API_USER_DEFINED_ACTION_RESPONSES diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 001add626f..8e3a61b279 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -255,7 +255,7 @@ template class APIRespondAction : public Action { bool return_response = std::get<1>(args); if (!return_response) { // Client doesn't want response data, just send success/error - this->parent_->send_action_response(call_id, success, error_message); + this->parent_->send_action_response(call_id, success, StringRef(error_message)); return; } } @@ -265,12 +265,12 @@ template class APIRespondAction : public Action { json::JsonBuilder builder; this->json_builder_(x..., builder.root()); std::string json_str = builder.serialize(); - this->parent_->send_action_response(call_id, success, error_message, + this->parent_->send_action_response(call_id, success, StringRef(error_message), reinterpret_cast(json_str.data()), json_str.size()); return; } #endif - this->parent_->send_action_response(call_id, success, error_message); + this->parent_->send_action_response(call_id, success, StringRef(error_message)); } protected: diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp index 5188b2047f..5743cbd671 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.cpp +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -47,7 +47,10 @@ struct DNSAnswer { void DNSServer::start(const network::IPAddress &ip) { this->server_ip_ = ip; - ESP_LOGV(TAG, "Starting DNS server on %s", ip.str().c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGV(TAG, "Starting DNS server on %s", ip.str_to(ip_buf)); +#endif // Create loop-monitored UDP socket this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP); diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index 9a4e342525..f47b67da6f 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -128,7 +128,9 @@ void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this-> bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); } -std::string CH422GGPIOPin::dump_summary() const { return str_sprintf("EXIO%u via CH422G", pin_); } +size_t CH422GGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "EXIO%u via CH422G", this->pin_); +} void CH422GGPIOPin::set_flags(gpio::Flags flags) { flags_ = flags; this->parent_->pin_mode(this->pin_, flags); diff --git a/esphome/components/ch422g/ch422g.h b/esphome/components/ch422g/ch422g.h index 1193a3db27..8ed63db90a 100644 --- a/esphome/components/ch422g/ch422g.h +++ b/esphome/components/ch422g/ch422g.h @@ -50,7 +50,7 @@ class CH422GGPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(CH422GComponent *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index fe81ae91fe..71fe15f0ae 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -1,11 +1,13 @@ #include "cse7766.h" -#include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" namespace esphome { namespace cse7766 { static const char *const TAG = "cse7766"; +static constexpr size_t CSE7766_RAW_DATA_SIZE = 24; void CSE7766Component::loop() { const uint32_t now = App.get_loop_component_start_time(); @@ -70,8 +72,8 @@ bool CSE7766Component::check_byte_() { void CSE7766Component::parse_data_() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE { - std::string s = format_hex_pretty(this->raw_data_, sizeof(this->raw_data_)); - ESP_LOGVV(TAG, "Raw data: %s", s.c_str()); + char hex_buf[format_hex_pretty_size(CSE7766_RAW_DATA_SIZE)]; + ESP_LOGVV(TAG, "Raw data: %s", format_hex_pretty_to(hex_buf, this->raw_data_, sizeof(this->raw_data_))); } #endif diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index b2e58694c8..4e6b4a7fd6 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -7,6 +7,7 @@ namespace esphome::epaper_spi { static const char *const TAG = "epaper_spi"; +static constexpr size_t EPAPER_MAX_CMD_LOG_BYTES = 128; static constexpr const char *const EPAPER_STATE_STRINGS[] = { "IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE", @@ -68,8 +69,11 @@ void EPaperBase::data(uint8_t value) { // The command is the first byte, length is the length of data only in the second byte, followed by the data. // [COMMAND, LENGTH, DATA...] void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(EPAPER_MAX_CMD_LOG_BYTES)]; ESP_LOGV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, - format_hex_pretty(ptr, length, '.', false).c_str()); + format_hex_pretty_to(hex_buf, ptr, length, '.')); +#endif this->dc_pin_->digital_write(false); this->enable(); diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index d8397a87cc..da550e58dc 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -85,6 +85,7 @@ CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" +CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" LOG_LEVELS_IDF = [ @@ -109,6 +110,21 @@ COMPILER_OPTIMIZATIONS = { "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", } +# ESP32 (original) chip revision options +# Setting minimum revision to 3.0 or higher: +# - Reduces flash size by excluding workaround code for older chip bugs +# - For PSRAM users: disables CONFIG_SPIRAM_CACHE_WORKAROUND, which saves significant +# IRAM by keeping C library functions in ROM instead of recompiling them +# See: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/chip_revision.html +ESP32_CHIP_REVISIONS = { + "0.0": "CONFIG_ESP32_REV_MIN_0", + "1.0": "CONFIG_ESP32_REV_MIN_1", + "1.1": "CONFIG_ESP32_REV_MIN_1_1", + "2.0": "CONFIG_ESP32_REV_MIN_2", + "3.0": "CONFIG_ESP32_REV_MIN_3", + "3.1": "CONFIG_ESP32_REV_MIN_3_1", +} + # Socket limit configuration for ESP-IDF # ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10 DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default @@ -566,6 +582,16 @@ def final_validate(config): path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC], ) ) + if ( + config[CONF_VARIANT] != VARIANT_ESP32 + and advanced.get(CONF_MINIMUM_CHIP_REVISION) is not None + ): + errs.append( + cv.Invalid( + f"'{CONF_MINIMUM_CHIP_REVISION}' is only supported on {VARIANT_ESP32}", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION], + ) + ) if advanced[CONF_EXECUTE_FROM_PSRAM]: if config[CONF_VARIANT] != VARIANT_ESP32S3: errs.append( @@ -694,6 +720,9 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean, cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, + cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of( + *ESP32_CHIP_REVISIONS + ), # DHCP server is needed for WiFi AP mode. When WiFi component is used, # it will handle disabling DHCP server when AP is not configured. # Default to false (disabled) when WiFi is not used. @@ -1017,6 +1046,16 @@ async def to_code(config): add_idf_sdkconfig_option( f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True ) + + # Set minimum chip revision for ESP32 variant + # Setting this to 3.0 or higher reduces flash size by excluding workaround code, + # and for PSRAM users saves significant IRAM by keeping C library functions in ROM. + if variant == VARIANT_ESP32: + min_rev = conf[CONF_ADVANCED].get(CONF_MINIMUM_CHIP_REVISION) + if min_rev is not None: + for rev, flag in ESP32_CHIP_REVISIONS.items(): + add_idf_sdkconfig_option(flag, rev == min_rev) + cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET") add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv") diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index a98245b889..4b53d3a172 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -97,10 +97,8 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi gpio_isr_handler_add(this->get_pin_num(), func, arg); } -std::string ESP32InternalGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%" PRIu32, static_cast(this->pin_)); - return buffer; +size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "GPIO%" PRIu32, static_cast(this->pin_)); } void ESP32InternalGPIOPin::setup() { diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index d30f4bdcba..3c13bd9b4f 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -24,7 +24,7 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return this->pin_; } diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 0ad54bbb15..4a6aec1892 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -4,6 +4,7 @@ #include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble_server/ble_2902.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -14,6 +15,7 @@ namespace esp32_improv { using namespace bytebuffer; static const char *const TAG = "esp32_improv.component"; +static constexpr size_t IMPROV_MAX_LOG_BYTES = 128; static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; static constexpr uint16_t STOP_ADVERTISING_DELAY = 10000; // Delay (ms) before stopping service to allow BLE clients to read the final state @@ -314,7 +316,11 @@ void ESP32ImprovComponent::dump_config() { void ESP32ImprovComponent::process_incoming_data_() { uint8_t length = this->incoming_data_[1]; - ESP_LOGV(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(IMPROV_MAX_LOG_BYTES)]; + ESP_LOGV(TAG, "Processing bytes - %s", + format_hex_pretty_to(hex_buf, this->incoming_data_.data(), this->incoming_data_.size())); +#endif if (this->incoming_data_.size() - 3 == length) { this->set_error_(improv::ERROR_NONE); improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_); @@ -403,8 +409,12 @@ void ESP32ImprovComponent::check_wifi_connection_() { #ifdef USE_WEBSERVER for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { if (ip.is_ip4()) { - char url_buffer[64]; - snprintf(url_buffer, sizeof(url_buffer), "http://%s:%d", ip.str().c_str(), USE_WEBSERVER_PORT); + // "http://" (7) + IPv4 max (15) + ":" (1) + port max (5) + null = 29 + char url_buffer[32]; + memcpy(url_buffer, "http://", 7); // NOLINT(bugprone-not-null-terminated-result) - str_to null-terminates + ip.str_to(url_buffer + 7); + size_t len = strlen(url_buffer); + snprintf(url_buffer + len, sizeof(url_buffer) - len, ":%d", USE_WEBSERVER_PORT); url_strings[url_count++] = url_buffer; break; } diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index 124df39ce3..7a5ee08984 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -98,10 +98,8 @@ void ESP8266GPIOPin::pin_mode(gpio::Flags flags) { pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT } -std::string ESP8266GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); - return buffer; +size_t ESP8266GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "GPIO%u", this->pin_); } bool ESP8266GPIOPin::digital_read() { diff --git a/esphome/components/esp8266/gpio.h b/esphome/components/esp8266/gpio.h index 213a5c54be..ff149abfbe 100644 --- a/esphome/components/esp8266/gpio.h +++ b/esphome/components/esp8266/gpio.h @@ -17,7 +17,7 @@ class ESP8266GPIOPin : public InternalGPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return pin_; } diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index a872cf7015..2135189bd5 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_SPEED, DEVICE_CLASS_SPEED, STATE_CLASS_MEASUREMENT, + STATE_CLASS_MEASUREMENT_ANGLE, UNIT_DEGREES, UNIT_KILOMETER_PER_HOUR, UNIT_METER, @@ -21,6 +22,7 @@ CONF_HDOP = "hdop" ICON_ALTIMETER = "mdi:altimeter" ICON_COMPASS = "mdi:compass" +ICON_CIRCLE_DOUBLE = "mdi:circle-double" ICON_LATITUDE = "mdi:latitude" ICON_LONGITUDE = "mdi:longitude" ICON_SATELLITE = "mdi:satellite-variant" @@ -50,7 +52,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_DEGREES, icon=ICON_LONGITUDE, accuracy_decimals=6, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_MEASUREMENT_ANGLE, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( unit_of_measurement=UNIT_KILOMETER_PER_HOUR, @@ -63,7 +65,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_DEGREES, icon=ICON_COMPASS, accuracy_decimals=2, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_MEASUREMENT_ANGLE, ), cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_METER, @@ -72,11 +74,14 @@ CONFIG_SCHEMA = cv.All( state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SATELLITES): sensor.sensor_schema( + # no unit_of_measurement icon=ICON_SATELLITE, accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HDOP): sensor.sensor_schema( + # no unit_of_measurement + icon=ICON_CIRCLE_DOUBLE, accuracy_decimals=3, state_class=STATE_CLASS_MEASUREMENT, ), diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index 73696bd2a5..70a05e4f72 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -34,9 +34,9 @@ void HLW8012Component::setup() { } void HLW8012Component::dump_config() { ESP_LOGCONFIG(TAG, "HLW8012:"); - LOG_PIN(" SEL Pin: ", this->sel_pin_) - LOG_PIN(" CF Pin: ", this->cf_pin_) - LOG_PIN(" CF1 Pin: ", this->cf1_pin_) + LOG_PIN(" SEL Pin: ", this->sel_pin_); + LOG_PIN(" CF Pin: ", this->cf_pin_); + LOG_PIN(" CF1 Pin: ", this->cf1_pin_); ESP_LOGCONFIG(TAG, " Change measurement mode every %" PRIu32 "\n" " Current resistor: %.1f mΩ\n" diff --git a/esphome/components/host/gpio.cpp b/esphome/components/host/gpio.cpp index e46f158513..f99b82bcc2 100644 --- a/esphome/components/host/gpio.cpp +++ b/esphome/components/host/gpio.cpp @@ -25,11 +25,7 @@ void HostGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::Interr } void HostGPIOPin::pin_mode(gpio::Flags flags) { ESP_LOGD(TAG, "Setting pin %d mode to %02X", pin_, (uint32_t) flags); } -std::string HostGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); - return buffer; -} +size_t HostGPIOPin::dump_summary(char *buffer, size_t len) const { return snprintf(buffer, len, "GPIO%u", this->pin_); } bool HostGPIOPin::digital_read() { return inverted_; } void HostGPIOPin::digital_write(bool value) { diff --git a/esphome/components/host/gpio.h b/esphome/components/host/gpio.h index ae677291b9..ea6b13f436 100644 --- a/esphome/components/host/gpio.h +++ b/esphome/components/host/gpio.h @@ -17,7 +17,7 @@ class HostGPIOPin : public InternalGPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return pin_; } diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index b7d3be63fe..cde6886109 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -7,6 +7,8 @@ namespace hte501 { static const char *const TAG = "hte501"; +static constexpr size_t HTE501_SERIAL_NUMBER_SIZE = 7; + void HTE501Component::setup() { uint8_t address[] = {0x70, 0x29}; uint8_t identification[9]; @@ -16,7 +18,10 @@ void HTE501Component::setup() { this->mark_failed(); return; } - ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(identification + 0, 7).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char serial_hex[format_hex_size(HTE501_SERIAL_NUMBER_SIZE)]; +#endif + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex_to(serial_hex, identification, HTE501_SERIAL_NUMBER_SIZE)); } void HTE501Component::dump_config() { diff --git a/esphome/components/jsn_sr04t/jsn_sr04t.cpp b/esphome/components/jsn_sr04t/jsn_sr04t.cpp index 84181dac48..6fd8b1bd65 100644 --- a/esphome/components/jsn_sr04t/jsn_sr04t.cpp +++ b/esphome/components/jsn_sr04t/jsn_sr04t.cpp @@ -39,7 +39,9 @@ void Jsnsr04tComponent::check_buffer_() { ESP_LOGV(TAG, "Distance from sensor: %umm, %.3fm", distance, meters); this->publish_state(meters); } else { - ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); + char hex_buf[format_hex_pretty_size(4)]; + ESP_LOGW(TAG, "Invalid data read from sensor: %s", + format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_.size())); } } else { ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); diff --git a/esphome/components/kuntze/kuntze.cpp b/esphome/components/kuntze/kuntze.cpp index 30f98aaa99..1b772d062c 100644 --- a/esphome/components/kuntze/kuntze.cpp +++ b/esphome/components/kuntze/kuntze.cpp @@ -1,4 +1,5 @@ #include "kuntze.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/application.h" @@ -10,11 +11,17 @@ static const char *const TAG = "kuntze"; static const uint8_t CMD_READ_REG = 0x03; static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832}; +// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5) +static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8; + void Kuntze::on_modbus_data(const std::vector &data) { auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); }; this->waiting_ = false; - ESP_LOGV(TAG, "Data: %s", format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(KUNTZE_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Data: %s", format_hex_pretty_to(hex_buf, data.data(), data.size())); float value = (float) get_16bit(0); for (int i = 0; i < data[3]; i++) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index bb2e4e2f4c..5ea47d5084 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -413,7 +413,8 @@ bool LD2410Component::handle_ack_data_() { return true; } if (!ld2410::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { - ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); + char hex_buf[format_hex_pretty_size(HEADER_FOOTER_SIZE)]; + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, HEADER_FOOTER_SIZE)); return true; } if (this->buffer_data_[COMMAND_STATUS] != 0x01) { @@ -597,11 +598,17 @@ void LD2410Component::readline_(int readch) { return; // Not enough data to process yet } if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { - ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)]; + ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_)); +#endif this->handle_periodic_data_(); this->buffer_pos_ = 0; // Reset position index for next message } else if (ld2410::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { - ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)]; + ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_)); +#endif if (this->handle_ack_data_()) { this->buffer_pos_ = 0; // Reset position index for next message } else { diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 0f6fe62d30..3d51800065 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -457,7 +457,8 @@ bool LD2412Component::handle_ack_data_() { return true; } if (!ld2412::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { - ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); + char hex_buf[format_hex_pretty_size(HEADER_FOOTER_SIZE)]; + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, HEADER_FOOTER_SIZE)); return true; } if (this->buffer_data_[COMMAND_STATUS] != 0x01) { @@ -670,11 +671,17 @@ void LD2412Component::readline_(int readch) { return; // Not enough data to process yet } if (ld2412::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { - ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)]; + ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_)); +#endif this->handle_periodic_data_(); this->buffer_pos_ = 0; // Reset position index for next message } else if (ld2412::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { - ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)]; + ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_)); +#endif if (this->handle_ack_data_()) { this->buffer_pos_ = 0; // Reset position index for next message } else { diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index e69ef31d4f..2c137c3578 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -607,7 +607,8 @@ bool LD2450Component::handle_ack_data_() { return true; } if (!ld2450::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { - ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); + char hex_buf[format_hex_pretty_size(HEADER_FOOTER_SIZE)]; + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, HEADER_FOOTER_SIZE)); return true; } if (this->buffer_data_[COMMAND_STATUS] != 0x01) { @@ -758,11 +759,17 @@ void LD2450Component::readline_(int readch) { } if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] && this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[1]) { - ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)]; + ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_)); +#endif this->handle_periodic_data_(); this->buffer_pos_ = 0; // Reset position index for next frame } else if (ld2450::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { - ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)]; + ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_)); +#endif if (this->handle_ack_data_()) { this->buffer_pos_ = 0; // Reset position index for next message } else { diff --git a/esphome/components/libretiny/gpio_arduino.cpp b/esphome/components/libretiny/gpio_arduino.cpp index 7a1e014ea4..0b14c77cf2 100644 --- a/esphome/components/libretiny/gpio_arduino.cpp +++ b/esphome/components/libretiny/gpio_arduino.cpp @@ -63,10 +63,8 @@ void ArduinoInternalGPIOPin::pin_mode(gpio::Flags flags) { pinMode(pin_, flags_to_mode(flags)); // NOLINT } -std::string ArduinoInternalGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u", pin_); - return buffer; +size_t ArduinoInternalGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u", this->pin_); } bool ArduinoInternalGPIOPin::digital_read() { diff --git a/esphome/components/libretiny/gpio_arduino.h b/esphome/components/libretiny/gpio_arduino.h index 3674748c18..30c7c33869 100644 --- a/esphome/components/libretiny/gpio_arduino.h +++ b/esphome/components/libretiny/gpio_arduino.h @@ -16,7 +16,7 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return pin_; } diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp index a377a1a192..13fe5a5323 100644 --- a/esphome/components/max6956/max6956.cpp +++ b/esphome/components/max6956/max6956.cpp @@ -161,10 +161,8 @@ void MAX6956GPIOPin::setup() { pin_mode(flags_); } void MAX6956GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool MAX6956GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MAX6956GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string MAX6956GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via Max6956", pin_); - return buffer; +size_t MAX6956GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via Max6956", this->pin_); } } // namespace max6956 diff --git a/esphome/components/max6956/max6956.h b/esphome/components/max6956/max6956.h index 0a1fd5e4b5..0c609b0b43 100644 --- a/esphome/components/max6956/max6956.h +++ b/esphome/components/max6956/max6956.h @@ -76,7 +76,7 @@ class MAX6956GPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(MAX6956 *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index be86cb2256..87c2668962 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -99,10 +99,8 @@ void MCP23016GPIOPin::setup() { pin_mode(flags_); } void MCP23016GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool MCP23016GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MCP23016GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string MCP23016GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via MCP23016", pin_); - return buffer; +size_t MCP23016GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via MCP23016", this->pin_); } } // namespace mcp23016 diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h index 781c207de0..c2bc885c95 100644 --- a/esphome/components/mcp23016/mcp23016.h +++ b/esphome/components/mcp23016/mcp23016.h @@ -60,7 +60,7 @@ class MCP23016GPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(MCP23016 *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 81324e794f..302f6b8280 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -16,8 +16,8 @@ template bool MCP23XXXGPIOPin::digital_read() { template void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -template std::string MCP23XXXGPIOPin::dump_summary() const { - return str_snprintf("%u via MCP23XXX", 15, pin_); +template size_t MCP23XXXGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via MCP23XXX", this->pin_); } template class MCP23XXXGPIOPin<8>; diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index cf0ef5d41c..fb992466d5 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -36,7 +36,7 @@ template class MCP23XXXGPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(MCP23XXXBase *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index 5a8a8e7205..4b358e384c 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -153,10 +153,8 @@ void MPR121GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_ - 4, value != this->inverted_); } -std::string MPR121GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "ELE%u on MPR121", this->pin_); - return buffer; +size_t MPR121GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "ELE%u on MPR121", this->pin_); } } // namespace mpr121 diff --git a/esphome/components/mpr121/mpr121.h b/esphome/components/mpr121/mpr121.h index 6dd2c38309..085018fff0 100644 --- a/esphome/components/mpr121/mpr121.h +++ b/esphome/components/mpr121/mpr121.h @@ -109,7 +109,7 @@ class MPR121GPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(MPR121Component *parent) { this->parent_ = parent; } void set_pin(uint8_t pin) { this->pin_ = pin; } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index ba701b90a3..c650c99f62 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -153,13 +153,14 @@ void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *mes #endif void MQTTClientComponent::dump_config() { + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, "MQTT:\n" " Server Address: %s:%u (%s)\n" " Username: " LOG_SECRET("'%s'") "\n" " Client ID: " LOG_SECRET("'%s'") "\n" " Clean Session: %s", - this->credentials_.address.c_str(), this->credentials_.port, this->ip_.str().c_str(), + this->credentials_.address.c_str(), this->credentials_.port, this->ip_.str_to(ip_buf), this->credentials_.username.c_str(), this->credentials_.client_id.c_str(), YESNO(this->credentials_.clean_session)); if (this->is_discovery_ip_enabled()) { @@ -246,7 +247,8 @@ void MQTTClientComponent::check_dnslookup_() { return; } - ESP_LOGD(TAG, "Resolved broker IP address to %s", this->ip_.str().c_str()); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGD(TAG, "Resolved broker IP address to %s", this->ip_.str_to(ip_buf)); this->start_connect_(); } #if defined(USE_ESP8266) && LWIP_VERSION_MAJOR == 1 diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index dfbb5a497e..d210bad004 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -7,12 +7,14 @@ #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" namespace esphome { namespace nextion { static const char *const TAG = "nextion.upload.arduino"; +static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 @@ -89,8 +91,10 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { EspClass::getFreeHeap()); upload_first_chunk_sent_ = true; if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request - ESP_LOGD(TAG, "Recv: [%s]", - format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; + ESP_LOGD( + TAG, "Recv: [%s]", + format_hex_pretty_to(hex_buf, reinterpret_cast(recv_string.data()), recv_string.size())); uint32_t result = 0; for (int j = 0; j < 4; ++j) { result += static_cast(recv_string[j + 1]) << (8 * j); @@ -107,8 +111,10 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { buffer = nullptr; return range_end + 1; } else if (recv_string[0] != 0x05 and recv_string[0] != 0x08) { // 0x05 == "ok" - ESP_LOGE(TAG, "Invalid response: [%s]", - format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; + ESP_LOGE( + TAG, "Invalid response: [%s]", + format_hex_pretty_to(hex_buf, reinterpret_cast(recv_string.data()), recv_string.size())); // Deallocate buffer allocator.deallocate(buffer, 4096); buffer = nullptr; @@ -274,8 +280,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { this->recv_ret_string_(response, 5000, true); // This can take some time to return // The Nextion display will, if it's ready to accept data, send a 0x05 byte. + char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; ESP_LOGD(TAG, "Upload resp: [%s] %zu B", - format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str(), + format_hex_pretty_to(hex_buf, reinterpret_cast(response.data()), response.size()), response.length()); ESP_LOGV(TAG, "Heap: %" PRIu32, EspClass::getFreeHeap()); diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 29a7e3c8d7..712fa8e78e 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -9,12 +9,14 @@ #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" namespace esphome { namespace nextion { static const char *const TAG = "nextion.upload.esp32"; +static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 @@ -110,8 +112,10 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r #endif upload_first_chunk_sent_ = true; if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request - ESP_LOGD(TAG, "Recv: [%s]", - format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; + ESP_LOGD( + TAG, "Recv: [%s]", + format_hex_pretty_to(hex_buf, reinterpret_cast(recv_string.data()), recv_string.size())); uint32_t result = 0; for (int j = 0; j < 4; ++j) { result += static_cast(recv_string[j + 1]) << (8 * j); @@ -128,8 +132,10 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r buffer = nullptr; return range_end + 1; } else if (recv_string[0] != 0x05 and recv_string[0] != 0x08) { // 0x05 == "ok" - ESP_LOGE(TAG, "Invalid response: [%s]", - format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; + ESP_LOGE( + TAG, "Invalid response: [%s]", + format_hex_pretty_to(hex_buf, reinterpret_cast(recv_string.data()), recv_string.size())); // Deallocate buffer allocator.deallocate(buffer, 4096); buffer = nullptr; @@ -287,8 +293,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { this->recv_ret_string_(response, 5000, true); // This can take some time to return // The Nextion display will, if it's ready to accept data, send a 0x05 byte. + char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; ESP_LOGD(TAG, "Upload resp: [%s] %zu B", - format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str(), + format_hex_pretty_to(hex_buf, reinterpret_cast(response.data()), response.size()), response.length()); ESP_LOGV(TAG, "Heap: %" PRIu32, esp_get_free_heap_size()); diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index d59b9584d1..750ef08b33 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -7,6 +7,7 @@ #include "opentherm.h" #include "esphome/core/helpers.h" +#include #ifdef USE_ESP32 #include "driver/timer.h" #include "esp_err.h" @@ -569,8 +570,8 @@ void OpenTherm::debug_data(OpenthermData &data) { to_string(data.f88()).c_str()); } void OpenTherm::debug_error(OpenThermError &error) const { - ESP_LOGD(TAG, "data: %s; clock: %s; capture: %s; bit_pos: %s", format_hex(error.data).c_str(), - to_string(clock_).c_str(), format_bin(error.capture).c_str(), to_string(error.bit_pos).c_str()); + ESP_LOGD(TAG, "data: 0x%08" PRIx32 "; clock: %u; capture: 0x%08" PRIx32 "; bit_pos: %u", error.data, this->clock_, + error.capture, error.bit_pos); } float OpenthermData::f88() { return ((float) this->s16()) / 256.0; } diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index c0056e780b..909bac5f05 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -180,10 +180,8 @@ void PCA6416AGPIOPin::setup() { pin_mode(flags_); } void PCA6416AGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCA6416AGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void PCA6416AGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string PCA6416AGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via PCA6416A", pin_); - return buffer; +size_t PCA6416AGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via PCA6416A", this->pin_); } } // namespace pca6416a diff --git a/esphome/components/pca6416a/pca6416a.h b/esphome/components/pca6416a/pca6416a.h index 10a4a64e9b..138a51cc20 100644 --- a/esphome/components/pca6416a/pca6416a.h +++ b/esphome/components/pca6416a/pca6416a.h @@ -52,7 +52,7 @@ class PCA6416AGPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(PCA6416AComponent *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index e8d49f66e2..a6f9c2396c 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -129,10 +129,8 @@ void PCA9554GPIOPin::setup() { pin_mode(flags_); } void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCA9554GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void PCA9554GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string PCA9554GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via PCA9554", pin_); - return buffer; +size_t PCA9554GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via PCA9554", this->pin_); } } // namespace pca9554 diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index 7b356b4068..bf752e50c9 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -59,7 +59,7 @@ class PCA9554GPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(PCA9554Component *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index 72d8865d7f..15418bfee5 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -104,10 +104,8 @@ void PCF8574GPIOPin::setup() { pin_mode(flags_); } void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void PCF8574GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string PCF8574GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via PCF8574", pin_); - return buffer; +size_t PCF8574GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via PCF8574", this->pin_); } } // namespace pcf8574 diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index fd1ea8af63..5203030142 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -54,7 +54,7 @@ class PCF8574GPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(PCF8574Component *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 517ca833e6..f3a1f013d9 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -164,7 +164,9 @@ bool PI4IOE5V6408GPIOPin::digital_read() { return this->parent_->digital_read(th void PI4IOE5V6408GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string PI4IOE5V6408GPIOPin::dump_summary() const { return str_sprintf("%u via PI4IOE5V6408", this->pin_); } +size_t PI4IOE5V6408GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via PI4IOE5V6408", this->pin_); +} } // namespace pi4ioe5v6408 } // namespace esphome diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h index 82b3076fab..4dc31201ce 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h @@ -52,7 +52,7 @@ class PI4IOE5V6408GPIOPin : public GPIOPin, public Parentedpin_ = pin; } void set_inverted(bool inverted) { this->inverted_ = inverted; } diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index fa8564f614..8f8740c925 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -12,6 +12,9 @@ static const uint8_t WAIT_I_RQ = 0x30; // RxIRq and IdleIRq static const char *const TAG = "rc522"; +// Max UID size for RFID tags (4, 7, or 10 bytes) +static constexpr size_t RC522_MAX_UID_SIZE = 10; + static const uint8_t RESET_COUNT = 5; void RC522::setup() { @@ -191,8 +194,9 @@ void RC522::loop() { if (status == STATUS_TIMEOUT) { ESP_LOGV(TAG, "STATE_READ_SERIAL_DONE -> TIMEOUT (no tag present) %d", status); } else { + char hex_buf[format_hex_pretty_size(RC522_MAX_UID_SIZE)]; ESP_LOGW(TAG, "Unexpected response. Read status is %d. Read bytes: %d (%s)", status, back_length_, - format_hex_pretty(buffer_, back_length_, '-', false).c_str()); + format_hex_pretty_to(hex_buf, buffer_, back_length_, '-')); } state_ = STATE_DONE; @@ -237,13 +241,18 @@ void RC522::loop() { trigger->process(rfid_uid); if (report) { - ESP_LOGD(TAG, "Found new tag '%s'", format_hex_pretty(rfid_uid, '-', false).c_str()); + char uid_buf[format_hex_pretty_size(RC522_MAX_UID_SIZE)]; + ESP_LOGD(TAG, "Found new tag '%s'", format_hex_pretty_to(uid_buf, rfid_uid.data(), rfid_uid.size(), '-')); } break; } case STATE_DONE: { if (!this->current_uid_.empty()) { - ESP_LOGV(TAG, "Tag '%s' removed", format_hex_pretty(this->current_uid_, '-', false).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char uid_buf[format_hex_pretty_size(RC522_MAX_UID_SIZE)]; + ESP_LOGV(TAG, "Tag '%s' removed", + format_hex_pretty_to(uid_buf, this->current_uid_.data(), this->current_uid_.size(), '-')); +#endif for (auto *trigger : this->triggers_ontagremoved_) trigger->process(this->current_uid_); } @@ -338,7 +347,10 @@ void RC522::pcd_clear_register_bit_mask_(PcdRegister reg, ///< The register to * @return STATUS_OK on success, STATUS_??? otherwise. */ void RC522::pcd_transceive_data_(uint8_t send_len) { - ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_hex_pretty(buffer_, send_len, '-', false).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(RC522_MAX_UID_SIZE)]; + ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_hex_pretty_to(hex_buf, buffer_, send_len, '-')); +#endif delayMicroseconds(1000); // we need 1 ms delay between antenna on and those communication commands send_len_ = send_len; // Prepare values for BitFramingReg @@ -412,8 +424,11 @@ RC522::StatusCode RC522::await_transceive_() { error_reg_value); // TODO: is this always due to collissions? return STATUS_ERROR; } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(RC522_MAX_UID_SIZE)]; ESP_LOGV(TAG, "received %d bytes: %s", back_length_, - format_hex_pretty(buffer_ + send_len_, back_length_, '-', false).c_str()); + format_hex_pretty_to(hex_buf, buffer_ + send_len_, back_length_, '-')); +#endif return STATUS_OK; } diff --git a/esphome/components/remote_base/haier_protocol.cpp b/esphome/components/remote_base/haier_protocol.cpp index ec5cb5775c..734f3c7789 100644 --- a/esphome/components/remote_base/haier_protocol.cpp +++ b/esphome/components/remote_base/haier_protocol.cpp @@ -1,4 +1,5 @@ #include "haier_protocol.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -12,6 +13,8 @@ constexpr uint32_t BIT_MARK_US = 540; constexpr uint32_t BIT_ONE_SPACE_US = 1650; constexpr uint32_t BIT_ZERO_SPACE_US = 580; constexpr unsigned int HAIER_IR_PACKET_BIT_SIZE = 112; +// Max data bytes in packet (excluding checksum) +constexpr size_t HAIER_MAX_DATA_BYTES = (HAIER_IR_PACKET_BIT_SIZE / 8); void HaierProtocol::encode_byte_(RemoteTransmitData *dst, uint8_t item) { for (uint8_t mask = 1 << 7; mask != 0; mask >>= 1) { @@ -77,7 +80,8 @@ optional HaierProtocol::decode(RemoteReceiveData src) { } void HaierProtocol::dump(const HaierData &data) { - ESP_LOGI(TAG, "Received Haier: %s", format_hex_pretty(data.data).c_str()); + char hex_buf[format_hex_pretty_size(HAIER_MAX_DATA_BYTES)]; + ESP_LOGI(TAG, "Received Haier: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); } } // namespace remote_base diff --git a/esphome/components/remote_base/mirage_protocol.cpp b/esphome/components/remote_base/mirage_protocol.cpp index 10d644a1cd..2ae877f193 100644 --- a/esphome/components/remote_base/mirage_protocol.cpp +++ b/esphome/components/remote_base/mirage_protocol.cpp @@ -1,4 +1,5 @@ #include "mirage_protocol.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -13,9 +14,12 @@ constexpr uint32_t BIT_ONE_SPACE_US = 1592; constexpr uint32_t BIT_ZERO_SPACE_US = 545; constexpr unsigned int MIRAGE_IR_PACKET_BIT_SIZE = 120; +// Max data bytes in packet (excluding checksum) +constexpr size_t MIRAGE_MAX_DATA_BYTES = (MIRAGE_IR_PACKET_BIT_SIZE / 8); void MirageProtocol::encode(RemoteTransmitData *dst, const MirageData &data) { - ESP_LOGI(TAG, "Transive Mirage: %s", format_hex_pretty(data.data).c_str()); + char hex_buf[format_hex_pretty_size(MIRAGE_MAX_DATA_BYTES)]; + ESP_LOGI(TAG, "Transmit Mirage: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); dst->set_carrier_frequency(38000); dst->reserve(5 + ((data.data.size() + 1) * 2)); dst->mark(HEADER_MARK_US); @@ -77,7 +81,8 @@ optional MirageProtocol::decode(RemoteReceiveData src) { } void MirageProtocol::dump(const MirageData &data) { - ESP_LOGI(TAG, "Received Mirage: %s", format_hex_pretty(data.data).c_str()); + char hex_buf[format_hex_pretty_size(MIRAGE_MAX_DATA_BYTES)]; + ESP_LOGI(TAG, "Received Mirage: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); } } // namespace remote_base diff --git a/esphome/components/rp2040/gpio.cpp b/esphome/components/rp2040/gpio.cpp index 3927815e46..2b1699f888 100644 --- a/esphome/components/rp2040/gpio.cpp +++ b/esphome/components/rp2040/gpio.cpp @@ -64,10 +64,8 @@ void RP2040GPIOPin::pin_mode(gpio::Flags flags) { pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT } -std::string RP2040GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); - return buffer; +size_t RP2040GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "GPIO%u", this->pin_); } bool RP2040GPIOPin::digital_read() { diff --git a/esphome/components/rp2040/gpio.h b/esphome/components/rp2040/gpio.h index 47a6fe17f2..a98e1dab14 100644 --- a/esphome/components/rp2040/gpio.h +++ b/esphome/components/rp2040/gpio.h @@ -18,7 +18,7 @@ class RP2040GPIOPin : public InternalGPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return pin_; } diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index b336bbcb65..3b5307805e 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -270,7 +270,10 @@ void ShellyDimmer::send_settings_() { } bool ShellyDimmer::send_command_(uint8_t cmd, const uint8_t *const payload, uint8_t len) { - ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, format_hex(payload, len).c_str()); + // Buffer for hex formatting: max frame size * 2 + null (covers any payload) + char hex_buf[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE * 2 + 1]; + ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, + format_hex_to(hex_buf, sizeof(hex_buf), payload, len)); // Prepare a command frame. uint8_t frame[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE]; diff --git a/esphome/components/sn74hc165/sn74hc165.cpp b/esphome/components/sn74hc165/sn74hc165.cpp index 416d9db293..718e0b86ed 100644 --- a/esphome/components/sn74hc165/sn74hc165.cpp +++ b/esphome/components/sn74hc165/sn74hc165.cpp @@ -64,7 +64,9 @@ float SN74HC165Component::get_setup_priority() const { return setup_priority::IO bool SN74HC165GPIOPin::digital_read() { return this->parent_->digital_read_(this->pin_) != this->inverted_; } -std::string SN74HC165GPIOPin::dump_summary() const { return str_snprintf("%u via SN74HC165", 18, pin_); } +size_t SN74HC165GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via SN74HC165", this->pin_); +} } // namespace sn74hc165 } // namespace esphome diff --git a/esphome/components/sn74hc165/sn74hc165.h b/esphome/components/sn74hc165/sn74hc165.h index 4684844687..5a3f3fe8ef 100644 --- a/esphome/components/sn74hc165/sn74hc165.h +++ b/esphome/components/sn74hc165/sn74hc165.h @@ -47,7 +47,7 @@ class SN74HC165GPIOPin : public GPIOPin, public Parented { void pin_mode(gpio::Flags flags) override {} bool digital_read() override; void digital_write(bool value) override{}; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_pin(uint16_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index a9ada432e4..6b5c5d9fc4 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -93,7 +93,9 @@ float SN74HC595Component::get_setup_priority() const { return setup_priority::IO void SN74HC595GPIOPin::digital_write(bool value) { this->parent_->digital_write_(this->pin_, value != this->inverted_); } -std::string SN74HC595GPIOPin::dump_summary() const { return str_snprintf("%u via SN74HC595", 18, pin_); } +size_t SN74HC595GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via SN74HC595", this->pin_); +} } // namespace sn74hc595 } // namespace esphome diff --git a/esphome/components/sn74hc595/sn74hc595.h b/esphome/components/sn74hc595/sn74hc595.h index 181015b1e6..1cf70c86b5 100644 --- a/esphome/components/sn74hc595/sn74hc595.h +++ b/esphome/components/sn74hc595/sn74hc595.h @@ -54,7 +54,7 @@ class SN74HC595GPIOPin : public GPIOPin, public Parented { void pin_mode(gpio::Flags flags) override {} bool digital_read() override { return false; } void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_pin(uint16_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index cd09f31dd7..0ecde83b8b 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -42,12 +42,17 @@ * M 6C - CRC over bytes 2 to F (Addition) \*********************************************************************************************/ #include "sonoff_d1.h" +#include "esphome/core/helpers.h" namespace esphome { namespace sonoff_d1 { static const char *const TAG = "sonoff_d1"; +// Protocol constants +static constexpr size_t SONOFF_D1_ACK_SIZE = 7; +static constexpr size_t SONOFF_D1_MAX_CMD_SIZE = 17; + uint8_t SonoffD1Output::calc_checksum_(const uint8_t *cmd, const size_t len) { uint8_t crc = 0; for (size_t i = 2; i < len - 1; i++) { @@ -86,8 +91,11 @@ bool SonoffD1Output::read_command_(uint8_t *cmd, size_t &len) { // Read a minimal packet if (this->read_array(cmd, 6)) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(6)]; ESP_LOGV(TAG, "[%04d] Reading from dimmer:", this->write_count_); - ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(cmd, 6).c_str()); + ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty_to(hex_buf, cmd, 6)); +#endif if (cmd[0] != 0xAA || cmd[1] != 0x55) { ESP_LOGW(TAG, "[%04d] RX: wrong header (%x%x, must be AA55)", this->write_count_, cmd[0], cmd[1]); @@ -101,7 +109,10 @@ bool SonoffD1Output::read_command_(uint8_t *cmd, size_t &len) { return false; } if (this->read_array(&cmd[6], cmd[5] + 1 /*checksum suffix*/)) { - ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(&cmd[6], cmd[5] + 1).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf2[format_hex_pretty_size(SONOFF_D1_MAX_CMD_SIZE)]; + ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty_to(hex_buf2, &cmd[6], cmd[5] + 1)); +#endif // Check the checksum uint8_t valid_checksum = this->calc_checksum_(cmd, cmd[5] + 7); @@ -145,9 +156,10 @@ bool SonoffD1Output::read_ack_(const uint8_t *cmd, const size_t len) { ESP_LOGD(TAG, "[%04d] Acknowledge received", this->write_count_); return true; } else { + char hex_buf[format_hex_pretty_size(SONOFF_D1_ACK_SIZE)]; ESP_LOGW(TAG, "[%04d] Unexpected acknowledge received (possible clash of RF/HA commands), expected ack was:", this->write_count_); - ESP_LOGW(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(ref_buffer, sizeof(ref_buffer)).c_str()); + ESP_LOGW(TAG, "[%04d] %s", this->write_count_, format_hex_pretty_to(hex_buf, ref_buffer, sizeof(ref_buffer))); } return false; } @@ -174,8 +186,11 @@ bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_a // 2. UART command initiated by this component can clash with a command initiated by RF uint32_t retries = 10; do { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(SONOFF_D1_MAX_CMD_SIZE)]; ESP_LOGV(TAG, "[%04d] Writing to the dimmer:", this->write_count_); - ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(cmd, len).c_str()); + ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty_to(hex_buf, cmd, len)); +#endif this->write_array(cmd, len); this->write_count_++; if (!needs_ack) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index c4876d1a74..36344a6d38 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -64,9 +64,9 @@ void SPIComponent::setup() { void SPIComponent::dump_config() { ESP_LOGCONFIG(TAG, "SPI bus:"); - LOG_PIN(" CLK Pin: ", this->clk_pin_) - LOG_PIN(" SDI Pin: ", this->sdi_pin_) - LOG_PIN(" SDO Pin: ", this->sdo_pin_) + LOG_PIN(" CLK Pin: ", this->clk_pin_); + LOG_PIN(" SDI Pin: ", this->sdi_pin_); + LOG_PIN(" SDO Pin: ", this->sdo_pin_); for (size_t i = 0; i != this->data_pins_.size(); i++) { ESP_LOGCONFIG(TAG, " Data pin %u: GPIO%d", i, this->data_pins_[i]); } diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 256cbcc65f..e237cf44f4 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -120,7 +120,11 @@ class NullPin : public GPIOPin { void digital_write(bool value) override {} - std::string dump_summary() const override { return std::string(); } + size_t dump_summary(char *buffer, size_t len) const override { + if (len > 0) + buffer[0] = '\0'; + return 0; + } protected: static GPIOPin *const NULL_PIN; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/sx1509/sx1509_gpio_pin.cpp b/esphome/components/sx1509/sx1509_gpio_pin.cpp index a74c8b60b8..41a99eba4b 100644 --- a/esphome/components/sx1509/sx1509_gpio_pin.cpp +++ b/esphome/components/sx1509/sx1509_gpio_pin.cpp @@ -12,10 +12,8 @@ void SX1509GPIOPin::setup() { pin_mode(flags_); } void SX1509GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool SX1509GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void SX1509GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string SX1509GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via sx1509", this->pin_); - return buffer; +size_t SX1509GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via sx1509", this->pin_); } } // namespace sx1509 diff --git a/esphome/components/sx1509/sx1509_gpio_pin.h b/esphome/components/sx1509/sx1509_gpio_pin.h index eb9207e882..5903af9d12 100644 --- a/esphome/components/sx1509/sx1509_gpio_pin.h +++ b/esphome/components/sx1509/sx1509_gpio_pin.h @@ -13,7 +13,7 @@ class SX1509GPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(SX1509Component *parent) { this->parent_ = parent; } void set_pin(uint8_t pin) { this->pin_ = pin; } diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp index c3449ce254..376de6a370 100644 --- a/esphome/components/tca9555/tca9555.cpp +++ b/esphome/components/tca9555/tca9555.cpp @@ -138,7 +138,9 @@ void TCA9555GPIOPin::setup() { this->pin_mode(this->flags_); } void TCA9555GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool TCA9555GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void TCA9555GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string TCA9555GPIOPin::dump_summary() const { return str_sprintf("%u via TCA9555", this->pin_); } +size_t TCA9555GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via TCA9555", this->pin_); +} } // namespace tca9555 } // namespace esphome diff --git a/esphome/components/tca9555/tca9555.h b/esphome/components/tca9555/tca9555.h index 0c236ae4e3..9f7273b1e7 100644 --- a/esphome/components/tca9555/tca9555.h +++ b/esphome/components/tca9555/tca9555.h @@ -48,7 +48,7 @@ class TCA9555GPIOPin : public GPIOPin, public Parented { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_pin(uint8_t pin) { this->pin_ = pin; } void set_inverted(bool inverted) { this->inverted_ = inverted; } diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp index d6513dbbe0..06481b628b 100644 --- a/esphome/components/tee501/tee501.cpp +++ b/esphome/components/tee501/tee501.cpp @@ -7,6 +7,8 @@ namespace tee501 { static const char *const TAG = "tee501"; +static constexpr size_t TEE501_SERIAL_NUMBER_SIZE = 7; + void TEE501Component::setup() { uint8_t address[] = {0x70, 0x29}; uint8_t identification[9]; @@ -17,7 +19,10 @@ void TEE501Component::setup() { this->mark_failed(); return; } - ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(identification + 0, 7).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char serial_hex[format_hex_size(TEE501_SERIAL_NUMBER_SIZE)]; +#endif + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex_to(serial_hex, identification, TEE501_SERIAL_NUMBER_SIZE)); } void TEE501Component::dump_config() { diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 12b14be9ff..2812fb6ad6 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -20,6 +20,8 @@ static const char *const TAG = "tuya"; static const int COMMAND_DELAY = 10; static const int RECEIVE_TIMEOUT = 300; static const int MAX_RETRIES = 5; +// Max bytes to log for datapoint values (larger values are truncated) +static constexpr size_t MAX_DATAPOINT_LOG_BYTES = 16; void Tuya::setup() { this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); @@ -51,7 +53,9 @@ void Tuya::dump_config() { } for (auto &info : this->datapoints_) { if (info.type == TuyaDatapointType::RAW) { - ESP_LOGCONFIG(TAG, " Datapoint %u: raw (value: %s)", info.id, format_hex_pretty(info.value_raw).c_str()); + char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)]; + ESP_LOGCONFIG(TAG, " Datapoint %u: raw (value: %s)", info.id, + format_hex_pretty_to(hex_buf, info.value_raw.data(), info.value_raw.size())); } else if (info.type == TuyaDatapointType::BOOLEAN) { ESP_LOGCONFIG(TAG, " Datapoint %u: switch (value: %s)", info.id, ONOFF(info.value_bool)); } else if (info.type == TuyaDatapointType::INTEGER) { @@ -122,8 +126,11 @@ bool Tuya::validate_message_() { // valid message const uint8_t *message_data = data + 6; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)]; ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, - format_hex_pretty(message_data, length).c_str(), static_cast(this->init_state_)); + format_hex_pretty_to(hex_buf, message_data, length), static_cast(this->init_state_)); +#endif this->handle_command_(command, version, message_data, length); // return false to reset rx buffer @@ -349,7 +356,11 @@ void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) { switch (datapoint.type) { case TuyaDatapointType::RAW: datapoint.value_raw = std::vector(data, data + data_size); - ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, format_hex_pretty(datapoint.value_raw).c_str()); + { + char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)]; + ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, + format_hex_pretty_to(hex_buf, datapoint.value_raw.data(), datapoint.value_raw.size())); + } break; case TuyaDatapointType::BOOLEAN: if (data_size != 1) { @@ -460,8 +471,12 @@ void Tuya::send_raw_command_(TuyaCommand command) { break; } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)]; ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", static_cast(command.cmd), - version, format_hex_pretty(command.payload).c_str(), static_cast(this->init_state_)); + version, format_hex_pretty_to(hex_buf, command.payload.data(), command.payload.size()), + static_cast(this->init_state_)); +#endif this->write_array({0x55, 0xAA, version, (uint8_t) command.cmd, len_hi, len_lo}); if (!command.payload.empty()) @@ -675,7 +690,8 @@ void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType } void Tuya::set_raw_datapoint_value_(uint8_t datapoint_id, const std::vector &value, bool forced) { - ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, format_hex_pretty(value).c_str()); + char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)]; + ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, format_hex_pretty_to(hex_buf, value.data(), value.size())); optional datapoint = this->get_datapoint_(datapoint_id); if (!datapoint.has_value()) { ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id); diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index daa6c52f98..4474efeb77 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -65,11 +65,14 @@ void UDPComponent::setup() { server.sin_port = htons(this->listen_port_); if (this->listen_address_.has_value()) { + // Only 16 bytes needed for IPv4, but use standard size for consistency + char addr_buf[network::IP_ADDRESS_BUFFER_SIZE]; + this->listen_address_.value().str_to(addr_buf); struct ip_mreq imreq = {}; imreq.imr_interface.s_addr = ESPHOME_INADDR_ANY; - inet_aton(this->listen_address_.value().str().c_str(), &imreq.imr_multiaddr); + inet_aton(addr_buf, &imreq.imr_multiaddr); server.sin_addr.s_addr = imreq.imr_multiaddr.s_addr; - ESP_LOGD(TAG, "Join multicast %s", this->listen_address_.value().str().c_str()); + ESP_LOGD(TAG, "Join multicast %s", addr_buf); err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)); if (err < 0) { ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno); diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index 221f07c80e..4c3a4b05df 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -8,6 +8,9 @@ namespace uponor_smatrix { static const char *const TAG = "uponor_smatrix"; +// Maximum bytes to log in verbose hex output +static constexpr size_t UPONOR_MAX_LOG_BYTES = 36; + void UponorSmatrixComponent::setup() { #ifdef USE_TIME if (this->time_id_ != nullptr) { @@ -97,8 +100,11 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { return false; } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_size(UPONOR_MAX_LOG_BYTES)]; +#endif ESP_LOGV(TAG, "Received packet: addr=%08X, data=%s, crc=%04X", device_address, - format_hex(&packet[4], packet_len - 6).c_str(), crc); + format_hex_to(hex_buf, &packet[4], packet_len - 6), crc); // Handle packet size_t data_len = (packet_len - 6) / 3; diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp index e474dcfe17..b9496a08de 100644 --- a/esphome/components/vbus/vbus.cpp +++ b/esphome/components/vbus/vbus.cpp @@ -8,6 +8,9 @@ namespace vbus { static const char *const TAG = "vbus"; +// Maximum bytes to log in verbose hex output (16 frames * 4 bytes = 64 bytes typical) +static constexpr size_t VBUS_MAX_LOG_BYTES = 64; + void VBus::dump_config() { ESP_LOGCONFIG(TAG, "VBus:"); check_uart_settings(9600); @@ -101,8 +104,11 @@ void VBus::loop() { this->buffer_.push_back(this->fbytes_[i]); if (++this->cframe_ < this->frames_) continue; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_size(VBUS_MAX_LOG_BYTES)]; +#endif ESP_LOGV(TAG, "P2 C%04x %04x->%04x: %s", this->command_, this->source_, this->dest_, - format_hex(this->buffer_).c_str()); + format_hex_to(hex_buf, this->buffer_.data(), this->buffer_.size())); for (auto &listener : this->listeners_) listener->on_message(this->command_, this->source_, this->dest_, this->buffer_); this->state_ = 0; diff --git a/esphome/components/waveshare_epaper/waveshare_213v3.cpp b/esphome/components/waveshare_epaper/waveshare_213v3.cpp index 068cb91d31..b55f3c8d26 100644 --- a/esphome/components/waveshare_epaper/waveshare_213v3.cpp +++ b/esphome/components/waveshare_epaper/waveshare_213v3.cpp @@ -177,10 +177,10 @@ uint32_t WaveshareEPaper2P13InV3::idle_timeout_() { return 5000; } void WaveshareEPaper2P13InV3::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this) ESP_LOGCONFIG(TAG, " Model: 2.13inV3"); - LOG_PIN(" CS Pin: ", this->cs_) - LOG_PIN(" Reset Pin: ", this->reset_pin_) - LOG_PIN(" DC Pin: ", this->dc_pin_) - LOG_PIN(" Busy Pin: ", this->busy_pin_) + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index ebe987cc65..3384a0572f 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -245,10 +245,8 @@ void WeikaiGPIOPin::setup() { this->pin_mode(this->flags_); } -std::string WeikaiGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via WeiKai %s", this->pin_, this->parent_->get_name()); - return buffer; +size_t WeikaiGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via WeiKai %s", this->pin_, this->parent_->get_name()); } /////////////////////////////////////////////////////////////////////////////// diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 987278213a..a27c14106d 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -278,7 +278,7 @@ class WeikaiGPIOPin : public GPIOPin { gpio::Flags get_flags() const override { return this->flags_; } void setup() override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void pin_mode(gpio::Flags flags) override { this->parent_->set_pin_direction_(this->pin_, flags); } bool digital_read() override { return this->parent_->read_pin_val_(this->pin_) != this->inverted_; } void digital_write(bool value) override { this->parent_->write_pin_val_(this->pin_, value != this->inverted_); } diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h index 7997baff65..fb0e71bcf6 100644 --- a/esphome/components/wifi/automation.h +++ b/esphome/components/wifi/automation.h @@ -45,7 +45,8 @@ template class WiFiConfigureAction : public Action, publi if (this->connecting_) return; // If already connected to the same AP, do nothing - if (global_wifi_component->wifi_ssid() == ssid) { + char ssid_buf[SSID_BUFFER_SIZE]; + if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), ssid.c_str()) == 0) { // Callback to notify the user that the connection was successful this->connect_trigger_->trigger(); return; @@ -94,7 +95,8 @@ template class WiFiConfigureAction : public Action, publi this->cancel_timeout("wifi-connect-timeout"); this->cancel_timeout("wifi-fallback-timeout"); this->connecting_ = false; - if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) { + char ssid_buf[SSID_BUFFER_SIZE]; + if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), this->new_sta_.get_ssid().c_str()) == 0) { // Callback to notify the user that the connection was successful this->connect_trigger_->trigger(); } else { diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 50c0938cf1..a0a7d3d946 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -655,12 +655,15 @@ void WiFiComponent::setup_ap_config_() { #ifdef USE_WIFI_MANUAL_IP auto manual_ip = this->ap_.get_manual_ip(); if (manual_ip.has_value()) { + char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " AP Static IP: '%s'\n" " AP Gateway: '%s'\n" " AP Subnet: '%s'", - manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(), - manual_ip->subnet.str().c_str()); + manual_ip->static_ip.str_to(static_ip_buf), manual_ip->gateway.str_to(gateway_buf), + manual_ip->subnet.str_to(subnet_buf)); } #endif @@ -816,8 +819,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { #ifdef USE_WIFI_MANUAL_IP if (ap.get_manual_ip().has_value()) { ManualIP m = *ap.get_manual_ip(); - ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str().c_str(), - m.gateway.str().c_str(), m.subnet.str().c_str(), m.dns1.str().c_str(), m.dns2.str().c_str()); + char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str_to(static_ip_buf), + m.gateway.str_to(gateway_buf), m.subnet.str_to(subnet_buf), m.dns1.str_to(dns1_buf), + m.dns2.str_to(dns2_buf)); } else #endif { @@ -1167,7 +1176,8 @@ void WiFiComponent::check_connecting_finished() { auto status = this->wifi_sta_connect_status_(); if (status == WiFiSTAConnectStatus::CONNECTED) { - if (wifi_ssid().empty()) { + char ssid_buf[SSID_BUFFER_SIZE]; + if (wifi_ssid_to(ssid_buf)[0] == '\0') { ESP_LOGW(TAG, "Connection incomplete"); this->retry_connect(); return; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 1c744648bb..055a13afc8 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -518,15 +518,12 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { auto it = event->event_info.connected; - char buf[33]; - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(), - it.channel); + ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=%s channel=%u", it.ssid_len, (const char *) it.ssid, + format_mac_address_pretty(it.bssid).c_str(), it.channel); s_sta_connected = true; #ifdef USE_WIFI_LISTENERS for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(buf, it.ssid_len), it.bssid); + listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); } // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #ifdef USE_WIFI_MANUAL_IP @@ -543,17 +540,15 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } case EVENT_STAMODE_DISCONNECTED: { auto it = event->event_info.disconnected; - char buf[33]; - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; if (it.reason == REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len, + (const char *) it.ssid); s_sta_connect_not_found = true; } else { char bssid_s[18]; format_mac_addr_upper(it.bssid, bssid_s); - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, - LOG_STR_ARG(get_disconnect_reason_str(it.reason))); + ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len, + (const char *) it.ssid, bssid_s, LOG_STR_ARG(get_disconnect_reason_str(it.reason))); s_sta_connect_error = true; } s_sta_connected = false; @@ -810,10 +805,13 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { network::IPAddress start_address = network::IPAddress(&info.ip); start_address += 99; lease.start_ip = start_address; - ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str().c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; +#endif + ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str_to(ip_buf)); start_address += 10; lease.end_ip = start_address; - ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str()); + ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str_to(ip_buf)); if (!wifi_softap_set_dhcps_lease(&lease)) { ESP_LOGE(TAG, "Set SoftAP DHCP lease failed"); return false; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 5d4d003d62..f68a095bff 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -734,16 +734,13 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_CONNECTED) { const auto &it = data->data.sta_connected; - char buf[33]; - assert(it.ssid_len <= 32); - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, - format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); + ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", it.ssid_len, + (const char *) it.ssid, format_mac_address_pretty(it.bssid).c_str(), it.channel, + get_auth_mode_str(it.authmode)); s_sta_connected = true; #ifdef USE_WIFI_LISTENERS for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(buf, it.ssid_len), it.bssid); + listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); } // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #ifdef USE_WIFI_MANUAL_IP @@ -757,21 +754,18 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { const auto &it = data->data.sta_disconnected; - char buf[33]; - assert(it.ssid_len <= 32); - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; if (it.reason == WIFI_REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len, + (const char *) it.ssid); s_sta_connect_not_found = true; } else if (it.reason == WIFI_REASON_ROAMING) { - ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf); + ESP_LOGI(TAG, "Disconnected ssid='%.*s' reason='Station Roaming'", it.ssid_len, (const char *) it.ssid); return; } else { char bssid_s[18]; format_mac_addr_upper(it.bssid, bssid_s); - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, - get_disconnect_reason_str(it.reason)); + ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len, + (const char *) it.ssid, bssid_s, get_disconnect_reason_str(it.reason)); s_sta_connect_error = true; } s_sta_connected = false; @@ -969,10 +963,13 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { network::IPAddress start_address = network::IPAddress(&info.ip); start_address += 99; lease.start_ip = start_address; - ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str().c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; +#endif + ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str_to(ip_buf)); start_address += 10; lease.end_ip = start_address; - ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str()); + ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str_to(ip_buf)); err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); if (err != ESP_OK) { @@ -984,8 +981,10 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { // Configure DHCP Option 114 (Captive Portal URI) if captive portal is enabled // This provides a standards-compliant way for clients to discover the captive portal if (captive_portal::global_captive_portal != nullptr) { - static char captive_portal_uri[32]; - snprintf(captive_portal_uri, sizeof(captive_portal_uri), "http://%s", network::IPAddress(&info.ip).str().c_str()); + // Buffer must be static - dhcps_set_option_info stores pointer, doesn't copy + static char captive_portal_uri[24]; // "http://" (7) + IPv4 max (15) + null + memcpy(captive_portal_uri, "http://", 7); // NOLINT(bugprone-not-null-terminated-result) - str_to null-terminates + network::IPAddress(&info.ip).str_to(captive_portal_uri + 7); err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_CAPTIVEPORTAL_URI, captive_portal_uri, strlen(captive_portal_uri)); if (err != ESP_OK) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 9b8653d0db..9bbd319f33 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -296,14 +296,12 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { auto it = info.wifi_sta_connected; - char buf[33]; - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, - format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); + ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", it.ssid_len, + (const char *) it.ssid, format_mac_address_pretty(it.bssid).c_str(), it.channel, + get_auth_mode_str(it.authmode)); #ifdef USE_WIFI_LISTENERS for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(buf, it.ssid_len), it.bssid); + listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); } // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #ifdef USE_WIFI_MANUAL_IP @@ -318,9 +316,6 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: { auto it = info.wifi_sta_disconnected; - char buf[33]; - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; // LibreTiny can send spurious disconnect events with empty ssid/bssid during connection. // These are typically "Association Leave" events that don't indicate actual failures: @@ -339,12 +334,13 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } if (it.reason == WIFI_REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len, + (const char *) it.ssid); } else { char bssid_s[18]; format_mac_addr_upper(it.bssid, bssid_s); - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, - get_disconnect_reason_str(it.reason)); + ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len, + (const char *) it.ssid, bssid_s, get_disconnect_reason_str(it.reason)); } uint8_t reason = it.reason; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 564870d74e..9f25063133 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -12,6 +12,9 @@ namespace xiaomi_ble { static const char *const TAG = "xiaomi_ble"; +// Maximum bytes to log in very verbose hex output (covers largest packet of ~24 bytes) +static constexpr size_t XIAOMI_MAX_LOG_BYTES = 32; + bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { // button pressed, 3 bytes, only byte 3 is used for supported devices so far if ((value_type == 0x1001) && (value_length == 3)) { @@ -263,7 +266,10 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address) { if ((raw.size() != 19) && ((raw.size() < 22) || (raw.size() > 24))) { ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size()); - ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(XIAOMI_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty_to(hex_buf, raw.data(), raw.size())); return false; } @@ -320,12 +326,17 @@ bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, c memcpy(mac_address + 4, mac_reverse + 1, 1); memcpy(mac_address + 5, mac_reverse, 1); ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed."); - ESP_LOGVV(TAG, " MAC address : %s", format_mac_address_pretty(mac_address).c_str()); - ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str()); - ESP_LOGVV(TAG, " Key : %s", format_hex_pretty(vector.key, vector.keysize).c_str()); - ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty(vector.iv, vector.ivsize).c_str()); - ESP_LOGVV(TAG, " Cipher : %s", format_hex_pretty(vector.ciphertext, vector.datasize).c_str()); - ESP_LOGVV(TAG, " Tag : %s", format_hex_pretty(vector.tag, vector.tagsize).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(mac_address, mac_buf); + char hex_buf[format_hex_pretty_size(XIAOMI_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, " MAC address : %s", mac_buf); + ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty_to(hex_buf, raw.data(), raw.size())); + ESP_LOGVV(TAG, " Key : %s", format_hex_pretty_to(hex_buf, vector.key, vector.keysize)); + ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty_to(hex_buf, vector.iv, vector.ivsize)); + ESP_LOGVV(TAG, " Cipher : %s", format_hex_pretty_to(hex_buf, vector.ciphertext, vector.datasize)); + ESP_LOGVV(TAG, " Tag : %s", format_hex_pretty_to(hex_buf, vector.tag, vector.tagsize)); mbedtls_ccm_free(&ctx); return false; } @@ -341,8 +352,11 @@ bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, c raw[0] &= ~0x08; ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption passed."); - ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", format_hex_pretty(raw.data() + cipher_pos, vector.datasize).c_str(), - static_cast(raw[4])); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(XIAOMI_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", + format_hex_pretty_to(hex_buf, raw.data() + cipher_pos, vector.datasize), static_cast(raw[4])); mbedtls_ccm_free(&ctx); return true; diff --git a/esphome/components/xl9535/xl9535.cpp b/esphome/components/xl9535/xl9535.cpp index 958fc5eede..dd6c8188eb 100644 --- a/esphome/components/xl9535/xl9535.cpp +++ b/esphome/components/xl9535/xl9535.cpp @@ -110,7 +110,9 @@ void XL9535Component::pin_mode(uint8_t pin, gpio::Flags mode) { void XL9535GPIOPin::setup() { this->pin_mode(this->flags_); } -std::string XL9535GPIOPin::dump_summary() const { return str_snprintf("%u via XL9535", 15, this->pin_); } +size_t XL9535GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via XL9535", this->pin_); +} void XL9535GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool XL9535GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } diff --git a/esphome/components/xl9535/xl9535.h b/esphome/components/xl9535/xl9535.h index 3b511fd9b3..be0e2fbd82 100644 --- a/esphome/components/xl9535/xl9535.h +++ b/esphome/components/xl9535/xl9535.h @@ -39,7 +39,7 @@ class XL9535GPIOPin : public GPIOPin { gpio::Flags get_flags() const override { return this->flags_; } void setup() override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; diff --git a/esphome/components/zephyr/gpio.cpp b/esphome/components/zephyr/gpio.cpp index 41b983535c..8041c361cc 100644 --- a/esphome/components/zephyr/gpio.cpp +++ b/esphome/components/zephyr/gpio.cpp @@ -85,10 +85,8 @@ void ZephyrGPIOPin::pin_mode(gpio::Flags flags) { } } -std::string ZephyrGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%u, P%u.%u", this->pin_, this->pin_ / 32, this->pin_ % 32); - return buffer; +size_t ZephyrGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "GPIO%u, P%u.%u", this->pin_, this->pin_ / 32, this->pin_ % 32); } bool ZephyrGPIOPin::digital_read() { diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index 6e8f81857a..b405f385bc 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -16,7 +16,7 @@ class ZephyrGPIOPin : public InternalGPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return this->pin_; } diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 4c9cc6b2b6..f8fa3b333e 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -7,6 +7,9 @@ #ifdef USE_ESP8266 #include #endif +#ifdef USE_ESP32 +#include +#endif #include "esphome/core/version.h" #include "esphome/core/hal.h" #include @@ -203,6 +206,19 @@ void Application::loop() { ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", build_time_str); #ifdef ESPHOME_PROJECT_NAME ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION); +#endif +#ifdef USE_ESP32 + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + ESP_LOGI(TAG, "ESP32 Chip: %s r%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, + chip_info.revision % 100, chip_info.cores); +#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) + // Suggest optimization for chips that don't need the PSRAM cache workaround + if (chip_info.revision >= 300) { + ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100, + chip_info.revision % 100); + } +#endif #endif } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 579edc065a..1fddc426d4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -166,6 +166,7 @@ #define USE_MQTT_IDF_ENQUEUE #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_OTA_ROLLBACK +#define USE_ESP32_MIN_CHIP_REVISION_SET #define USE_BLUETOOTH_PROXY #define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index b7616a9ad3..8508b93411 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -9,7 +9,8 @@ static const char *const TAG = "entity_base"; // Entity Name const StringRef &EntityBase::get_name() const { return this->name_; } -void EntityBase::set_name(const char *name) { +void EntityBase::set_name(const char *name) { this->set_name(name, 0); } +void EntityBase::set_name(const char *name, uint32_t object_id_hash) { this->name_ = StringRef(name); if (this->name_.empty()) { #ifdef USE_DEVICES @@ -18,11 +19,29 @@ void EntityBase::set_name(const char *name) { } else #endif { - this->name_ = StringRef(App.get_friendly_name()); + // Bug-for-bug compatibility with OLD behavior: + // - With MAC suffix: OLD code used App.get_friendly_name() directly (no fallback) + // - Without MAC suffix: OLD code used pre-computed object_id with fallback to device name + const std::string &friendly = App.get_friendly_name(); + if (App.is_name_add_mac_suffix_enabled()) { + // MAC suffix enabled - use friendly_name directly (even if empty) for compatibility + this->name_ = StringRef(friendly); + } else { + // No MAC suffix - fallback to device name if friendly_name is empty + this->name_ = StringRef(!friendly.empty() ? friendly : App.get_name()); + } } this->flags_.has_own_name = false; + // Dynamic name - must calculate hash at runtime + this->calc_object_id_(); } else { this->flags_.has_own_name = true; + // Static name - use pre-computed hash if provided + if (object_id_hash != 0) { + this->object_id_hash_ = object_id_hash; + } else { + this->calc_object_id_(); + } } } @@ -45,69 +64,30 @@ void EntityBase::set_icon(const char *icon) { #endif } -// Check if the object_id is dynamic (changes with MAC suffix) -bool EntityBase::is_object_id_dynamic_() const { - return !this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled(); -} - -// Entity Object ID +// Entity Object ID - computed on-demand from name std::string EntityBase::get_object_id() const { - // Check if `App.get_friendly_name()` is constant or dynamic. - if (this->is_object_id_dynamic_()) { - // `App.get_friendly_name()` is dynamic. - return str_sanitize(str_snake_case(App.get_friendly_name())); - } - // `App.get_friendly_name()` is constant. - return this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_; -} -void EntityBase::set_object_id(const char *object_id) { - this->object_id_c_str_ = object_id; - this->calc_object_id_(); -} - -void EntityBase::set_name_and_object_id(const char *name, const char *object_id) { - this->set_name(name); - this->object_id_c_str_ = object_id; - this->calc_object_id_(); -} - -// Calculate Object ID Hash from Entity Name -void EntityBase::calc_object_id_() { char buf[OBJECT_ID_MAX_LEN]; - StringRef object_id = this->get_object_id_to(buf); - this->object_id_hash_ = fnv1_hash(object_id.c_str()); + size_t len = this->write_object_id_to(buf, sizeof(buf)); + return std::string(buf, len); } -// Format dynamic object_id: sanitized snake_case of friendly_name -static size_t format_dynamic_object_id(char *buf, size_t buf_size) { - const std::string &name = App.get_friendly_name(); - size_t len = std::min(name.size(), buf_size - 1); - for (size_t i = 0; i < len; i++) { - buf[i] = to_sanitized_char(to_snake_case_char(name[i])); - } - buf[len] = '\0'; - return len; +// Calculate Object ID Hash directly from name using snake_case + sanitize +void EntityBase::calc_object_id_() { + this->object_id_hash_ = fnv1_hash_object_id(this->name_.c_str(), this->name_.size()); } size_t EntityBase::write_object_id_to(char *buf, size_t buf_size) const { - if (this->is_object_id_dynamic_()) { - return format_dynamic_object_id(buf, buf_size); + size_t len = std::min(this->name_.size(), buf_size - 1); + for (size_t i = 0; i < len; i++) { + buf[i] = to_sanitized_char(to_snake_case_char(this->name_[i])); } - const char *src = this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_; - size_t len = strlen(src); - if (len >= buf_size) - len = buf_size - 1; - memcpy(buf, src, len); buf[len] = '\0'; return len; } StringRef EntityBase::get_object_id_to(std::span buf) const { - if (this->is_object_id_dynamic_()) { - size_t len = format_dynamic_object_id(buf.data(), buf.size()); - return StringRef(buf.data(), len); - } - return this->object_id_c_str_ == nullptr ? StringRef() : StringRef(this->object_id_c_str_); + size_t len = this->write_object_id_to(buf.data(), buf.size()); + return StringRef(buf.data(), len); } uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index a5c69f132c..a45c7795bf 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -28,6 +28,9 @@ class EntityBase { // Get/set the name of this Entity const StringRef &get_name() const; void set_name(const char *name); + /// Set name with pre-computed object_id hash (avoids runtime hash calculation) + /// Use hash=0 for dynamic names that need runtime calculation + void set_name(const char *name, uint32_t object_id_hash); // Get whether this Entity has its own name or it should use the device friendly_name. bool has_own_name() const { return this->flags_.has_own_name; } @@ -43,10 +46,6 @@ class EntityBase { "which will remain available longer. get_object_id() will be removed in 2026.7.0", "2025.12.0") std::string get_object_id() const; - void set_object_id(const char *object_id); - - // Set both name and object_id in one call (reduces generated code size) - void set_name_and_object_id(const char *name, const char *object_id); // Get the unique Object ID of this Entity uint32_t get_object_id_hash(); @@ -142,11 +141,7 @@ class EntityBase { protected: void calc_object_id_(); - /// Check if the object_id is dynamic (changes with MAC suffix) - bool is_object_id_dynamic_() const; - StringRef name_; - const char *object_id_c_str_{nullptr}; #ifdef USE_ENTITY_ICON const char *icon_c_str_{nullptr}; #endif diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index f360b4d809..c1801c0bda 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -15,7 +15,7 @@ from esphome.const import ( from esphome.core import CORE, ID from esphome.cpp_generator import MockObj, add, get_variable import esphome.final_validate as fv -from esphome.helpers import sanitize, snake_case +from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case from esphome.types import ConfigType, EntityMetadata _LOGGER = logging.getLogger(__name__) @@ -75,34 +75,18 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: config: Configuration dictionary containing entity settings platform: The platform name (e.g., "sensor", "binary_sensor") """ - # Get device info - device_name: str | None = None - device_id_obj: ID | None + # Get device info if configured if device_id_obj := config.get(CONF_DEVICE_ID): device: MockObj = await get_variable(device_id_obj) add(var.set_device(device)) - # Get device name for object ID calculation - device_name = device_id_obj.id - # Calculate base object_id using the same logic as C++ - # This must match the C++ behavior in esphome/core/entity_base.cpp - base_object_id = get_base_entity_object_id( - config[CONF_NAME], CORE.friendly_name, device_name - ) - - if not config[CONF_NAME]: - _LOGGER.debug( - "Entity has empty name, using '%s' as object_id base", base_object_id - ) - - # Set both name and object_id in one call to reduce generated code size - add(var.set_name_and_object_id(config[CONF_NAME], base_object_id)) - _LOGGER.debug( - "Setting object_id '%s' for entity '%s' on platform '%s'", - base_object_id, - config[CONF_NAME], - platform, - ) + # Set the entity name with pre-computed object_id hash + # For named entities: pre-compute hash from entity name + # For empty-name entities: pass 0, C++ calculates hash at runtime from + # device name, friendly_name, or app name (bug-for-bug compatibility) + entity_name = config[CONF_NAME] + object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0 + add(var.set_name(entity_name, object_id_hash)) # Only set disabled_by_default if True (default is False) if config[CONF_DISABLED_BY_DEFAULT]: add(var.set_disabled_by_default(True)) diff --git a/esphome/core/gpio.cpp b/esphome/core/gpio.cpp new file mode 100644 index 0000000000..21e88b5b6d --- /dev/null +++ b/esphome/core/gpio.cpp @@ -0,0 +1,24 @@ +#include "esphome/core/gpio.h" +#include "esphome/core/log.h" + +namespace esphome { + +#ifdef USE_ESP8266 +void log_pin(const char *tag, const __FlashStringHelper *prefix, GPIOPin *pin) { + if (pin == nullptr) + return; + static constexpr size_t LOG_PIN_PREFIX_MAX_LEN = 32; + char prefix_buf[LOG_PIN_PREFIX_MAX_LEN]; + strncpy_P(prefix_buf, reinterpret_cast(prefix), sizeof(prefix_buf) - 1); + prefix_buf[sizeof(prefix_buf) - 1] = '\0'; + log_pin_with_prefix(tag, prefix_buf, pin); +} +#else +void log_pin(const char *tag, const char *prefix, GPIOPin *pin) { + if (pin == nullptr) + return; + log_pin_with_prefix(tag, prefix, pin); +} +#endif + +} // namespace esphome diff --git a/esphome/core/gpio.h b/esphome/core/gpio.h index dd6f14fef9..f2f85e18bc 100644 --- a/esphome/core/gpio.h +++ b/esphome/core/gpio.h @@ -1,13 +1,22 @@ #pragma once +#include #include +#include #include +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + namespace esphome { -#define LOG_PIN(prefix, pin) \ - if ((pin) != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix "%s", (pin)->dump_summary().c_str()); \ - } +/// Maximum buffer size for dump_summary output +inline constexpr size_t GPIO_SUMMARY_MAX_LEN = 48; + +#ifdef USE_ESP8266 +#define LOG_PIN(prefix, pin) log_pin(TAG, F(prefix), pin) +#else +#define LOG_PIN(prefix, pin) log_pin(TAG, prefix, pin) +#endif // put GPIO flags in a namespace to not pollute esphome namespace namespace gpio { @@ -64,7 +73,17 @@ class GPIOPin { virtual void digital_write(bool value) = 0; - virtual std::string dump_summary() const = 0; + /// Write a summary of this pin to the provided buffer. + /// @param buffer The buffer to write to + /// @param len The size of the buffer (must be > 0) + /// @return The number of characters that would be written (excluding null terminator), + /// which may exceed len-1 if truncation occurred (snprintf semantics) + virtual size_t dump_summary(char *buffer, size_t len) const; + + /// Get a summary of this pin as a string. + /// @deprecated Use dump_summary(char*, size_t) instead. Will be removed in 2026.7.0. + ESPDEPRECATED("Override dump_summary(char*, size_t) instead. Will be removed in 2026.7.0.", "2026.1.0") + virtual std::string dump_summary() const; virtual bool is_internal() { return false; } }; @@ -103,4 +122,41 @@ class InternalGPIOPin : public GPIOPin { virtual void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const = 0; }; +// Inline default implementations for GPIOPin virtual methods. +// These provide bridge functionality for backwards compatibility with external components. + +// Default implementation bridges to old std::string method for backwards compatibility. +inline size_t GPIOPin::dump_summary(char *buffer, size_t len) const { + if (len == 0) + return 0; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + std::string s = this->dump_summary(); +#pragma GCC diagnostic pop + size_t copy_len = std::min(s.size(), len - 1); + memcpy(buffer, s.c_str(), copy_len); + buffer[copy_len] = '\0'; + return s.size(); // Return would-be length (snprintf semantics) +} + +// Default implementation returns empty string. +// External components should override this if they haven't migrated to buffer-based version. +// Remove before 2026.7.0 +inline std::string GPIOPin::dump_summary() const { return {}; } + +// Inline helper for log_pin - allows compiler to inline into log_pin in gpio.cpp +inline void log_pin_with_prefix(const char *tag, const char *prefix, GPIOPin *pin) { + char buffer[GPIO_SUMMARY_MAX_LEN]; + size_t len = pin->dump_summary(buffer, sizeof(buffer)); + len = std::min(len, sizeof(buffer) - 1); + esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, "%s%.*s", prefix, (int) len, buffer); +} + +// log_pin function declarations - implementation in gpio.cpp +#ifdef USE_ESP8266 +void log_pin(const char *tag, const __FlashStringHelper *prefix, GPIOPin *pin); +#else +void log_pin(const char *tag, const char *prefix, GPIOPin *pin); +#endif + } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index ac7a96a8c8..f7a14ed2ec 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -529,6 +529,20 @@ constexpr char to_sanitized_char(char c) { /// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores. std::string str_sanitize(const std::string &str); +/// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations. +/// This computes object_id hashes directly from names without creating an intermediate buffer. +/// IMPORTANT: Must match Python fnv1_hash_object_id() in esphome/helpers.py. +/// If you modify this function, update the Python version and tests in both places. +inline uint32_t fnv1_hash_object_id(const char *str, size_t len) { + uint32_t hash = FNV1_OFFSET_BASIS; + for (size_t i = 0; i < len; i++) { + hash *= FNV1_PRIME; + // Apply snake_case (space->underscore, uppercase->lowercase) then sanitize + hash ^= static_cast(to_sanitized_char(to_snake_case_char(str[i]))); + } + return hash; +} + /// snprintf-like function returning std::string of maximum length \p len (excluding null terminator). std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...); diff --git a/esphome/helpers.py b/esphome/helpers.py index d1623d1d3c..ae142b7f8b 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -35,6 +35,10 @@ IS_MACOS = platform.system() == "Darwin" IS_WINDOWS = platform.system() == "Windows" IS_LINUX = platform.system() == "Linux" +# FNV-1 hash constants (must match C++ in esphome/core/helpers.h) +FNV1_OFFSET_BASIS = 2166136261 +FNV1_PRIME = 16777619 + def ensure_unique_string(preferred_string, current_strings): test_string = preferred_string @@ -49,8 +53,17 @@ def ensure_unique_string(preferred_string, current_strings): return test_string +def fnv1_hash(string: str) -> int: + """FNV-1 32-bit hash function (multiply then XOR).""" + hash_value = FNV1_OFFSET_BASIS + for char in string: + hash_value = (hash_value * FNV1_PRIME) & 0xFFFFFFFF + hash_value ^= ord(char) + return hash_value + + def fnv1a_32bit_hash(string: str) -> int: - """FNV-1a 32-bit hash function. + """FNV-1a 32-bit hash function (XOR then multiply). Note: This uses 32-bit hash instead of 64-bit for several reasons: 1. ESPHome targets 32-bit microcontrollers with limited RAM (often <320KB) @@ -63,13 +76,22 @@ def fnv1a_32bit_hash(string: str) -> int: a handful of area_ids and device_ids (typically <10 areas and <100 devices), making collisions virtually impossible. """ - hash_value = 2166136261 + hash_value = FNV1_OFFSET_BASIS for char in string: hash_value ^= ord(char) - hash_value = (hash_value * 16777619) & 0xFFFFFFFF + hash_value = (hash_value * FNV1_PRIME) & 0xFFFFFFFF return hash_value +def fnv1_hash_object_id(name: str) -> int: + """Compute FNV-1 hash of name with snake_case + sanitize transformations. + + IMPORTANT: Must produce same result as C++ fnv1_hash_object_id() in helpers.h. + Used for pre-computing entity object_id hashes at code generation time. + """ + return fnv1_hash(sanitize(snake_case(name))) + + def strip_accents(value: str) -> str: """Remove accents from a string.""" import unicodedata diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index 86e0705023..ce4e64681f 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -29,7 +29,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main): ) # Then - assert 'bs_1->set_name_and_object_id("test bs1", "test_bs1");' in main_cpp + assert 'bs_1->set_name("test bs1",' in main_cpp assert "bs_1->set_pin(" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index b21665288c..797b6fb1a4 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -26,7 +26,7 @@ def test_button_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert 'wol_1->set_name_and_object_id("wol_test_1", "wol_test_1");' in main_cpp + assert 'wol_1->set_name("wol_test_1",' in main_cpp assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index bfc3131f6d..6b047bc62f 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -25,7 +25,7 @@ def test_text_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert 'it_1->set_name_and_object_id("test 1 text", "test_1_text");' in main_cpp + assert 'it_1->set_name("test 1 text",' in main_cpp def test_text_config_value_internal_set(generate_main): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 934ee67cef..1593d0b6d8 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -25,18 +25,9 @@ def test_text_sensor_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert ( - 'ts_1->set_name_and_object_id("Template Text Sensor 1", "template_text_sensor_1");' - in main_cpp - ) - assert ( - 'ts_2->set_name_and_object_id("Template Text Sensor 2", "template_text_sensor_2");' - in main_cpp - ) - assert ( - 'ts_3->set_name_and_object_id("Template Text Sensor 3", "template_text_sensor_3");' - in main_cpp - ) + assert 'ts_1->set_name("Template Text Sensor 1",' in main_cpp + assert 'ts_2->set_name("Template Text Sensor 2",' in main_cpp + assert 'ts_3->set_name("Template Text Sensor 3",' in main_cpp def test_text_sensor_config_value_internal_set(generate_main): diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 965363972f..50e8d4122b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -51,6 +51,9 @@ if platform.system() == "Windows": import pty # not available on Windows +# Register assert rewrite for entity_utils so assertions have proper error messages +pytest.register_assert_rewrite("tests.integration.entity_utils") + def _get_platformio_env(cache_dir: Path) -> dict[str, str]: """Get environment variables for PlatformIO with shared cache.""" diff --git a/tests/integration/entity_utils.py b/tests/integration/entity_utils.py new file mode 100644 index 0000000000..7596983ee2 --- /dev/null +++ b/tests/integration/entity_utils.py @@ -0,0 +1,145 @@ +"""Utilities for computing entity object_id in integration tests. + +This module contains the algorithm that aioesphomeapi will use to compute +object_id client-side from API data. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case + +if TYPE_CHECKING: + from aioesphomeapi import DeviceInfo, EntityInfo + + +def compute_object_id(name: str) -> str: + """Compute object_id from name using snake_case + sanitize.""" + return sanitize(snake_case(name)) + + +def infer_name_add_mac_suffix(device_info: DeviceInfo) -> bool: + """Infer name_add_mac_suffix from device name ending with MAC suffix.""" + mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower() + return device_info.name.endswith(f"-{mac_suffix}") + + +def _get_name_for_object_id( + entity: EntityInfo, + device_info: DeviceInfo, + device_id_to_name: dict[int, str], +) -> str: + """Get the name used for object_id computation. + + This is the algorithm that aioesphomeapi will use to determine which + name to use for computing object_id client-side from API data. + + Args: + entity: The entity to get name for + device_info: Device info from the API + device_id_to_name: Mapping of device_id to device name for sub-devices + + Returns: + The name to use for object_id computation + """ + if entity.name: + # Named entity: use entity name + return entity.name + if entity.device_id != 0: + # Empty name on sub-device: use sub-device name + return device_id_to_name[entity.device_id] + if infer_name_add_mac_suffix(device_info) or device_info.friendly_name: + # Empty name on main device with MAC suffix or friendly_name: use friendly_name + # (even if empty - this is bug-for-bug compatibility for MAC suffix case) + return device_info.friendly_name + # Empty name on main device, no friendly_name: use device name + return device_info.name + + +def compute_entity_object_id( + entity: EntityInfo, + device_info: DeviceInfo, + device_id_to_name: dict[int, str], +) -> str: + """Compute expected object_id for an entity. + + Args: + entity: The entity to compute object_id for + device_info: Device info from the API + device_id_to_name: Mapping of device_id to device name for sub-devices + + Returns: + The computed object_id string + """ + name_for_id = _get_name_for_object_id(entity, device_info, device_id_to_name) + return compute_object_id(name_for_id) + + +def compute_entity_hash( + entity: EntityInfo, + device_info: DeviceInfo, + device_id_to_name: dict[int, str], +) -> int: + """Compute expected object_id hash for an entity. + + Args: + entity: The entity to compute hash for + device_info: Device info from the API + device_id_to_name: Mapping of device_id to device name for sub-devices + + Returns: + The computed FNV-1 hash + """ + name_for_id = _get_name_for_object_id(entity, device_info, device_id_to_name) + return fnv1_hash_object_id(name_for_id) + + +def verify_entity_object_id( + entity: EntityInfo, + device_info: DeviceInfo, + device_id_to_name: dict[int, str], +) -> None: + """Verify an entity's object_id and hash match the expected values. + + Args: + entity: The entity to verify + device_info: Device info from the API + device_id_to_name: Mapping of device_id to device name for sub-devices + + Raises: + AssertionError: If object_id or hash doesn't match expected value + """ + expected_object_id = compute_entity_object_id( + entity, device_info, device_id_to_name + ) + assert entity.object_id == expected_object_id, ( + f"object_id mismatch for entity '{entity.name}': " + f"expected '{expected_object_id}', got '{entity.object_id}'" + ) + + expected_hash = compute_entity_hash(entity, device_info, device_id_to_name) + assert entity.key == expected_hash, ( + f"hash mismatch for entity '{entity.name}': " + f"expected {expected_hash:#x}, got {entity.key:#x}" + ) + + +def verify_all_entities( + entities: list[EntityInfo], + device_info: DeviceInfo, +) -> None: + """Verify all entities have correct object_id and hash values. + + Args: + entities: List of entities to verify + device_info: Device info from the API + + Raises: + AssertionError: If any entity's object_id or hash doesn't match + """ + # Build device_id -> name lookup from sub-devices + device_id_to_name = {d.device_id: d.name for d in device_info.devices} + + for entity in entities: + verify_entity_object_id(entity, device_info, device_id_to_name) diff --git a/tests/integration/fixtures/fnv1_hash_object_id.yaml b/tests/integration/fixtures/fnv1_hash_object_id.yaml new file mode 100644 index 0000000000..2097b2fbf9 --- /dev/null +++ b/tests/integration/fixtures/fnv1_hash_object_id.yaml @@ -0,0 +1,76 @@ +esphome: + name: fnv1-hash-object-id-test + platformio_options: + build_flags: + - "-DDEBUG" + on_boot: + - lambda: |- + using esphome::fnv1_hash_object_id; + + // Test basic lowercase (hash matches Python fnv1_hash_object_id("foo")) + uint32_t hash_foo = fnv1_hash_object_id("foo", 3); + if (hash_foo == 0x408f5e13) { + ESP_LOGI("FNV1_OID", "foo PASSED"); + } else { + ESP_LOGE("FNV1_OID", "foo FAILED: 0x%08x != 0x408f5e13", hash_foo); + } + + // Test uppercase conversion (should match lowercase) + uint32_t hash_Foo = fnv1_hash_object_id("Foo", 3); + if (hash_Foo == 0x408f5e13) { + ESP_LOGI("FNV1_OID", "upper PASSED"); + } else { + ESP_LOGE("FNV1_OID", "upper FAILED: 0x%08x != 0x408f5e13", hash_Foo); + } + + // Test space to underscore conversion ("foo bar" -> "foo_bar") + uint32_t hash_space = fnv1_hash_object_id("foo bar", 7); + if (hash_space == 0x3ae35aa1) { + ESP_LOGI("FNV1_OID", "space PASSED"); + } else { + ESP_LOGE("FNV1_OID", "space FAILED: 0x%08x != 0x3ae35aa1", hash_space); + } + + // Test underscore preserved ("foo_bar") + uint32_t hash_underscore = fnv1_hash_object_id("foo_bar", 7); + if (hash_underscore == 0x3ae35aa1) { + ESP_LOGI("FNV1_OID", "underscore PASSED"); + } else { + ESP_LOGE("FNV1_OID", "underscore FAILED: 0x%08x != 0x3ae35aa1", hash_underscore); + } + + // Test hyphen preserved ("foo-bar") + uint32_t hash_hyphen = fnv1_hash_object_id("foo-bar", 7); + if (hash_hyphen == 0x438b12e3) { + ESP_LOGI("FNV1_OID", "hyphen PASSED"); + } else { + ESP_LOGE("FNV1_OID", "hyphen FAILED: 0x%08x != 0x438b12e3", hash_hyphen); + } + + // Test special chars become underscore ("foo!bar" -> "foo_bar") + uint32_t hash_special = fnv1_hash_object_id("foo!bar", 7); + if (hash_special == 0x3ae35aa1) { + ESP_LOGI("FNV1_OID", "special PASSED"); + } else { + ESP_LOGE("FNV1_OID", "special FAILED: 0x%08x != 0x3ae35aa1", hash_special); + } + + // Test complex name ("My Sensor Name" -> "my_sensor_name") + uint32_t hash_complex = fnv1_hash_object_id("My Sensor Name", 14); + if (hash_complex == 0x2760962a) { + ESP_LOGI("FNV1_OID", "complex PASSED"); + } else { + ESP_LOGE("FNV1_OID", "complex FAILED: 0x%08x != 0x2760962a", hash_complex); + } + + // Test empty string returns FNV1_OFFSET_BASIS + uint32_t hash_empty = fnv1_hash_object_id("", 0); + if (hash_empty == 0x811c9dc5) { + ESP_LOGI("FNV1_OID", "empty PASSED"); + } else { + ESP_LOGE("FNV1_OID", "empty FAILED: 0x%08x != 0x811c9dc5", hash_empty); + } + +host: +api: +logger: diff --git a/tests/integration/fixtures/object_id_api_verification.yaml b/tests/integration/fixtures/object_id_api_verification.yaml new file mode 100644 index 0000000000..386270fc2c --- /dev/null +++ b/tests/integration/fixtures/object_id_api_verification.yaml @@ -0,0 +1,125 @@ +esphome: + name: object-id-test + friendly_name: Test Device + # Enable MAC suffix - host MAC is 98:35:69:ab:f6:79, suffix is "abf679" + # friendly_name becomes "Test Device abf679" + name_add_mac_suffix: true + # Sub-devices for testing empty-name entities on devices + devices: + - id: sub_device_1 + name: Sub Device One + - id: sub_device_2 + name: Sub Device Two + +host: + +api: + +logger: + +sensor: + # Test 1: Basic name -> object_id = "temperature_sensor" + - platform: template + name: "Temperature Sensor" + id: sensor_basic + lambda: return 42.0; + update_interval: 60s + + # Test 2: Uppercase name -> object_id = "uppercase_name" + - platform: template + name: "UPPERCASE NAME" + id: sensor_uppercase + lambda: return 43.0; + update_interval: 60s + + # Test 3: Special characters -> object_id = "special__chars_" + - platform: template + name: "Special!@Chars#" + id: sensor_special + lambda: return 44.0; + update_interval: 60s + + # Test 4: Hyphen preserved -> object_id = "temp-sensor" + - platform: template + name: "Temp-Sensor" + id: sensor_hyphen + lambda: return 45.0; + update_interval: 60s + + # Test 5: Underscore preserved -> object_id = "temp_sensor" + - platform: template + name: "Temp_Sensor" + id: sensor_underscore + lambda: return 46.0; + update_interval: 60s + + # Test 6: Mixed case with spaces -> object_id = "living_room_temperature" + - platform: template + name: "Living Room Temperature" + id: sensor_mixed + lambda: return 47.0; + update_interval: 60s + + # Test 7: Empty name - uses friendly_name with MAC suffix + # friendly_name = "Test Device abf679" -> object_id = "test_device_abf679" + - platform: template + name: "" + id: sensor_empty_name + lambda: return 48.0; + update_interval: 60s + +binary_sensor: + # Test 8: Different platform same conversion rules + - platform: template + name: "Door Open" + id: binary_door + lambda: return true; + + # Test 9: Numbers in name -> object_id = "sensor_123" + - platform: template + name: "Sensor 123" + id: binary_numbers + lambda: return false; + +switch: + # Test 10: Long name with multiple spaces + - platform: template + name: "My Very Long Switch Name Here" + id: switch_long + lambda: return false; + turn_on_action: + - logger.log: "on" + turn_off_action: + - logger.log: "off" + +text_sensor: + # Test 11: Name starting with number (should work fine) + - platform: template + name: "123 Start" + id: text_num_start + lambda: return {"test"}; + update_interval: 60s + +button: + # Test 12: Named entity on sub-device -> object_id from entity name + - platform: template + name: "Device Button" + id: button_on_device + device_id: sub_device_1 + on_press: [] + + # Test 13: Empty name on sub-device -> object_id from device name + # Device name "Sub Device One" -> object_id = "sub_device_one" + - platform: template + name: "" + id: button_empty_on_device1 + device_id: sub_device_1 + on_press: [] + + # Test 14: Empty name on different sub-device + # Device name "Sub Device Two" -> object_id = "sub_device_two" + - platform: template + name: "" + id: button_empty_on_device2 + device_id: sub_device_2 + on_press: [] diff --git a/tests/integration/fixtures/object_id_friendly_name_no_mac_suffix.yaml b/tests/integration/fixtures/object_id_friendly_name_no_mac_suffix.yaml new file mode 100644 index 0000000000..7a86e37d08 --- /dev/null +++ b/tests/integration/fixtures/object_id_friendly_name_no_mac_suffix.yaml @@ -0,0 +1,27 @@ +esphome: + name: test-device + # friendly_name set but NO MAC suffix + # Empty-name entity should use friendly_name for object_id + friendly_name: My Friendly Device + +host: + +api: + +logger: + +sensor: + # Empty name entity - should use friendly_name for object_id + # friendly_name = "My Friendly Device" -> object_id = "my_friendly_device" + - platform: template + name: "" + id: sensor_empty_name + lambda: return 42.0; + update_interval: 60s + + # Named entity for comparison + - platform: template + name: "Temperature" + id: sensor_named + lambda: return 43.0; + update_interval: 60s diff --git a/tests/integration/fixtures/object_id_no_friendly_name_no_mac_suffix.yaml b/tests/integration/fixtures/object_id_no_friendly_name_no_mac_suffix.yaml new file mode 100644 index 0000000000..4a947e0f6a --- /dev/null +++ b/tests/integration/fixtures/object_id_no_friendly_name_no_mac_suffix.yaml @@ -0,0 +1,25 @@ +esphome: + name: test-device + # No friendly_name set, no MAC suffix + # OLD behavior: object_id = device name because Python pre-computed with fallback + +host: + +api: + +logger: + +sensor: + # Empty name entity - OLD behavior used device name as fallback + - platform: template + name: "" + id: sensor_empty_name + lambda: return 42.0; + update_interval: 60s + + # Named entity for comparison + - platform: template + name: "Temperature" + id: sensor_named + lambda: return 43.0; + update_interval: 60s diff --git a/tests/integration/fixtures/object_id_no_friendly_name_with_mac_suffix.yaml b/tests/integration/fixtures/object_id_no_friendly_name_with_mac_suffix.yaml new file mode 100644 index 0000000000..ab12e670a0 --- /dev/null +++ b/tests/integration/fixtures/object_id_no_friendly_name_with_mac_suffix.yaml @@ -0,0 +1,26 @@ +esphome: + name: test-device + # No friendly_name set, MAC suffix enabled + # OLD behavior: object_id = "" (empty) because is_object_id_dynamic_() used App.get_friendly_name() directly + name_add_mac_suffix: true + +host: + +api: + +logger: + +sensor: + # Empty name entity - OLD behavior produced empty object_id when MAC suffix enabled + - platform: template + name: "" + id: sensor_empty_name + lambda: return 42.0; + update_interval: 60s + + # Named entity for comparison + - platform: template + name: "Temperature" + id: sensor_named + lambda: return 43.0; + update_interval: 60s diff --git a/tests/integration/test_fnv1_hash_object_id.py b/tests/integration/test_fnv1_hash_object_id.py new file mode 100644 index 0000000000..23e8ca04c2 --- /dev/null +++ b/tests/integration/test_fnv1_hash_object_id.py @@ -0,0 +1,75 @@ +"""Integration test for fnv1_hash_object_id function. + +This test verifies that the C++ fnv1_hash_object_id() function in +esphome/core/helpers.h produces the same hash values as the Python +fnv1_hash_object_id() function in esphome/helpers.py. + +If this test fails, one of the implementations has diverged and needs +to be updated to match the other. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_fnv1_hash_object_id( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that C++ fnv1_hash_object_id matches Python implementation.""" + + test_results: dict[str, str] = {} + all_tests_complete = asyncio.Event() + expected_tests = { + "foo", + "upper", + "space", + "underscore", + "hyphen", + "special", + "complex", + "empty", + } + + def on_log_line(line: str) -> None: + """Capture log lines with test results.""" + # Strip ANSI escape codes + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + # Look for our test result messages + # Format: "[timestamp][level][FNV1_OID:line]: test_name PASSED" + match = re.search(r"\[FNV1_OID:\d+\]:\s+(\w+)\s+(PASSED|FAILED)", clean_line) + if match: + test_name = match.group(1) + result = match.group(2) + test_results[test_name] = result + if set(test_results.keys()) >= expected_tests: + all_tests_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "fnv1-hash-object-id-test" + + # Wait for all tests to complete or timeout + try: + await asyncio.wait_for(all_tests_complete.wait(), timeout=2.0) + except TimeoutError: + pytest.fail(f"Tests timed out. Got results for: {set(test_results.keys())}") + + # Verify all tests passed + for test_name in expected_tests: + assert test_name in test_results, f"{test_name} test not found" + assert test_results[test_name] == "PASSED", ( + f"{test_name} test failed - C++ and Python hash mismatch" + ) diff --git a/tests/integration/test_object_id_api_verification.py b/tests/integration/test_object_id_api_verification.py new file mode 100644 index 0000000000..c8603e0682 --- /dev/null +++ b/tests/integration/test_object_id_api_verification.py @@ -0,0 +1,176 @@ +"""Integration test to verify object_id from API matches Python computation. + +This test verifies a three-way match between: +1. C++ object_id generation (get_object_id_to using to_sanitized_char/to_snake_case_char) +2. C++ hash generation (fnv1_hash_object_id in helpers.h) +3. Python computation (sanitize/snake_case in helpers.py, fnv1_hash_object_id) + +The API response contains C++ computed values, so verifying API == Python +implicitly verifies C++ == Python == API for both object_id and hash. + +This is important for the planned migration to remove object_id from the API +protocol and have clients (like aioesphomeapi) compute it from the name. +See: https://github.com/esphome/backlog/issues/76 + +Test cases covered: +- Named entities with various characters (uppercase, special chars, hyphens, etc.) +- Empty-name entities on main device (uses device's friendly_name with MAC suffix) +- Empty-name entities on sub-devices (uses sub-device's name) +- Named entities on sub-devices (uses entity name, not device name) +- MAC suffix handling (name_add_mac_suffix modifies friendly_name at runtime) +- Both object_id string and hash (key) verification +""" + +from __future__ import annotations + +import pytest + +from esphome.helpers import fnv1_hash_object_id + +from .entity_utils import compute_object_id, verify_all_entities +from .types import APIClientConnectedFactory, RunCompiledFunction + +# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679" +MAC_SUFFIX = "abf679" + + +# Expected entities with their own names and expected object_ids +# Format: (entity_name, expected_object_id) +NAMED_ENTITIES = [ + # sensor platform + ("Temperature Sensor", "temperature_sensor"), + ("UPPERCASE NAME", "uppercase_name"), + ("Special!@Chars#", "special__chars_"), + ("Temp-Sensor", "temp-sensor"), + ("Temp_Sensor", "temp_sensor"), + ("Living Room Temperature", "living_room_temperature"), + # binary_sensor platform + ("Door Open", "door_open"), + ("Sensor 123", "sensor_123"), + # switch platform + ("My Very Long Switch Name Here", "my_very_long_switch_name_here"), + # text_sensor platform + ("123 Start", "123_start"), + # button platform - named entity on sub-device (uses entity name, not device name) + ("Device Button", "device_button"), +] + +# Sub-device names and their expected object_ids for empty-name entities +# Format: (device_name, expected_object_id) +SUB_DEVICE_EMPTY_NAME_ENTITIES = [ + ("Sub Device One", "sub_device_one"), + ("Sub Device Two", "sub_device_two"), +] + + +@pytest.mark.asyncio +async def test_object_id_api_verification( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that object_id from API matches Python computation. + + Tests: + 1. Named entities - object_id computed from entity name + 2. Empty-name entities - object_id computed from friendly_name (with MAC suffix) + 3. Hash verification - key can be computed from name + 4. Generic verification - all entities can have object_id computed from API data + """ + async with run_compiled(yaml_config), api_client_connected() as client: + # Get device info + device_info = await client.device_info() + assert device_info is not None + + # Device name should include MAC suffix (hyphen separator) + assert device_info.name == f"object-id-test-{MAC_SUFFIX}", ( + f"Device name mismatch: got '{device_info.name}'" + ) + # Friendly name should include MAC suffix (space separator) + expected_friendly_name = f"Test Device {MAC_SUFFIX}" + assert device_info.friendly_name == expected_friendly_name, ( + f"Friendly name mismatch: got '{device_info.friendly_name}'" + ) + + # Get all entities + entities, _ = await client.list_entities_services() + + # Create a map of entity names to entity info + entity_map = {} + for entity in entities: + entity_map[entity.name] = entity + + # === Test 1: Verify each named entity === + for entity_name, expected_object_id in NAMED_ENTITIES: + assert entity_name in entity_map, ( + f"Entity '{entity_name}' not found in API response. " + f"Available: {list(entity_map.keys())}" + ) + + entity = entity_map[entity_name] + + # Verify object_id matches expected + assert entity.object_id == expected_object_id, ( + f"Entity '{entity_name}': object_id mismatch. " + f"API returned '{entity.object_id}', expected '{expected_object_id}'" + ) + + # Verify Python computation matches + computed = compute_object_id(entity_name) + assert computed == expected_object_id, ( + f"Entity '{entity_name}': Python computation mismatch. " + f"Computed '{computed}', expected '{expected_object_id}'" + ) + + # Verify hash can be computed from the name + hash_from_name = fnv1_hash_object_id(entity_name) + assert hash_from_name == entity.key, ( + f"Entity '{entity_name}': hash mismatch. " + f"Python hash {hash_from_name:#x}, API key {entity.key:#x}" + ) + + # === Test 2: Verify empty-name entities === + # Empty-name entities have name="" in API, object_id comes from: + # - Main device: friendly_name (with MAC suffix) + # - Sub-device: device name + + # Get all empty-name entities + empty_name_entities = [e for e in entities if e.name == ""] + # We expect 3: 1 on main device, 2 on sub-devices + assert len(empty_name_entities) == 3, ( + f"Expected 3 empty-name entities, got {len(empty_name_entities)}" + ) + + # Build device_id -> device_name map from device_info + device_id_to_name = {d.device_id: d.name for d in device_info.devices} + + # Verify each empty-name entity + for entity in empty_name_entities: + if entity.device_id == 0: + # Main device - uses friendly_name with MAC suffix + expected_name = expected_friendly_name + else: + # Sub-device - uses device name + assert entity.device_id in device_id_to_name, ( + f"Entity device_id {entity.device_id} not found in devices" + ) + expected_name = device_id_to_name[entity.device_id] + + expected_object_id = compute_object_id(expected_name) + assert entity.object_id == expected_object_id, ( + f"Empty-name entity (device_id={entity.device_id}): object_id mismatch. " + f"API: '{entity.object_id}', expected: '{expected_object_id}' " + f"(from name '{expected_name}')" + ) + + # Verify hash matches + expected_hash = fnv1_hash_object_id(expected_name) + assert entity.key == expected_hash, ( + f"Empty-name entity (device_id={entity.device_id}): hash mismatch. " + f"API key: {entity.key:#x}, expected: {expected_hash:#x}" + ) + + # === Test 3: Verify ALL entities using the algorithm from entity_utils === + # This uses the algorithm that aioesphomeapi will use to compute object_id + # client-side from API data. + verify_all_entities(entities, device_info) diff --git a/tests/integration/test_object_id_friendly_name_no_mac_suffix.py b/tests/integration/test_object_id_friendly_name_no_mac_suffix.py new file mode 100644 index 0000000000..7199a2b371 --- /dev/null +++ b/tests/integration/test_object_id_friendly_name_no_mac_suffix.py @@ -0,0 +1,81 @@ +"""Integration test for object_id with friendly_name but no MAC suffix. + +This test covers Branch 4 of the algorithm: +- Empty name on main device +- NO MAC suffix enabled +- friendly_name IS set +- Result: use friendly_name for object_id +""" + +from __future__ import annotations + +import pytest + +from esphome.helpers import fnv1_hash_object_id + +from .entity_utils import ( + compute_object_id, + infer_name_add_mac_suffix, + verify_all_entities, +) +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_object_id_friendly_name_no_mac_suffix( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test object_id when friendly_name is set but no MAC suffix. + + This covers Branch 4 of the algorithm: + - Empty name entity + - name_add_mac_suffix = false (or not set) + - friendly_name = "My Friendly Device" + - Expected: object_id = "my_friendly_device" + """ + async with run_compiled(yaml_config), api_client_connected() as client: + device_info = await client.device_info() + assert device_info is not None + + # Device name should NOT include MAC suffix + assert device_info.name == "test-device" + + # Friendly name should be set + assert device_info.friendly_name == "My Friendly Device" + + entities, _ = await client.list_entities_services() + + # Find the empty-name entity + empty_name_entities = [e for e in entities if e.name == ""] + assert len(empty_name_entities) == 1 + + entity = empty_name_entities[0] + + # Should use friendly_name for object_id (Branch 4) + expected_object_id = compute_object_id("My Friendly Device") + assert expected_object_id == "my_friendly_device" # Verify our expectation + assert entity.object_id == expected_object_id, ( + f"Expected object_id '{expected_object_id}' from friendly_name, " + f"got '{entity.object_id}'" + ) + + # Hash should match friendly_name + expected_hash = fnv1_hash_object_id("My Friendly Device") + assert entity.key == expected_hash, ( + f"Expected hash {expected_hash:#x}, got {entity.key:#x}" + ) + + # Named entity should work normally + named_entities = [e for e in entities if e.name == "Temperature"] + assert len(named_entities) == 1 + assert named_entities[0].object_id == "temperature" + + # Verify our inference: no MAC suffix in this test + assert not infer_name_add_mac_suffix(device_info), ( + "Device name should NOT have MAC suffix" + ) + + # Verify the full algorithm from entity_utils works for ALL entities + verify_all_entities(entities, device_info) diff --git a/tests/integration/test_object_id_no_friendly_name.py b/tests/integration/test_object_id_no_friendly_name.py new file mode 100644 index 0000000000..b548f02fde --- /dev/null +++ b/tests/integration/test_object_id_no_friendly_name.py @@ -0,0 +1,140 @@ +"""Integration tests for object_id when friendly_name is not set. + +These tests verify bug-for-bug compatibility with the old behavior: + +1. With MAC suffix enabled + no friendly_name: + - OLD: is_object_id_dynamic_() was true, used App.get_friendly_name() directly + - OLD: object_id = "" (empty) because friendly_name was empty + - NEW: Must maintain same behavior for compatibility + +2. Without MAC suffix + no friendly_name: + - OLD: is_object_id_dynamic_() was false, used pre-computed object_id_c_str_ + - OLD: Python computed object_id with fallback to device name + - NEW: Must maintain same behavior (object_id = device name) +""" + +from __future__ import annotations + +import pytest + +from esphome.helpers import fnv1_hash_object_id + +from .entity_utils import compute_object_id, verify_all_entities +from .types import APIClientConnectedFactory, RunCompiledFunction + +# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679" +MAC_SUFFIX = "abf679" + +# FNV1 offset basis - hash of empty string +FNV1_OFFSET_BASIS = 2166136261 + + +@pytest.mark.asyncio +async def test_object_id_no_friendly_name_with_mac_suffix( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test object_id when friendly_name not set but MAC suffix enabled. + + OLD behavior (bug-for-bug compatibility): + - is_object_id_dynamic_() returned true (no own name AND mac suffix enabled) + - format_dynamic_object_id() used App.get_friendly_name() directly + - Since friendly_name was empty, object_id was empty + + This was arguably a bug, but we maintain it for compatibility. + """ + async with run_compiled(yaml_config), api_client_connected() as client: + device_info = await client.device_info() + assert device_info is not None + + # Device name should include MAC suffix + expected_device_name = f"test-device-{MAC_SUFFIX}" + assert device_info.name == expected_device_name + + # Friendly name should be empty (not set in config) + assert device_info.friendly_name == "" + + entities, _ = await client.list_entities_services() + + # Find the empty-name entity + empty_name_entities = [e for e in entities if e.name == ""] + assert len(empty_name_entities) == 1 + + entity = empty_name_entities[0] + + # OLD behavior: object_id was empty because App.get_friendly_name() was empty + # This is bug-for-bug compatibility + assert entity.object_id == "", ( + f"Expected empty object_id for bug-for-bug compatibility, " + f"got '{entity.object_id}'" + ) + + # Hash should be FNV1_OFFSET_BASIS (hash of empty string) + assert entity.key == FNV1_OFFSET_BASIS, ( + f"Expected hash of empty string ({FNV1_OFFSET_BASIS:#x}), " + f"got {entity.key:#x}" + ) + + # Named entity should work normally + named_entities = [e for e in entities if e.name == "Temperature"] + assert len(named_entities) == 1 + assert named_entities[0].object_id == "temperature" + + # Verify the full algorithm from entity_utils works for ALL entities + verify_all_entities(entities, device_info) + + +@pytest.mark.asyncio +async def test_object_id_no_friendly_name_no_mac_suffix( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test object_id when friendly_name not set and no MAC suffix. + + OLD behavior: + - is_object_id_dynamic_() returned false (mac suffix not enabled) + - Used object_id_c_str_ which was pre-computed in Python + - Python used get_base_entity_object_id() with fallback to CORE.name + + Result: object_id = sanitize(snake_case(device_name)) + """ + async with run_compiled(yaml_config), api_client_connected() as client: + device_info = await client.device_info() + assert device_info is not None + + # Device name should NOT include MAC suffix + assert device_info.name == "test-device" + + # Friendly name should be empty (not set in config) + assert device_info.friendly_name == "" + + entities, _ = await client.list_entities_services() + + # Find the empty-name entity + empty_name_entities = [e for e in entities if e.name == ""] + assert len(empty_name_entities) == 1 + + entity = empty_name_entities[0] + + # OLD behavior: object_id was computed from device name + expected_object_id = compute_object_id("test-device") + assert entity.object_id == expected_object_id, ( + f"Expected object_id '{expected_object_id}' from device name, " + f"got '{entity.object_id}'" + ) + + # Hash should match device name + expected_hash = fnv1_hash_object_id("test-device") + assert entity.key == expected_hash, ( + f"Expected hash {expected_hash:#x}, got {entity.key:#x}" + ) + + # Named entity should work normally + named_entities = [e for e in entities if e.name == "Temperature"] + assert len(named_entities) == 1 + assert named_entities[0].object_id == "temperature" + + # Verify the full algorithm from entity_utils works for ALL entities + verify_all_entities(entities, device_info) diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 01de0f27f9..a58d4784ce 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -27,13 +27,9 @@ from esphome.helpers import sanitize, snake_case from .common import load_config_from_fixture -# Pre-compiled regex patterns for extracting object IDs from expressions -# Matches both old format: .set_object_id("obj_id") -# and new format: .set_name_and_object_id("name", "obj_id") -OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)') -COMBINED_PATTERN = re.compile( - r'\.set_name_and_object_id\(["\'].*?["\']\s*,\s*["\'](.*?)["\']\)' -) +# Pre-compiled regex pattern for extracting names from set_name calls +# Matches: .set_name("name", hash) or .set_name("name") +SET_NAME_PATTERN = re.compile(r'\.set_name\(["\']([^"\']*)["\']') FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers" @@ -276,14 +272,21 @@ def setup_test_environment() -> Generator[list[str], None, None]: def extract_object_id_from_expressions(expressions: list[str]) -> str | None: - """Extract the object ID that was set from the generated expressions.""" + """Extract the object ID that would be computed from set_name calls. + + Since object_id is now computed from the name (via snake_case + sanitize), + we extract the name from set_name() calls and compute the expected object_id. + For empty names, we fall back to CORE.friendly_name or CORE.name. + """ for expr in expressions: - # First try new combined format: .set_name_and_object_id("name", "obj_id") - if match := COMBINED_PATTERN.search(expr): - return match.group(1) - # Fall back to old format: .set_object_id("obj_id") - if match := OBJECT_ID_PATTERN.search(expr): - return match.group(1) + if match := SET_NAME_PATTERN.search(expr): + name = match.group(1) + if name: + return sanitize(snake_case(name)) + # Empty name - fall back to friendly_name or device name + if CORE.friendly_name: + return sanitize(snake_case(CORE.friendly_name)) + return sanitize(snake_case(CORE.name)) if CORE.name else None return None @@ -757,3 +760,140 @@ def test_entity_duplicate_validator_same_name_no_enhanced_message() -> None: r"Each entity on a device must have a unique name within its platform\.$", ): validator(config2) + + +@pytest.mark.asyncio +async def test_setup_entity_empty_name_with_device( + setup_test_environment: list[str], +) -> None: + """Test setup_entity with empty entity name on a sub-device. + + For empty-name entities, Python passes 0 and C++ calculates the hash + at runtime from the device's actual name. + """ + added_expressions = setup_test_environment + + # Mock get_variable to return a mock device + original_get_variable = entity_helpers.get_variable + + async def mock_get_variable(id_: ID) -> MockObj: + return MockObj("sub_device_1") + + entity_helpers.get_variable = mock_get_variable + + var = MockObj("sensor1") + device_id = ID("sub_device_1", type="Device") + + config = { + CONF_NAME: "", + CONF_DISABLED_BY_DEFAULT: False, + CONF_DEVICE_ID: device_id, + } + + await setup_entity(var, config, "sensor") + + entity_helpers.get_variable = original_get_variable + + # Check that set_device was called + assert any("sensor1.set_device" in expr for expr in added_expressions) + + # For empty-name entities, Python passes 0 - C++ calculates hash at runtime + assert any('set_name("", 0)' in expr for expr in added_expressions), ( + f"Expected set_name with hash 0, got {added_expressions}" + ) + + +@pytest.mark.asyncio +async def test_setup_entity_empty_name_with_mac_suffix( + setup_test_environment: list[str], +) -> None: + """Test setup_entity with empty name and MAC suffix enabled. + + For empty-name entities, Python passes 0 and C++ calculates the hash + at runtime from friendly_name (bug-for-bug compatibility). + """ + added_expressions = setup_test_environment + + # Set up CORE.config with name_add_mac_suffix enabled + CORE.config = {"name_add_mac_suffix": True} + # Set friendly_name to a specific value + CORE.friendly_name = "My Device" + + var = MockObj("sensor1") + + config = { + CONF_NAME: "", + CONF_DISABLED_BY_DEFAULT: False, + } + + await setup_entity(var, config, "sensor") + + # For empty-name entities, Python passes 0 - C++ calculates hash at runtime + assert any('set_name("", 0)' in expr for expr in added_expressions), ( + f"Expected set_name with hash 0, got {added_expressions}" + ) + + +@pytest.mark.asyncio +async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name( + setup_test_environment: list[str], +) -> None: + """Test setup_entity with empty name, MAC suffix enabled, but no friendly_name. + + For empty-name entities, Python passes 0 and C++ calculates the hash + at runtime. In this case C++ will hash the empty friendly_name + (bug-for-bug compatibility). + """ + added_expressions = setup_test_environment + + # Set up CORE.config with name_add_mac_suffix enabled + CORE.config = {"name_add_mac_suffix": True} + # Set friendly_name to empty + CORE.friendly_name = "" + + var = MockObj("sensor1") + + config = { + CONF_NAME: "", + CONF_DISABLED_BY_DEFAULT: False, + } + + await setup_entity(var, config, "sensor") + + # For empty-name entities, Python passes 0 - C++ calculates hash at runtime + assert any('set_name("", 0)' in expr for expr in added_expressions), ( + f"Expected set_name with hash 0, got {added_expressions}" + ) + + +@pytest.mark.asyncio +async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name( + setup_test_environment: list[str], +) -> None: + """Test setup_entity with empty name, no MAC suffix, and no friendly_name. + + For empty-name entities, Python passes 0 and C++ calculates the hash + at runtime from the device name. + """ + added_expressions = setup_test_environment + + # No MAC suffix (either not set or False) + CORE.config = {} + # No friendly_name + CORE.friendly_name = "" + # Device name is set + CORE.name = "my-test-device" + + var = MockObj("sensor1") + + config = { + CONF_NAME: "", + CONF_DISABLED_BY_DEFAULT: False, + } + + await setup_entity(var, config, "sensor") + + # For empty-name entities, Python passes 0 - C++ calculates hash at runtime + assert any('set_name("", 0)' in expr for expr in added_expressions), ( + f"Expected set_name with hash 0, got {added_expressions}" + ) diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 47b945e0eb..159d3230ab 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -279,6 +279,77 @@ def test_sanitize(text, expected): assert actual == expected +@pytest.mark.parametrize( + ("name", "expected_hash"), + ( + # Basic strings - hash of sanitize(snake_case(name)) + ("foo", 0x408F5E13), + ("Foo", 0x408F5E13), # Same as "foo" (lowercase) + ("FOO", 0x408F5E13), # Same as "foo" (lowercase) + # Spaces become underscores + ("foo bar", 0x3AE35AA1), # Transforms to "foo_bar" + ("Foo Bar", 0x3AE35AA1), # Same (lowercase + underscore) + # Already snake_case + ("foo_bar", 0x3AE35AA1), + # Special chars become underscores + ("foo!bar", 0x3AE35AA1), # Transforms to "foo_bar" + ("foo@bar", 0x3AE35AA1), # Transforms to "foo_bar" + # Hyphens are preserved + ("foo-bar", 0x438B12E3), + # Numbers are preserved + ("foo123", 0xF3B0067D), + # Empty string + ("", 0x811C9DC5), # FNV1_OFFSET_BASIS (no chars processed) + # Single char + ("a", 0x050C5D7E), + # Mixed case and spaces + ("My Sensor Name", 0x2760962A), # Transforms to "my_sensor_name" + ), +) +def test_fnv1_hash_object_id(name, expected_hash): + """Test fnv1_hash_object_id produces expected hashes. + + These expected values were computed to match the C++ implementation + in esphome/core/helpers.h. If this test fails after modifying either + implementation, ensure both Python and C++ versions stay in sync. + """ + actual = helpers.fnv1_hash_object_id(name) + + assert actual == expected_hash + + +def _fnv1_hash_py(s: str) -> int: + """Python implementation of FNV-1 hash for verification.""" + hash_val = 2166136261 # FNV1_OFFSET_BASIS + for c in s: + hash_val = (hash_val * 16777619) & 0xFFFFFFFF # FNV1_PRIME + hash_val ^= ord(c) + return hash_val + + +@pytest.mark.parametrize( + "name", + ( + "Simple", + "With Space", + "MixedCase", + "special!@#chars", + "already_snake_case", + "123numbers", + ), +) +def test_fnv1_hash_object_id_matches_manual_calculation(name): + """Verify fnv1_hash_object_id matches snake_case + sanitize + standard FNV-1.""" + # Manual calculation: snake_case -> sanitize -> fnv1_hash + transformed = helpers.sanitize(helpers.snake_case(name)) + expected = _fnv1_hash_py(transformed) + + # Direct calculation via fnv1_hash_object_id + actual = helpers.fnv1_hash_object_id(name) + + assert actual == expected + + @pytest.mark.parametrize( "text, expected", ((["127.0.0.1", "fe80::1", "2001::2"], ["2001::2", "127.0.0.1", "fe80::1"]),),