Compare commits

..

98 Commits

Author SHA1 Message Date
J. Nick Koston
5f88ab80f4 Merge branch 'set_time_string_literals' into integration 2026-01-18 23:29:14 -10:00
J. Nick Koston
48e7e7aeb3 hdr 2026-01-18 23:18:38 -10:00
J. Nick Koston
54a4d60f5d [datetime] Add const char * overloads for string parsing to avoid heap allocation 2026-01-18 23:09:24 -10:00
J. Nick Koston
d41980d0d2 [datetime] Add const char * overloads for string parsing to avoid heap allocation 2026-01-18 23:06:17 -10:00
J. Nick Koston
d85702bf32 Merge remote-tracking branch 'origin/mqtt_reduce_heap_alloc' into integration 2026-01-18 22:13:51 -10:00
J. Nick Koston
2e3e61f464 Update esphome/components/mqtt/mqtt_component.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 22:13:34 -10:00
J. Nick Koston
115e17e95b Merge branch 'mqtt_reduce_heap_alloc' into integration 2026-01-18 22:12:28 -10:00
J. Nick Koston
43f0dd091a tweak 2026-01-18 22:12:07 -10:00
J. Nick Koston
64bf247f7b Merge branch 'mqtt_reduce_heap_alloc' into integration 2026-01-18 22:11:00 -10:00
J. Nick Koston
0117519c81 [mqtt] Reduce heap allocations in hot paths 2026-01-18 22:10:48 -10:00
J. Nick Koston
d83457bbe1 [mqtt] Reduce heap allocations in hot paths 2026-01-18 22:02:36 -10:00
J. Nick Koston
a26d01f536 Merge branch 'mqtt_resend' into integration 2026-01-18 21:36:40 -10:00
J. Nick Koston
90e67d72a5 fix 2026-01-18 21:11:36 -10:00
J. Nick Koston
994e8970f5 Revert "[logger] Optimize ESP8266 UART write path with direct FIFO register access"
This reverts commit 122e7ac01e.
2026-01-18 20:54:03 -10:00
J. Nick Koston
5ac917835e Revert "cleanup"
This reverts commit bac836d2a7.
2026-01-18 20:54:02 -10:00
J. Nick Koston
986caddb6c Merge branch 'logger_perf_8266' into integration 2026-01-18 20:38:49 -10:00
J. Nick Koston
bac836d2a7 cleanup 2026-01-18 20:37:34 -10:00
J. Nick Koston
72f71f59b3 Merge branch 'logger_perf_8266' into integration 2026-01-18 20:36:32 -10:00
J. Nick Koston
122e7ac01e [logger] Optimize ESP8266 UART write path with direct FIFO register access 2026-01-18 20:34:14 -10:00
J. Nick Koston
86a1b4cf69 [select][fan] Use StringRef for on_value/on_preset_set triggers to avoid heap allocation (#13324) 2026-01-18 19:51:11 -10:00
J. Nick Koston
207b59fe16 Merge branch 'lock_dupe_code' into integration 2026-01-18 19:50:38 -10:00
J. Nick Koston
83c68e246d [lock] Extract set_state_ helper to reduce code duplication 2026-01-18 19:49:20 -10:00
J. Nick Koston
c629e88f4b Merge branch 'alarm_control_panel_reduce_heap_alloc_code' into integration 2026-01-18 19:36:10 -10:00
J. Nick Koston
b078eb8523 [alarm_control_panel] Reduce heap allocations in arm/disarm methods 2026-01-18 19:34:31 -10:00
J. Nick Koston
98e0a82e66 Merge branch 'cs5460a_loop' into integration 2026-01-18 19:16:10 -10:00
J. Nick Koston
b4e0a0a15a [cs5460a] Remove unnecessary empty loop override 2026-01-18 19:13:48 -10:00
J. Nick Koston
d0869fbc67 Merge branch 'sprintf_group' into integration 2026-01-18 19:06:45 -10:00
J. Nick Koston
a3d926dc54 Merge remote-tracking branch 'upstream/dev' into sprintf_group
# Conflicts:
#	esphome/components/pipsolar/output/pipsolar_output.cpp
2026-01-18 18:56:11 -10:00
J. Nick Koston
d8a28f6fba [scheduler] Replace resize() with erase() to save ~ 436 bytes flash (#13214) 2026-01-18 18:54:30 -10:00
J. Nick Koston
e80a940222 [gdk101] Use stack buffer to eliminate heap allocation for firmware version (#13224) 2026-01-18 18:52:49 -10:00
J. Nick Koston
e99dbe05f7 [toshiba] Replace to_string with stack buffer in debug logging (#13296) 2026-01-18 18:52:34 -10:00
J. Nick Koston
f453a8d9a1 [dfrobot_sen0395] Reduce heap allocations in command building (#13219) 2026-01-18 18:44:56 -10:00
J. Nick Koston
126190d26a [ezo] Replace str_sprintf with stack-based formatting (#13218)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:44:41 -10:00
J. Nick Koston
e40201a98d [cse7766] Use stack buffer for verbose debug logging (#13217) 2026-01-18 18:44:27 -10:00
J. Nick Koston
8142f5db44 [zephyr] Avoid heap allocation in preferences key formatting (#13215) 2026-01-18 18:43:50 -10:00
J. Nick Koston
98ccab87a7 [tormatic] Use stack buffers instead of str_sprintf in debug methods (#13225) 2026-01-18 18:43:36 -10:00
J. Nick Koston
b9e72a8774 [daikin_arc] Fix undefined behavior in sprintf calls (#13279)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:43:19 -10:00
J. Nick Koston
d9fc625c6a [web_server] Simplify datetime formatting with buf_append_printf (#13281) 2026-01-18 18:43:05 -10:00
J. Nick Koston
dfbf79d6d6 [homeassistant] Use buf_append_printf for ESP8266 flash optimization (#13284) 2026-01-18 18:42:19 -10:00
J. Nick Koston
ea0fac96cb [core][mqtt] Add str_sanitize_to(), soft-deprecate str_sanitize() (#13233) 2026-01-18 18:42:04 -10:00
J. Nick Koston
3182222d60 [esp32_hosted] Use stack buffer instead of str_sprintf for version string (#13226) 2026-01-18 18:41:47 -10:00
J. Nick Koston
d8849b16f2 [gpio] Use buf_append_printf in dump_summary for ESP8266 flash optimization (#13283) 2026-01-18 18:41:34 -10:00
J. Nick Koston
635983f163 [uptime] Use buf_append_printf for ESP8266 flash optimization (#13282) 2026-01-18 18:41:19 -10:00
J. Nick Koston
6cbe672004 [tuya] Use buf_append_printf for ESP8266 flash optimization (#13287) 2026-01-18 18:41:07 -10:00
J. Nick Koston
226867b05c [esp8266] Use direct SDK calls instead of Arduino ESP class wrappers (#13353) 2026-01-18 18:40:53 -10:00
J. Nick Koston
67871a1683 [ccs811] Use buf_append_printf for buffer safety and ESP8266 flash optimization (#13300) 2026-01-18 18:40:14 -10:00
J. Nick Koston
f60c03e350 [syslog] Use buf_append_printf for ESP8266 flash optimization (#13286) 2026-01-18 18:39:53 -10:00
J. Nick Koston
eb66429144 [sml] Use stack buffers instead of str_sprintf (#13222)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:39:23 -10:00
J. Nick Koston
0f3bac5dd6 [nextion] Replace to_string with stack buffer and fix unsafe sprintf (#13295) 2026-01-18 18:37:29 -10:00
J. Nick Koston
5b92d0b89e [wiegand] Replace heap-allocating to_string with stack buffers (#13294) 2026-01-18 18:37:14 -10:00
J. Nick Koston
052b05df56 [tuya] Replace unsafe sprintf with snprintf in light color formatting (#13292) 2026-01-18 18:37:02 -10:00
J. Nick Koston
7b0db659d1 [rc522_spi] Replace unsafe sprintf with buf_append_printf (#13291) 2026-01-18 18:36:46 -10:00
J. Nick Koston
2f7270cf8f [uart] Replace unsafe sprintf with buf_append_printf in debugger (#13288) 2026-01-18 18:36:32 -10:00
J. Nick Koston
b44727aee6 [socket] Eliminate heap allocations in set_sockaddr() (#13228) 2026-01-18 18:29:31 -10:00
J. Nick Koston
1a55254258 [status] Convert to PollingComponent to reduce CPU usage (#13342) 2026-01-18 18:28:24 -10:00
J. Nick Koston
baf2b0e3c9 [api] Fix truncation of Home Assistant attributes longer than 255 characters (#13348) 2026-01-18 18:23:11 -10:00
J. Nick Koston
88fadb242c [mqtt] Eliminate per-component loop overhead for MQTT entities 2026-01-18 17:54:51 -10:00
J. Nick Koston
326fd4fe68 Merge branch 'libretiny_heap' into integration 2026-01-18 14:43:07 -10:00
J. Nick Koston
76b1201c96 [wifi] LibreTiny: Eliminate heap allocations in WiFi scan path 2026-01-18 14:40:48 -10:00
J. Nick Koston
7efb72c511 Merge branch 'ard_debug_no_heap' into integration 2026-01-18 14:02:44 -10:00
J. Nick Koston
07a731b97d missed some 2026-01-18 14:02:33 -10:00
J. Nick Koston
db37ae0e3c Merge branch 'esp8266_sdk' into integration 2026-01-18 14:01:08 -10:00
J. Nick Koston
d2bf991bfb Merge branch 'ard_debug_no_heap' into integration 2026-01-18 14:01:02 -10:00
J. Nick Koston
f8b33562c1 cleanup messy 2026-01-18 14:00:14 -10:00
J. Nick Koston
cf17a079b7 cleanup messy 2026-01-18 13:57:52 -10:00
J. Nick Koston
a451625120 cleanup messy 2026-01-18 13:57:07 -10:00
J. Nick Koston
c180d0c49c [esp8266] Use direct SDK calls instead of Arduino ESP class wrappers 2026-01-18 13:50:46 -10:00
J. Nick Koston
bacc4ed4e5 Merge branch 'ard_debug_no_heap' into integration 2026-01-18 13:46:49 -10:00
J. Nick Koston
7acde0ab60 [debug] ESP8266: Eliminate heap allocations from Arduino String functions 2026-01-18 13:45:49 -10:00
J. Nick Koston
98c8142f86 Merge branch 'esp8266_wifi_reduce_heap_alloc' into integration 2026-01-18 12:18:37 -10:00
J. Nick Koston
4ed68c6884 [wifi] ESP8266: Use direct SDK calls to reduce flash and heap allocation 2026-01-18 12:16:18 -10:00
J. Nick Koston
680e92a226 [core] Add str_endswith_ignore_case to avoid heap allocation in audio file type detection (#13313) 2026-01-18 08:36:56 -10:00
J. Nick Koston
6ab321db1a Merge branch 'globals_polling' into integration 2026-01-18 00:40:47 -10:00
J. Nick Koston
c1cba269b3 [globals] Convert restoring globals to PollingComponent to reduce CPU usage 2026-01-18 00:35:17 -10:00
J. Nick Koston
0e2f0bae21 Merge branch 'status_binary_sensor' into integration 2026-01-17 22:47:57 -10:00
J. Nick Koston
7175299cae [status] Convert to PollingComponent to reduce CPU usage 2026-01-17 22:40:15 -10:00
J. Nick Koston
db0b32bfc9 [network] Fix IPAddress::str_to() to lowercase IPv6 hex digits (#13325) 2026-01-17 18:06:54 -10:00
J. Nick Koston
21794e28e5 [modbus_controller] Use stack buffers instead of heap-allocating string helpers (#13221)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-17 17:26:51 -10:00
J. Nick Koston
42ad20c231 Merge branch 'esp32_ble_tracker_opt' into integration 2026-01-17 16:52:46 -10:00
J. Nick Koston
ae5a3e616a improve comment 2026-01-17 16:26:18 -10:00
J. Nick Koston
59eeeb5fe5 Merge branch 'esp32_ble_tracker_opt' into integration 2026-01-17 16:23:37 -10:00
J. Nick Koston
759278191b simpler 2026-01-17 16:23:30 -10:00
J. Nick Koston
03eaec853a Merge branch 'esp32_ble_tracker_opt' into integration 2026-01-17 16:17:19 -10:00
J. Nick Koston
4549e375c1 adjust 2026-01-17 16:16:58 -10:00
J. Nick Koston
a8b07af2a3 fixes 2026-01-17 16:12:51 -10:00
J. Nick Koston
f011dc658d Merge branch 'esp32_ble_tracker_opt' into integration 2026-01-17 15:56:41 -10:00
J. Nick Koston
728236270c [weikai] Replace bitset to_string with format_bin_to (#13297) 2026-01-17 15:53:01 -10:00
J. Nick Koston
01cdc4ed58 [core] Add fnv1_hash_extend() string overloads, use in atm90e32 (#13326) 2026-01-17 15:52:19 -10:00
J. Nick Koston
d6a0c8ffbb [template] Store alarm control panel codes in flash instead of heap (#13329) 2026-01-17 15:52:06 -10:00
J. Nick Koston
f003fac5d8 document, document, document 2026-01-17 15:51:45 -10:00
J. Nick Koston
4cc0f874f7 [wireguard] Store configuration strings in flash instead of heap (#13331) 2026-01-17 15:51:26 -10:00
J. Nick Koston
ed58b9372f [template] Store text initial_value in flash and avoid heap allocation in setup (#13332) 2026-01-17 15:51:12 -10:00
J. Nick Koston
ee2a81923b [sun] Store text sensor format string in flash (#13335) 2026-01-17 15:51:01 -10:00
J. Nick Koston
0a1e7ee50b [pipsolar] Store command strings in flash (#13336) 2026-01-17 15:50:42 -10:00
J. Nick Koston
4d4283bcfa [udp] Store addresses in flash instead of heap (#13330) 2026-01-17 15:50:23 -10:00
J. Nick Koston
6b02f5dfbd [esp32_ble_tracker] Optimize loop with state change tracking for ~85% CPU reduction 2026-01-17 15:47:37 -10:00
J. Nick Koston
526bd58d1c Update esphome/components/sim800l/sim800l.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-16 13:57:09 -10:00
J. Nick Koston
1ed478fd5f Update esphome/components/pipsolar/output/pipsolar_output.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-16 13:57:04 -10:00
66 changed files with 711 additions and 381 deletions

View File

@@ -67,52 +67,29 @@ void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback)
this->ready_callback_.add(std::move(callback));
}
void AlarmControlPanel::arm_away(optional<std::string> code) {
void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
const char *code) {
auto call = this->make_call();
call.arm_away();
if (code.has_value())
call.set_code(code.value());
(call.*arm_method)();
if (code != nullptr)
call.set_code(code);
call.perform();
}
void AlarmControlPanel::arm_home(optional<std::string> code) {
auto call = this->make_call();
call.arm_home();
if (code.has_value())
call.set_code(code.value());
call.perform();
void AlarmControlPanel::arm_away(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_away, code); }
void AlarmControlPanel::arm_home(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_home, code); }
void AlarmControlPanel::arm_night(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_night, code); }
void AlarmControlPanel::arm_vacation(const char *code) {
this->arm_with_code_(&AlarmControlPanelCall::arm_vacation, code);
}
void AlarmControlPanel::arm_night(optional<std::string> code) {
auto call = this->make_call();
call.arm_night();
if (code.has_value())
call.set_code(code.value());
call.perform();
void AlarmControlPanel::arm_custom_bypass(const char *code) {
this->arm_with_code_(&AlarmControlPanelCall::arm_custom_bypass, code);
}
void AlarmControlPanel::arm_vacation(optional<std::string> code) {
auto call = this->make_call();
call.arm_vacation();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::arm_custom_bypass(optional<std::string> code) {
auto call = this->make_call();
call.arm_custom_bypass();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::disarm(optional<std::string> code) {
auto call = this->make_call();
call.disarm();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::disarm(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::disarm, code); }
} // namespace esphome::alarm_control_panel

View File

@@ -76,37 +76,53 @@ class AlarmControlPanel : public EntityBase {
*
* @param code The code
*/
void arm_away(optional<std::string> code = nullopt);
void arm_away(const char *code = nullptr);
void arm_away(const optional<std::string> &code) {
this->arm_away(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in home mode
*
* @param code The code
*/
void arm_home(optional<std::string> code = nullopt);
void arm_home(const char *code = nullptr);
void arm_home(const optional<std::string> &code) {
this->arm_home(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in night mode
*
* @param code The code
*/
void arm_night(optional<std::string> code = nullopt);
void arm_night(const char *code = nullptr);
void arm_night(const optional<std::string> &code) {
this->arm_night(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in vacation mode
*
* @param code The code
*/
void arm_vacation(optional<std::string> code = nullopt);
void arm_vacation(const char *code = nullptr);
void arm_vacation(const optional<std::string> &code) {
this->arm_vacation(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in custom bypass mode
*
* @param code The code
*/
void arm_custom_bypass(optional<std::string> code = nullopt);
void arm_custom_bypass(const char *code = nullptr);
void arm_custom_bypass(const optional<std::string> &code) {
this->arm_custom_bypass(code.has_value() ? code.value().c_str() : nullptr);
}
/** disarm the alarm
*
* @param code The code
*/
void disarm(optional<std::string> code = nullopt);
void disarm(const char *code = nullptr);
void disarm(const optional<std::string> &code) { this->disarm(code.has_value() ? code.value().c_str() : nullptr); }
/** Get the state
*
@@ -118,6 +134,8 @@ class AlarmControlPanel : public EntityBase {
protected:
friend AlarmControlPanelCall;
// Helper to reduce code duplication for arm/disarm methods
void arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), const char *code);
// in order to store last panel state in flash
ESPPreferenceObject pref_;
// current state

View File

@@ -10,8 +10,10 @@ static const char *const TAG = "alarm_control_panel";
AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {}
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) {
this->code_ = code;
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
if (code != nullptr) {
this->code_ = std::string(code);
}
return *this;
}

View File

@@ -14,7 +14,8 @@ class AlarmControlPanelCall {
public:
AlarmControlPanelCall(AlarmControlPanel *parent);
AlarmControlPanelCall &set_code(const std::string &code);
AlarmControlPanelCall &set_code(const char *code);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
AlarmControlPanelCall &arm_away();
AlarmControlPanelCall &arm_home();
AlarmControlPanelCall &arm_night();

View File

@@ -66,15 +66,7 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override {
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_away();
call.perform();
}
void play(const Ts &...x) override { this->alarm_control_panel_->arm_away(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
@@ -86,15 +78,7 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override {
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_home();
call.perform();
}
void play(const Ts &...x) override { this->alarm_control_panel_->arm_home(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
@@ -106,15 +90,7 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override {
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_night();
call.perform();
}
void play(const Ts &...x) override { this->alarm_control_panel_->arm_night(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;

View File

@@ -1712,17 +1712,16 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
}
// Create null-terminated state for callback (parse_number needs null-termination)
// HA state max length is 255, so 256 byte buffer covers all cases
char state_buf[256];
size_t copy_len = msg.state.size();
if (copy_len >= sizeof(state_buf)) {
copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator
// HA state max length is 255 characters, but attributes can be much longer
// Use stack buffer for common case (states), heap fallback for large attributes
size_t state_len = msg.state.size();
SmallBufferWithHeapFallback<256> state_buf_alloc(state_len + 1);
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
if (state_len > 0) {
memcpy(state_buf, msg.state.c_str(), state_len);
}
if (copy_len > 0) {
memcpy(state_buf, msg.state.c_str(), copy_len);
}
state_buf[copy_len] = '\0';
it.callback(StringRef(state_buf, copy_len));
state_buf[state_len] = '\0';
it.callback(StringRef(state_buf, state_len));
}
}
#endif

View File

@@ -135,8 +135,8 @@ void BluetoothConnection::loop() {
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->send_service_ == DONE_SENDING_SERVICES)) {
if (this->state() != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->send_service_ == DONE_SENDING_SERVICES)) {
this->disable_loop();
}
}

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

@@ -106,9 +106,9 @@ DateCall &DateCall::set_date(uint16_t year, uint8_t month, uint8_t day) {
DateCall &DateCall::set_date(ESPTime time) { return this->set_date(time.year, time.month, time.day_of_month); };
DateCall &DateCall::set_date(const std::string &date) {
DateCall &DateCall::set_date(const char *date, size_t len) {
ESPTime val{};
if (!ESPTime::strptime(date, val)) {
if (!ESPTime::strptime(date, len, val)) {
ESP_LOGE(TAG, "Could not convert the date string to an ESPTime object");
return *this;
}

View File

@@ -67,7 +67,9 @@ class DateCall {
void perform();
DateCall &set_date(uint16_t year, uint8_t month, uint8_t day);
DateCall &set_date(ESPTime time);
DateCall &set_date(const std::string &date);
DateCall &set_date(const char *date, size_t len);
DateCall &set_date(const char *date) { return this->set_date(date, strlen(date)); }
DateCall &set_date(const std::string &date) { return this->set_date(date.c_str(), date.size()); }
DateCall &set_year(uint16_t year) {
this->year_ = year;

View File

@@ -163,9 +163,9 @@ DateTimeCall &DateTimeCall::set_datetime(ESPTime datetime) {
datetime.second);
};
DateTimeCall &DateTimeCall::set_datetime(const std::string &datetime) {
DateTimeCall &DateTimeCall::set_datetime(const char *datetime, size_t len) {
ESPTime val{};
if (!ESPTime::strptime(datetime, val)) {
if (!ESPTime::strptime(datetime, len, val)) {
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this;
}

View File

@@ -71,7 +71,11 @@ class DateTimeCall {
void perform();
DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
DateTimeCall &set_datetime(ESPTime datetime);
DateTimeCall &set_datetime(const std::string &datetime);
DateTimeCall &set_datetime(const char *datetime, size_t len);
DateTimeCall &set_datetime(const char *datetime) { return this->set_datetime(datetime, strlen(datetime)); }
DateTimeCall &set_datetime(const std::string &datetime) {
return this->set_datetime(datetime.c_str(), datetime.size());
}
DateTimeCall &set_datetime(time_t epoch_seconds);
DateTimeCall &set_year(uint16_t year) {

View File

@@ -74,9 +74,9 @@ TimeCall &TimeCall::set_time(uint8_t hour, uint8_t minute, uint8_t second) {
TimeCall &TimeCall::set_time(ESPTime time) { return this->set_time(time.hour, time.minute, time.second); };
TimeCall &TimeCall::set_time(const std::string &time) {
TimeCall &TimeCall::set_time(const char *time, size_t len) {
ESPTime val{};
if (!ESPTime::strptime(time, val)) {
if (!ESPTime::strptime(time, len, val)) {
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this;
}

View File

@@ -69,7 +69,9 @@ class TimeCall {
void perform();
TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second);
TimeCall &set_time(ESPTime time);
TimeCall &set_time(const std::string &time);
TimeCall &set_time(const char *time, size_t len);
TimeCall &set_time(const char *time) { return this->set_time(time, strlen(time)); }
TimeCall &set_time(const std::string &time) { return this->set_time(time.c_str(), time.size()); }
TimeCall &set_hour(uint8_t hour) {
this->hour_ = hour;

View File

@@ -3,21 +3,80 @@
#include "esphome/core/log.h"
#include <Esp.h>
extern "C" {
#include <user_interface.h>
// Global reset info struct populated by SDK at boot
extern struct rst_info resetInfo;
// Core version - either a string pointer or a version number to format as hex
extern uint32_t core_version;
extern const char *core_release;
}
namespace esphome {
namespace debug {
static const char *const TAG = "debug";
// Get reset reason string from reason code (no heap allocation)
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
static const LogString *get_reset_reason_str(uint32_t reason) {
switch (reason) {
case REASON_DEFAULT_RST:
return LOG_STR("Power On");
case REASON_WDT_RST:
return LOG_STR("Hardware Watchdog");
case REASON_EXCEPTION_RST:
return LOG_STR("Exception");
case REASON_SOFT_WDT_RST:
return LOG_STR("Software Watchdog");
case REASON_SOFT_RESTART:
return LOG_STR("Software/System restart");
case REASON_DEEP_SLEEP_AWAKE:
return LOG_STR("Deep-Sleep Wake");
case REASON_EXT_SYS_RST:
return LOG_STR("External System");
default:
return LOG_STR("Unknown");
}
}
// Size for core version hex buffer
static constexpr size_t CORE_VERSION_BUFFER_SIZE = 12;
// Get core version string (no heap allocation)
// Returns either core_release directly or formats core_version as hex into provided buffer
static const char *get_core_version_str(std::span<char, CORE_VERSION_BUFFER_SIZE> buffer) {
if (core_release != nullptr) {
return core_release;
}
snprintf_P(buffer.data(), CORE_VERSION_BUFFER_SIZE, PSTR("%08x"), core_version);
return buffer.data();
}
// Size for reset info buffer
static constexpr size_t RESET_INFO_BUFFER_SIZE = 200;
// Get detailed reset info string (no heap allocation)
// For watchdog/exception resets, includes detailed exception info
static const char *get_reset_info_str(std::span<char, RESET_INFO_BUFFER_SIZE> buffer, uint32_t reason) {
if (reason >= REASON_WDT_RST && reason <= REASON_SOFT_WDT_RST) {
snprintf_P(buffer.data(), RESET_INFO_BUFFER_SIZE,
PSTR("Fatal exception:%d flag:%d (%s) epc1:0x%08x epc2:0x%08x epc3:0x%08x excvaddr:0x%08x depc:0x%08x"),
static_cast<int>(resetInfo.exccause), static_cast<int>(reason),
LOG_STR_ARG(get_reset_reason_str(reason)), resetInfo.epc1, resetInfo.epc2, resetInfo.epc3,
resetInfo.excvaddr, resetInfo.depc);
return buffer.data();
}
return LOG_STR_ARG(get_reset_reason_str(reason));
}
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
char *buf = buffer.data();
#if !defined(CLANG_TIDY)
String reason = ESP.getResetReason(); // NOLINT
snprintf_P(buf, RESET_REASON_BUFFER_SIZE, PSTR("%s"), reason.c_str());
return buf;
#else
buf[0] = '\0';
return buf;
#endif
// Copy from flash to provided buffer
strncpy_P(buffer.data(), (PGM_P) get_reset_reason_str(resetInfo.reason), RESET_REASON_BUFFER_SIZE - 1);
buffer[RESET_REASON_BUFFER_SIZE - 1] = '\0';
return buffer.data();
}
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
@@ -33,37 +92,42 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data();
const char *flash_mode;
const LogString *flash_mode;
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
case FM_QIO:
flash_mode = "QIO";
flash_mode = LOG_STR("QIO");
break;
case FM_QOUT:
flash_mode = "QOUT";
flash_mode = LOG_STR("QOUT");
break;
case FM_DIO:
flash_mode = "DIO";
flash_mode = LOG_STR("DIO");
break;
case FM_DOUT:
flash_mode = "DOUT";
flash_mode = LOG_STR("DOUT");
break;
default:
flash_mode = "UNKNOWN";
flash_mode = LOG_STR("UNKNOWN");
}
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance)
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,
LOG_STR_ARG(flash_mode));
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode);
LOG_STR_ARG(flash_mode));
#if !defined(CLANG_TIDY)
char reason_buffer[RESET_REASON_BUFFER_SIZE];
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
const char *reset_reason = get_reset_reason_(reason_buffer);
char core_version_buffer[CORE_VERSION_BUFFER_SIZE];
char reset_info_buffer[RESET_INFO_BUFFER_SIZE];
// NOLINTBEGIN(readability-static-accessed-through-instance)
uint32_t chip_id = ESP.getChipId();
uint8_t boot_version = ESP.getBootVersion();
uint8_t boot_mode = ESP.getBootMode();
uint8_t cpu_freq = ESP.getCpuFreqMHz();
uint32_t flash_chip_id = ESP.getFlashChipId();
const char *sdk_version = ESP.getSdkVersion();
// NOLINTEND(readability-static-accessed-through-instance)
ESP_LOGD(TAG,
"Chip ID: 0x%08" PRIX32 "\n"
@@ -74,19 +138,18 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
"Flash Chip ID=0x%08" PRIX32 "\n"
"Reset Reason: %s\n"
"Reset Info: %s",
chip_id, ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), boot_version, boot_mode, cpu_freq, flash_chip_id,
reset_reason, ESP.getResetInfo().c_str());
chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq,
flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason));
pos = buf_append_printf(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
pos = buf_append_printf(buf, size, pos, "|SDK: %s", ESP.getSdkVersion());
pos = buf_append_printf(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str());
pos = buf_append_printf(buf, size, pos, "|SDK: %s", sdk_version);
pos = buf_append_printf(buf, size, pos, "|Core: %s", get_core_version_str(core_version_buffer));
pos = buf_append_printf(buf, size, pos, "|Boot: %u", boot_version);
pos = buf_append_printf(buf, size, pos, "|Mode: %u", boot_mode);
pos = buf_append_printf(buf, size, pos, "|CPU: %u", cpu_freq);
pos = buf_append_printf(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|%s", ESP.getResetInfo().c_str());
#endif
pos = buf_append_printf(buf, size, pos, "|%s", get_reset_info_str(reset_info_buffer, resetInfo.reason));
return pos;
}

View File

@@ -50,7 +50,7 @@ void BLEClientBase::loop() {
this->set_state(espbt::ClientState::INIT);
return;
}
if (this->state_ == espbt::ClientState::INIT) {
if (this->state() == espbt::ClientState::INIT) {
auto ret = esp_ble_gattc_app_register(this->app_id);
if (ret) {
ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
@@ -60,7 +60,7 @@ void BLEClientBase::loop() {
}
// If idle, we can disable the loop as connect()
// will enable it again when a connection is needed.
else if (this->state_ == espbt::ClientState::IDLE) {
else if (this->state() == espbt::ClientState::IDLE) {
this->disable_loop();
}
}
@@ -86,7 +86,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
return false;
if (this->address_ == 0 || device.address_uint64() != this->address_)
return false;
if (this->state_ != espbt::ClientState::IDLE)
if (this->state() != espbt::ClientState::IDLE)
return false;
this->log_event_("Found device");
@@ -102,10 +102,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
void BLEClientBase::connect() {
// Prevent duplicate connection attempts
if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED ||
this->state_ == espbt::ClientState::ESTABLISHED) {
if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED ||
this->state() == espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_,
espbt::client_state_to_string(this->state_));
espbt::client_state_to_string(this->state()));
return;
}
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
@@ -133,12 +133,12 @@ void BLEClientBase::connect() {
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
void BLEClientBase::disconnect() {
if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) {
if (this->state() == espbt::ClientState::IDLE || this->state() == espbt::ClientState::DISCONNECTING) {
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_,
espbt::client_state_to_string(this->state_));
espbt::client_state_to_string(this->state()));
return;
}
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
if (this->state() == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
this->address_str_);
this->want_disconnect_ = true;
@@ -150,7 +150,7 @@ void BLEClientBase::disconnect() {
void BLEClientBase::unconditional_disconnect() {
// Disconnect without checking the state.
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_);
if (this->state_ == espbt::ClientState::DISCONNECTING) {
if (this->state() == espbt::ClientState::DISCONNECTING) {
this->log_error_("Already disconnecting");
return;
}
@@ -170,7 +170,7 @@ void BLEClientBase::unconditional_disconnect() {
this->log_gattc_warning_("esp_ble_gattc_close", err);
}
if (this->state_ == espbt::ClientState::DISCOVERED) {
if (this->state() == espbt::ClientState::DISCOVERED) {
this->set_address(0);
this->set_state(espbt::ClientState::IDLE);
} else {
@@ -295,18 +295,18 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
// error, if the error occurred at the BTA/GATT layer. This can result in the event
// arriving after we've already transitioned to IDLE state.
if (this->state_ == espbt::ClientState::IDLE) {
if (this->state() == espbt::ClientState::IDLE) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
this->address_str_, param->open.status);
break;
}
if (this->state_ != espbt::ClientState::CONNECTING) {
if (this->state() != espbt::ClientState::CONNECTING) {
// This should not happen but lets log it in case it does
// because it means we have a bad assumption about how the
// ESP BT stack works.
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
this->address_str_, espbt::client_state_to_string(this->state_), param->open.status);
this->address_str_, espbt::client_state_to_string(this->state()), param->open.status);
}
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
this->log_gattc_warning_("Connection open", param->open.status);
@@ -327,7 +327,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
// Cached connections already connected with medium parameters, no update needed
// only set our state, subclients might have more stuff to do yet.
this->state_ = espbt::ClientState::ESTABLISHED;
this->set_state_internal_(espbt::ClientState::ESTABLISHED);
break;
}
// For V3_WITHOUT_CACHE, we already set fast params before connecting
@@ -356,7 +356,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
return false;
// Check if we were disconnected while waiting for service discovery
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
this->state_ == espbt::ClientState::CONNECTED) {
this->state() == espbt::ClientState::CONNECTED) {
this->log_warning_("Remote closed during discovery");
} else {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_,
@@ -433,7 +433,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
#endif
}
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
this->state_ = espbt::ClientState::ESTABLISHED;
this->set_state_internal_(espbt::ClientState::ESTABLISHED);
break;
}
case ESP_GATTC_READ_DESCR_EVT: {

View File

@@ -44,7 +44,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void unconditional_disconnect();
void release_services();
bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
bool connected() { return this->state() == espbt::ClientState::ESTABLISHED; }
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }

View File

@@ -105,15 +105,13 @@ void ESP32BLETracker::loop() {
}
// Check for scan timeout - moved here from scheduler to avoid false reboots
// when the loop is blocked
// when the loop is blocked. This must run every iteration for safety.
if (this->scanner_state_ == ScannerState::RUNNING) {
switch (this->scan_timeout_state_) {
case ScanTimeoutState::MONITORING: {
uint32_t now = App.get_loop_component_start_time();
uint32_t timeout_ms = this->scan_duration_ * 2000;
// Robust time comparison that handles rollover correctly
// This works because unsigned arithmetic wraps around predictably
if ((now - this->scan_start_time_) > timeout_ms) {
if ((App.get_loop_component_start_time() - this->scan_start_time_) > this->scan_timeout_ms_) {
// First time we've seen the timeout exceeded - wait one more loop iteration
// This ensures all components have had a chance to process pending events
// This is because esp32_ble may not have run yet and called
@@ -128,13 +126,31 @@ void ESP32BLETracker::loop() {
ESP_LOGE(TAG, "Scan never terminated, rebooting");
App.reboot();
break;
case ScanTimeoutState::INACTIVE:
// This case should be unreachable - scanner and timeout states are always synchronized
break;
}
}
// Fast path: skip expensive client state counting and processing
// if no state has changed since last loop iteration.
//
// How state changes ensure we reach the code below:
// - handle_scanner_failure_(): scanner_state_ becomes FAILED via set_scanner_state_(), or
// scan_set_param_failed_ requires scanner_state_==RUNNING which can only be reached via
// set_scanner_state_(RUNNING) in gap_scan_start_complete_() (scan params are set during
// STARTING, not RUNNING, so version is always incremented before this condition is true)
// - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_()
// - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or
// connecting client finishes (state change), or scanner reaches RUNNING/IDLE
//
// All conditions that affect the logic below are tied to state changes that increment
// state_version_, so the fast path is safe.
if (this->state_version_ == this->last_processed_version_) {
return;
}
this->last_processed_version_ = this->state_version_;
// State changed - do full processing
ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts;
@@ -142,6 +158,7 @@ void ESP32BLETracker::loop() {
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
}
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
if (this->scanner_state_ == ScannerState::FAILED ||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
this->handle_scanner_failure_();
@@ -160,6 +177,8 @@ void ESP32BLETracker::loop() {
*/
// Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
// all clients are idle (their state changes increment version when they finish)
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(false);
@@ -168,8 +187,9 @@ void ESP32BLETracker::loop() {
this->start_scan_(false); // first = false
}
}
// If there is a discovered client and no connecting
// clients, then promote the discovered client to ready to connect.
// Promote discovered clients: reached when a client's state becomes DISCOVERED (via set_state()),
// or when a blocking condition clears (connecting client finishes, scanner reaches RUNNING/IDLE).
// All these trigger state_version_ increment, so we'll process and check promotion eligibility.
// We check both RUNNING and IDLE states because:
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
// - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
@@ -236,6 +256,7 @@ void ESP32BLETracker::start_scan_(bool first) {
// Start timeout monitoring in loop() instead of using scheduler
// This prevents false reboots when the loop is blocked
this->scan_start_time_ = App.get_loop_component_start_time();
this->scan_timeout_ms_ = this->scan_duration_ * 2000;
this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
@@ -253,6 +274,10 @@ void ESP32BLETracker::start_scan_(bool first) {
void ESP32BLETracker::register_client(ESPBTClient *client) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
client->app_id = ++this->app_id_;
// Give client a pointer to our state_version_ so it can notify us of state changes.
// This enables loop() fast-path optimization - we skip expensive work when no state changed.
// Safe because ESP32BLETracker (singleton) outlives all registered clients.
client->set_tracker_state_version(&this->state_version_);
this->clients_.push_back(client);
this->recalculate_advertisement_parser_types();
#endif
@@ -382,6 +407,7 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
this->scanner_state_ = state;
this->state_version_++;
for (auto *listener : this->scanner_state_listeners_) {
listener->on_scanner_state(state);
}

View File

@@ -216,6 +216,19 @@ enum class ConnectionType : uint8_t {
V3_WITHOUT_CACHE
};
/// Base class for BLE GATT clients that connect to remote devices.
///
/// State Change Tracking Design:
/// -----------------------------
/// ESP32BLETracker::loop() needs to know when client states change to avoid
/// expensive polling. Rather than checking all clients every iteration (~7000/min),
/// we use a version counter owned by ESP32BLETracker that clients increment on
/// state changes. The tracker compares versions to skip work when nothing changed.
///
/// Ownership: ESP32BLETracker owns state_version_. Clients hold a non-owning
/// pointer (tracker_state_version_) set during register_client(). Clients
/// increment the counter through this pointer when their state changes.
/// The pointer may be null if the client is not registered with a tracker.
class ESPBTClient : public ESPBTDeviceListener {
public:
virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
@@ -225,26 +238,49 @@ class ESPBTClient : public ESPBTDeviceListener {
virtual void disconnect() = 0;
bool disconnect_pending() const { return this->want_disconnect_; }
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
/// Set the client state with IDLE handling (clears want_disconnect_).
/// Notifies the tracker of state change for loop optimization.
virtual void set_state(ClientState st) {
this->state_ = st;
this->set_state_internal_(st);
if (st == ClientState::IDLE) {
this->want_disconnect_ = false;
}
}
ClientState state() const { return state_; }
ClientState state() const { return this->state_; }
/// Called by ESP32BLETracker::register_client() to enable state change notifications.
/// The pointer must remain valid for the lifetime of the client (guaranteed since
/// ESP32BLETracker is a singleton that outlives all clients).
void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; }
// Memory optimized layout
uint8_t app_id; // App IDs are small integers assigned sequentially
protected:
// Group 1: 1-byte types
ClientState state_{ClientState::INIT};
/// Set state without IDLE handling - use for direct state transitions.
/// Increments the tracker's state version counter to signal that loop()
/// should do full processing on the next iteration.
void set_state_internal_(ClientState st) {
this->state_ = st;
// Notify tracker that state changed (tracker_state_version_ is owned by ESP32BLETracker)
if (this->tracker_state_version_ != nullptr) {
(*this->tracker_state_version_)++;
}
}
// want_disconnect_ is set to true when a disconnect is requested
// while the client is connecting. This is used to disconnect the
// client as soon as we get the connection id (conn_id_) from the
// ESP_GATTC_OPEN_EVT event.
bool want_disconnect_{false};
// 2 bytes used, 2 bytes padding
private:
ClientState state_{ClientState::INIT};
/// Non-owning pointer to ESP32BLETracker::state_version_. When this client's
/// state changes, we increment the tracker's counter to signal that loop()
/// should perform full processing. Null if client not registered with tracker.
uint8_t *tracker_state_version_{nullptr};
};
class ESP32BLETracker : public Component,
@@ -380,6 +416,16 @@ class ESP32BLETracker : public Component,
// Group 4: 1-byte types (enums, uint8_t, bool)
uint8_t app_id_{0};
uint8_t scan_start_fail_count_{0};
/// Version counter for loop() fast-path optimization. Incremented when:
/// - Scanner state changes (via set_scanner_state_())
/// - Any registered client's state changes (clients hold pointer to this counter)
/// Owned by this class; clients receive non-owning pointer via register_client().
/// When loop() sees state_version_ == last_processed_version_, it skips expensive
/// client state counting and takes the fast path (just timeout check + return).
uint8_t state_version_{0};
/// Last state_version_ value when loop() did full processing. Compared against
/// state_version_ to detect if any state changed since last iteration.
uint8_t last_processed_version_{0};
ScannerState scanner_state_{ScannerState::IDLE};
bool scan_continuous_;
bool scan_active_;
@@ -396,6 +442,8 @@ class ESP32BLETracker : public Component,
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
};
uint32_t scan_start_time_{0};
/// Precomputed timeout value: scan_duration_ * 2000
uint32_t scan_timeout_ms_{0};
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
};

View File

@@ -6,7 +6,11 @@
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <Arduino.h>
#include <Esp.h>
#include <core_esp8266_features.h>
extern "C" {
#include <user_interface.h>
}
namespace esphome {
@@ -16,23 +20,19 @@ void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() {
ESP.restart(); // NOLINT(readability-static-accessed-through-instance)
system_restart();
// restart() doesn't always end execution
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
yield();
}
}
void arch_init() {}
void IRAM_ATTR HOT arch_feed_wdt() {
ESP.wdtFeed(); // NOLINT(readability-static-accessed-through-instance)
}
void IRAM_ATTR HOT arch_feed_wdt() { system_soft_wdt_feed(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() {
return ESP.getCycleCount(); // NOLINT(readability-static-accessed-through-instance)
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
void force_link_symbols() {

View File

@@ -6,6 +6,7 @@ from esphome.const import (
CONF_INITIAL_VALUE,
CONF_RESTORE_VALUE,
CONF_TYPE,
CONF_UPDATE_INTERVAL,
CONF_VALUE,
)
from esphome.core import CoroPriority, coroutine_with_priority
@@ -13,25 +14,37 @@ from esphome.core import CoroPriority, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
globals_ns = cg.esphome_ns.namespace("globals")
GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component)
RestoringGlobalsComponent = globals_ns.class_("RestoringGlobalsComponent", cg.Component)
RestoringGlobalsComponent = globals_ns.class_(
"RestoringGlobalsComponent", cg.PollingComponent
)
RestoringGlobalStringComponent = globals_ns.class_(
"RestoringGlobalStringComponent", cg.Component
"RestoringGlobalStringComponent", cg.PollingComponent
)
GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action)
CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length"
def validate_update_interval(config):
if CONF_UPDATE_INTERVAL in config and not config.get(CONF_RESTORE_VALUE, False):
raise cv.Invalid("update_interval requires restore_value to be true")
return config
MULTI_CONF = True
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent),
cv.Required(CONF_TYPE): cv.string_strict,
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
}
).extend(cv.COMPONENT_SCHEMA)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent),
cv.Required(CONF_TYPE): cv.string_strict,
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
cv.Optional(CONF_UPDATE_INTERVAL): cv.update_interval,
}
).extend(cv.COMPONENT_SCHEMA),
validate_update_interval,
)
# Run with low priority so that namespaces are registered first
@@ -65,6 +78,8 @@ async def to_code(config):
value = value.encode()
hash_ = int(hashlib.md5(value).hexdigest()[:8], 16)
cg.add(glob.set_name_hash(hash_))
if CONF_UPDATE_INTERVAL in config:
cg.add(glob.set_update_interval(config[CONF_UPDATE_INTERVAL]))
@automation.register_action(

View File

@@ -5,8 +5,7 @@
#include "esphome/core/helpers.h"
#include <cstring>
namespace esphome {
namespace globals {
namespace esphome::globals {
template<typename T> class GlobalsComponent : public Component {
public:
@@ -24,13 +23,14 @@ template<typename T> class GlobalsComponent : public Component {
T value_{};
};
template<typename T> class RestoringGlobalsComponent : public Component {
template<typename T> class RestoringGlobalsComponent : public PollingComponent {
public:
using value_type = T;
explicit RestoringGlobalsComponent() = default;
explicit RestoringGlobalsComponent(T initial_value) : value_(initial_value) {}
explicit RestoringGlobalsComponent() : PollingComponent(1000) {}
explicit RestoringGlobalsComponent(T initial_value) : PollingComponent(1000), value_(initial_value) {}
explicit RestoringGlobalsComponent(
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
: PollingComponent(1000) {
memcpy(this->value_, initial_value.data(), sizeof(T));
}
@@ -44,7 +44,7 @@ template<typename T> class RestoringGlobalsComponent : public Component {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void loop() override { store_value_(); }
void update() override { store_value_(); }
void on_shutdown() override { store_value_(); }
@@ -66,13 +66,14 @@ template<typename T> class RestoringGlobalsComponent : public Component {
};
// Use with string or subclasses of strings
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public Component {
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public PollingComponent {
public:
using value_type = T;
explicit RestoringGlobalStringComponent() = default;
explicit RestoringGlobalStringComponent(T initial_value) { this->value_ = initial_value; }
explicit RestoringGlobalStringComponent() : PollingComponent(1000) {}
explicit RestoringGlobalStringComponent(T initial_value) : PollingComponent(1000) { this->value_ = initial_value; }
explicit RestoringGlobalStringComponent(
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
: PollingComponent(1000) {
memcpy(this->value_, initial_value.data(), sizeof(T));
}
@@ -90,7 +91,7 @@ template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public C
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void loop() override { store_value_(); }
void update() override { store_value_(); }
void on_shutdown() override { store_value_(); }
@@ -144,5 +145,4 @@ template<typename T> T &id(GlobalsComponent<T> *value) { return value->value();
template<typename T> T &id(RestoringGlobalsComponent<T> *value) { return value->value(); }
template<typename T, uint8_t SZ> T &id(RestoringGlobalStringComponent<T, SZ> *value) { return value->value(); }
} // namespace globals
} // namespace esphome
} // namespace esphome::globals

View File

@@ -42,8 +42,8 @@ ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t
}
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const {
SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes
uint8_t *buffer = buffer_alloc.get(len + 1);
SmallBufferWithHeapFallback<17> buffer_alloc(len + 1); // Most I2C writes are <= 16 bytes
uint8_t *buffer = buffer_alloc.get();
buffer[0] = a_register;
std::copy(data, data + len, buffer + 1);
@@ -51,8 +51,8 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
}
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const {
SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register
uint8_t *buffer = buffer_alloc.get(len + 2);
SmallBufferWithHeapFallback<18> buffer_alloc(len + 2); // Most I2C writes are <= 16 bytes + 2 for register
uint8_t *buffer = buffer_alloc.get();
buffer[0] = a_register >> 8;
buffer[1] = a_register;

View File

@@ -11,22 +11,6 @@
namespace esphome {
namespace i2c {
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
public:
uint8_t *get(size_t size) {
if (size <= STACK_SIZE) {
return this->stack_buffer_;
}
this->heap_buffer_ = std::unique_ptr<uint8_t[]>(new uint8_t[size]);
return this->heap_buffer_.get();
}
private:
uint8_t stack_buffer_[STACK_SIZE];
std::unique_ptr<uint8_t[]> heap_buffer_;
};
/// @brief Error codes returned by I2CBus and I2CDevice methods
enum ErrorCode {
NO_ERROR = 0, ///< No error found during execution of method
@@ -92,8 +76,8 @@ class I2CBus {
total_len += read_buffers[i].len;
}
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small
uint8_t *buffer = buffer_alloc.get(total_len);
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small
uint8_t *buffer = buffer_alloc.get();
auto err = this->write_readv(address, nullptr, 0, buffer, total_len);
if (err != ERROR_OK)
@@ -116,8 +100,8 @@ class I2CBus {
total_len += write_buffers[i].len;
}
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small
uint8_t *buffer = buffer_alloc.get(total_len);
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small
uint8_t *buffer = buffer_alloc.get();
size_t pos = 0;
for (size_t i = 0; i != count; i++) {

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

@@ -234,6 +234,7 @@ class Logger : public Component {
#endif
protected:
void write_msg_(const char *msg, size_t len);
// RAII guard for recursion flags - sets flag on construction, clears on destruction
class RecursionGuard {
public:
@@ -260,7 +261,6 @@ class Logger : public Component {
#endif
#endif
void process_messages_();
void write_msg_(const char *msg, size_t len);
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
// It's the caller's responsibility to initialize buffer_at (typically to 0)

View File

@@ -271,9 +271,9 @@ class ServerRegister {
// Formats a raw value into a string representation based on the value type for debugging
std::string format_value(int64_t value) const {
// max 48: float with %.1f can be up to 42 chars (3.4e38 → 38 integer digits + decimal + 1 digit + sign + null)
// int64_t max is 20 chars + sign + null = 22, so 48 covers both
char buf[48];
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
// plus null terminator = 43, rounded to 44 for 4-byte alignment
char buf[44];
switch (this->value_type) {
case SensorValueType::U_WORD:
case SensorValueType::U_DWORD:

View File

@@ -43,7 +43,7 @@ void MQTTAlarmControlPanelComponent::setup() {
void MQTTAlarmControlPanelComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
LOG_MQTT_COMPONENT(true, true);
ESP_LOGCONFIG(TAG,
" Supported Features: %" PRIu32 "\n"
" Requires Code to Disarm: %s\n"

View File

@@ -19,7 +19,7 @@ void MQTTBinarySensorComponent::setup() {
void MQTTBinarySensorComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Binary Sensor '%s':", this->binary_sensor_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false)
LOG_MQTT_COMPONENT(true, false);
}
MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor)
: binary_sensor_(binary_sensor) {

View File

@@ -406,6 +406,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

@@ -27,20 +27,23 @@ inline char *append_char(char *p, char c) {
// Max lengths for stack-based topic building.
// These limits are enforced at Python config validation time in mqtt/__init__.py
// using cv.Length() validators for topic_prefix and discovery_prefix.
// MQTT_COMPONENT_TYPE_MAX_LEN and MQTT_SUFFIX_MAX_LEN are defined in mqtt_component.h.
// MQTT_COMPONENT_TYPE_MAX_LEN, MQTT_SUFFIX_MAX_LEN, and MQTT_DEFAULT_TOPIC_MAX_LEN are in mqtt_component.h.
// ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h.
// This ensures the stack buffers below are always large enough.
static constexpr size_t TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
// Stack buffer sizes - safe because all inputs are length-validated at config time
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
static constexpr size_t DEFAULT_TOPIC_MAX_LEN =
TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 +
ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
// Function implementation of LOG_MQTT_COMPONENT macro to reduce code size
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic) {
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (state_topic)
ESP_LOGCONFIG(tag, " State Topic: '%s'", obj->get_state_topic_to_(buf).c_str());
if (command_topic)
ESP_LOGCONFIG(tag, " Command Topic: '%s'", obj->get_command_topic_to_(buf).c_str());
}
void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; }
void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; }
@@ -69,19 +72,18 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove
return std::string(buf, p - buf);
}
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
size_t suffix_len) const {
const std::string &topic_prefix = global_mqtt_client->get_topic_prefix();
if (topic_prefix.empty()) {
// If the topic_prefix is null, the default topic should be null
return "";
return StringRef(); // Empty topic_prefix means no default topic
}
const char *comp_type = this->component_type();
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
char buf[DEFAULT_TOPIC_MAX_LEN];
char *p = buf;
char *p = buf.data();
p = append_str(p, topic_prefix.data(), topic_prefix.size());
p = append_char(p, '/');
@@ -89,21 +91,44 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con
p = append_char(p, '/');
p = append_str(p, object_id.c_str(), object_id.size());
p = append_char(p, '/');
p = append_str(p, suffix.data(), suffix.size());
p = append_str(p, suffix, suffix_len);
*p = '\0';
return std::string(buf, p - buf);
return StringRef(buf.data(), p - buf.data());
}
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
StringRef ref = this->get_default_topic_for_to_(buf, suffix.data(), suffix.size());
return std::string(ref.c_str(), ref.size());
}
StringRef MQTTComponent::get_state_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const {
if (this->custom_state_topic_.has_value()) {
// Returns ref to existing data for static/value, uses buf only for lambda case
return this->custom_state_topic_.ref_or_copy_to(buf.data(), buf.size());
}
return this->get_default_topic_for_to_(buf, "state", 5);
}
StringRef MQTTComponent::get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const {
if (this->custom_command_topic_.has_value()) {
// Returns ref to existing data for static/value, uses buf only for lambda case
return this->custom_command_topic_.ref_or_copy_to(buf.data(), buf.size());
}
return this->get_default_topic_for_to_(buf, "command", 7);
}
std::string MQTTComponent::get_state_topic_() const {
if (this->custom_state_topic_.has_value())
return this->custom_state_topic_.value();
return this->get_default_topic_for_("state");
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
StringRef ref = this->get_state_topic_to_(buf);
return std::string(ref.c_str(), ref.size());
}
std::string MQTTComponent::get_command_topic_() const {
if (this->custom_command_topic_.has_value())
return this->custom_command_topic_.value();
return this->get_default_topic_for_("command");
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
StringRef ref = this->get_command_topic_to_(buf);
return std::string(ref.c_str(), ref.size());
}
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
@@ -168,10 +193,14 @@ bool MQTTComponent::send_discovery_() {
break;
}
if (config.state_topic)
root[MQTT_STATE_TOPIC] = this->get_state_topic_();
if (config.command_topic)
root[MQTT_COMMAND_TOPIC] = this->get_command_topic_();
if (config.state_topic) {
char state_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
root[MQTT_STATE_TOPIC] = this->get_state_topic_to_(state_topic_buf);
}
if (config.command_topic) {
char command_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
root[MQTT_COMMAND_TOPIC] = this->get_command_topic_to_(command_topic_buf);
}
if (this->command_retain_)
root[MQTT_COMMAND_RETAIN] = true;
@@ -309,7 +338,9 @@ void MQTTComponent::set_availability(std::string topic, std::string payload_avai
}
void MQTTComponent::disable_availability() { this->set_availability("", "", ""); }
void MQTTComponent::call_setup() {
if (this->is_internal())
// Cache is_internal result once during setup - topics don't change after this
this->is_internal_ = this->compute_is_internal_();
if (this->is_internal_)
return;
this->setup();
@@ -329,16 +360,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_()) {
@@ -365,26 +392,28 @@ StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX
}
StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); }
bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); }
bool MQTTComponent::is_internal() {
bool MQTTComponent::compute_is_internal_() {
if (this->custom_state_topic_.has_value()) {
// If the custom state_topic is null, return true as it is internal and should not publish
// If the custom state_topic is empty, return true as it is internal and should not publish
// else, return false, as it is explicitly set to a topic, so it is not internal and should publish
return this->get_state_topic_().empty();
// Using is_empty() avoids heap allocation for non-lambda cases
return this->custom_state_topic_.is_empty();
}
if (this->custom_command_topic_.has_value()) {
// If the custom command_topic is null, return true as it is internal and should not publish
// If the custom command_topic is empty, return true as it is internal and should not publish
// else, return false, as it is explicitly set to a topic, so it is not internal and should publish
return this->get_command_topic_().empty();
// Using is_empty() avoids heap allocation for non-lambda cases
return this->custom_command_topic_.is_empty();
}
// No custom topics have been set
if (this->get_default_topic_for_("").empty()) {
// If the default topic prefix is null, then the component, by default, is internal and should not publish
// No custom topics have been set - check topic_prefix directly to avoid allocation
if (global_mqtt_client->get_topic_prefix().empty()) {
// If the default topic prefix is empty, then the component, by default, is internal and should not publish
return true;
}
// Use ESPHome's component internal state if topic_prefix is not null with no custom state_topic or command_topic
// Use ESPHome's component internal state if topic_prefix is not empty with no custom state_topic or command_topic
return this->get_entity()->is_internal();
}

View File

@@ -20,17 +20,22 @@ struct SendDiscoveryConfig {
bool command_topic{true}; ///< If the command topic should be included. Default to true.
};
// Max lengths for stack-based topic building (must match mqtt_component.cpp)
// Max lengths for stack-based topic building.
// These limits are enforced at Python config validation time in mqtt/__init__.py
// using cv.Length() validators for topic_prefix and discovery_prefix.
// This ensures the stack buffers are always large enough.
static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20;
static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
// Stack buffer size - safe because all inputs are length-validated at config time
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN =
MQTT_TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
#define LOG_MQTT_COMPONENT(state_topic, command_topic) \
if (state_topic) { \
ESP_LOGCONFIG(TAG, " State Topic: '%s'", this->get_state_topic_().c_str()); \
} \
if (command_topic) { \
ESP_LOGCONFIG(TAG, " Command Topic: '%s'", this->get_command_topic_().c_str()); \
}
class MQTTComponent; // Forward declaration
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
#define LOG_MQTT_COMPONENT(state_topic, command_topic) log_mqtt_component(TAG, this, state_topic, command_topic)
// Macro to define component_type() with compile-time length verification
// Usage: MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")
@@ -74,6 +79,8 @@ static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
* a clean separation.
*/
class MQTTComponent : public Component {
friend void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
public:
/// Constructs a MQTTComponent.
explicit MQTTComponent();
@@ -81,8 +88,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.
@@ -90,7 +95,8 @@ class MQTTComponent : public Component {
virtual bool send_initial_state() = 0;
virtual bool is_internal();
/// Returns cached is_internal result (computed once during setup).
bool is_internal() const { return this->is_internal_; }
/// Set QOS for state messages.
void set_qos(uint8_t qos);
@@ -133,6 +139,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.
@@ -178,7 +187,16 @@ class MQTTComponent : public Component {
/// Helper method to get the discovery topic for this component.
std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const;
/** Get this components state/command/... topic.
/** Get this components state/command/... topic into a buffer.
*
* @param buf The buffer to write to (must be exactly MQTT_DEFAULT_TOPIC_MAX_LEN).
* @param suffix The suffix/key such as "state" or "command".
* @return StringRef pointing to the buffer with the topic.
*/
StringRef get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
size_t suffix_len) const;
/** Get this components state/command/... topic (allocates std::string).
*
* @param suffix The suffix/key such as "state" or "command".
* @return The full topic.
@@ -199,10 +217,20 @@ class MQTTComponent : public Component {
/// Get whether the underlying Entity is disabled by default
bool is_disabled_by_default_() const;
/// Get the MQTT topic that new states will be shared to.
/// Get the MQTT state topic into a buffer (no heap allocation for non-lambda custom topics).
/// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes.
/// @return StringRef pointing to the topic in the buffer.
StringRef get_state_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
/// Get the MQTT command topic into a buffer (no heap allocation for non-lambda custom topics).
/// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes.
/// @return StringRef pointing to the topic in the buffer.
StringRef get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
/// Get the MQTT topic that new states will be shared to (allocates std::string).
std::string get_state_topic_() const;
/// Get the MQTT topic for listening to commands.
/// Get the MQTT topic for listening to commands (allocates std::string).
std::string get_command_topic_() const;
bool is_connected_() const;
@@ -220,12 +248,18 @@ class MQTTComponent : public Component {
std::unique_ptr<Availability> availability_;
bool command_retain_{false};
bool retain_{true};
uint8_t qos_{0};
uint8_t subscribe_qos_{0};
bool discovery_enabled_{true};
bool resend_state_{false};
// Packed bitfields - QoS values are 0-2, bools are flags
uint8_t qos_ : 2 {0};
uint8_t subscribe_qos_ : 2 {0};
bool command_retain_ : 1 {false};
bool retain_ : 1 {true};
bool discovery_enabled_ : 1 {true};
bool resend_state_ : 1 {false};
bool is_internal_ : 1 {false}; ///< Cached result of compute_is_internal_(), set during setup
/// Compute is_internal status based on topics and entity state.
/// Called once during setup to cache the result.
bool compute_is_internal_();
};
} // namespace esphome::mqtt

View File

@@ -51,7 +51,7 @@ void MQTTCoverComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str());
auto traits = this->cover_->get_traits();
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
LOG_MQTT_COMPONENT(true, has_command_topic)
LOG_MQTT_COMPONENT(true, has_command_topic);
if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG,
" Position State Topic: '%s'\n"

View File

@@ -36,7 +36,7 @@ void MQTTDateComponent::setup() {
void MQTTDateComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Date '%s':", this->date_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
LOG_MQTT_COMPONENT(true, true);
}
MQTT_COMPONENT_TYPE(MQTTDateComponent, "date")

View File

@@ -47,7 +47,7 @@ void MQTTDateTimeComponent::setup() {
void MQTTDateTimeComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT DateTime '%s':", this->datetime_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
LOG_MQTT_COMPONENT(true, true);
}
MQTT_COMPONENT_TYPE(MQTTDateTimeComponent, "datetime")

View File

@@ -90,7 +90,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
bool MQTTJSONLightComponent::send_initial_state() { return this->publish_state_(); }
void MQTTJSONLightComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Light '%s':", this->state_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
LOG_MQTT_COMPONENT(true, true);
}
} // namespace esphome::mqtt

View File

@@ -30,7 +30,7 @@ void MQTTNumberComponent::setup() {
void MQTTNumberComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Number '%s':", this->number_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false)
LOG_MQTT_COMPONENT(true, false);
}
MQTT_COMPONENT_TYPE(MQTTNumberComponent, "number")

View File

@@ -25,7 +25,7 @@ void MQTTSelectComponent::setup() {
void MQTTSelectComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Select '%s':", this->select_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false)
LOG_MQTT_COMPONENT(true, false);
}
MQTT_COMPONENT_TYPE(MQTTSelectComponent, "select")

View File

@@ -28,7 +28,7 @@ void MQTTSensorComponent::dump_config() {
if (this->get_expire_after() > 0) {
ESP_LOGCONFIG(TAG, " Expire After: %" PRIu32 "s", this->get_expire_after() / 1000);
}
LOG_MQTT_COMPONENT(true, false)
LOG_MQTT_COMPONENT(true, false);
}
MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")

View File

@@ -26,7 +26,7 @@ void MQTTTextComponent::setup() {
void MQTTTextComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT text '%s':", this->text_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
LOG_MQTT_COMPONENT(true, true);
}
MQTT_COMPONENT_TYPE(MQTTTextComponent, "text")

View File

@@ -36,7 +36,7 @@ void MQTTTimeComponent::setup() {
void MQTTTimeComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Time '%s':", this->time_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
LOG_MQTT_COMPONENT(true, true);
}
MQTT_COMPONENT_TYPE(MQTTTimeComponent, "time")

View File

@@ -39,7 +39,7 @@ void MQTTValveComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT valve '%s':", this->valve_->get_name().c_str());
auto traits = this->valve_->get_traits();
bool has_command_topic = traits.get_supports_position();
LOG_MQTT_COMPONENT(true, has_command_topic)
LOG_MQTT_COMPONENT(true, has_command_topic);
if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG,
" Position State Topic: '%s'\n"

View File

@@ -8,12 +8,12 @@ namespace pipsolar {
static const char *const TAG = "pipsolar.output";
void PipsolarOutput::write_state(float state) {
char tmp[10];
char tmp[16];
snprintf(tmp, sizeof(tmp), this->set_command_, state);
if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) {
ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state);
this->parent_->queue_command(tmp);
this->parent_->queue_command(std::string(tmp));
} else {
ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp);
}

View File

@@ -45,20 +45,20 @@ void Pipsolar::loop() {
} else {
ESP_LOGD(TAG, "command not successful");
}
this->command_queue_[this->command_queue_position_].clear();
this->command_queue_[this->command_queue_position_] = std::string("");
this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
this->state_ = STATE_IDLE;
} else {
// crc failed
// no log message necessary, check_incoming_crc_() logs
this->command_queue_[this->command_queue_position_].clear();
this->command_queue_[this->command_queue_position_] = std::string("");
this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
this->state_ = STATE_IDLE;
}
} else {
ESP_LOGD(TAG, "command %s response length not OK: with length %zu",
this->command_queue_[this->command_queue_position_].c_str(), this->read_pos_);
this->command_queue_[this->command_queue_position_].clear();
this->command_queue_[this->command_queue_position_] = std::string("");
this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
this->state_ = STATE_IDLE;
}
@@ -127,7 +127,7 @@ void Pipsolar::loop() {
const char *command = this->command_queue_[this->command_queue_position_].c_str();
this->command_start_millis_ = millis();
ESP_LOGD(TAG, "command %s timeout", command);
this->command_queue_[this->command_queue_position_].clear();
this->command_queue_[this->command_queue_position_] = std::string("");
this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
this->state_ = STATE_IDLE;
return;
@@ -722,7 +722,7 @@ void Pipsolar::publish_binary_sensor_(esphome::optional<bool> b, binary_sensor::
}
}
esphome::optional<bool> Pipsolar::get_bit_(const std::string &bits, uint8_t bit_pos) {
esphome::optional<bool> Pipsolar::get_bit_(std::string bits, uint8_t bit_pos) {
if (bit_pos >= bits.length()) {
return {};
}

View File

@@ -224,7 +224,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
void publish_binary_sensor_(esphome::optional<bool> b, binary_sensor::BinarySensor *sensor);
esphome::optional<bool> get_bit_(const std::string &bits, uint8_t bit_pos);
esphome::optional<bool> get_bit_(std::string bits, uint8_t bit_pos);
std::string command_queue_[COMMAND_QUEUE_LENGTH];
uint8_t command_queue_position_ = 0;

View File

@@ -51,7 +51,7 @@ void Sim800LComponent::update() {
} else if (state_ == STATE_RECEIVED_SMS) {
// Serial Buffer should have flushed.
// Send cmd to delete received sms
char delete_cmd[20]; // "AT+CMGD=" (8) + int (max 11) + null = 20
char delete_cmd[20]; // "AT+CMGD=" (8) + uint8_t (max 3) + null = 12 <= 20
buf_append_printf(delete_cmd, sizeof(delete_cmd), 0, "AT+CMGD=%d", this->parse_index_);
this->send_cmd_(delete_cmd);
this->state_ = STATE_CHECK_SMS;

View File

@@ -106,7 +106,7 @@ std::string bytes_repr(const BytesView &buffer) {
for (auto const value : buffer) {
// max 3: 2 hex digits + null
char hex_buf[3];
snprintf(hex_buf, sizeof(hex_buf), "%02x", value & 0xff);
snprintf(hex_buf, sizeof(hex_buf), "%02x", static_cast<unsigned int>(value));
repr += hex_buf;
}
return repr;

View File

@@ -7,14 +7,14 @@ DEPENDENCIES = ["network"]
status_ns = cg.esphome_ns.namespace("status")
StatusBinarySensor = status_ns.class_(
"StatusBinarySensor", binary_sensor.BinarySensor, cg.Component
"StatusBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent
)
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(
StatusBinarySensor,
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.COMPONENT_SCHEMA)
).extend(cv.polling_component_schema("1s"))
async def to_code(config):

View File

@@ -10,12 +10,11 @@
#include "esphome/components/api/api_server.h"
#endif
namespace esphome {
namespace status {
namespace esphome::status {
static const char *const TAG = "status";
void StatusBinarySensor::loop() {
void StatusBinarySensor::update() {
bool status = network::is_connected();
#ifdef USE_MQTT
if (mqtt::global_mqtt_client != nullptr) {
@@ -33,5 +32,4 @@ void StatusBinarySensor::loop() {
void StatusBinarySensor::setup() { this->publish_initial_state(false); }
void StatusBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Status Binary Sensor", this); }
} // namespace status
} // namespace esphome
} // namespace esphome::status

View File

@@ -3,12 +3,11 @@
#include "esphome/core/component.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace status {
namespace esphome::status {
class StatusBinarySensor : public binary_sensor::BinarySensor, public Component {
class StatusBinarySensor : public binary_sensor::BinarySensor, public PollingComponent {
public:
void loop() override;
void update() override;
void setup() override;
void dump_config() override;
@@ -16,5 +15,4 @@ class StatusBinarySensor : public binary_sensor::BinarySensor, public Component
bool is_status_binary_sensor() const override { return true; }
};
} // namespace status
} // namespace esphome
} // namespace esphome::status

View File

@@ -4,7 +4,8 @@ namespace esphome {
namespace teleinfo {
static const char *const TAG = "teleinfo_sensor";
void TeleInfoSensor::publish_val(const char *val) {
TeleInfoSensor::TeleInfoSensor(const char *tag) { this->tag = std::string(tag); }
void TeleInfoSensor::publish_val(const std::string &val) {
auto newval = parse_number<float>(val).value_or(0.0f);
publish_state(newval);
}

View File

@@ -7,8 +7,8 @@ namespace teleinfo {
class TeleInfoSensor : public TeleInfoListener, public sensor::Sensor, public Component {
public:
TeleInfoSensor(const char *tag) { this->tag = tag; }
void publish_val(const char *val) override;
TeleInfoSensor(const char *tag);
void publish_val(const std::string &val) override;
void dump_config() override;
};

View File

@@ -172,15 +172,15 @@ void TeleInfo::loop() {
/* Advance buf_finger to end of group */
buf_finger += field_len + 1 + 1 + 1;
publish_value_(tag_, val_);
publish_value_(std::string(tag_), std::string(val_));
}
state_ = OFF;
break;
}
}
void TeleInfo::publish_value_(const char *tag, const char *val) {
void TeleInfo::publish_value_(const std::string &tag, const std::string &val) {
for (auto *element : teleinfo_listeners_) {
if (strcmp(tag, element->tag) != 0)
if (tag != element->tag)
continue;
element->publish_val(val);
}

View File

@@ -18,8 +18,8 @@ static const uint16_t MAX_TIMESTAMP_SIZE = 14;
class TeleInfoListener {
public:
const char *tag{nullptr};
virtual void publish_val(const char *val){};
std::string tag;
virtual void publish_val(const std::string &val){};
};
class TeleInfo : public PollingComponent, public uart::UARTDevice {
public:
@@ -48,7 +48,7 @@ class TeleInfo : public PollingComponent, public uart::UARTDevice {
} state_{OFF};
bool read_chars_until_(bool drop, uint8_t c);
bool check_crc_(const char *grp, const char *grp_end);
void publish_value_(const char *tag, const char *val);
void publish_value_(const std::string &tag, const std::string &val);
};
} // namespace teleinfo
} // namespace esphome

View File

@@ -4,7 +4,8 @@ namespace esphome {
namespace teleinfo {
static const char *const TAG = "teleinfo_text_sensor";
void TeleInfoTextSensor::publish_val(const char *val) { publish_state(val); }
TeleInfoTextSensor::TeleInfoTextSensor(const char *tag) { this->tag = std::string(tag); }
void TeleInfoTextSensor::publish_val(const std::string &val) { publish_state(val); }
void TeleInfoTextSensor::dump_config() { LOG_TEXT_SENSOR(" ", "Teleinfo Text Sensor", this); }
} // namespace teleinfo
} // namespace esphome

View File

@@ -5,8 +5,8 @@ namespace esphome {
namespace teleinfo {
class TeleInfoTextSensor : public TeleInfoListener, public text_sensor::TextSensor, public Component {
public:
TeleInfoTextSensor(const char *tag) { this->tag = tag; }
void publish_val(const char *val) override;
TeleInfoTextSensor(const char *tag);
void publish_val(const std::string &val) override;
void dump_config() override;
};
} // namespace teleinfo

View File

@@ -24,7 +24,7 @@ void TuyaTextSensor::setup() {
}
case TuyaDatapointType::ENUM: {
char buf[4]; // uint8_t max is 3 digits + null
snprintf(buf, sizeof(buf), "%u", datapoint.value_enum);
buf_append_printf(buf, sizeof(buf), 0, "%u", datapoint.value_enum);
ESP_LOGD(TAG, "MCU reported text sensor %u is: %s", datapoint.id, buf);
this->publish_state(buf);
break;

View File

@@ -920,7 +920,16 @@ bssid_t WiFiComponent::wifi_bssid() {
}
return bssid;
}
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
std::string WiFiComponent::wifi_ssid() {
struct station_config conf {};
if (!wifi_station_get_config(&conf)) {
return "";
}
// conf.ssid is uint8[32], not null-terminated if full
auto *ssid_s = reinterpret_cast<const char *>(conf.ssid);
size_t len = strnlen(ssid_s, sizeof(conf.ssid));
return {ssid_s, len};
}
const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer) {
struct station_config conf {};
if (!wifi_station_get_config(&conf)) {
@@ -934,16 +943,24 @@ const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer
return buffer.data();
}
int8_t WiFiComponent::wifi_rssi() {
if (WiFi.status() != WL_CONNECTED)
if (wifi_station_get_connect_status() != STATION_GOT_IP)
return WIFI_RSSI_DISCONNECTED;
int8_t rssi = WiFi.RSSI();
sint8 rssi = wifi_station_get_rssi();
// Values >= 31 are error codes per NONOS SDK API, not valid RSSI readings
return rssi >= 31 ? WIFI_RSSI_DISCONNECTED : rssi;
}
int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; }
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; }
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {(const ip_addr_t *) WiFi.dnsIP(num)}; }
int32_t WiFiComponent::get_wifi_channel() { return wifi_get_channel(); }
network::IPAddress WiFiComponent::wifi_subnet_mask_() {
struct ip_info ip {};
wifi_get_ip_info(STATION_IF, &ip);
return network::IPAddress(&ip.netmask);
}
network::IPAddress WiFiComponent::wifi_gateway_ip_() {
struct ip_info ip {};
wifi_get_ip_info(STATION_IF, &ip);
return network::IPAddress(&ip.gw);
}
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); }
void WiFiComponent::wifi_loop_() {}
} // namespace esphome::wifi

View File

@@ -660,21 +660,27 @@ void WiFiComponent::wifi_scan_done_callback_() {
this->scan_result_.clear();
this->scan_done_ = true;
int16_t num = WiFi.scanComplete();
if (num < 0)
// Access scan data directly to avoid String allocation from WiFi.SSID(i)
// WiFi.scan is public in LibreTiny (WiFi.h)
if (WiFi.scan == nullptr || WiFi.scan->running)
return;
this->scan_result_.init(static_cast<unsigned int>(num));
for (int i = 0; i < num; i++) {
String ssid = WiFi.SSID(i);
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
int32_t rssi = WiFi.RSSI(i);
uint8_t *bssid = WiFi.BSSID(i);
int32_t channel = WiFi.channel(i);
uint8_t num = WiFi.scan->count;
if (num == 0) {
WiFi.scanDelete();
return;
}
this->scan_result_.emplace_back(bssid_t{bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]},
std::string(ssid.c_str()), channel, rssi, authmode != WIFI_AUTH_OPEN,
ssid.length() == 0);
this->scan_result_.init(num);
for (uint8_t i = 0; i < num; i++) {
const auto &ap = WiFi.scan->ap[i];
const char *ssid_cstr = ap.ssid;
size_t ssid_len = ssid_cstr ? strlen(ssid_cstr) : 0;
this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3],
ap.bssid.addr[4], ap.bssid.addr[5]},
std::string(ssid_cstr ? ssid_cstr : "", ssid_len), ap.channel, ap.rssi,
ap.auth != WIFI_AUTH_OPEN, ssid_len == 0);
}
WiFi.scanDelete();
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS

View File

@@ -4,6 +4,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/string_ref.h"
#include <concepts>
#include <functional>
#include <utility>
@@ -190,15 +191,55 @@ template<typename T, typename... X> class TemplatableValue {
/// Get the static string pointer (only valid if is_static_string() returns true)
const char *get_static_string() const { return this->static_str_; }
protected:
enum : uint8_t {
NONE,
VALUE,
LAMBDA,
STATELESS_LAMBDA,
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
} type_;
/// Check if the string value is empty without allocating (for std::string specialization).
/// For NONE, returns true. For STATIC_STRING/VALUE, checks without allocation.
/// For LAMBDA/STATELESS_LAMBDA, must call value() which may allocate.
bool is_empty() const requires std::same_as<T, std::string> {
switch (this->type_) {
case NONE:
return true;
case STATIC_STRING:
return this->static_str_ == nullptr || this->static_str_[0] == '\0';
case VALUE:
return this->value_->empty();
default: // LAMBDA/STATELESS_LAMBDA - must call value()
return this->value().empty();
}
}
/// Get a StringRef to the string value without heap allocation when possible.
/// For STATIC_STRING/VALUE, returns reference to existing data (no allocation).
/// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer.
/// @param lambda_buf Buffer used only for lambda case (must remain valid while StringRef is used).
/// @param lambda_buf_size Size of the buffer.
/// @return StringRef pointing to the string data.
StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as<T, std::string> {
switch (this->type_) {
case NONE:
return StringRef();
case STATIC_STRING:
if (this->static_str_ == nullptr)
return StringRef();
return StringRef(this->static_str_, strlen(this->static_str_));
case VALUE:
return StringRef(this->value_->data(), this->value_->size());
default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy
std::string result = this->value();
size_t copy_len = std::min(result.size(), lambda_buf_size - 1);
memcpy(lambda_buf, result.data(), copy_len);
lambda_buf[copy_len] = '\0';
return StringRef(lambda_buf, copy_len);
}
}
}
protected : enum : uint8_t {
NONE,
VALUE,
LAMBDA,
STATELESS_LAMBDA,
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
} type_;
// For std::string, use heap pointer to minimize union size (4 bytes vs 12+).
// For other types, store value inline as before.
using ValueStorage = std::conditional_t<USE_HEAP_STORAGE, T *, T>;

View File

@@ -441,6 +441,35 @@ template<typename T> class FixedVector {
const T *end() const { return data_ + size_; }
};
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
/// This is useful when most operations need a small buffer but occasionally need larger ones.
/// The stack buffer avoids heap allocation in the common case, while heap fallback handles edge cases.
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
public:
explicit SmallBufferWithHeapFallback(size_t size) {
if (size <= STACK_SIZE) {
this->buffer_ = this->stack_buffer_;
} else {
this->heap_buffer_ = new uint8_t[size];
this->buffer_ = this->heap_buffer_;
}
}
~SmallBufferWithHeapFallback() { delete[] this->heap_buffer_; }
// Delete copy and move operations to prevent double-delete
SmallBufferWithHeapFallback(const SmallBufferWithHeapFallback &) = delete;
SmallBufferWithHeapFallback &operator=(const SmallBufferWithHeapFallback &) = delete;
SmallBufferWithHeapFallback(SmallBufferWithHeapFallback &&) = delete;
SmallBufferWithHeapFallback &operator=(SmallBufferWithHeapFallback &&) = delete;
uint8_t *get() { return this->buffer_; }
private:
uint8_t stack_buffer_[STACK_SIZE];
uint8_t *heap_buffer_{nullptr};
uint8_t *buffer_;
};
///@}
/// @name Mathematics

View File

@@ -67,7 +67,7 @@ std::string ESPTime::strftime(const char *format) {
std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); }
bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) {
uint16_t year;
uint8_t month;
uint8_t day;
@@ -75,40 +75,41 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
uint8_t minute;
uint8_t second;
int num;
const int ilen = static_cast<int>(len);
if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, // NOLINT
&second, &num) == 6 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, // NOLINT
&second, &num) == 6 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = second;
} else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, &num) == 5 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, &num) == 5 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = 0;
} else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
} else if (sscanf(time_to_parse, "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
num == ilen) {
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = second;
} else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
} else if (sscanf(time_to_parse, "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
num == ilen) {
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = 0;
} else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;

View File

@@ -2,6 +2,7 @@
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <span>
#include <string>
@@ -80,11 +81,20 @@ struct ESPTime {
}
/** Convert a string to ESPTime struct as specified by the format argument.
* @param time_to_parse null-terminated c string formatet like this: 2020-08-25 05:30:00.
* @param time_to_parse c string formatted like this: 2020-08-25 05:30:00.
* @param len length of the string (not including null terminator if present)
* @param esp_time an instance of a ESPTime struct
* @return the success sate of the parsing
* @return the success state of the parsing
*/
static bool strptime(const std::string &time_to_parse, ESPTime &esp_time);
static bool strptime(const char *time_to_parse, size_t len, ESPTime &esp_time);
/// @copydoc strptime(const char *, size_t, ESPTime &)
static bool strptime(const char *time_to_parse, ESPTime &esp_time) {
return strptime(time_to_parse, strlen(time_to_parse), esp_time);
}
/// @copydoc strptime(const char *, size_t, ESPTime &)
static bool strptime(const std::string &time_to_parse, ESPTime &esp_time) {
return strptime(time_to_parse.c_str(), time_to_parse.size(), esp_time);
}
/// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance.
static ESPTime from_c_tm(struct tm *c_tm, time_t c_time);

View File

@@ -108,6 +108,25 @@ text_sensor:
format: "HA Empty state updated: %s"
args: ['x.c_str()']
# Test long attribute handling (>255 characters)
# HA states are limited to 255 chars, but attributes are not
- platform: homeassistant
name: "HA Long Attribute"
entity_id: sensor.long_data
attribute: long_value
id: ha_long_attribute
on_value:
then:
- logger.log:
format: "HA Long attribute received, length: %d"
args: ['x.size()']
# Log the first 50 and last 50 chars to verify no truncation
- lambda: |-
if (x.size() >= 100) {
ESP_LOGI("test", "Long attribute first 50 chars: %.50s", x.c_str());
ESP_LOGI("test", "Long attribute last 50 chars: %s", x.c_str() + x.size() - 50);
}
# Number component for testing HA number control
number:
- platform: template

View File

@@ -40,6 +40,7 @@ async def test_api_homeassistant(
humidity_update_future = loop.create_future()
motion_update_future = loop.create_future()
weather_update_future = loop.create_future()
long_attr_future = loop.create_future()
# Number future
ha_number_future = loop.create_future()
@@ -58,6 +59,7 @@ async def test_api_homeassistant(
humidity_update_pattern = re.compile(r"HA Humidity state updated: ([\d.]+)")
motion_update_pattern = re.compile(r"HA Motion state changed: (ON|OFF)")
weather_update_pattern = re.compile(r"HA Weather condition updated: (\w+)")
long_attr_pattern = re.compile(r"HA Long attribute received, length: (\d+)")
# Number pattern
ha_number_pattern = re.compile(r"Setting HA number to: ([\d.]+)")
@@ -143,8 +145,14 @@ async def test_api_homeassistant(
elif not weather_update_future.done() and weather_update_pattern.search(line):
weather_update_future.set_result(line)
# Check number pattern
elif not ha_number_future.done() and ha_number_pattern.search(line):
# Check long attribute pattern - separate if since it can come at different times
if not long_attr_future.done():
match = long_attr_pattern.search(line)
if match:
long_attr_future.set_result(int(match.group(1)))
# Check number pattern - separate if since it can come at different times
if not ha_number_future.done():
match = ha_number_pattern.search(line)
if match:
ha_number_future.set_result(match.group(1))
@@ -179,6 +187,14 @@ async def test_api_homeassistant(
client.send_home_assistant_state("binary_sensor.external_motion", "", "ON")
client.send_home_assistant_state("weather.home", "condition", "sunny")
# Send a long attribute (300 characters) to test that attributes aren't truncated
# HA states are limited to 255 chars, but attributes are NOT limited
# This tests the fix for the 256-byte buffer truncation bug
long_attr_value = "X" * 300 # 300 chars - enough to expose truncation bug
client.send_home_assistant_state(
"sensor.long_data", "long_value", long_attr_value
)
# Test edge cases for zero-copy implementation safety
# Empty entity_id should be silently ignored (no crash)
client.send_home_assistant_state("", "", "should_be_ignored")
@@ -225,6 +241,13 @@ async def test_api_homeassistant(
number_value = await asyncio.wait_for(ha_number_future, timeout=5.0)
assert number_value == "42.5", f"Unexpected number value: {number_value}"
# Long attribute test - verify 300 chars weren't truncated to 255
long_attr_len = await asyncio.wait_for(long_attr_future, timeout=5.0)
assert long_attr_len == 300, (
f"Long attribute was truncated! Expected 300 chars, got {long_attr_len}. "
"This indicates the 256-byte truncation bug."
)
# Wait for completion
await asyncio.wait_for(tests_complete_future, timeout=5.0)