mirror of
https://github.com/esphome/esphome.git
synced 2026-01-19 09:36:24 -07:00
Compare commits
98 Commits
pipsolar_t
...
integratio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f88ab80f4 | ||
|
|
48e7e7aeb3 | ||
|
|
54a4d60f5d | ||
|
|
d41980d0d2 | ||
|
|
d85702bf32 | ||
|
|
2e3e61f464 | ||
|
|
115e17e95b | ||
|
|
43f0dd091a | ||
|
|
64bf247f7b | ||
|
|
0117519c81 | ||
|
|
d83457bbe1 | ||
|
|
a26d01f536 | ||
|
|
90e67d72a5 | ||
|
|
994e8970f5 | ||
|
|
5ac917835e | ||
|
|
986caddb6c | ||
|
|
bac836d2a7 | ||
|
|
72f71f59b3 | ||
|
|
122e7ac01e | ||
|
|
86a1b4cf69 | ||
|
|
207b59fe16 | ||
|
|
83c68e246d | ||
|
|
c629e88f4b | ||
|
|
b078eb8523 | ||
|
|
98e0a82e66 | ||
|
|
b4e0a0a15a | ||
|
|
d0869fbc67 | ||
|
|
a3d926dc54 | ||
|
|
d8a28f6fba | ||
|
|
e80a940222 | ||
|
|
e99dbe05f7 | ||
|
|
f453a8d9a1 | ||
|
|
126190d26a | ||
|
|
e40201a98d | ||
|
|
8142f5db44 | ||
|
|
98ccab87a7 | ||
|
|
b9e72a8774 | ||
|
|
d9fc625c6a | ||
|
|
dfbf79d6d6 | ||
|
|
ea0fac96cb | ||
|
|
3182222d60 | ||
|
|
d8849b16f2 | ||
|
|
635983f163 | ||
|
|
6cbe672004 | ||
|
|
226867b05c | ||
|
|
67871a1683 | ||
|
|
f60c03e350 | ||
|
|
eb66429144 | ||
|
|
0f3bac5dd6 | ||
|
|
5b92d0b89e | ||
|
|
052b05df56 | ||
|
|
7b0db659d1 | ||
|
|
2f7270cf8f | ||
|
|
b44727aee6 | ||
|
|
1a55254258 | ||
|
|
baf2b0e3c9 | ||
|
|
88fadb242c | ||
|
|
326fd4fe68 | ||
|
|
76b1201c96 | ||
|
|
7efb72c511 | ||
|
|
07a731b97d | ||
|
|
db37ae0e3c | ||
|
|
d2bf991bfb | ||
|
|
f8b33562c1 | ||
|
|
cf17a079b7 | ||
|
|
a451625120 | ||
|
|
c180d0c49c | ||
|
|
bacc4ed4e5 | ||
|
|
7acde0ab60 | ||
|
|
98c8142f86 | ||
|
|
4ed68c6884 | ||
|
|
680e92a226 | ||
|
|
6ab321db1a | ||
|
|
c1cba269b3 | ||
|
|
0e2f0bae21 | ||
|
|
7175299cae | ||
|
|
db0b32bfc9 | ||
|
|
21794e28e5 | ||
|
|
42ad20c231 | ||
|
|
ae5a3e616a | ||
|
|
59eeeb5fe5 | ||
|
|
759278191b | ||
|
|
03eaec853a | ||
|
|
4549e375c1 | ||
|
|
a8b07af2a3 | ||
|
|
f011dc658d | ||
|
|
728236270c | ||
|
|
01cdc4ed58 | ||
|
|
d6a0c8ffbb | ||
|
|
f003fac5d8 | ||
|
|
4cc0f874f7 | ||
|
|
ed58b9372f | ||
|
|
ee2a81923b | ||
|
|
0a1e7ee50b | ||
|
|
4d4283bcfa | ||
|
|
6b02f5dfbd | ||
|
|
526bd58d1c | ||
|
|
1ed478fd5f |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,6 @@ class CS5460AComponent : public Component,
|
||||
void restart() { restart_(); }
|
||||
|
||||
void setup() override;
|
||||
void loop() override {}
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user