Compare commits

..

6 Commits

Author SHA1 Message Date
J. Nick Koston
4293f8fe89 [core] Eliminate global constructor overhead for component vectors 2026-01-19 22:09:08 -10:00
Jonathan Swoboda
ed4ebffa74 [x9c] Fix potentiometer unable to decrement (#13382)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 22:57:54 -05:00
J. Nick Koston
c213de4861 [mapping] Use stack buffers for numeric key error logging (#13299) 2026-01-19 17:42:08 -10:00
J. Nick Koston
6cf320fd60 [mqtt] Eliminate per-entity loop overhead and heap churn (#13356) 2026-01-19 17:41:55 -10:00
J. Nick Koston
aeea340bc6 [cs5460a] Remove unnecessary empty loop override (#13357) 2026-01-19 17:41:03 -10:00
J. Nick Koston
d0e50ed030 [lock] Extract set_state_ helper to reduce code duplication (#13359) 2026-01-19 17:40:51 -10:00
13 changed files with 64 additions and 78 deletions

View File

@@ -76,7 +76,6 @@ class CS5460AComponent : public Component,
void restart() { restart_(); }
void setup() override;
void loop() override {}
void dump_config() override;
protected:

View File

@@ -28,16 +28,14 @@ const LogString *lock_state_to_string(LockState state) {
Lock::Lock() : state(LOCK_STATE_NONE) {}
LockCall Lock::make_call() { return LockCall(this); }
void Lock::lock() {
void Lock::set_state_(LockState state) {
auto call = this->make_call();
call.set_state(LOCK_STATE_LOCKED);
this->control(call);
}
void Lock::unlock() {
auto call = this->make_call();
call.set_state(LOCK_STATE_UNLOCKED);
call.set_state(state);
this->control(call);
}
void Lock::lock() { this->set_state_(LOCK_STATE_LOCKED); }
void Lock::unlock() { this->set_state_(LOCK_STATE_UNLOCKED); }
void Lock::open() {
if (traits.get_supports_open()) {
ESP_LOGD(TAG, "'%s' Opening.", this->get_name().c_str());

View File

@@ -156,6 +156,9 @@ class Lock : public EntityBase {
protected:
friend LockCall;
/// Helper for lock/unlock convenience methods
void set_state_(LockState state);
/** Perform the open latch action with hardware. This method is optional to implement
* when creating a new lock.
*

View File

@@ -2,6 +2,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <map>
#include <string>
@@ -43,8 +44,17 @@ template<typename K, typename V> class Mapping {
esph_log_e(TAG, "Key '%p' not found in mapping", key);
} else if constexpr (std::is_same_v<K, std::string>) {
esph_log_e(TAG, "Key '%s' not found in mapping", key.c_str());
} else if constexpr (std::is_integral_v<K>) {
char buf[24]; // enough for 64-bit integer
if constexpr (std::is_unsigned_v<K>) {
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(key));
} else {
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, static_cast<int64_t>(key));
}
esph_log_e(TAG, "Key '%s' not found in mapping", buf);
} else {
esph_log_e(TAG, "Key '%s' not found in mapping", to_string(key).c_str());
// All supported key types are handled above - this should never be reached
static_assert(sizeof(K) == 0, "Unsupported key type for Mapping error logging");
}
return {};
}

View File

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

View File

@@ -98,17 +98,7 @@ void MQTTClientComponent::send_device_info_() {
uint8_t index = 0;
for (auto &ip : network::get_ip_addresses()) {
if (ip.is_set()) {
char key[8]; // "ip" + up to 3 digits + null
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
if (index == 0) {
key[0] = 'i';
key[1] = 'p';
key[2] = '\0';
} else {
buf_append_printf(key, sizeof(key), 0, "ip%u", index);
}
ip.str_to(ip_buf);
root[key] = ip_buf;
root["ip" + (index == 0 ? "" : esphome::to_string(index))] = ip.str();
index++;
}
}
@@ -413,6 +403,12 @@ void MQTTClientComponent::loop() {
this->last_connected_ = now;
this->resubscribe_subscriptions_();
// Process pending resends for all MQTT components centrally
// This is more efficient than each component polling in its own loop
for (MQTTComponent *component : this->children_) {
component->process_resend();
}
}
break;
}

View File

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

View File

@@ -81,8 +81,6 @@ class MQTTComponent : public Component {
/// Override setup_ so that we can call send_discovery() when needed.
void call_setup() override;
void call_loop() override;
void call_dump_config() override;
/// Send discovery info the Home Assistant, override this.
@@ -133,6 +131,9 @@ class MQTTComponent : public Component {
/// Internal method for the MQTT client base to schedule a resend of the state on reconnect.
void schedule_resend_state();
/// Process pending resend if needed (called by MQTTClientComponent)
void process_resend();
/** Send a MQTT message.
*
* @param topic The topic.

View File

@@ -175,7 +175,7 @@ bool MQTTFanComponent::publish_state() {
auto traits = this->state_->get_traits();
if (traits.supports_speed()) {
char buf[12];
size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed);
int len = snprintf(buf, sizeof(buf), "%d", this->state_->speed);
bool success = this->publish(this->get_speed_level_state_topic(), buf, len);
failed = failed || !success;
}

View File

@@ -75,7 +75,7 @@ bool MQTTNumberComponent::send_initial_state() {
}
bool MQTTNumberComponent::publish_state(float value) {
char buffer[64];
buf_append_printf(buffer, sizeof(buffer), 0, "%f", value);
snprintf(buffer, sizeof(buffer), "%f", value);
return this->publish(this->get_state_topic_(), buffer);
}

View File

@@ -6,7 +6,7 @@ namespace x9c {
static const char *const TAG = "x9c.output";
void X9cOutput::trim_value(int change_amount) {
void X9cOutput::trim_value(int32_t change_amount) {
if (change_amount == 0) {
return;
}
@@ -47,17 +47,17 @@ void X9cOutput::setup() {
if (this->initial_value_ <= 0.50) {
this->trim_value(-101); // Set min value (beyond 0)
this->trim_value(static_cast<uint32_t>(roundf(this->initial_value_ * 100)));
this->trim_value(lroundf(this->initial_value_ * 100));
} else {
this->trim_value(101); // Set max value (beyond 100)
this->trim_value(static_cast<uint32_t>(roundf(this->initial_value_ * 100) - 100));
this->trim_value(lroundf(this->initial_value_ * 100) - 100);
}
this->pot_value_ = this->initial_value_;
this->write_state(this->initial_value_);
}
void X9cOutput::write_state(float state) {
this->trim_value(static_cast<uint32_t>(roundf((state - this->pot_value_) * 100)));
this->trim_value(lroundf((state - this->pot_value_) * 100));
this->pot_value_ = state;
}

View File

@@ -18,7 +18,7 @@ class X9cOutput : public output::FloatOutput, public Component {
void setup() override;
void dump_config() override;
void trim_value(int change_amount);
void trim_value(int32_t change_amount);
protected:
void write_state(float state) override;

View File

@@ -47,18 +47,21 @@ struct ComponentPriorityOverride {
};
// Error messages for failed components
// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead
// This is never freed as error messages persist for the lifetime of the device
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<ComponentErrorMessage>> component_error_messages;
std::vector<ComponentErrorMessage> *component_error_messages = nullptr;
// Setup priority overrides - freed after setup completes
// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<ComponentPriorityOverride>> setup_priority_overrides;
std::vector<ComponentPriorityOverride> *setup_priority_overrides = nullptr;
// Helper to store error messages - reduces duplication between deprecated and new API
// Remove before 2026.6.0 when deprecated const char* API is removed
void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) {
// Lazy allocate the error messages vector if needed
if (!component_error_messages) {
component_error_messages = std::make_unique<std::vector<ComponentErrorMessage>>();
component_error_messages = new std::vector<ComponentErrorMessage>();
}
// Check if this component already has an error message
for (auto &entry : *component_error_messages) {
@@ -467,7 +470,7 @@ float Component::get_actual_setup_priority() const {
void Component::set_setup_priority(float priority) {
// Lazy allocate the vector if needed
if (!setup_priority_overrides) {
setup_priority_overrides = std::make_unique<std::vector<ComponentPriorityOverride>>();
setup_priority_overrides = new std::vector<ComponentPriorityOverride>();
// Reserve some space to avoid reallocations (most configs have < 10 overrides)
setup_priority_overrides->reserve(10);
}
@@ -553,7 +556,8 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
void clear_setup_priority_overrides() {
// Free the setup priority map completely
setup_priority_overrides.reset();
delete setup_priority_overrides;
setup_priority_overrides = nullptr;
}
} // namespace esphome