Compare commits

..

45 Commits

Author SHA1 Message Date
J. Nick Koston
a932ea343d [api] Split process_batch_ to reduce stack on single-message hot path
Clamp buffer pre-allocation to MAX_BATCH_PACKET_SIZE.
2026-02-10 07:23:22 -06:00
J. Nick Koston
bab8d1e8b2 optimize batching 2026-02-10 06:50:13 -06:00
tronikos
e3141211c3 [water_heater] Add On/Off and Away mode support to template platform (#13839)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-10 12:45:18 +00:00
dependabot[bot]
e85a022c77 Bump esphome-dashboard from 20260110.0 to 20260210.0 (#13905)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 11:49:59 +00:00
dependabot[bot]
1c3af30299 Bump aioesphomeapi from 43.14.0 to 44.0.0 (#13906)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 11:45:31 +00:00
tronikos
5caed68cd9 [api] Deprecate WATER_HEATER_COMMAND_HAS_STATE (#13892)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-10 05:36:56 -06:00
Cody Cutrer
b97a728cf1 [ld2450] add on_data callback (#13601)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-09 22:40:44 -05:00
Jonathan Swoboda
dcbb020479 [uart] Fix available() return type to size_t across components (#13898)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 20:02:41 -05:00
J. Nick Koston
87ac263264 [dsmr] Batch UART reads to reduce per-loop overhead (#13826) 2026-02-10 00:32:52 +00:00
Sean Kelly
097901e9c8 [aqi] Fix AQI calculation for specific pm2.5 or pm10 readings (#13770) 2026-02-09 19:30:37 -05:00
J. Nick Koston
01a90074ba [ld2420] Batch UART reads to reduce loop overhead (#13821)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-10 00:25:34 +00:00
J. Nick Koston
57b85a8400 [dlms_meter] Batch UART reads to reduce per-loop overhead (#13828)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-10 00:24:20 +00:00
J. Nick Koston
2edfcf278f [hlk_fm22x] Replace per-cycle vector allocation with member buffer (#13859) 2026-02-09 18:21:10 -06:00
J. Nick Koston
bcd4a9fc39 [pylontech] Batch UART reads to reduce loop overhead (#13824) 2026-02-09 18:20:53 -06:00
J. Nick Koston
78df8be31f [logger] Resolve thread name once and pass through logging chain (#13836) 2026-02-09 18:16:27 -06:00
J. Nick Koston
dacc557a16 [uart] Convert parity_to_str to PROGMEM_STRING_TABLE (#13805) 2026-02-09 18:15:48 -06:00
J. Nick Koston
3767c5ec91 [scheduler] Make core timer ID collisions impossible with type-safe internal IDs (#13882)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-09 16:48:08 -06:00
George Joseph
7c1327f96a [mipi_dsi] Add WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD 3.4C and 4C (#13840) 2026-02-10 09:44:47 +11:00
Jonathan Swoboda
475db750e0 [uart] Change available() return type from int to size_t (#13893)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 17:41:16 -05:00
dependabot[bot]
8f74b027b4 Bump setuptools from 80.10.2 to 82.0.0 (#13897) 2026-02-09 16:40:32 -06:00
tomaszduda23
b2b9e0cb0a [nrf52,zigee] print reporting status (#13890)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-02-09 16:00:08 -05:00
tronikos
dbf202bf0d Add get_away and get_on in WaterHeaterCall and deprecate get_state (#13891) 2026-02-09 20:57:36 +00:00
J. Nick Koston
b6fdd29953 [voice_assistant] Replace timer unordered_map with vector to eliminate per-tick heap allocation (#13857) 2026-02-09 14:42:40 -06:00
Clyde Stubbs
00256e3ca0 [mipi_rgb] Allow use on P4 (#13740) 2026-02-10 06:35:41 +11:00
J. Nick Koston
e0712cc53b [scheduler] Make core timer ID collisions impossible with type-safe internal IDs (#13882)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-09 13:16:22 -06:00
J. Nick Koston
6c6da8a3cd [api] Skip class generation for empty SOURCE_CLIENT protobuf messages (#13880) 2026-02-09 18:45:24 +00:00
J. Nick Koston
e4ea016d1e [ci] Block new std::to_string() usage, suggest snprintf alternatives (#13369) 2026-02-09 12:26:19 -06:00
J. Nick Koston
41a9588d81 [i2c] Replace switch with if-else to avoid CSWTCH table in RAM (#13815) 2026-02-09 12:26:06 -06:00
J. Nick Koston
cd55eb927d [modbus] Batch UART reads to reduce loop overhead (#13822) 2026-02-09 12:21:15 -06:00
J. Nick Koston
4a9ff48f02 [nextion] Batch UART reads to reduce loop overhead (#13823)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-09 12:20:50 -06:00
J. Nick Koston
8fffe7453d [seeed_mr24hpc1/mr60fda2/mr60bha2] Batch UART reads to reduce per-loop overhead (#13825) 2026-02-09 12:18:12 -06:00
J. Nick Koston
a5ee451043 [tuya] Batch UART reads to reduce per-loop overhead (#13827) 2026-02-09 12:17:58 -06:00
J. Nick Koston
e176cf50ab [dfplayer] Batch UART reads to reduce per-loop overhead (#13832) 2026-02-09 12:15:28 -06:00
J. Nick Koston
e7a900fbaa [rf_bridge] Batch UART reads to reduce per-loop overhead (#13831) 2026-02-09 12:15:15 -06:00
J. Nick Koston
623f33c9f9 [rd03d] Batch UART reads to reduce per-loop overhead (#13830) 2026-02-09 12:15:04 -06:00
J. Nick Koston
8b24112be5 [pipsolar] Batch UART reads to reduce per-loop overhead (#13829) 2026-02-09 12:14:48 -06:00
J. Nick Koston
d33f23dc43 [ld2410] Batch UART reads to reduce loop overhead (#13820) 2026-02-09 12:07:55 -06:00
J. Nick Koston
c43d3889b0 [modbus] Use stack buffer instead of heap vector in send() (#13853) 2026-02-09 12:07:42 -06:00
J. Nick Koston
50fe8e51f9 [ld2412] Batch UART reads to reduce loop overhead (#13819) 2026-02-09 12:07:28 -06:00
J. Nick Koston
c7883cb5ae [ld2450] Batch UART reads to reduce loop overhead (#13818) 2026-02-09 12:06:38 -06:00
J. Nick Koston
3b0df145b7 [cse7766] Batch UART reads to reduce loop overhead (#13817) 2026-02-09 12:05:59 -06:00
J. Nick Koston
2383b6b8b4 [core] Deprecate set_retry, cancel_retry, and RetryResult (#13845) 2026-02-09 12:05:32 -06:00
J. Nick Koston
c658d7b57f [api] Merge auth check into base read_message, eliminate APIServerConnection (#13873) 2026-02-09 12:02:02 -06:00
Jonathan Swoboda
04a6238c7b [esp32] Set UV_CACHE_DIR inside data dir so Clean All clears it (#13888)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 17:49:58 +00:00
J. Nick Koston
919afa1553 [web_server_base] Fix RP2040 compilation when Crypto-no-arduino is present (#13887) 2026-02-09 12:47:59 -05:00
105 changed files with 1516 additions and 744 deletions

View File

@@ -1155,9 +1155,11 @@ enum WaterHeaterCommandHasField {
WATER_HEATER_COMMAND_HAS_NONE = 0; WATER_HEATER_COMMAND_HAS_NONE = 0;
WATER_HEATER_COMMAND_HAS_MODE = 1; WATER_HEATER_COMMAND_HAS_MODE = 1;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
WATER_HEATER_COMMAND_HAS_STATE = 4; WATER_HEATER_COMMAND_HAS_STATE = 4 [deprecated=true];
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
WATER_HEATER_COMMAND_HAS_ON_STATE = 32;
WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64;
} }
message WaterHeaterCommandRequest { message WaterHeaterCommandRequest {

View File

@@ -1343,8 +1343,12 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ
call.set_target_temperature_low(msg.target_temperature_low); call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH) if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
call.set_target_temperature_high(msg.target_temperature_high); call.set_target_temperature_high(msg.target_temperature_high);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) { if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE) ||
(msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0); call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
}
if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_ON_STATE) ||
(msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0); call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
} }
call.perform(); call.perform();
@@ -1895,10 +1899,6 @@ bool APIConnection::schedule_batch_() {
} }
void APIConnection::process_batch_() { void APIConnection::process_batch_() {
// Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value,
"MessageInfo must remain trivially destructible with this placement-new approach");
if (this->deferred_batch_.empty()) { if (this->deferred_batch_.empty()) {
this->flags_.batch_scheduled = false; this->flags_.batch_scheduled = false;
return; return;
@@ -1923,6 +1923,10 @@ void APIConnection::process_batch_() {
for (size_t i = 0; i < num_items; i++) { for (size_t i = 0; i < num_items; i++) {
total_estimated_size += this->deferred_batch_[i].estimated_size; total_estimated_size += this->deferred_batch_[i].estimated_size;
} }
// Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
total_estimated_size = MAX_BATCH_PACKET_SIZE;
}
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size); this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
@@ -1946,7 +1950,20 @@ void APIConnection::process_batch_() {
return; return;
} }
size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH); // Multi-message path — heavy stack frame isolated in separate noinline function
this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size);
}
// Separated from process_batch_() so the single-message fast path gets a minimal
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) {
// Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value,
"MessageInfo must remain trivially destructible with this placement-new approach");
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
const uint8_t frame_overhead = header_padding + footer_size;
// Stack-allocated array for message info // Stack-allocated array for message info
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)]; alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
@@ -1973,7 +1990,7 @@ void APIConnection::process_batch_() {
// Message was encoded successfully // Message was encoded successfully
// payload_size is header_padding + actual payload size + footer_size // payload_size is header_padding + actual payload size + footer_size
uint16_t proto_payload_size = payload_size - header_padding - footer_size; uint16_t proto_payload_size = payload_size - frame_overhead;
// Use placement new to construct MessageInfo in pre-allocated stack array // Use placement new to construct MessageInfo in pre-allocated stack array
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
// Explicit destruction is not needed because MessageInfo is trivially destructible, // Explicit destruction is not needed because MessageInfo is trivially destructible,
@@ -1989,42 +2006,38 @@ void APIConnection::process_batch_() {
current_offset = shared_buf.size() + footer_size; current_offset = shared_buf.size() + footer_size;
} }
if (items_processed == 0) { if (items_processed > 0) {
this->deferred_batch_.clear(); // Add footer space for the last message (for Noise protocol MAC)
return; if (footer_size > 0) {
} shared_buf.resize(shared_buf.size() + footer_size);
}
// Add footer space for the last message (for Noise protocol MAC) // Send all collected messages
if (footer_size > 0) { APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
shared_buf.resize(shared_buf.size() + footer_size); std::span<const MessageInfo>(message_info, items_processed));
} if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
// Send all collected messages }
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging // Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result // It's safe to use the buffer for logging at this point regardless of send result
for (size_t i = 0; i < items_processed; i++) { for (size_t i = 0; i < items_processed; i++) {
const auto &item = this->deferred_batch_[i]; const auto &item = this->deferred_batch_[i];
this->log_batch_item_(item); this->log_batch_item_(item);
} }
#endif #endif
// Handle remaining items more efficiently // Partial batch — remove processed items and reschedule
if (items_processed < this->deferred_batch_.size()) { if (items_processed < this->deferred_batch_.size()) {
// Remove processed items from the beginning this->deferred_batch_.remove_front(items_processed);
this->deferred_batch_.remove_front(items_processed); this->schedule_batch_();
// Reschedule for remaining items return;
this->schedule_batch_(); }
} else {
// All items processed
this->clear_batch_();
} }
// All items processed (or none could be processed)
this->clear_batch_();
} }
// Dispatch message encoding based on message_type // Dispatch message encoding based on message_type

View File

@@ -28,7 +28,7 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH, static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH"); "MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
class APIConnection final : public APIServerConnection { class APIConnection final : public APIServerConnectionBase {
public: public:
friend class APIServer; friend class APIServer;
friend class ListEntitiesIterator; friend class ListEntitiesIterator;
@@ -549,8 +549,8 @@ class APIConnection final : public APIServerConnection {
batch_start_time = 0; batch_start_time = 0;
} }
// Remove processed items from the front // Remove processed items from the front — noinline to keep memmove out of warm callers
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); } bool empty() const { return items.empty(); }
size_t size() const { return items.size(); } size_t size() const { return items.size(); }
@@ -622,6 +622,8 @@ class APIConnection final : public APIServerConnection {
bool schedule_batch_(); bool schedule_batch_();
void process_batch_(); void process_batch_();
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) __attribute__((noinline));
void clear_batch_() { void clear_batch_() {
this->deferred_batch_.clear(); this->deferred_batch_.clear();
this->flags_.batch_scheduled = false; this->flags_.batch_scheduled = false;

View File

@@ -147,6 +147,8 @@ enum WaterHeaterCommandHasField : uint32_t {
WATER_HEATER_COMMAND_HAS_STATE = 4, WATER_HEATER_COMMAND_HAS_STATE = 4,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8, WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16, WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
WATER_HEATER_COMMAND_HAS_ON_STATE = 32,
WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64,
}; };
#ifdef USE_NUMBER #ifdef USE_NUMBER
enum NumberMode : uint32_t { enum NumberMode : uint32_t {
@@ -440,19 +442,6 @@ class PingResponse final : public ProtoMessage {
protected: protected:
}; };
class DeviceInfoRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 9;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "device_info_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#ifdef USE_AREAS #ifdef USE_AREAS
class AreaInfo final : public ProtoMessage { class AreaInfo final : public ProtoMessage {
public: public:
@@ -546,19 +535,6 @@ class DeviceInfoResponse final : public ProtoMessage {
protected: protected:
}; };
class ListEntitiesRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 11;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class ListEntitiesDoneResponse final : public ProtoMessage { class ListEntitiesDoneResponse final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 19; static constexpr uint8_t MESSAGE_TYPE = 19;
@@ -572,19 +548,6 @@ class ListEntitiesDoneResponse final : public ProtoMessage {
protected: protected:
}; };
class SubscribeStatesRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 20;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_states_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
public: public:
@@ -1037,19 +1000,6 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
}; };
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
class SubscribeHomeassistantServicesRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 34;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_homeassistant_services_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class HomeassistantServiceMap final : public ProtoMessage { class HomeassistantServiceMap final : public ProtoMessage {
public: public:
StringRef key{}; StringRef key{};
@@ -1117,19 +1067,6 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
}; };
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 38;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_home_assistant_states_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class SubscribeHomeAssistantStateResponse final : public ProtoMessage { class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 39; static constexpr uint8_t MESSAGE_TYPE = 39;
@@ -2160,19 +2097,6 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage {
protected: protected:
}; };
class SubscribeBluetoothConnectionsFreeRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 80;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_bluetooth_connections_free_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class BluetoothConnectionsFreeResponse final : public ProtoMessage { class BluetoothConnectionsFreeResponse final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 81; static constexpr uint8_t MESSAGE_TYPE = 81;
@@ -2279,19 +2203,6 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage {
protected: protected:
}; };
class UnsubscribeBluetoothLEAdvertisementsRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 87;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "unsubscribe_bluetooth_le_advertisements_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class BluetoothDeviceClearCacheResponse final : public ProtoMessage { class BluetoothDeviceClearCacheResponse final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 88; static constexpr uint8_t MESSAGE_TYPE = 88;

View File

@@ -385,6 +385,10 @@ const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::Water
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"; return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH: case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"; return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH";
case enums::WATER_HEATER_COMMAND_HAS_ON_STATE:
return "WATER_HEATER_COMMAND_HAS_ON_STATE";
case enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE:
return "WATER_HEATER_COMMAND_HAS_AWAY_STATE";
default: default:
return "UNKNOWN"; return "UNKNOWN";
} }
@@ -764,10 +768,6 @@ const char *PingResponse::dump_to(DumpBuffer &out) const {
out.append("PingResponse {}"); out.append("PingResponse {}");
return out.c_str(); return out.c_str();
} }
const char *DeviceInfoRequest::dump_to(DumpBuffer &out) const {
out.append("DeviceInfoRequest {}");
return out.c_str();
}
#ifdef USE_AREAS #ifdef USE_AREAS
const char *AreaInfo::dump_to(DumpBuffer &out) const { const char *AreaInfo::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "AreaInfo"); MessageDumpHelper helper(out, "AreaInfo");
@@ -848,18 +848,10 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
#endif #endif
return out.c_str(); return out.c_str();
} }
const char *ListEntitiesRequest::dump_to(DumpBuffer &out) const {
out.append("ListEntitiesRequest {}");
return out.c_str();
}
const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const { const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const {
out.append("ListEntitiesDoneResponse {}"); out.append("ListEntitiesDoneResponse {}");
return out.c_str(); return out.c_str();
} }
const char *SubscribeStatesRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeStatesRequest {}");
return out.c_str();
}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const { const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse"); MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse");
@@ -1191,10 +1183,6 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
const char *SubscribeHomeassistantServicesRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeHomeassistantServicesRequest {}");
return out.c_str();
}
const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const { const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "HomeassistantServiceMap"); MessageDumpHelper helper(out, "HomeassistantServiceMap");
dump_field(out, "key", this->key); dump_field(out, "key", this->key);
@@ -1245,10 +1233,6 @@ const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const {
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
const char *SubscribeHomeAssistantStatesRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeHomeAssistantStatesRequest {}");
return out.c_str();
}
const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const { const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse"); MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse");
dump_field(out, "entity_id", this->entity_id); dump_field(out, "entity_id", this->entity_id);
@@ -1924,10 +1908,6 @@ const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const {
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
return out.c_str(); return out.c_str();
} }
const char *SubscribeBluetoothConnectionsFreeRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeBluetoothConnectionsFreeRequest {}");
return out.c_str();
}
const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const { const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse"); MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse");
dump_field(out, "free", this->free); dump_field(out, "free", this->free);
@@ -1970,10 +1950,6 @@ const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const {
dump_field(out, "error", this->error); dump_field(out, "error", this->error);
return out.c_str(); return out.c_str();
} }
const char *UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(DumpBuffer &out) const {
out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}");
return out.c_str();
}
const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const { const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse"); MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse");
dump_field(out, "address", this->address); dump_field(out, "address", this->address);

View File

@@ -21,6 +21,23 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
#endif #endif
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required
case DisconnectRequest::MESSAGE_TYPE: // No setup required
case PingRequest::MESSAGE_TYPE: // No setup required
break;
case 9 /* DeviceInfoRequest is empty */: // Connection setup only
if (!this->check_connection_setup_()) {
return;
}
break;
default:
if (!this->check_authenticated_()) {
return;
}
break;
}
switch (msg_type) { switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: { case HelloRequest::MESSAGE_TYPE: {
HelloRequest msg; HelloRequest msg;
@@ -59,21 +76,21 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_ping_response(); this->on_ping_response();
break; break;
} }
case DeviceInfoRequest::MESSAGE_TYPE: { case 9 /* DeviceInfoRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_device_info_request")); this->log_receive_message_(LOG_STR("on_device_info_request"));
#endif #endif
this->on_device_info_request(); this->on_device_info_request();
break; break;
} }
case ListEntitiesRequest::MESSAGE_TYPE: { case 11 /* ListEntitiesRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_list_entities_request")); this->log_receive_message_(LOG_STR("on_list_entities_request"));
#endif #endif
this->on_list_entities_request(); this->on_list_entities_request();
break; break;
} }
case SubscribeStatesRequest::MESSAGE_TYPE: { case 20 /* SubscribeStatesRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_states_request")); this->log_receive_message_(LOG_STR("on_subscribe_states_request"));
#endif #endif
@@ -134,7 +151,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: { case 34 /* SubscribeHomeassistantServicesRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request")); this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"));
#endif #endif
@@ -152,7 +169,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break; break;
} }
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: { case 38 /* SubscribeHomeAssistantStatesRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request")); this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"));
#endif #endif
@@ -359,7 +376,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: { case 80 /* SubscribeBluetoothConnectionsFreeRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request")); this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"));
#endif #endif
@@ -368,7 +385,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { case 87 /* UnsubscribeBluetoothLEAdvertisementsRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request")); this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"));
#endif #endif
@@ -623,28 +640,4 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
} }
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements for messages
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required
case DisconnectRequest::MESSAGE_TYPE: // No setup required
case PingRequest::MESSAGE_TYPE: // No setup required
break; // Skip all checks for these messages
case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only
if (!this->check_connection_setup_()) {
return; // Connection not setup
}
break;
default:
// All other messages require authentication (which includes connection check)
if (!this->check_authenticated_()) {
return; // Authentication failed
}
break;
}
// Call base implementation to process the message
APIServerConnectionBase::read_message(msg_size, msg_type, msg_data);
}
} // namespace esphome::api } // namespace esphome::api

View File

@@ -228,9 +228,4 @@ class APIServerConnectionBase : public ProtoService {
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
}; };
class APIServerConnection : public APIServerConnectionBase {
protected:
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
};
} // namespace esphome::api } // namespace esphome::api

View File

@@ -25,7 +25,9 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
private: private:
// Helper to convert value to string - handles the case where value is already a string // Helper to convert value to string - handles the case where value is already a string
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); } template<typename T> static std::string value_to_string(T &&val) {
return to_string(std::forward<T>(val)); // NOLINT
}
// Overloads for string types - needed because std::to_string doesn't support them // Overloads for string types - needed because std::to_string doesn't support them
static std::string value_to_string(char *val) { static std::string value_to_string(char *val) {

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <algorithm>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include "abstract_aqi_calculator.h" #include "abstract_aqi_calculator.h"
@@ -14,7 +15,11 @@ class AQICalculator : public AbstractAQICalculator {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index)); float aqi = std::max(pm2_5_index, pm10_0_index);
if (aqi < 0.0f) {
aqi = 0.0f;
}
return static_cast<uint16_t>(std::lround(aqi));
} }
protected: protected:
@@ -22,13 +27,27 @@ class AQICalculator : public AbstractAQICalculator {
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f}, static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
{35.5f, 55.4f}, {55.5f, 125.4f}, // clang-format off
{125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}}; {0.0f, 9.1f},
{9.1f, 35.5f},
{35.5f, 55.5f},
{55.5f, 125.5f},
{125.5f, 225.5f},
{225.5f, std::numeric_limits<float>::max()}
// clang-format on
};
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f}, static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
{155.0f, 254.0f}, {255.0f, 354.0f}, // clang-format off
{355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}}; {0.0f, 55.0f},
{55.0f, 155.0f},
{155.0f, 255.0f},
{255.0f, 355.0f},
{355.0f, 425.0f},
{425.0f, std::numeric_limits<float>::max()}
// clang-format on
};
static float calculate_index(float value, const float array[NUM_LEVELS][2]) { static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
int grid_index = get_grid_index(value, array); int grid_index = get_grid_index(value, array);
@@ -45,7 +64,10 @@ class AQICalculator : public AbstractAQICalculator {
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
for (int i = 0; i < NUM_LEVELS; i++) { for (int i = 0; i < NUM_LEVELS; i++) {
if (value >= array[i][0] && value <= array[i][1]) { const bool in_range =
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
: (value < array[i][1])); // others exclusive on hi
if (in_range) {
return i; return i;
} }
} }

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <algorithm>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include "abstract_aqi_calculator.h" #include "abstract_aqi_calculator.h"
@@ -12,7 +13,11 @@ class CAQICalculator : public AbstractAQICalculator {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index)); float aqi = std::max(pm2_5_index, pm10_0_index);
if (aqi < 0.0f) {
aqi = 0.0f;
}
return static_cast<uint16_t>(std::lround(aqi));
} }
protected: protected:
@@ -21,10 +26,24 @@ class CAQICalculator : public AbstractAQICalculator {
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = { static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
{0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}}; // clang-format off
{0.0f, 15.1f},
{15.1f, 30.1f},
{30.1f, 55.1f},
{55.1f, 110.1f},
{110.1f, std::numeric_limits<float>::max()}
// clang-format on
};
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = { static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
{0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}}; // clang-format off
{0.0f, 25.1f},
{25.1f, 50.1f},
{50.1f, 90.1f},
{90.1f, 180.1f},
{180.1f, std::numeric_limits<float>::max()}
// clang-format on
};
static float calculate_index(float value, const float array[NUM_LEVELS][2]) { static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
int grid_index = get_grid_index(value, array); int grid_index = get_grid_index(value, array);
@@ -42,7 +61,10 @@ class CAQICalculator : public AbstractAQICalculator {
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
for (int i = 0; i < NUM_LEVELS; i++) { for (int i = 0; i < NUM_LEVELS; i++) {
if (value >= array[i][0] && value <= array[i][1]) { const bool in_range =
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
: (value < array[i][1])); // others exclusive on hi
if (in_range) {
return i; return i;
} }
} }

View File

@@ -5,6 +5,14 @@ namespace esphome::binary_sensor {
static const char *const TAG = "binary_sensor.automation"; static const char *const TAG = "binary_sensor.automation";
// MultiClickTrigger timeout IDs.
// MultiClickTrigger is its own Component instance, so the scheduler scopes
// IDs by component pointer — no risk of collisions between instances.
constexpr uint32_t MULTICLICK_TRIGGER_ID = 0;
constexpr uint32_t MULTICLICK_COOLDOWN_ID = 1;
constexpr uint32_t MULTICLICK_IS_VALID_ID = 2;
constexpr uint32_t MULTICLICK_IS_NOT_VALID_ID = 3;
void MultiClickTrigger::on_state_(bool state) { void MultiClickTrigger::on_state_(bool state) {
// Handle duplicate events // Handle duplicate events
if (state == this->last_state_) { if (state == this->last_state_) {
@@ -27,7 +35,7 @@ void MultiClickTrigger::on_state_(bool state) {
evt.min_length, evt.max_length); evt.min_length, evt.max_length);
this->at_index_ = 1; this->at_index_ = 1;
if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) { if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) {
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); }); this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
} else { } else {
this->schedule_is_valid_(evt.min_length); this->schedule_is_valid_(evt.min_length);
this->schedule_is_not_valid_(evt.max_length); this->schedule_is_not_valid_(evt.max_length);
@@ -57,13 +65,13 @@ void MultiClickTrigger::on_state_(bool state) {
this->schedule_is_not_valid_(evt.max_length); this->schedule_is_not_valid_(evt.max_length);
} else if (*this->at_index_ + 1 != this->timing_.size()) { } else if (*this->at_index_ + 1 != this->timing_.size()) {
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
this->cancel_timeout("is_not_valid"); this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->schedule_is_valid_(evt.min_length); this->schedule_is_valid_(evt.min_length);
} else { } else {
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
this->is_valid_ = false; this->is_valid_ = false;
this->cancel_timeout("is_not_valid"); this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); }); this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
} }
*this->at_index_ = *this->at_index_ + 1; *this->at_index_ = *this->at_index_ + 1;
@@ -71,14 +79,14 @@ void MultiClickTrigger::on_state_(bool state) {
void MultiClickTrigger::schedule_cooldown_() { void MultiClickTrigger::schedule_cooldown_() {
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_); ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
this->is_in_cooldown_ = true; this->is_in_cooldown_ = true;
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() { this->set_timeout(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() {
ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again."); ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again.");
this->is_in_cooldown_ = false; this->is_in_cooldown_ = false;
}); });
this->at_index_.reset(); this->at_index_.reset();
this->cancel_timeout("trigger"); this->cancel_timeout(MULTICLICK_TRIGGER_ID);
this->cancel_timeout("is_valid"); this->cancel_timeout(MULTICLICK_IS_VALID_ID);
this->cancel_timeout("is_not_valid"); this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
} }
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
if (min_length == 0) { if (min_length == 0) {
@@ -86,13 +94,13 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
return; return;
} }
this->is_valid_ = false; this->is_valid_ = false;
this->set_timeout("is_valid", min_length, [this]() { this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS"); ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = true; this->is_valid_ = true;
}); });
} }
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
this->set_timeout("is_not_valid", max_length, [this]() { this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS"); ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = false; this->is_valid_ = false;
this->schedule_cooldown_(); this->schedule_cooldown_();
@@ -106,9 +114,9 @@ void MultiClickTrigger::cancel() {
void MultiClickTrigger::trigger_() { void MultiClickTrigger::trigger_() {
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!"); ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
this->at_index_.reset(); this->at_index_.reset();
this->cancel_timeout("trigger"); this->cancel_timeout(MULTICLICK_TRIGGER_ID);
this->cancel_timeout("is_valid"); this->cancel_timeout(MULTICLICK_IS_VALID_ID);
this->cancel_timeout("is_not_valid"); this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->trigger(); this->trigger();
} }

View File

@@ -6,6 +6,14 @@ namespace esphome::binary_sensor {
static const char *const TAG = "sensor.filter"; static const char *const TAG = "sensor.filter";
// Timeout IDs for filter classes.
// Each filter is its own Component instance, so the scheduler scopes
// IDs by component pointer — no risk of collisions between instances.
constexpr uint32_t FILTER_TIMEOUT_ID = 0;
// AutorepeatFilter needs two distinct IDs (both timeouts on the same component)
constexpr uint32_t AUTOREPEAT_TIMING_ID = 0;
constexpr uint32_t AUTOREPEAT_ON_OFF_ID = 1;
void Filter::output(bool value) { void Filter::output(bool value) {
if (this->next_ == nullptr) { if (this->next_ == nullptr) {
this->parent_->send_state_internal(value); this->parent_->send_state_internal(value);
@@ -23,16 +31,16 @@ void Filter::input(bool value) {
} }
void TimeoutFilter::input(bool value) { void TimeoutFilter::input(bool value) {
this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); }); this->set_timeout(FILTER_TIMEOUT_ID, this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
// we do not de-dup here otherwise changes from invalid to valid state will not be output // we do not de-dup here otherwise changes from invalid to valid state will not be output
this->output(value); this->output(value);
} }
optional<bool> DelayedOnOffFilter::new_value(bool value) { optional<bool> DelayedOnOffFilter::new_value(bool value) {
if (value) { if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); this->set_timeout(FILTER_TIMEOUT_ID, this->on_delay_.value(), [this]() { this->output(true); });
} else { } else {
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); }); this->set_timeout(FILTER_TIMEOUT_ID, this->off_delay_.value(), [this]() { this->output(false); });
} }
return {}; return {};
} }
@@ -41,10 +49,10 @@ float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HA
optional<bool> DelayedOnFilter::new_value(bool value) { optional<bool> DelayedOnFilter::new_value(bool value) {
if (value) { if (value) {
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); }); this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(true); });
return {}; return {};
} else { } else {
this->cancel_timeout("ON"); this->cancel_timeout(FILTER_TIMEOUT_ID);
return false; return false;
} }
} }
@@ -53,10 +61,10 @@ float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDW
optional<bool> DelayedOffFilter::new_value(bool value) { optional<bool> DelayedOffFilter::new_value(bool value) {
if (!value) { if (!value) {
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); }); this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(false); });
return {}; return {};
} else { } else {
this->cancel_timeout("OFF"); this->cancel_timeout(FILTER_TIMEOUT_ID);
return true; return true;
} }
} }
@@ -76,8 +84,8 @@ optional<bool> AutorepeatFilter::new_value(bool value) {
this->next_timing_(); this->next_timing_();
return true; return true;
} else { } else {
this->cancel_timeout("TIMING"); this->cancel_timeout(AUTOREPEAT_TIMING_ID);
this->cancel_timeout("ON_OFF"); this->cancel_timeout(AUTOREPEAT_ON_OFF_ID);
this->active_timing_ = 0; this->active_timing_ = 0;
return false; return false;
} }
@@ -88,8 +96,10 @@ void AutorepeatFilter::next_timing_() {
// 1st time: starts waiting the first delay // 1st time: starts waiting the first delay
// 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on // 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on
// last time: no delay to start but have to bump the index to reflect the last // last time: no delay to start but have to bump the index to reflect the last
if (this->active_timing_ < this->timings_.size()) if (this->active_timing_ < this->timings_.size()) {
this->set_timeout("TIMING", this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); }); this->set_timeout(AUTOREPEAT_TIMING_ID, this->timings_[this->active_timing_].delay,
[this]() { this->next_timing_(); });
}
if (this->active_timing_ <= this->timings_.size()) { if (this->active_timing_ <= this->timings_.size()) {
this->active_timing_++; this->active_timing_++;
@@ -104,7 +114,8 @@ void AutorepeatFilter::next_timing_() {
void AutorepeatFilter::next_value_(bool val) { void AutorepeatFilter::next_value_(bool val) {
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
this->output(val); // This is at least the second one so not initial this->output(val); // This is at least the second one so not initial
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); this->set_timeout(AUTOREPEAT_ON_OFF_ID, val ? timing.time_on : timing.time_off,
[this, val]() { this->next_value_(!val); });
} }
float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -115,7 +126,7 @@ optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
optional<bool> SettleFilter::new_value(bool value) { optional<bool> SettleFilter::new_value(bool value) {
if (!this->steady_) { if (!this->steady_) {
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() { this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this, value]() {
this->steady_ = true; this->steady_ = true;
this->output(value); this->output(value);
}); });
@@ -123,7 +134,7 @@ optional<bool> SettleFilter::new_value(bool value) {
} else { } else {
this->steady_ = false; this->steady_ = false;
this->output(value); this->output(value);
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; });
return value; return value;
} }
} }

View File

@@ -46,16 +46,16 @@ static const uint32_t PKT_TIMEOUT_MS = 200;
void BL0942::loop() { void BL0942::loop() {
DataPacket buffer; DataPacket buffer;
int avail = this->available(); size_t avail = this->available();
if (!avail) { if (!avail) {
return; return;
} }
if (static_cast<size_t>(avail) < sizeof(buffer)) { if (avail < sizeof(buffer)) {
if (!this->rx_start_) { if (!this->rx_start_) {
this->rx_start_ = millis(); this->rx_start_ = millis();
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%d bytes)", avail); ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail);
this->read_array((uint8_t *) &buffer, avail); this->read_array((uint8_t *) &buffer, avail);
this->rx_start_ = 0; this->rx_start_ = 0;
} }

View File

@@ -16,8 +16,8 @@ void CSE7766Component::loop() {
} }
// Early return prevents updating last_transmission_ when no data is available. // Early return prevents updating last_transmission_ when no data is available.
int avail = this->available(); size_t avail = this->available();
if (avail <= 0) { if (avail == 0) {
return; return;
} }
@@ -27,7 +27,7 @@ void CSE7766Component::loop() {
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call. // At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
uint8_t buf[CSE7766_RAW_DATA_SIZE]; uint8_t buf[CSE7766_RAW_DATA_SIZE];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -133,10 +133,10 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
void DFPlayer::loop() { void DFPlayer::loop() {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
int avail = this->available(); size_t avail = this->available();
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -28,7 +28,7 @@ void DlmsMeterComponent::dump_config() {
void DlmsMeterComponent::loop() { void DlmsMeterComponent::loop() {
// Read while data is available, netznoe uses two frames so allow 2x max frame length // Read while data is available, netznoe uses two frames so allow 2x max frame length
int avail = this->available(); size_t avail = this->available();
if (avail > 0) { if (avail > 0) {
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size(); size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
if (remaining == 0) { if (remaining == 0) {
@@ -36,12 +36,12 @@ void DlmsMeterComponent::loop() {
} else { } else {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
// Cap reads to remaining buffer capacity. // Cap reads to remaining buffer capacity.
if (static_cast<size_t>(avail) > remaining) { if (avail > remaining) {
avail = remaining; avail = remaining;
} }
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -120,9 +120,9 @@ void Dsmr::stop_requesting_data_() {
void Dsmr::drain_rx_buffer_() { void Dsmr::drain_rx_buffer_() {
uint8_t buf[64]; uint8_t buf[64];
int avail; size_t avail;
while ((avail = this->available()) > 0) { while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) { if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
break; break;
} }
} }
@@ -134,16 +134,15 @@ void Dsmr::reset_telegram_() {
this->bytes_read_ = 0; this->bytes_read_ = 0;
this->crypt_bytes_read_ = 0; this->crypt_bytes_read_ = 0;
this->crypt_telegram_len_ = 0; this->crypt_telegram_len_ = 0;
this->last_read_time_ = 0;
} }
void Dsmr::receive_telegram_() { void Dsmr::receive_telegram_() {
while (this->available_within_timeout_()) { while (this->available_within_timeout_()) {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
uint8_t buf[64]; uint8_t buf[64];
int avail = this->available(); size_t avail = this->available();
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) if (!this->read_array(buf, to_read))
return; return;
avail -= to_read; avail -= to_read;
@@ -207,9 +206,9 @@ void Dsmr::receive_encrypted_telegram_() {
while (this->available_within_timeout_()) { while (this->available_within_timeout_()) {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
uint8_t buf[64]; uint8_t buf[64];
int avail = this->available(); size_t avail = this->available();
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) if (!this->read_array(buf, to_read))
return; return;
avail -= to_read; avail -= to_read;

View File

@@ -1435,6 +1435,10 @@ async def to_code(config):
CORE.relative_internal_path(".espressif") CORE.relative_internal_path(".espressif")
) )
# Set the uv cache inside the data dir so "Clean All" clears it.
# Avoids persistent corrupted cache from mid-stream download failures.
os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache"))
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")

View File

@@ -48,7 +48,7 @@ class ESPBTUUID {
// Remove before 2026.8.0 // Remove before 2026.8.0
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0") ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
std::string to_string() const; std::string to_string() const; // NOLINT
const char *to_str(std::span<char, UUID_STR_LEN> output) const; const char *to_str(std::span<char, UUID_STR_LEN> output) const;
protected: protected:

View File

@@ -1,20 +1,16 @@
#include "hlk_fm22x.h" #include "hlk_fm22x.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include <array>
#include <cinttypes> #include <cinttypes>
namespace esphome::hlk_fm22x { namespace esphome::hlk_fm22x {
static const char *const TAG = "hlk_fm22x"; static const char *const TAG = "hlk_fm22x";
// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name)
static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36;
void HlkFm22xComponent::setup() { void HlkFm22xComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
this->set_enrolling_(false); this->set_enrolling_(false);
while (this->available()) { while (this->available() > 0) {
this->read(); this->read();
} }
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); }); this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
@@ -35,7 +31,7 @@ void HlkFm22xComponent::update() {
} }
void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) { void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) {
if (name.length() > 31) { if (name.length() > HLK_FM22X_NAME_SIZE - 1) {
ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str()); ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
return; return;
} }
@@ -88,7 +84,7 @@ void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *da
} }
this->wait_cycles_ = 0; this->wait_cycles_ = 0;
this->active_command_ = command; this->active_command_ = command;
while (this->available()) while (this->available() > 0)
this->read(); this->read();
this->write((uint8_t) (START_CODE >> 8)); this->write((uint8_t) (START_CODE >> 8));
this->write((uint8_t) (START_CODE & 0xFF)); this->write((uint8_t) (START_CODE & 0xFF));
@@ -137,17 +133,24 @@ void HlkFm22xComponent::recv_command_() {
checksum ^= byte; checksum ^= byte;
length |= byte; length |= byte;
std::vector<uint8_t> data; if (length > HLK_FM22X_MAX_RESPONSE_SIZE) {
data.reserve(length); ESP_LOGE(TAG, "Response too large: %u bytes", length);
// Discard exactly the remaining payload and checksum for this frame
for (uint16_t i = 0; i < length + 1 && this->available() > 0; ++i)
this->read();
return;
}
for (uint16_t idx = 0; idx < length; ++idx) { for (uint16_t idx = 0; idx < length; ++idx) {
byte = this->read(); byte = this->read();
checksum ^= byte; checksum ^= byte;
data.push_back(byte); this->recv_buf_[idx] = byte;
} }
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)]; char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)];
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size())); ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type,
format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length));
#endif #endif
byte = this->read(); byte = this->read();
@@ -157,10 +160,10 @@ void HlkFm22xComponent::recv_command_() {
} }
switch (response_type) { switch (response_type) {
case HlkFm22xResponseType::NOTE: case HlkFm22xResponseType::NOTE:
this->handle_note_(data); this->handle_note_(this->recv_buf_.data(), length);
break; break;
case HlkFm22xResponseType::REPLY: case HlkFm22xResponseType::REPLY:
this->handle_reply_(data); this->handle_reply_(this->recv_buf_.data(), length);
break; break;
default: default:
ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type); ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
@@ -168,11 +171,15 @@ void HlkFm22xComponent::recv_command_() {
} }
} }
void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) { void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) {
if (length < 1) {
ESP_LOGE(TAG, "Empty note data");
return;
}
switch (data[0]) { switch (data[0]) {
case HlkFm22xNoteType::FACE_STATE: case HlkFm22xNoteType::FACE_STATE:
if (data.size() < 17) { if (length < 17) {
ESP_LOGE(TAG, "Invalid face note data size: %u", data.size()); ESP_LOGE(TAG, "Invalid face note data size: %zu", length);
break; break;
} }
{ {
@@ -209,9 +216,13 @@ void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
} }
} }
void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) { void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
auto expected = this->active_command_; auto expected = this->active_command_;
this->active_command_ = HlkFm22xCommand::NONE; this->active_command_ = HlkFm22xCommand::NONE;
if (length < 2) {
ESP_LOGE(TAG, "Reply too short: %zu bytes", length);
return;
}
if (data[0] != (uint8_t) expected) { if (data[0] != (uint8_t) expected) {
ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]); ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
return; return;
@@ -238,16 +249,20 @@ void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
} }
switch (expected) { switch (expected) {
case HlkFm22xCommand::VERIFY: { case HlkFm22xCommand::VERIFY: {
if (length < 4 + HLK_FM22X_NAME_SIZE) {
ESP_LOGE(TAG, "VERIFY response too short: %zu bytes", length);
break;
}
int16_t face_id = ((int16_t) data[2] << 8) | data[3]; int16_t face_id = ((int16_t) data[2] << 8) | data[3];
std::string name(data.begin() + 4, data.begin() + 36); const char *name_ptr = reinterpret_cast<const char *>(data + 4);
ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str()); ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr);
if (this->last_face_id_sensor_ != nullptr) { if (this->last_face_id_sensor_ != nullptr) {
this->last_face_id_sensor_->publish_state(face_id); this->last_face_id_sensor_->publish_state(face_id);
} }
if (this->last_face_name_text_sensor_ != nullptr) { if (this->last_face_name_text_sensor_ != nullptr) {
this->last_face_name_text_sensor_->publish_state(name); this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE);
} }
this->face_scan_matched_callback_.call(face_id, name); this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE));
break; break;
} }
case HlkFm22xCommand::ENROLL: { case HlkFm22xCommand::ENROLL: {
@@ -266,9 +281,8 @@ void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); }); this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
break; break;
case HlkFm22xCommand::GET_VERSION: case HlkFm22xCommand::GET_VERSION:
if (this->version_text_sensor_ != nullptr) { if (this->version_text_sensor_ != nullptr && length > 2) {
std::string version(data.begin() + 2, data.end()); this->version_text_sensor_->publish_state(reinterpret_cast<const char *>(data + 2), length - 2);
this->version_text_sensor_->publish_state(version);
} }
this->defer([this]() { this->get_face_count_(); }); this->defer([this]() { this->get_face_count_(); });
break; break;

View File

@@ -7,12 +7,15 @@
#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/uart/uart.h" #include "esphome/components/uart/uart.h"
#include <array>
#include <utility> #include <utility>
#include <vector>
namespace esphome::hlk_fm22x { namespace esphome::hlk_fm22x {
static const uint16_t START_CODE = 0xEFAA; static const uint16_t START_CODE = 0xEFAA;
static constexpr size_t HLK_FM22X_NAME_SIZE = 32;
// Maximum response payload: command(1) + result(1) + face_id(2) + name(32) = 36
static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36;
enum HlkFm22xCommand { enum HlkFm22xCommand {
NONE = 0x00, NONE = 0x00,
RESET = 0x10, RESET = 0x10,
@@ -118,10 +121,11 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice {
void get_face_count_(); void get_face_count_();
void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0); void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0);
void recv_command_(); void recv_command_();
void handle_note_(const std::vector<uint8_t> &data); void handle_note_(const uint8_t *data, size_t length);
void handle_reply_(const std::vector<uint8_t> &data); void handle_reply_(const uint8_t *data, size_t length);
void set_enrolling_(bool enrolling); void set_enrolling_(bool enrolling);
std::array<uint8_t, HLK_FM22X_MAX_RESPONSE_SIZE> recv_buf_;
HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE; HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE;
uint16_t wait_cycles_ = 0; uint16_t wait_cycles_ = 0;
sensor::Sensor *face_count_sensor_{nullptr}; sensor::Sensor *face_count_sensor_{nullptr};

View File

@@ -134,25 +134,23 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
for (size_t j = 0; j != read_count; j++) for (size_t j = 0; j != read_count; j++)
read_buffer[j] = wire_->read(); read_buffer[j] = wire_->read();
} }
switch (status) { // Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266
case 0: if (status == 0)
return ERROR_OK; return ERROR_OK;
case 1: if (status == 1) {
// transmit buffer not large enough ESP_LOGVV(TAG, "TX failed: buffer not large enough");
ESP_LOGVV(TAG, "TX failed: buffer not large enough"); return ERROR_UNKNOWN;
return ERROR_UNKNOWN;
case 2:
case 3:
ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status);
return ERROR_NOT_ACKNOWLEDGED;
case 5:
ESP_LOGVV(TAG, "TX failed: timeout");
return ERROR_UNKNOWN;
case 4:
default:
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
return ERROR_UNKNOWN;
} }
if (status == 2 || status == 3) {
ESP_LOGVV(TAG, "TX failed: not acknowledged: %u", status);
return ERROR_NOT_ACKNOWLEDGED;
}
if (status == 5) {
ESP_LOGVV(TAG, "TX failed: timeout");
return ERROR_UNKNOWN;
}
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
return ERROR_UNKNOWN;
} }
/// Perform I2C bus recovery, see: /// Perform I2C bus recovery, see:

View File

@@ -276,10 +276,10 @@ void LD2410Component::restart_and_read_all_info() {
void LD2410Component::loop() { void LD2410Component::loop() {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
int avail = this->available(); size_t avail = this->available();
uint8_t buf[MAX_LINE_LENGTH]; uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -311,10 +311,10 @@ void LD2412Component::restart_and_read_all_info() {
void LD2412Component::loop() { void LD2412Component::loop() {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
int avail = this->available(); size_t avail = this->available();
uint8_t buf[MAX_LINE_LENGTH]; uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -542,10 +542,10 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) { void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
int avail = this->available(); size_t avail = this->available();
uint8_t buf[MAX_LINE_LENGTH]; uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -1,7 +1,8 @@
from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import uart from esphome.components import uart
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_THROTTLE from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID
AUTO_LOAD = ["ld24xx"] AUTO_LOAD = ["ld24xx"]
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
@@ -11,6 +12,8 @@ MULTI_CONF = True
ld2450_ns = cg.esphome_ns.namespace("ld2450") ld2450_ns = cg.esphome_ns.namespace("ld2450")
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice) LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template())
CONF_LD2450_ID = "ld2450_id" CONF_LD2450_ID = "ld2450_id"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
@@ -20,6 +23,11 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_THROTTLE): cv.invalid( cv.Optional(CONF_THROTTLE): cv.invalid(
f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead"
), ),
cv.Optional(CONF_ON_DATA): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LD2450DataTrigger),
}
),
} }
) )
.extend(uart.UART_DEVICE_SCHEMA) .extend(uart.UART_DEVICE_SCHEMA)
@@ -45,3 +53,6 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await uart.register_uart_device(var, config) await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_DATA, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

View File

@@ -277,10 +277,10 @@ void LD2450Component::dump_config() {
void LD2450Component::loop() { void LD2450Component::loop() {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
int avail = this->available(); size_t avail = this->available();
uint8_t buf[MAX_LINE_LENGTH]; uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }
@@ -413,6 +413,10 @@ void LD2450Component::restart_and_read_all_info() {
this->set_timeout(1500, [this]() { this->read_all_info(); }); this->set_timeout(1500, [this]() { this->read_all_info(); });
} }
void LD2450Component::add_on_data_callback(std::function<void()> &&callback) {
this->data_callback_.add(std::move(callback));
}
// Send command with values to LD2450 // Send command with values to LD2450
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
ESP_LOGV(TAG, "Sending COMMAND %02X", command); ESP_LOGV(TAG, "Sending COMMAND %02X", command);
@@ -613,6 +617,8 @@ void LD2450Component::handle_periodic_data_() {
this->still_presence_millis_ = App.get_loop_component_start_time(); this->still_presence_millis_ = App.get_loop_component_start_time();
} }
#endif #endif
this->data_callback_.call();
} }
bool LD2450Component::handle_ack_data_() { bool LD2450Component::handle_ack_data_() {

View File

@@ -141,6 +141,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1, int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1,
int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2); int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2);
/// Add a callback that will be called after each successfully processed periodic data frame.
void add_on_data_callback(std::function<void()> &&callback);
protected: protected:
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
void set_config_mode_(bool enable); void set_config_mode_(bool enable);
@@ -190,6 +193,15 @@ class LD2450Component : public Component, public uart::UARTDevice {
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{}; std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{};
#endif #endif
LazyCallbackManager<void()> data_callback_;
};
class LD2450DataTrigger : public Trigger<> {
public:
explicit LD2450DataTrigger(LD2450Component *parent) {
parent->add_on_data_callback([this]() { this->trigger(); });
}
}; };
} // namespace esphome::ld2450 } // namespace esphome::ld2450

View File

@@ -36,8 +36,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
#endif #endif
// Fast path: main thread, no recursion (99.9% of all logs) // Fast path: main thread, no recursion (99.9% of all logs)
// Pass nullptr for thread_name since we already know this is the main task
if (is_main_task && !this->main_task_recursion_guard_) [[likely]] { if (is_main_task && !this->main_task_recursion_guard_) [[likely]] {
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args); this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
return; return;
} }
@@ -47,21 +48,23 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
} }
// Non-main thread handling (~0.1% of logs) // Non-main thread handling (~0.1% of logs)
// Resolve thread name once and pass it through the logging chain.
// ESP32/LibreTiny: use TaskHandle_t overload to avoid redundant xTaskGetCurrentTaskHandle()
// (we already have the handle from the main task check above).
// Host: pass a stack buffer for pthread_getname_np to write into.
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
this->log_vprintf_non_main_thread_(level, tag, line, format, args, current_task); const char *thread_name = get_thread_name_(current_task);
#else // USE_HOST #else // USE_HOST
this->log_vprintf_non_main_thread_(level, tag, line, format, args); char thread_name_buf[THREAD_NAME_BUF_SIZE];
const char *thread_name = this->get_thread_name_(thread_name_buf);
#endif #endif
this->log_vprintf_non_main_thread_(level, tag, line, format, args, thread_name);
} }
// Handles non-main thread logging only // Handles non-main thread logging only
// Kept separate from hot path to improve instruction cache performance // Kept separate from hot path to improve instruction cache performance
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
TaskHandle_t current_task) { const char *thread_name) {
#else // USE_HOST
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args) {
#endif
// Check if already in recursion for this non-main thread/task // Check if already in recursion for this non-main thread/task
if (this->is_non_main_task_recursive_()) { if (this->is_non_main_task_recursive_()) {
return; return;
@@ -73,12 +76,8 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
bool message_sent = false; bool message_sent = false;
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
// For non-main threads/tasks, queue the message for callbacks // For non-main threads/tasks, queue the message for callbacks
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
message_sent = message_sent =
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args); this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), thread_name, format, args);
#else // USE_HOST
message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), format, args);
#endif
if (message_sent) { if (message_sent) {
// Enable logger loop to process the buffered message // Enable logger loop to process the buffered message
// This is safe to call from any context including ISRs // This is safe to call from any context including ISRs
@@ -101,19 +100,27 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
#endif #endif
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE}; LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE};
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
this->write_to_console_(buf); this->write_to_console_(buf);
} }
// RAII guard automatically resets on return // RAII guard automatically resets on return
} }
#else #else
// Implementation for all other platforms (single-task, no threading) // Implementation for single-task platforms (ESP8266, RP2040, Zephyr)
// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path.
// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking.
// Not a problem in practice yet since Zephyr has no API support (logs are console-only).
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
if (level > this->level_for(tag) || global_recursion_guard_) if (level > this->level_for(tag) || global_recursion_guard_)
return; return;
#ifdef USE_ZEPHYR
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); char tmp[MAX_POINTER_REPRESENTATION];
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args,
this->get_thread_name_(tmp));
#else // Other single-task platforms don't have thread names, so pass nullptr
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
#endif
} }
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY #endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
@@ -129,7 +136,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
if (level > this->level_for(tag) || global_recursion_guard_) if (level > this->level_for(tag) || global_recursion_guard_)
return; return;
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
} }
#endif // USE_STORE_LOG_STR_IN_FLASH #endif // USE_STORE_LOG_STR_IN_FLASH

View File

@@ -2,6 +2,7 @@
#include <cstdarg> #include <cstdarg>
#include <map> #include <map>
#include <span>
#include <type_traits> #include <type_traits>
#if defined(USE_ESP32) || defined(USE_HOST) #if defined(USE_ESP32) || defined(USE_HOST)
#include <pthread.h> #include <pthread.h>
@@ -124,6 +125,10 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
// "0x" + 2 hex digits per byte + '\0' // "0x" + 2 hex digits per byte + '\0'
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
// Stack buffer size for retrieving thread/task names from the OS
// macOS allows up to 64 bytes, Linux up to 16
static constexpr size_t THREAD_NAME_BUF_SIZE = 64;
// Buffer wrapper for log formatting functions // Buffer wrapper for log formatting functions
struct LogBuffer { struct LogBuffer {
char *data; char *data;
@@ -408,34 +413,24 @@ class Logger : public Component {
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
// Handles non-main thread logging only (~0.1% of calls) // Handles non-main thread logging only (~0.1% of calls)
#if defined(USE_ESP32) || defined(USE_LIBRETINY) // thread_name is resolved by the caller from the task handle, avoiding redundant lookups
// ESP32/LibreTiny: Pass task handle to avoid calling xTaskGetCurrentTaskHandle() twice
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
TaskHandle_t current_task); const char *thread_name);
#else // USE_HOST
// Host: No task handle parameter needed (not used in send_message_thread_safe)
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args);
#endif
#endif #endif
void process_messages_(); void process_messages_();
void write_msg_(const char *msg, uint16_t len); void write_msg_(const char *msg, uint16_t len);
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
// thread_name: name of the calling thread/task, or nullptr for main task (callers already know which task they're on)
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
va_list args, LogBuffer &buf) { va_list args, LogBuffer &buf, const char *thread_name) {
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST) buf.write_header(level, tag, line, thread_name);
buf.write_header(level, tag, line, this->get_thread_name_());
#elif defined(USE_ZEPHYR)
char tmp[MAX_POINTER_REPRESENTATION];
buf.write_header(level, tag, line, this->get_thread_name_(tmp));
#else
buf.write_header(level, tag, line, nullptr);
#endif
buf.format_body(format, args); buf.format_body(format, args);
} }
#ifdef USE_STORE_LOG_STR_IN_FLASH #ifdef USE_STORE_LOG_STR_IN_FLASH
// Format a log message with flash string format and write it to a buffer with header, footer, and null terminator // Format a log message with flash string format and write it to a buffer with header, footer, and null terminator
// ESP8266-only (single-task), thread_name is always nullptr
inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line, inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line,
const __FlashStringHelper *format, va_list args, const __FlashStringHelper *format, va_list args,
LogBuffer &buf) { LogBuffer &buf) {
@@ -466,9 +461,10 @@ class Logger : public Component {
// Helper to format and send a log message to both console and listeners // Helper to format and send a log message to both console and listeners
// Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings // Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings
// thread_name: name of the calling thread/task, or nullptr for main task
template<typename FormatType> template<typename FormatType>
inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line, inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line,
FormatType format, va_list args) { FormatType format, va_list args, const char *thread_name) {
RecursionGuard guard(recursion_guard); RecursionGuard guard(recursion_guard);
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
#ifdef USE_STORE_LOG_STR_IN_FLASH #ifdef USE_STORE_LOG_STR_IN_FLASH
@@ -477,7 +473,7 @@ class Logger : public Component {
} else } else
#endif #endif
{ {
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
} }
this->notify_listeners_(level, tag, buf); this->notify_listeners_(level, tag, buf);
this->write_log_buffer_to_console_(buf); this->write_log_buffer_to_console_(buf);
@@ -565,37 +561,57 @@ class Logger : public Component {
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
#endif #endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) // --- get_thread_name_ overloads (per-platform) ---
const char *HOT get_thread_name_(
#ifdef USE_ZEPHYR #if defined(USE_ESP32) || defined(USE_LIBRETINY)
char *buff // Primary overload - takes a task handle directly to avoid redundant xTaskGetCurrentTaskHandle() calls
// when the caller already has the handle (e.g. from the main task check in log_vprintf_)
const char *get_thread_name_(TaskHandle_t task) {
if (task == this->main_task_) {
return nullptr; // Main task
}
#if defined(USE_ESP32)
return pcTaskGetName(task);
#elif defined(USE_LIBRETINY)
return pcTaskGetTaskName(task);
#endif #endif
) { }
#ifdef USE_ZEPHYR
// Convenience overload - gets the current task handle and delegates
const char *HOT get_thread_name_() { return this->get_thread_name_(xTaskGetCurrentTaskHandle()); }
#elif defined(USE_HOST)
// Takes a caller-provided buffer for the thread name (stack-allocated for thread safety)
const char *HOT get_thread_name_(std::span<char> buff) {
pthread_t current_thread = pthread_self();
if (pthread_equal(current_thread, main_thread_)) {
return nullptr; // Main thread
}
// For non-main threads, get the thread name into the caller-provided buffer
if (pthread_getname_np(current_thread, buff.data(), buff.size()) == 0) {
return buff.data();
}
return nullptr;
}
#elif defined(USE_ZEPHYR)
const char *HOT get_thread_name_(std::span<char> buff) {
k_tid_t current_task = k_current_get(); k_tid_t current_task = k_current_get();
#else
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
#endif
if (current_task == main_task_) { if (current_task == main_task_) {
return nullptr; // Main task return nullptr; // Main task
} else {
#if defined(USE_ESP32)
return pcTaskGetName(current_task);
#elif defined(USE_LIBRETINY)
return pcTaskGetTaskName(current_task);
#elif defined(USE_ZEPHYR)
const char *name = k_thread_name_get(current_task);
if (name) {
// zephyr print task names only if debug component is present
return name;
}
std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task);
return buff;
#endif
} }
const char *name = k_thread_name_get(current_task);
if (name) {
// zephyr print task names only if debug component is present
return name;
}
std::snprintf(buff.data(), buff.size(), "%p", current_task);
return buff.data();
} }
#endif #endif
// --- Non-main task recursion guards (per-platform) ---
#if defined(USE_ESP32) || defined(USE_HOST) #if defined(USE_ESP32) || defined(USE_HOST)
// RAII guard for non-main task recursion using pthread TLS // RAII guard for non-main task recursion using pthread TLS
class NonMainTaskRecursionGuard { class NonMainTaskRecursionGuard {
@@ -635,22 +651,6 @@ class Logger : public Component {
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); } inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
#endif #endif
#ifdef USE_HOST
const char *HOT get_thread_name_() {
pthread_t current_thread = pthread_self();
if (pthread_equal(current_thread, main_thread_)) {
return nullptr; // Main thread
}
// For non-main threads, return the thread name
// We store it in thread-local storage to avoid allocation
static thread_local char thread_name_buf[32];
if (pthread_getname_np(current_thread, thread_name_buf, sizeof(thread_name_buf)) == 0) {
return thread_name_buf;
}
return nullptr;
}
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Disable loop when task buffer is empty (with USB CDC check on ESP32) // Disable loop when task buffer is empty (with USB CDC check on ESP32)
inline void disable_loop_when_buffer_empty_() { inline void disable_loop_when_buffer_empty_() {

View File

@@ -59,7 +59,7 @@ void TaskLogBuffer::release_message_main_loop(void *token) {
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed); last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
} }
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle, bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args) { const char *format, va_list args) {
// First, calculate the exact length needed using a null buffer (no actual writing) // First, calculate the exact length needed using a null buffer (no actual writing)
va_list args_copy; va_list args_copy;
@@ -95,7 +95,6 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
// Store the thread name now instead of waiting until main loop processing // Store the thread name now instead of waiting until main loop processing
// This avoids crashes if the task completes or is deleted between when this message // This avoids crashes if the task completes or is deleted between when this message
// is enqueued and when it's processed by the main loop // is enqueued and when it's processed by the main loop
const char *thread_name = pcTaskGetName(task_handle);
if (thread_name != nullptr) { if (thread_name != nullptr) {
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1); strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination

View File

@@ -58,7 +58,7 @@ class TaskLogBuffer {
void release_message_main_loop(void *token); void release_message_main_loop(void *token);
// Thread-safe - send a message to the ring buffer from any thread // Thread-safe - send a message to the ring buffer from any thread
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle, bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args); const char *format, va_list args);
// Check if there are messages ready to be processed using an atomic counter for performance // Check if there are messages ready to be processed using an atomic counter for performance

View File

@@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) {
} }
} }
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
va_list args) { const char *format, va_list args) {
// Acquire a slot // Acquire a slot
int slot_index = this->acquire_write_slot_(); int slot_index = this->acquire_write_slot_();
if (slot_index < 0) { if (slot_index < 0) {
@@ -85,11 +85,9 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
msg.tag = tag; msg.tag = tag;
msg.line = line; msg.line = line;
// Get thread name using pthread // Store the thread name now to avoid crashes if thread exits before processing
char thread_name_buf[LogMessage::MAX_THREAD_NAME_SIZE]; if (thread_name != nullptr) {
// pthread_getname_np works the same on Linux and macOS strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1);
if (pthread_getname_np(pthread_self(), thread_name_buf, sizeof(thread_name_buf)) == 0) {
strncpy(msg.thread_name, thread_name_buf, sizeof(msg.thread_name) - 1);
msg.thread_name[sizeof(msg.thread_name) - 1] = '\0'; msg.thread_name[sizeof(msg.thread_name) - 1] = '\0';
} else { } else {
msg.thread_name[0] = '\0'; msg.thread_name[0] = '\0';

View File

@@ -86,7 +86,8 @@ class TaskLogBufferHost {
// Thread-safe - send a message to the buffer from any thread // Thread-safe - send a message to the buffer from any thread
// Returns true if message was queued, false if buffer is full // Returns true if message was queued, false if buffer is full
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, va_list args); bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args);
// Check if there are messages ready to be processed // Check if there are messages ready to be processed
inline bool HOT has_messages() const { inline bool HOT has_messages() const {

View File

@@ -101,7 +101,7 @@ void TaskLogBufferLibreTiny::release_message_main_loop() {
} }
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
TaskHandle_t task_handle, const char *format, va_list args) { const char *thread_name, const char *format, va_list args) {
// First, calculate the exact length needed using a null buffer (no actual writing) // First, calculate the exact length needed using a null buffer (no actual writing)
va_list args_copy; va_list args_copy;
va_copy(args_copy, args); va_copy(args_copy, args);
@@ -162,7 +162,6 @@ bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char
msg->line = line; msg->line = line;
// Store the thread name now to avoid crashes if task is deleted before processing // Store the thread name now to avoid crashes if task is deleted before processing
const char *thread_name = pcTaskGetTaskName(task_handle);
if (thread_name != nullptr) { if (thread_name != nullptr) {
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1); strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; msg->thread_name[sizeof(msg->thread_name) - 1] = '\0';

View File

@@ -70,7 +70,7 @@ class TaskLogBufferLibreTiny {
void release_message_main_loop(); void release_message_main_loop();
// Thread-safe - send a message to the buffer from any thread // Thread-safe - send a message to the buffer from any thread
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle, bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args); const char *format, va_list args);
// Fast check using volatile counter - no lock needed // Fast check using volatile counter - no lock needed

View File

@@ -120,3 +120,101 @@ DriverChip(
(0xB2, 0x10), (0xB2, 0x10),
], ],
) )
DriverChip(
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C",
height=800,
width=800,
hsync_back_porch=20,
hsync_pulse_width=20,
hsync_front_porch=40,
vsync_back_porch=12,
vsync_pulse_width=4,
vsync_front_porch=24,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xE0, 0x00), # select userpage
(0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8),
(0x80, 0x01), # Select number of lanes (2)
(0xE0, 0x01), # select page 1
(0x00, 0x00), (0x01, 0x41), (0x03, 0x10), (0x04, 0x44), (0x17, 0x00), (0x18, 0xD0), (0x19, 0x00), (0x1A, 0x00),
(0x1B, 0xD0), (0x1C, 0x00), (0x24, 0xFE), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x08), (0x3A, 0x0A),
(0x3C, 0x78), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x00), (0x41, 0x64), (0x42, 0xC7), (0x43, 0x18),
(0x44, 0x0B), (0x45, 0x14), (0x55, 0x02), (0x57, 0x49), (0x59, 0x0A), (0x5A, 0x1B), (0x5B, 0x19), (0x5D, 0x7F),
(0x5E, 0x56), (0x5F, 0x43), (0x60, 0x37), (0x61, 0x33), (0x62, 0x25), (0x63, 0x2A), (0x64, 0x16), (0x65, 0x30),
(0x66, 0x2F), (0x67, 0x32), (0x68, 0x53), (0x69, 0x43), (0x6A, 0x4C), (0x6B, 0x40), (0x6C, 0x3D), (0x6D, 0x31),
(0x6E, 0x20), (0x6F, 0x0F), (0x70, 0x7F), (0x71, 0x56), (0x72, 0x43), (0x73, 0x37), (0x74, 0x33), (0x75, 0x25),
(0x76, 0x2A), (0x77, 0x16), (0x78, 0x30), (0x79, 0x2F), (0x7A, 0x32), (0x7B, 0x53), (0x7C, 0x43), (0x7D, 0x4C),
(0x7E, 0x40), (0x7F, 0x3D), (0x80, 0x31), (0x81, 0x20), (0x82, 0x0F),
(0xE0, 0x02), # select page 2
(0x00, 0x5F), (0x01, 0x5F), (0x02, 0x5E), (0x03, 0x5E), (0x04, 0x50), (0x05, 0x48), (0x06, 0x48), (0x07, 0x4A),
(0x08, 0x4A), (0x09, 0x44), (0x0A, 0x44), (0x0B, 0x46), (0x0C, 0x46), (0x0D, 0x5F), (0x0E, 0x5F), (0x0F, 0x57),
(0x10, 0x57), (0x11, 0x77), (0x12, 0x77), (0x13, 0x40), (0x14, 0x42), (0x15, 0x5F), (0x16, 0x5F), (0x17, 0x5F),
(0x18, 0x5E), (0x19, 0x5E), (0x1A, 0x50), (0x1B, 0x49), (0x1C, 0x49), (0x1D, 0x4B), (0x1E, 0x4B), (0x1F, 0x45),
(0x20, 0x45), (0x21, 0x47), (0x22, 0x47), (0x23, 0x5F), (0x24, 0x5F), (0x25, 0x57), (0x26, 0x57), (0x27, 0x77),
(0x28, 0x77), (0x29, 0x41), (0x2A, 0x43), (0x2B, 0x5F), (0x2C, 0x1E), (0x2D, 0x1E), (0x2E, 0x1F), (0x2F, 0x1F),
(0x30, 0x10), (0x31, 0x07), (0x32, 0x07), (0x33, 0x05), (0x34, 0x05), (0x35, 0x0B), (0x36, 0x0B), (0x37, 0x09),
(0x38, 0x09), (0x39, 0x1F), (0x3A, 0x1F), (0x3B, 0x17), (0x3C, 0x17), (0x3D, 0x17), (0x3E, 0x17), (0x3F, 0x03),
(0x40, 0x01), (0x41, 0x1F), (0x42, 0x1E), (0x43, 0x1E), (0x44, 0x1F), (0x45, 0x1F), (0x46, 0x10), (0x47, 0x06),
(0x48, 0x06), (0x49, 0x04), (0x4A, 0x04), (0x4B, 0x0A), (0x4C, 0x0A), (0x4D, 0x08), (0x4E, 0x08), (0x4F, 0x1F),
(0x50, 0x1F), (0x51, 0x17), (0x52, 0x17), (0x53, 0x17), (0x54, 0x17), (0x55, 0x02), (0x56, 0x00), (0x57, 0x1F),
(0xE0, 0x02), # select page 2
(0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x30), (0x5C, 0x01), (0x5D, 0x30), (0x5E, 0x01), (0x5F, 0x02),
(0x60, 0x30), (0x61, 0x03), (0x62, 0x04), (0x63, 0x04), (0x64, 0xA6), (0x65, 0x43), (0x66, 0x30), (0x67, 0x73),
(0x68, 0x05), (0x69, 0x04), (0x6A, 0x7F), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88),
(0x75, 0xD9), (0x76, 0x00), (0x77, 0x33), (0x78, 0x43),
(0xE0, 0x00), # select userpage
],
)
DriverChip(
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-4C",
height=720,
width=720,
hsync_back_porch=20,
hsync_pulse_width=20,
hsync_front_porch=40,
vsync_back_porch=12,
vsync_pulse_width=4,
vsync_front_porch=24,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xE0, 0x00), # select userpage
(0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8),
(0x80, 0x01), # Select number of lanes (2)
(0xE0, 0x01), # select page 1
(0x00, 0x00), (0x01, 0x41), (0x03, 0x10), (0x04, 0x44), (0x17, 0x00), (0x18, 0xD0), (0x19, 0x00), (0x1A, 0x00),
(0x1B, 0xD0), (0x1C, 0x00), (0x24, 0xFE), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x08), (0x3A, 0x0A),
(0x3C, 0x78), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x04), (0x41, 0x64), (0x42, 0xC7), (0x43, 0x18),
(0x44, 0x0B), (0x45, 0x14), (0x55, 0x02), (0x57, 0x49), (0x59, 0x0A), (0x5A, 0x1B), (0x5B, 0x19), (0x5D, 0x7F),
(0x5E, 0x56), (0x5F, 0x43), (0x60, 0x37), (0x61, 0x33), (0x62, 0x25), (0x63, 0x2A), (0x64, 0x16), (0x65, 0x30),
(0x66, 0x2F), (0x67, 0x32), (0x68, 0x53), (0x69, 0x43), (0x6A, 0x4C), (0x6B, 0x40), (0x6C, 0x3D), (0x6D, 0x31),
(0x6E, 0x20), (0x6F, 0x0F), (0x70, 0x7F), (0x71, 0x56), (0x72, 0x43), (0x73, 0x37), (0x74, 0x33), (0x75, 0x25),
(0x76, 0x2A), (0x77, 0x16), (0x78, 0x30), (0x79, 0x2F), (0x7A, 0x32), (0x7B, 0x53), (0x7C, 0x43), (0x7D, 0x4C),
(0x7E, 0x40), (0x7F, 0x3D), (0x80, 0x31), (0x81, 0x20), (0x82, 0x0F),
(0xE0, 0x02), # select page 2
(0x00, 0x5F), (0x01, 0x5F), (0x02, 0x5E), (0x03, 0x5E), (0x04, 0x50), (0x05, 0x48), (0x06, 0x48), (0x07, 0x4A),
(0x08, 0x4A), (0x09, 0x44), (0x0A, 0x44), (0x0B, 0x46), (0x0C, 0x46), (0x0D, 0x5F), (0x0E, 0x5F), (0x0F, 0x57),
(0x10, 0x57), (0x11, 0x77), (0x12, 0x77), (0x13, 0x40), (0x14, 0x42), (0x15, 0x5F), (0x16, 0x5F), (0x17, 0x5F),
(0x18, 0x5E), (0x19, 0x5E), (0x1A, 0x50), (0x1B, 0x49), (0x1C, 0x49), (0x1D, 0x4B), (0x1E, 0x4B), (0x1F, 0x45),
(0x20, 0x45), (0x21, 0x47), (0x22, 0x47), (0x23, 0x5F), (0x24, 0x5F), (0x25, 0x57), (0x26, 0x57), (0x27, 0x77),
(0x28, 0x77), (0x29, 0x41), (0x2A, 0x43), (0x2B, 0x5F), (0x2C, 0x1E), (0x2D, 0x1E), (0x2E, 0x1F), (0x2F, 0x1F),
(0x30, 0x10), (0x31, 0x07), (0x32, 0x07), (0x33, 0x05), (0x34, 0x05), (0x35, 0x0B), (0x36, 0x0B), (0x37, 0x09),
(0x38, 0x09), (0x39, 0x1F), (0x3A, 0x1F), (0x3B, 0x17), (0x3C, 0x17), (0x3D, 0x17), (0x3E, 0x17), (0x3F, 0x03),
(0x40, 0x01), (0x41, 0x1F), (0x42, 0x1E), (0x43, 0x1E), (0x44, 0x1F), (0x45, 0x1F), (0x46, 0x10), (0x47, 0x06),
(0x48, 0x06), (0x49, 0x04), (0x4A, 0x04), (0x4B, 0x0A), (0x4C, 0x0A), (0x4D, 0x08), (0x4E, 0x08), (0x4F, 0x1F),
(0x50, 0x1F), (0x51, 0x17), (0x52, 0x17), (0x53, 0x17), (0x54, 0x17), (0x55, 0x02), (0x56, 0x00), (0x57, 0x1F),
(0xE0, 0x02), # select page 2
(0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x30), (0x5C, 0x01), (0x5D, 0x30), (0x5E, 0x01), (0x5F, 0x02),
(0x60, 0x30), (0x61, 0x03), (0x62, 0x04), (0x63, 0x04), (0x64, 0xA6), (0x65, 0x43), (0x66, 0x30), (0x67, 0x73),
(0x68, 0x05), (0x69, 0x04), (0x6A, 0x7F), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88),
(0x75, 0xD9), (0x76, 0x00), (0x77, 0x33), (0x78, 0x43),
(0xE0, 0x00), # select userpage
]
)

View File

@@ -11,7 +11,7 @@ from esphome.components.const import (
CONF_DRAW_ROUNDING, CONF_DRAW_ROUNDING,
) )
from esphome.components.display import CONF_SHOW_TEST_CARD from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.esp32 import VARIANT_ESP32S3, only_on_variant from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S3, only_on_variant
from esphome.components.mipi import ( from esphome.components.mipi import (
COLOR_ORDERS, COLOR_ORDERS,
CONF_DE_PIN, CONF_DE_PIN,
@@ -225,7 +225,7 @@ def _config_schema(config):
return cv.All( return cv.All(
schema, schema,
cv.only_on_esp32, cv.only_on_esp32,
only_on_variant(supported=[VARIANT_ESP32S3]), only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
)(config) )(config)

View File

@@ -1,4 +1,4 @@
#ifdef USE_ESP32_VARIANT_ESP32S3 #if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
#include "mipi_rgb.h" #include "mipi_rgb.h"
#include "esphome/core/gpio.h" #include "esphome/core/gpio.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
@@ -401,4 +401,4 @@ void MipiRgb::dump_config() {
} // namespace mipi_rgb } // namespace mipi_rgb
} // namespace esphome } // namespace esphome
#endif // USE_ESP32_VARIANT_ESP32S3 #endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#ifdef USE_ESP32_VARIANT_ESP32S3 #if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
#include "esphome/core/gpio.h" #include "esphome/core/gpio.h"
#include "esphome/components/display/display.h" #include "esphome/components/display/display.h"
#include "esp_lcd_panel_ops.h" #include "esp_lcd_panel_ops.h"
@@ -28,7 +28,7 @@ class MipiRgb : public display::Display {
void setup() override; void setup() override;
void loop() override; void loop() override;
void update() override; void update() override;
void fill(Color color); void fill(Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
@@ -115,7 +115,7 @@ class MipiRgbSpi : public MipiRgb,
void write_command_(uint8_t value); void write_command_(uint8_t value);
void write_data_(uint8_t value); void write_data_(uint8_t value);
void write_init_sequence_(); void write_init_sequence_();
void dump_config(); void dump_config() override;
GPIOPin *dc_pin_{nullptr}; GPIOPin *dc_pin_{nullptr};
std::vector<uint8_t> init_sequence_; std::vector<uint8_t> init_sequence_;

View File

@@ -20,10 +20,10 @@ void Modbus::loop() {
const uint32_t now = App.get_loop_component_start_time(); const uint32_t now = App.get_loop_component_start_time();
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
int avail = this->available(); size_t avail = this->available();
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }
@@ -228,39 +228,50 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
return; return;
} }
std::vector<uint8_t> data; static constexpr size_t ADDR_SIZE = 1;
data.push_back(address); static constexpr size_t FC_SIZE = 1;
data.push_back(function_code); static constexpr size_t START_ADDR_SIZE = 2;
static constexpr size_t NUM_ENTITIES_SIZE = 2;
static constexpr size_t BYTE_COUNT_SIZE = 1;
static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits<uint8_t>::max();
static constexpr size_t CRC_SIZE = 2;
static constexpr size_t MAX_FRAME_SIZE =
ADDR_SIZE + FC_SIZE + START_ADDR_SIZE + NUM_ENTITIES_SIZE + BYTE_COUNT_SIZE + MAX_PAYLOAD_SIZE + CRC_SIZE;
uint8_t data[MAX_FRAME_SIZE];
size_t pos = 0;
data[pos++] = address;
data[pos++] = function_code;
if (this->role == ModbusRole::CLIENT) { if (this->role == ModbusRole::CLIENT) {
data.push_back(start_address >> 8); data[pos++] = start_address >> 8;
data.push_back(start_address >> 0); data[pos++] = start_address >> 0;
if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL && if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) { function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
data.push_back(number_of_entities >> 8); data[pos++] = number_of_entities >> 8;
data.push_back(number_of_entities >> 0); data[pos++] = number_of_entities >> 0;
} }
} }
if (payload != nullptr) { if (payload != nullptr) {
if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS || if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple
data.push_back(payload_len); // Byte count is required for write data[pos++] = payload_len; // Byte count is required for write
} else { } else {
payload_len = 2; // Write single register or coil payload_len = 2; // Write single register or coil
} }
for (int i = 0; i < payload_len; i++) { for (int i = 0; i < payload_len; i++) {
data.push_back(payload[i]); data[pos++] = payload[i];
} }
} }
auto crc = crc16(data.data(), data.size()); auto crc = crc16(data, pos);
data.push_back(crc >> 0); data[pos++] = crc >> 0;
data.push_back(crc >> 8); data[pos++] = crc >> 8;
if (this->flow_control_pin_ != nullptr) if (this->flow_control_pin_ != nullptr)
this->flow_control_pin_->digital_write(true); this->flow_control_pin_->digital_write(true);
this->write_array(data); this->write_array(data, pos);
this->flush(); this->flush();
if (this->flow_control_pin_ != nullptr) if (this->flow_control_pin_ != nullptr)
@@ -270,7 +281,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif #endif
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data.data(), data.size())); ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data, pos));
} }
// Helper function for lambdas // Helper function for lambdas

View File

@@ -398,10 +398,10 @@ bool Nextion::remove_from_q_(bool report_empty) {
void Nextion::process_serial_() { void Nextion::process_serial_() {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
int avail = this->available(); size_t avail = this->available();
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -14,9 +14,9 @@ void Pipsolar::setup() {
void Pipsolar::empty_uart_buffer_() { void Pipsolar::empty_uart_buffer_() {
uint8_t buf[64]; uint8_t buf[64];
int avail; size_t avail;
while ((avail = this->available()) > 0) { while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) { if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
break; break;
} }
} }
@@ -97,10 +97,10 @@ void Pipsolar::loop() {
} }
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) { if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
int avail = this->available(); size_t avail = this->available();
while (avail > 0) { while (avail > 0) {
uint8_t buf[64]; uint8_t buf[64];
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -56,14 +56,14 @@ void PylontechComponent::setup() {
void PylontechComponent::update() { this->write_str("pwr\n"); } void PylontechComponent::update() { this->write_str("pwr\n"); }
void PylontechComponent::loop() { void PylontechComponent::loop() {
int avail = this->available(); size_t avail = this->available();
if (avail > 0) { if (avail > 0) {
// pylontech sends a lot of data very suddenly // pylontech sends a lot of data very suddenly
// we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
int recv = 0; int recv = 0;
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -82,10 +82,10 @@ void RD03DComponent::dump_config() {
void RD03DComponent::loop() { void RD03DComponent::loop() {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
int avail = this->available(); size_t avail = this->available();
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -136,10 +136,10 @@ void RFBridgeComponent::loop() {
this->last_bridge_byte_ = now; this->last_bridge_byte_ = now;
} }
int avail = this->available(); size_t avail = this->available();
while (avail > 0) { while (avail > 0) {
uint8_t buf[64]; uint8_t buf[64];
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -107,10 +107,10 @@ void MR24HPC1Component::update_() {
// main loop // main loop
void MR24HPC1Component::loop() { void MR24HPC1Component::loop() {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
int avail = this->available(); size_t avail = this->available();
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -31,10 +31,10 @@ void MR60BHA2Component::dump_config() {
// main loop // main loop
void MR60BHA2Component::loop() { void MR60BHA2Component::loop() {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
int avail = this->available(); size_t avail = this->available();
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -50,10 +50,10 @@ void MR60FDA2Component::setup() {
// main loop // main loop
void MR60FDA2Component::loop() { void MR60FDA2Component::loop() {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
int avail = this->available(); size_t avail = this->available();
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -9,6 +9,11 @@ namespace esphome::sensor {
static const char *const TAG = "sensor.filter"; static const char *const TAG = "sensor.filter";
// Filter scheduler IDs.
// Each filter is its own Component instance, so the scheduler scopes
// IDs by component pointer — no risk of collisions between instances.
constexpr uint32_t FILTER_ID = 0;
// Filter // Filter
void Filter::input(float value) { void Filter::input(float value) {
ESP_LOGVV(TAG, "Filter(%p)::input(%f)", this, value); ESP_LOGVV(TAG, "Filter(%p)::input(%f)", this, value);
@@ -191,7 +196,7 @@ optional<float> ThrottleAverageFilter::new_value(float value) {
return {}; return {};
} }
void ThrottleAverageFilter::setup() { void ThrottleAverageFilter::setup() {
this->set_interval("throttle_average", this->time_period_, [this]() { this->set_interval(FILTER_ID, this->time_period_, [this]() {
ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::interval(sum=%f, n=%i)", this, this->sum_, this->n_); ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::interval(sum=%f, n=%i)", this, this->sum_, this->n_);
if (this->n_ == 0) { if (this->n_ == 0) {
if (this->have_nan_) if (this->have_nan_)
@@ -383,7 +388,7 @@ optional<float> TimeoutFilterConfigured::new_value(float value) {
// DebounceFilter // DebounceFilter
optional<float> DebounceFilter::new_value(float value) { optional<float> DebounceFilter::new_value(float value) {
this->set_timeout("debounce", this->time_period_, [this, value]() { this->output(value); }); this->set_timeout(FILTER_ID, this->time_period_, [this, value]() { this->output(value); });
return {}; return {};
} }
@@ -406,7 +411,7 @@ optional<float> HeartbeatFilter::new_value(float value) {
} }
void HeartbeatFilter::setup() { void HeartbeatFilter::setup() {
this->set_interval("heartbeat", this->time_period_, [this]() { this->set_interval(FILTER_ID, this->time_period_, [this]() {
ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_), ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_),
this->last_input_); this->last_input_);
if (!this->has_value_) if (!this->has_value_)

View File

@@ -3,6 +3,7 @@ import esphome.codegen as cg
from esphome.components import water_heater from esphome.components import water_heater
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AWAY,
CONF_ID, CONF_ID,
CONF_MODE, CONF_MODE,
CONF_OPTIMISTIC, CONF_OPTIMISTIC,
@@ -18,6 +19,7 @@ from esphome.types import ConfigType
from .. import template_ns from .. import template_ns
CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_CURRENT_TEMPERATURE = "current_temperature"
CONF_IS_ON = "is_on"
TemplateWaterHeater = template_ns.class_( TemplateWaterHeater = template_ns.class_(
"TemplateWaterHeater", cg.Component, water_heater.WaterHeater "TemplateWaterHeater", cg.Component, water_heater.WaterHeater
@@ -51,6 +53,8 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
water_heater.validate_water_heater_mode water_heater.validate_water_heater_mode
), ),
cv.Optional(CONF_AWAY): cv.returning_lambda,
cv.Optional(CONF_IS_ON): cv.returning_lambda,
} }
) )
.extend(cv.COMPONENT_SCHEMA) .extend(cv.COMPONENT_SCHEMA)
@@ -98,6 +102,22 @@ async def to_code(config: ConfigType) -> None:
if CONF_SUPPORTED_MODES in config: if CONF_SUPPORTED_MODES in config:
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
if CONF_AWAY in config:
template_ = await cg.process_lambda(
config[CONF_AWAY],
[],
return_type=cg.optional.template(bool),
)
cg.add(var.set_away_lambda(template_))
if CONF_IS_ON in config:
template_ = await cg.process_lambda(
config[CONF_IS_ON],
[],
return_type=cg.optional.template(bool),
)
cg.add(var.set_is_on_lambda(template_))
@automation.register_action( @automation.register_action(
"water_heater.template.publish", "water_heater.template.publish",
@@ -110,6 +130,8 @@ async def to_code(config: ConfigType) -> None:
cv.Optional(CONF_MODE): cv.templatable( cv.Optional(CONF_MODE): cv.templatable(
water_heater.validate_water_heater_mode water_heater.validate_water_heater_mode
), ),
cv.Optional(CONF_AWAY): cv.templatable(cv.boolean),
cv.Optional(CONF_IS_ON): cv.templatable(cv.boolean),
} }
), ),
) )
@@ -134,4 +156,12 @@ async def water_heater_template_publish_to_code(
template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode) template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode)
cg.add(var.set_mode(template_)) cg.add(var.set_mode(template_))
if CONF_AWAY in config:
template_ = await cg.templatable(config[CONF_AWAY], args, bool)
cg.add(var.set_away(template_))
if CONF_IS_ON in config:
template_ = await cg.templatable(config[CONF_IS_ON], args, bool)
cg.add(var.set_is_on(template_))
return var return var

View File

@@ -11,12 +11,15 @@ class TemplateWaterHeaterPublishAction : public Action<Ts...>, public Parented<T
TEMPLATABLE_VALUE(float, current_temperature) TEMPLATABLE_VALUE(float, current_temperature)
TEMPLATABLE_VALUE(float, target_temperature) TEMPLATABLE_VALUE(float, target_temperature)
TEMPLATABLE_VALUE(water_heater::WaterHeaterMode, mode) TEMPLATABLE_VALUE(water_heater::WaterHeaterMode, mode)
TEMPLATABLE_VALUE(bool, away)
TEMPLATABLE_VALUE(bool, is_on)
void play(const Ts &...x) override { void play(const Ts &...x) override {
if (this->current_temperature_.has_value()) { if (this->current_temperature_.has_value()) {
this->parent_->set_current_temperature(this->current_temperature_.value(x...)); this->parent_->set_current_temperature(this->current_temperature_.value(x...));
} }
bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value(); bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value() || this->away_.has_value() ||
this->is_on_.has_value();
if (needs_call) { if (needs_call) {
auto call = this->parent_->make_call(); auto call = this->parent_->make_call();
if (this->target_temperature_.has_value()) { if (this->target_temperature_.has_value()) {
@@ -25,6 +28,12 @@ class TemplateWaterHeaterPublishAction : public Action<Ts...>, public Parented<T
if (this->mode_.has_value()) { if (this->mode_.has_value()) {
call.set_mode(this->mode_.value(x...)); call.set_mode(this->mode_.value(x...));
} }
if (this->away_.has_value()) {
call.set_away(this->away_.value(x...));
}
if (this->is_on_.has_value()) {
call.set_on(this->is_on_.value(x...));
}
call.perform(); call.perform();
} else { } else {
this->parent_->publish_state(); this->parent_->publish_state();

View File

@@ -17,7 +17,7 @@ void TemplateWaterHeater::setup() {
} }
} }
if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() && if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
!this->mode_f_.has_value()) !this->mode_f_.has_value() && !this->away_f_.has_value() && !this->is_on_f_.has_value())
this->disable_loop(); this->disable_loop();
} }
@@ -32,6 +32,12 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
if (this->target_temperature_f_.has_value()) { if (this->target_temperature_f_.has_value()) {
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE); traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
} }
if (this->away_f_.has_value()) {
traits.set_supports_away_mode(true);
}
if (this->is_on_f_.has_value()) {
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_ON_OFF);
}
return traits; return traits;
} }
@@ -62,6 +68,22 @@ void TemplateWaterHeater::loop() {
} }
} }
auto away = this->away_f_.call();
if (away.has_value()) {
if (*away != this->is_away()) {
this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *away);
changed = true;
}
}
auto is_on = this->is_on_f_.call();
if (is_on.has_value()) {
if (*is_on != this->is_on()) {
this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *is_on);
changed = true;
}
}
if (changed) { if (changed) {
this->publish_state(); this->publish_state();
} }
@@ -90,6 +112,17 @@ void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) {
} }
} }
if (call.get_away().has_value()) {
if (this->optimistic_) {
this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *call.get_away());
}
}
if (call.get_on().has_value()) {
if (this->optimistic_) {
this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *call.get_on());
}
}
this->set_trigger_.trigger(); this->set_trigger_.trigger();
if (this->optimistic_) { if (this->optimistic_) {

View File

@@ -24,6 +24,8 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
this->target_temperature_f_.set(std::forward<F>(f)); this->target_temperature_f_.set(std::forward<F>(f));
} }
template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); } template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); }
template<typename F> void set_away_lambda(F &&f) { this->away_f_.set(std::forward<F>(f)); }
template<typename F> void set_is_on_lambda(F &&f) { this->is_on_f_.set(std::forward<F>(f)); }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
@@ -49,6 +51,8 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
TemplateLambda<float> current_temperature_f_; TemplateLambda<float> current_temperature_f_;
TemplateLambda<float> target_temperature_f_; TemplateLambda<float> target_temperature_f_;
TemplateLambda<water_heater::WaterHeaterMode> mode_f_; TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
TemplateLambda<bool> away_f_;
TemplateLambda<bool> is_on_f_;
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
water_heater::WaterHeaterModeMask supported_modes_; water_heater::WaterHeaterModeMask supported_modes_;
bool optimistic_{true}; bool optimistic_{true};

View File

@@ -251,7 +251,7 @@ void Tormatic::stop_at_target_() {
// Read a GateStatus from the unit. The unit only sends messages in response to // Read a GateStatus from the unit. The unit only sends messages in response to
// status requests or commands, so a message needs to be sent first. // status requests or commands, so a message needs to be sent first.
optional<GateStatus> Tormatic::read_gate_status_() { optional<GateStatus> Tormatic::read_gate_status_() {
if (this->available() < static_cast<int>(sizeof(MessageHeader))) { if (this->available() < sizeof(MessageHeader)) {
return {}; return {};
} }

View File

@@ -32,10 +32,10 @@ void Tuya::setup() {
void Tuya::loop() { void Tuya::loop() {
// Read all available bytes in batches to reduce UART call overhead. // Read all available bytes in batches to reduce UART call overhead.
int avail = this->available(); size_t avail = this->available();
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf)); size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -3,12 +3,16 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cinttypes> #include <cinttypes>
namespace esphome::uart { namespace esphome::uart {
static const char *const TAG = "uart"; static const char *const TAG = "uart";
// UART parity strings indexed by UARTParityOptions enum (0-2): NONE, EVEN, ODD
PROGMEM_STRING_TABLE(UARTParityStrings, "NONE", "EVEN", "ODD", "UNKNOWN");
void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity, void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity,
uint8_t data_bits) { uint8_t data_bits) {
if (this->parent_->get_baud_rate() != baud_rate) { if (this->parent_->get_baud_rate() != baud_rate) {
@@ -30,16 +34,7 @@ void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UART
} }
const LogString *parity_to_str(UARTParityOptions parity) { const LogString *parity_to_str(UARTParityOptions parity) {
switch (parity) { return UARTParityStrings::get_log_str(static_cast<uint8_t>(parity), UARTParityStrings::LAST_INDEX);
case UART_CONFIG_PARITY_NONE:
return LOG_STR("NONE");
case UART_CONFIG_PARITY_EVEN:
return LOG_STR("EVEN");
case UART_CONFIG_PARITY_ODD:
return LOG_STR("ODD");
default:
return LOG_STR("UNKNOWN");
}
} }
} // namespace esphome::uart } // namespace esphome::uart

View File

@@ -43,7 +43,7 @@ class UARTDevice {
return res; return res;
} }
int available() { return this->parent_->available(); } size_t available() { return this->parent_->available(); }
void flush() { this->parent_->flush(); } void flush() { this->parent_->flush(); }

View File

@@ -5,13 +5,13 @@ namespace esphome::uart {
static const char *const TAG = "uart"; static const char *const TAG = "uart";
bool UARTComponent::check_read_timeout_(size_t len) { bool UARTComponent::check_read_timeout_(size_t len) {
if (this->available() >= int(len)) if (this->available() >= len)
return true; return true;
uint32_t start_time = millis(); uint32_t start_time = millis();
while (this->available() < int(len)) { while (this->available() < len) {
if (millis() - start_time > 100) { if (millis() - start_time > 100) {
ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available()); ESP_LOGE(TAG, "Reading from UART timed out at byte %zu!", this->available());
return false; return false;
} }
yield(); yield();

View File

@@ -69,7 +69,7 @@ class UARTComponent {
// Pure virtual method to return the number of bytes available for reading. // Pure virtual method to return the number of bytes available for reading.
// @return Number of available bytes. // @return Number of available bytes.
virtual int available() = 0; virtual size_t available() = 0;
// Pure virtual method to block until all bytes have been written to the UART bus. // Pure virtual method to block until all bytes have been written to the UART bus.
virtual void flush() = 0; virtual void flush() = 0;

View File

@@ -206,7 +206,7 @@ bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) {
#endif #endif
return true; return true;
} }
int ESP8266UartComponent::available() { size_t ESP8266UartComponent::available() {
if (this->hw_serial_ != nullptr) { if (this->hw_serial_ != nullptr) {
return this->hw_serial_->available(); return this->hw_serial_->available();
} else { } else {
@@ -329,11 +329,14 @@ uint8_t ESP8266SoftwareSerial::peek_byte() {
void ESP8266SoftwareSerial::flush() { void ESP8266SoftwareSerial::flush() {
// Flush is a NO-OP with software serial, all bytes are written immediately. // Flush is a NO-OP with software serial, all bytes are written immediately.
} }
int ESP8266SoftwareSerial::available() { size_t ESP8266SoftwareSerial::available() {
int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_); // Read volatile rx_in_pos_ once to avoid TOCTOU race with ISR.
if (avail < 0) // When in >= out, data is contiguous: [out..in).
return avail + this->rx_buffer_size_; // When in < out, data wraps: [out..buf_size) + [0..in).
return avail; size_t in = this->rx_in_pos_;
if (in >= this->rx_out_pos_)
return in - this->rx_out_pos_;
return this->rx_buffer_size_ - this->rx_out_pos_ + in;
} }
} // namespace esphome::uart } // namespace esphome::uart

View File

@@ -23,7 +23,7 @@ class ESP8266SoftwareSerial {
void write_byte(uint8_t data); void write_byte(uint8_t data);
int available(); size_t available();
protected: protected:
static void gpio_intr(ESP8266SoftwareSerial *arg); static void gpio_intr(ESP8266SoftwareSerial *arg);
@@ -57,7 +57,7 @@ class ESP8266UartComponent : public UARTComponent, public Component {
bool peek_byte(uint8_t *data) override; bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override; bool read_array(uint8_t *data, size_t len) override;
int available() override; size_t available() override;
void flush() override; void flush() override;
uint32_t get_config(); uint32_t get_config();

View File

@@ -338,7 +338,7 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
return read_len == (int32_t) length_to_read; return read_len == (int32_t) length_to_read;
} }
int IDFUARTComponent::available() { size_t IDFUARTComponent::available() {
size_t available = 0; size_t available = 0;
esp_err_t err; esp_err_t err;

View File

@@ -22,7 +22,7 @@ class IDFUARTComponent : public UARTComponent, public Component {
bool peek_byte(uint8_t *data) override; bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override; bool read_array(uint8_t *data, size_t len) override;
int available() override; size_t available() override;
void flush() override; void flush() override;
uint8_t get_hw_serial_number() { return this->uart_num_; } uint8_t get_hw_serial_number() { return this->uart_num_; }

View File

@@ -265,7 +265,7 @@ bool HostUartComponent::read_array(uint8_t *data, size_t len) {
return true; return true;
} }
int HostUartComponent::available() { size_t HostUartComponent::available() {
if (this->file_descriptor_ == -1) { if (this->file_descriptor_ == -1) {
return 0; return 0;
} }
@@ -275,9 +275,10 @@ int HostUartComponent::available() {
this->update_error_(strerror(errno)); this->update_error_(strerror(errno));
return 0; return 0;
} }
size_t result = available;
if (this->has_peek_) if (this->has_peek_)
available++; result++;
return available; return result;
}; };
void HostUartComponent::flush() { void HostUartComponent::flush() {

View File

@@ -17,7 +17,7 @@ class HostUartComponent : public UARTComponent, public Component {
void write_array(const uint8_t *data, size_t len) override; void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override; bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override; bool read_array(uint8_t *data, size_t len) override;
int available() override; size_t available() override;
void flush() override; void flush() override;
void set_name(std::string port_name) { port_name_ = port_name; }; void set_name(std::string port_name) { port_name_ = port_name; };

View File

@@ -169,7 +169,7 @@ bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) {
return true; return true;
} }
int LibreTinyUARTComponent::available() { return this->serial_->available(); } size_t LibreTinyUARTComponent::available() { return this->serial_->available(); }
void LibreTinyUARTComponent::flush() { void LibreTinyUARTComponent::flush() {
ESP_LOGVV(TAG, " Flushing"); ESP_LOGVV(TAG, " Flushing");
this->serial_->flush(); this->serial_->flush();

View File

@@ -21,7 +21,7 @@ class LibreTinyUARTComponent : public UARTComponent, public Component {
bool peek_byte(uint8_t *data) override; bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override; bool read_array(uint8_t *data, size_t len) override;
int available() override; size_t available() override;
void flush() override; void flush() override;
uint16_t get_config(); uint16_t get_config();

View File

@@ -186,7 +186,7 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) {
#endif #endif
return true; return true;
} }
int RP2040UartComponent::available() { return this->serial_->available(); } size_t RP2040UartComponent::available() { return this->serial_->available(); }
void RP2040UartComponent::flush() { void RP2040UartComponent::flush() {
ESP_LOGVV(TAG, " Flushing"); ESP_LOGVV(TAG, " Flushing");
this->serial_->flush(); this->serial_->flush();

View File

@@ -24,7 +24,7 @@ class RP2040UartComponent : public UARTComponent, public Component {
bool peek_byte(uint8_t *data) override; bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override; bool read_array(uint8_t *data, size_t len) override;
int available() override; size_t available() override;
void flush() override; void flush() override;
uint16_t get_config(); uint16_t get_config();

View File

@@ -81,7 +81,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC
void write_array(const uint8_t *data, size_t len) override; void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override; bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override; bool read_array(uint8_t *data, size_t len) override;
int available() override; size_t available() override;
void flush() override; void flush() override;
protected: protected:

View File

@@ -318,12 +318,12 @@ bool USBCDCACMInstance::read_array(uint8_t *data, size_t len) {
return bytes_read == original_len; return bytes_read == original_len;
} }
int USBCDCACMInstance::available() { size_t USBCDCACMInstance::available() {
UBaseType_t waiting = 0; UBaseType_t waiting = 0;
if (this->usb_rx_ringbuf_ != nullptr) { if (this->usb_rx_ringbuf_ != nullptr) {
vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting); vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
} }
return static_cast<int>(waiting) + (this->has_peek_ ? 1 : 0); return waiting + (this->has_peek_ ? 1 : 0);
} }
void USBCDCACMInstance::flush() { void USBCDCACMInstance::flush() {

View File

@@ -97,7 +97,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
bool peek_byte(uint8_t *data) override; bool peek_byte(uint8_t *data) override;
; ;
bool read_array(uint8_t *data, size_t len) override; bool read_array(uint8_t *data, size_t len) override;
int available() override { return static_cast<int>(this->input_buffer_.get_available()); } size_t available() override { return this->input_buffer_.get_available(); }
void flush() override {} void flush() override {}
void check_logger_conflict() override {} void check_logger_conflict() override {}
void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_parity(UARTParityOptions parity) { this->parity_ = parity; }

View File

@@ -371,7 +371,12 @@ async def to_code(config):
if on_timer_tick := config.get(CONF_ON_TIMER_TICK): if on_timer_tick := config.get(CONF_ON_TIMER_TICK):
await automation.build_automation( await automation.build_automation(
var.get_timer_tick_trigger(), var.get_timer_tick_trigger(),
[(cg.std_vector.template(Timer), "timers")], [
(
cg.std_vector.template(Timer).operator("const").operator("ref"),
"timers",
)
],
on_timer_tick, on_timer_tick,
) )
has_timers = True has_timers = True

View File

@@ -859,35 +859,43 @@ void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) {
} }
void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse &msg) { void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse &msg) {
Timer timer = { // Find existing timer or add a new one
.id = msg.timer_id, auto it = this->timers_.begin();
.name = msg.name, for (; it != this->timers_.end(); ++it) {
.total_seconds = msg.total_seconds, if (it->id == msg.timer_id)
.seconds_left = msg.seconds_left, break;
.is_active = msg.is_active, }
}; if (it == this->timers_.end()) {
this->timers_[timer.id] = timer; this->timers_.push_back({});
it = this->timers_.end() - 1;
}
it->id = msg.timer_id;
it->name = msg.name;
it->total_seconds = msg.total_seconds;
it->seconds_left = msg.seconds_left;
it->is_active = msg.is_active;
char timer_buf[Timer::TO_STR_BUFFER_SIZE]; char timer_buf[Timer::TO_STR_BUFFER_SIZE];
ESP_LOGD(TAG, ESP_LOGD(TAG,
"Timer Event\n" "Timer Event\n"
" Type: %" PRId32 "\n" " Type: %" PRId32 "\n"
" %s", " %s",
msg.event_type, timer.to_str(timer_buf)); msg.event_type, it->to_str(timer_buf));
switch (msg.event_type) { switch (msg.event_type) {
case api::enums::VOICE_ASSISTANT_TIMER_STARTED: case api::enums::VOICE_ASSISTANT_TIMER_STARTED:
this->timer_started_trigger_.trigger(timer); this->timer_started_trigger_.trigger(*it);
break; break;
case api::enums::VOICE_ASSISTANT_TIMER_UPDATED: case api::enums::VOICE_ASSISTANT_TIMER_UPDATED:
this->timer_updated_trigger_.trigger(timer); this->timer_updated_trigger_.trigger(*it);
break; break;
case api::enums::VOICE_ASSISTANT_TIMER_CANCELLED: case api::enums::VOICE_ASSISTANT_TIMER_CANCELLED:
this->timer_cancelled_trigger_.trigger(timer); this->timer_cancelled_trigger_.trigger(*it);
this->timers_.erase(timer.id); this->timers_.erase(it);
break; break;
case api::enums::VOICE_ASSISTANT_TIMER_FINISHED: case api::enums::VOICE_ASSISTANT_TIMER_FINISHED:
this->timer_finished_trigger_.trigger(timer); this->timer_finished_trigger_.trigger(*it);
this->timers_.erase(timer.id); this->timers_.erase(it);
break; break;
} }
@@ -901,16 +909,12 @@ void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse
} }
void VoiceAssistant::timer_tick_() { void VoiceAssistant::timer_tick_() {
std::vector<Timer> res; for (auto &timer : this->timers_) {
res.reserve(this->timers_.size());
for (auto &pair : this->timers_) {
auto &timer = pair.second;
if (timer.is_active && timer.seconds_left > 0) { if (timer.is_active && timer.seconds_left > 0) {
timer.seconds_left--; timer.seconds_left--;
} }
res.push_back(timer);
} }
this->timer_tick_trigger_.trigger(res); this->timer_tick_trigger_.trigger(this->timers_);
} }
void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) { void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) {

View File

@@ -24,7 +24,6 @@
#include "esphome/components/socket/socket.h" #include "esphome/components/socket/socket.h"
#include <span> #include <span>
#include <unordered_map>
#include <vector> #include <vector>
namespace esphome { namespace esphome {
@@ -83,7 +82,7 @@ struct Timer {
} }
// Remove before 2026.8.0 // Remove before 2026.8.0
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0") ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
std::string to_string() const { std::string to_string() const { // NOLINT
char buffer[TO_STR_BUFFER_SIZE]; char buffer[TO_STR_BUFFER_SIZE];
return this->to_str(buffer); return this->to_str(buffer);
} }
@@ -226,9 +225,9 @@ class VoiceAssistant : public Component {
Trigger<Timer> *get_timer_updated_trigger() { return &this->timer_updated_trigger_; } Trigger<Timer> *get_timer_updated_trigger() { return &this->timer_updated_trigger_; }
Trigger<Timer> *get_timer_cancelled_trigger() { return &this->timer_cancelled_trigger_; } Trigger<Timer> *get_timer_cancelled_trigger() { return &this->timer_cancelled_trigger_; }
Trigger<Timer> *get_timer_finished_trigger() { return &this->timer_finished_trigger_; } Trigger<Timer> *get_timer_finished_trigger() { return &this->timer_finished_trigger_; }
Trigger<std::vector<Timer>> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; } Trigger<const std::vector<Timer> &> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; }
void set_has_timers(bool has_timers) { this->has_timers_ = has_timers; } void set_has_timers(bool has_timers) { this->has_timers_ = has_timers; }
const std::unordered_map<std::string, Timer> &get_timers() const { return this->timers_; } const std::vector<Timer> &get_timers() const { return this->timers_; }
protected: protected:
bool allocate_buffers_(); bool allocate_buffers_();
@@ -267,13 +266,13 @@ class VoiceAssistant : public Component {
api::APIConnection *api_client_{nullptr}; api::APIConnection *api_client_{nullptr};
std::unordered_map<std::string, Timer> timers_; std::vector<Timer> timers_;
void timer_tick_(); void timer_tick_();
Trigger<Timer> timer_started_trigger_; Trigger<Timer> timer_started_trigger_;
Trigger<Timer> timer_finished_trigger_; Trigger<Timer> timer_finished_trigger_;
Trigger<Timer> timer_updated_trigger_; Trigger<Timer> timer_updated_trigger_;
Trigger<Timer> timer_cancelled_trigger_; Trigger<Timer> timer_cancelled_trigger_;
Trigger<std::vector<Timer>> timer_tick_trigger_; Trigger<const std::vector<Timer> &> timer_tick_trigger_;
bool has_timers_{false}; bool has_timers_{false};
bool timer_tick_running_{false}; bool timer_tick_running_{false};

View File

@@ -90,9 +90,22 @@ class WaterHeaterCall {
float get_target_temperature_low() const { return this->target_temperature_low_; } float get_target_temperature_low() const { return this->target_temperature_low_; }
float get_target_temperature_high() const { return this->target_temperature_high_; } float get_target_temperature_high() const { return this->target_temperature_high_; }
/// Get state flags value /// Get state flags value
ESPDEPRECATED("get_state() is deprecated, use get_away() and get_on() instead. (Removed in 2026.8.0)", "2026.2.0")
uint32_t get_state() const { return this->state_; } uint32_t get_state() const { return this->state_; }
/// Get mask of state flags that are being changed
uint32_t get_state_mask() const { return this->state_mask_; } optional<bool> get_away() const {
if (this->state_mask_ & WATER_HEATER_STATE_AWAY) {
return (this->state_ & WATER_HEATER_STATE_AWAY) != 0;
}
return {};
}
optional<bool> get_on() const {
if (this->state_mask_ & WATER_HEATER_STATE_ON) {
return (this->state_ & WATER_HEATER_STATE_ON) != 0;
}
return {};
}
protected: protected:
void validate_(); void validate_();

View File

@@ -1,8 +1,11 @@
from pathlib import Path
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority from esphome.coroutine import CoroPriority
from esphome.helpers import copy_file_if_changed
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
@@ -49,5 +52,15 @@ async def to_code(config):
CORE.add_platformio_option( CORE.add_platformio_option(
"lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"] "lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"]
) )
# ESPAsyncWebServer uses Hash library for sha1() on RP2040
cg.add_library("Hash", None)
# Fix Hash.h include conflict: Crypto-no-arduino (used by dsmr)
# provides a Hash.h that shadows the framework's Hash library.
# Prepend the framework Hash path so it's found first.
copy_file_if_changed(
Path(__file__).parent / "fix_rp2040_hash.py.script",
CORE.relative_build_path("fix_rp2040_hash.py"),
)
cg.add_platformio_option("extra_scripts", ["pre:fix_rp2040_hash.py"])
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6") cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")

View File

@@ -0,0 +1,11 @@
# ESPAsyncWebServer includes <Hash.h> expecting the Arduino-Pico framework's Hash
# library (which provides sha1() functions). However, the Crypto-no-arduino library
# (used by dsmr) also provides a Hash.h that can shadow the framework version when
# PlatformIO's chain+ LDF mode auto-discovers it as a dependency.
# Prepend the framework Hash path to CXXFLAGS so it is found first.
import os
Import("env")
framework_dir = env.PioPlatform().get_package_dir("framework-arduinopico")
hash_src = os.path.join(framework_dir, "libraries", "Hash", "src")
env.Prepend(CXXFLAGS=["-I" + hash_src])

View File

@@ -401,7 +401,7 @@ bool WeikaiChannel::peek_byte(uint8_t *buffer) {
return this->receive_buffer_.peek(*buffer); return this->receive_buffer_.peek(*buffer);
} }
int WeikaiChannel::available() { size_t WeikaiChannel::available() {
size_t available = this->receive_buffer_.count(); size_t available = this->receive_buffer_.count();
if (!available) if (!available)
available = xfer_fifo_to_buffer_(); available = xfer_fifo_to_buffer_();

View File

@@ -374,7 +374,7 @@ class WeikaiChannel : public uart::UARTComponent {
/// @brief Returns the number of bytes in the receive buffer /// @brief Returns the number of bytes in the receive buffer
/// @return the number of bytes available in the receiver fifo /// @return the number of bytes available in the receiver fifo
int available() override; size_t available() override;
/// @brief Flush the output fifo. /// @brief Flush the output fifo.
/// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data /// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data

View File

@@ -3,6 +3,7 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <zephyr/settings/settings.h> #include <zephyr/settings/settings.h>
#include <zephyr/storage/flash_map.h> #include <zephyr/storage/flash_map.h>
#include "esphome/core/hal.h"
extern "C" { extern "C" {
#include <zboss_api.h> #include <zboss_api.h>
@@ -223,6 +224,7 @@ void ZigbeeComponent::dump_config() {
get_wipe_on_boot(), YESNO(zb_zdo_joined()), zb_get_current_channel(), zb_get_current_page(), get_wipe_on_boot(), YESNO(zb_zdo_joined()), zb_get_current_channel(), zb_get_current_page(),
zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), extended_pan_id_buf, zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), extended_pan_id_buf,
zb_get_pan_id()); zb_get_pan_id());
dump_reporting_();
} }
static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) { static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) {
@@ -244,6 +246,33 @@ void ZigbeeComponent::factory_reset() {
ZB_SCHEDULE_APP_CALLBACK(zb_bdb_reset_via_local_action, 0); ZB_SCHEDULE_APP_CALLBACK(zb_bdb_reset_via_local_action, 0);
} }
void ZigbeeComponent::dump_reporting_() {
#ifdef ESPHOME_LOG_HAS_VERBOSE
auto now = millis();
bool first = true;
for (zb_uint8_t j = 0; j < ZCL_CTX().device_ctx->ep_count; j++) {
if (ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info) {
zb_zcl_reporting_info_t *rep_info = ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info;
for (zb_uint8_t i = 0; i < ZCL_CTX().device_ctx->ep_desc_list[j]->rep_info_count; i++) {
if (!first) {
ESP_LOGV(TAG, "");
}
first = false;
ESP_LOGV(TAG, "Endpoint: %d, cluster_id %d, attr_id %d, flags %d, report in %ums", rep_info->ep,
rep_info->cluster_id, rep_info->attr_id, rep_info->flags,
ZB_ZCL_GET_REPORTING_FLAG(rep_info, ZB_ZCL_REPORT_TIMER_STARTED)
? ZB_TIME_BEACON_INTERVAL_TO_MSEC(rep_info->run_time) - now
: 0);
ESP_LOGV(TAG, "Min_interval %ds, max_interval %ds, def_min_interval %ds, def_max_interval %ds",
rep_info->u.send_info.min_interval, rep_info->u.send_info.max_interval,
rep_info->u.send_info.def_min_interval, rep_info->u.send_info.def_max_interval);
rep_info++;
}
}
}
#endif
}
} // namespace esphome::zigbee } // namespace esphome::zigbee
extern "C" void zboss_signal_handler(zb_uint8_t param) { extern "C" void zboss_signal_handler(zb_uint8_t param) {

View File

@@ -87,6 +87,7 @@ class ZigbeeComponent : public Component {
#ifdef USE_ZIGBEE_WIPE_ON_BOOT #ifdef USE_ZIGBEE_WIPE_ON_BOOT
void erase_flash_(int area); void erase_flash_(int area);
#endif #endif
void dump_reporting_();
std::array<std::function<void(zb_bufid_t bufid)>, ZIGBEE_ENDPOINTS_COUNT> callbacks_{}; std::array<std::function<void(zb_bufid_t bufid)>, ZIGBEE_ENDPOINTS_COUNT> callbacks_{};
CallbackManager<void()> join_cb_; CallbackManager<void()> join_cb_;
Trigger<> join_trigger_; Trigger<> join_trigger_;

View File

@@ -191,15 +191,17 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
// instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution) // instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution)
if constexpr (sizeof...(Ts) == 0) { if constexpr (sizeof...(Ts) == 0) {
App.scheduler.set_timer_common_( App.scheduler.set_timer_common_(
this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::STATIC_STRING, "delay", 0, this->delay_.value(), this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr,
static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION), this->delay_.value(),
[this]() { this->play_next_(); }, [this]() { this->play_next_(); },
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
} else { } else {
// For delays with arguments, use std::bind to preserve argument values // For delays with arguments, use std::bind to preserve argument values
// Arguments must be copied because original references may be invalid after delay // Arguments must be copied because original references may be invalid after delay
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...); auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::STATIC_STRING, App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL,
"delay", 0, this->delay_.value(x...), std::move(f), nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION),
this->delay_.value(x...), std::move(f),
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
} }
} }
@@ -208,7 +210,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
void play(const Ts &...x) override { /* ignore - see play_complex */ void play(const Ts &...x) override { /* ignore - see play_complex */
} }
void stop() override { this->cancel_timeout("delay"); } void stop() override { this->cancel_timeout(InternalSchedulerID::DELAY_ACTION); }
}; };
template<typename... Ts> class LambdaAction : public Action<Ts...> { template<typename... Ts> class LambdaAction : public Action<Ts...> {

View File

@@ -152,7 +152,10 @@ void Component::set_retry(const std::string &name, uint32_t initial_wait_time, u
void Component::set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts, void Component::set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
#pragma GCC diagnostic pop
} }
bool Component::cancel_retry(const std::string &name) { // NOLINT bool Component::cancel_retry(const std::string &name) { // NOLINT
@@ -163,7 +166,10 @@ bool Component::cancel_retry(const std::string &name) { // NOLINT
} }
bool Component::cancel_retry(const char *name) { // NOLINT bool Component::cancel_retry(const char *name) { // NOLINT
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
return App.scheduler.cancel_retry(this, name); return App.scheduler.cancel_retry(this, name);
#pragma GCC diagnostic pop
} }
void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) { // NOLINT void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) { // NOLINT
@@ -195,18 +201,38 @@ void Component::set_timeout(uint32_t id, uint32_t timeout, std::function<void()>
bool Component::cancel_timeout(uint32_t id) { return App.scheduler.cancel_timeout(this, id); } bool Component::cancel_timeout(uint32_t id) { return App.scheduler.cancel_timeout(this, id); }
void Component::set_timeout(InternalSchedulerID id, uint32_t timeout, std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, id, timeout, std::move(f));
}
bool Component::cancel_timeout(InternalSchedulerID id) { return App.scheduler.cancel_timeout(this, id); }
void Component::set_interval(uint32_t id, uint32_t interval, std::function<void()> &&f) { // NOLINT void Component::set_interval(uint32_t id, uint32_t interval, std::function<void()> &&f) { // NOLINT
App.scheduler.set_interval(this, id, interval, std::move(f)); App.scheduler.set_interval(this, id, interval, std::move(f));
} }
bool Component::cancel_interval(uint32_t id) { return App.scheduler.cancel_interval(this, id); } bool Component::cancel_interval(uint32_t id) { return App.scheduler.cancel_interval(this, id); }
void Component::set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, void Component::set_interval(InternalSchedulerID id, uint32_t interval, std::function<void()> &&f) { // NOLINT
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT App.scheduler.set_interval(this, id, interval, std::move(f));
App.scheduler.set_retry(this, id, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
} }
bool Component::cancel_retry(uint32_t id) { return App.scheduler.cancel_retry(this, id); } bool Component::cancel_interval(InternalSchedulerID id) { return App.scheduler.cancel_interval(this, id); }
void Component::set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
App.scheduler.set_retry(this, id, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
#pragma GCC diagnostic pop
}
bool Component::cancel_retry(uint32_t id) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
return App.scheduler.cancel_retry(this, id);
#pragma GCC diagnostic pop
}
void Component::call_loop() { this->loop(); } void Component::call_loop() { this->loop(); }
void Component::call_setup() { this->setup(); } void Component::call_setup() { this->setup(); }
@@ -371,7 +397,10 @@ void Component::set_interval(uint32_t interval, std::function<void()> &&f) { //
} }
void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f, void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f,
float backoff_increase_factor) { // NOLINT float backoff_increase_factor) { // NOLINT
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
#pragma GCC diagnostic pop
} }
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
bool Component::is_ready() const { bool Component::is_ready() const {
@@ -516,12 +545,12 @@ void PollingComponent::call_setup() {
void PollingComponent::start_poller() { void PollingComponent::start_poller() {
// Register interval. // Register interval.
this->set_interval("update", this->get_update_interval(), [this]() { this->update(); }); this->set_interval(InternalSchedulerID::POLLING_UPDATE, this->get_update_interval(), [this]() { this->update(); });
} }
void PollingComponent::stop_poller() { void PollingComponent::stop_poller() {
// Clear the interval to suspend component // Clear the interval to suspend component
this->cancel_interval("update"); this->cancel_interval(InternalSchedulerID::POLLING_UPDATE);
} }
uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; }

View File

@@ -49,6 +49,14 @@ extern const float LATE;
static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL;
/// Type-safe scheduler IDs for core base classes.
/// Uses a separate NameType (NUMERIC_ID_INTERNAL) so IDs can never collide
/// with component-level NUMERIC_ID values, even if the uint32_t values overlap.
enum class InternalSchedulerID : uint32_t {
POLLING_UPDATE = 0, // PollingComponent interval
DELAY_ACTION = 1, // DelayAction timeout
};
// Forward declaration // Forward declaration
class PollingComponent; class PollingComponent;
@@ -68,6 +76,7 @@ extern const uint8_t STATUS_LED_OK;
extern const uint8_t STATUS_LED_WARNING; extern const uint8_t STATUS_LED_WARNING;
extern const uint8_t STATUS_LED_ERROR; extern const uint8_t STATUS_LED_ERROR;
// Remove before 2026.8.0
enum class RetryResult { DONE, RETRY }; enum class RetryResult { DONE, RETRY };
extern const uint16_t WARN_IF_BLOCKING_OVER_MS; extern const uint16_t WARN_IF_BLOCKING_OVER_MS;
@@ -334,6 +343,8 @@ class Component {
*/ */
void set_interval(uint32_t id, uint32_t interval, std::function<void()> &&f); // NOLINT void set_interval(uint32_t id, uint32_t interval, std::function<void()> &&f); // NOLINT
void set_interval(InternalSchedulerID id, uint32_t interval, std::function<void()> &&f); // NOLINT
void set_interval(uint32_t interval, std::function<void()> &&f); // NOLINT void set_interval(uint32_t interval, std::function<void()> &&f); // NOLINT
/** Cancel an interval function. /** Cancel an interval function.
@@ -346,69 +357,42 @@ class Component {
bool cancel_interval(const std::string &name); // NOLINT bool cancel_interval(const std::string &name); // NOLINT
bool cancel_interval(const char *name); // NOLINT bool cancel_interval(const char *name); // NOLINT
bool cancel_interval(uint32_t id); // NOLINT bool cancel_interval(uint32_t id); // NOLINT
bool cancel_interval(InternalSchedulerID id); // NOLINT
/** Set an retry function with a unique name. Empty name means no cancelling possible. /// @deprecated set_retry is deprecated. Use set_timeout or set_interval instead. Removed in 2026.8.0.
* // Remove before 2026.8.0
* This will call the retry function f on the next scheduler loop. f should return RetryResult::DONE if ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
* it is successful and no repeat is required. Otherwise, returning RetryResult::RETRY will call f "2026.2.0")
* again in the future.
*
* The first retry of f happens after `initial_wait_time` milliseconds. The delay between retries is
* increased by multiplying by `backoff_increase_factor` each time. If no backoff_increase_factor is
* supplied (default = 1.0), the wait time will stay constant.
*
* The retry function f needs to accept a single argument: the number of attempts remaining. On the
* final retry of f, this value will be 0.
*
* This retry function can also be cancelled by name via cancel_retry().
*
* IMPORTANT: Do not rely on this having correct timing. This is only called from
* loop() and therefore can be significantly delayed.
*
* REMARK: It is an error to supply a negative or zero `backoff_increase_factor`, and 1.0 will be used instead.
*
* REMARK: The interval between retries is stored into a `uint32_t`, so this doesn't behave correctly
* if `initial_wait_time * (backoff_increase_factor ** (max_attempts - 2))` overflows.
*
* @param name The identifier for this retry function.
* @param initial_wait_time The time in ms before f is called again
* @param max_attempts The maximum number of executions
* @param f The function (or lambda) that should be called
* @param backoff_increase_factor time between retries is multiplied by this factor on every retry after the first
* @see cancel_retry()
*/
// Remove before 2026.7.0
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
// Remove before 2026.8.0
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
"2026.2.0")
void set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT void set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
/** Set a retry function with a numeric ID (zero heap allocation). // Remove before 2026.8.0
* ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
* @param id The numeric identifier for this retry function "2026.2.0")
* @param initial_wait_time The wait time after the first execution
* @param max_attempts The max number of attempts
* @param f The function to call
* @param backoff_increase_factor The factor to increase the retry interval by
*/
void set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT void set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
// Remove before 2026.8.0
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
"2026.2.0")
void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f, // NOLINT void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f, // NOLINT
float backoff_increase_factor = 1.0f); // NOLINT float backoff_increase_factor = 1.0f); // NOLINT
/** Cancel a retry function. // Remove before 2026.8.0
* ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
* @param name The identifier for this retry function.
* @return Whether a retry function was deleted.
*/
// Remove before 2026.7.0
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
bool cancel_retry(const std::string &name); // NOLINT bool cancel_retry(const std::string &name); // NOLINT
bool cancel_retry(const char *name); // NOLINT // Remove before 2026.8.0
bool cancel_retry(uint32_t id); // NOLINT ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
bool cancel_retry(const char *name); // NOLINT
// Remove before 2026.8.0
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
bool cancel_retry(uint32_t id); // NOLINT
/** Set a timeout function with a unique name. /** Set a timeout function with a unique name.
* *
@@ -452,6 +436,8 @@ class Component {
*/ */
void set_timeout(uint32_t id, uint32_t timeout, std::function<void()> &&f); // NOLINT void set_timeout(uint32_t id, uint32_t timeout, std::function<void()> &&f); // NOLINT
void set_timeout(InternalSchedulerID id, uint32_t timeout, std::function<void()> &&f); // NOLINT
void set_timeout(uint32_t timeout, std::function<void()> &&f); // NOLINT void set_timeout(uint32_t timeout, std::function<void()> &&f); // NOLINT
/** Cancel a timeout function. /** Cancel a timeout function.
@@ -464,6 +450,7 @@ class Component {
bool cancel_timeout(const std::string &name); // NOLINT bool cancel_timeout(const std::string &name); // NOLINT
bool cancel_timeout(const char *name); // NOLINT bool cancel_timeout(const char *name); // NOLINT
bool cancel_timeout(uint32_t id); // NOLINT bool cancel_timeout(uint32_t id); // NOLINT
bool cancel_timeout(InternalSchedulerID id); // NOLINT
/** Defer a callback to the next loop() call. /** Defer a callback to the next loop() call.
* *

View File

@@ -53,9 +53,12 @@ struct SchedulerNameLog {
} else if (name_type == NameType::HASHED_STRING) { } else if (name_type == NameType::HASHED_STRING) {
ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("hash:0x%08" PRIX32), hash_or_id); ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("hash:0x%08" PRIX32), hash_or_id);
return buffer; return buffer;
} else { // NUMERIC_ID } else if (name_type == NameType::NUMERIC_ID) {
ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("id:%" PRIu32), hash_or_id); ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("id:%" PRIu32), hash_or_id);
return buffer; return buffer;
} else { // NUMERIC_ID_INTERNAL
ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("iid:%" PRIu32), hash_or_id);
return buffer;
} }
} }
}; };
@@ -137,6 +140,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
case NameType::NUMERIC_ID: case NameType::NUMERIC_ID:
item->set_numeric_id(hash_or_id); item->set_numeric_id(hash_or_id);
break; break;
case NameType::NUMERIC_ID_INTERNAL:
item->set_internal_id(hash_or_id);
break;
} }
item->type = type; item->type = type;
item->callback = std::move(func); item->callback = std::move(func);
@@ -252,6 +258,11 @@ bool HOT Scheduler::cancel_interval(Component *component, uint32_t id) {
return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::INTERVAL); return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::INTERVAL);
} }
// Suppress deprecation warnings for RetryResult usage in the still-present (but deprecated) retry implementation.
// Remove before 2026.8.0 along with all retry code.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
struct RetryArgs { struct RetryArgs {
// Ordered to minimize padding on 32-bit systems // Ordered to minimize padding on 32-bit systems
std::function<RetryResult(uint8_t)> func; std::function<RetryResult(uint8_t)> func;
@@ -364,6 +375,8 @@ bool HOT Scheduler::cancel_retry(Component *component, uint32_t id) {
return this->cancel_retry_(component, NameType::NUMERIC_ID, nullptr, id); return this->cancel_retry_(component, NameType::NUMERIC_ID, nullptr, id);
} }
#pragma GCC diagnostic pop // End suppression of deprecated RetryResult warnings
optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) { optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
// IMPORTANT: This method should only be called from the main thread (loop task). // IMPORTANT: This method should only be called from the main thread (loop task).
// It performs cleanup and accesses items_[0] without holding a lock, which is only // It performs cleanup and accesses items_[0] without holding a lock, which is only

View File

@@ -46,11 +46,20 @@ class Scheduler {
void set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func); void set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func);
/// Set a timeout with a numeric ID (zero heap allocation) /// Set a timeout with a numeric ID (zero heap allocation)
void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function<void()> func); void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function<void()> func);
/// Set a timeout with an internal scheduler ID (separate namespace from component NUMERIC_ID)
void set_timeout(Component *component, InternalSchedulerID id, uint32_t timeout, std::function<void()> func) {
this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID_INTERNAL, nullptr,
static_cast<uint32_t>(id), timeout, std::move(func));
}
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
bool cancel_timeout(Component *component, const std::string &name); bool cancel_timeout(Component *component, const std::string &name);
bool cancel_timeout(Component *component, const char *name); bool cancel_timeout(Component *component, const char *name);
bool cancel_timeout(Component *component, uint32_t id); bool cancel_timeout(Component *component, uint32_t id);
bool cancel_timeout(Component *component, InternalSchedulerID id) {
return this->cancel_item_(component, NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast<uint32_t>(id),
SchedulerItem::TIMEOUT);
}
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> func); void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> func);
@@ -66,24 +75,45 @@ class Scheduler {
void set_interval(Component *component, const char *name, uint32_t interval, std::function<void()> func); void set_interval(Component *component, const char *name, uint32_t interval, std::function<void()> func);
/// Set an interval with a numeric ID (zero heap allocation) /// Set an interval with a numeric ID (zero heap allocation)
void set_interval(Component *component, uint32_t id, uint32_t interval, std::function<void()> func); void set_interval(Component *component, uint32_t id, uint32_t interval, std::function<void()> func);
/// Set an interval with an internal scheduler ID (separate namespace from component NUMERIC_ID)
void set_interval(Component *component, InternalSchedulerID id, uint32_t interval, std::function<void()> func) {
this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID_INTERNAL, nullptr,
static_cast<uint32_t>(id), interval, std::move(func));
}
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
bool cancel_interval(Component *component, const std::string &name); bool cancel_interval(Component *component, const std::string &name);
bool cancel_interval(Component *component, const char *name); bool cancel_interval(Component *component, const char *name);
bool cancel_interval(Component *component, uint32_t id); bool cancel_interval(Component *component, uint32_t id);
bool cancel_interval(Component *component, InternalSchedulerID id) {
return this->cancel_item_(component, NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast<uint32_t>(id),
SchedulerItem::INTERVAL);
}
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") // Remove before 2026.8.0
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
"2026.2.0")
void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f); std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
// Remove before 2026.8.0
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
"2026.2.0")
void set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts, void set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f); std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
/// Set a retry with a numeric ID (zero heap allocation) // Remove before 2026.8.0
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
"2026.2.0")
void set_retry(Component *component, uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, void set_retry(Component *component, uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f); std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") // Remove before 2026.8.0
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
bool cancel_retry(Component *component, const std::string &name); bool cancel_retry(Component *component, const std::string &name);
// Remove before 2026.8.0
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
bool cancel_retry(Component *component, const char *name); bool cancel_retry(Component *component, const char *name);
// Remove before 2026.8.0
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
bool cancel_retry(Component *component, uint32_t id); bool cancel_retry(Component *component, uint32_t id);
// Calculate when the next scheduled item should run // Calculate when the next scheduled item should run
@@ -100,11 +130,12 @@ class Scheduler {
void process_to_add(); void process_to_add();
// Name storage type discriminator for SchedulerItem // Name storage type discriminator for SchedulerItem
// Used to distinguish between static strings, hashed strings, and numeric IDs // Used to distinguish between static strings, hashed strings, numeric IDs, and internal numeric IDs
enum class NameType : uint8_t { enum class NameType : uint8_t {
STATIC_STRING = 0, // const char* pointer to static/flash storage STATIC_STRING = 0, // const char* pointer to static/flash storage
HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string
NUMERIC_ID = 2 // uint32_t numeric identifier NUMERIC_ID = 2, // uint32_t numeric identifier (component-level)
NUMERIC_ID_INTERNAL = 3 // uint32_t numeric identifier (core/internal, separate namespace)
}; };
protected: protected:
@@ -135,7 +166,7 @@ class Scheduler {
// Bit-packed fields (4 bits used, 4 bits padding in 1 byte) // Bit-packed fields (4 bits used, 4 bits padding in 1 byte)
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
NameType name_type_ : 2; // Discriminator for name_ union (STATIC_STRING, HASHED_STRING, NUMERIC_ID) NameType name_type_ : 2; // Discriminator for name_ union (03, see NameType enum)
bool is_retry : 1; // True if this is a retry timeout bool is_retry : 1; // True if this is a retry timeout
// 4 bits padding // 4 bits padding
#else #else
@@ -143,7 +174,7 @@ class Scheduler {
// Bit-packed fields (5 bits used, 3 bits padding in 1 byte) // Bit-packed fields (5 bits used, 3 bits padding in 1 byte)
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
bool remove : 1; bool remove : 1;
NameType name_type_ : 2; // Discriminator for name_ union (STATIC_STRING, HASHED_STRING, NUMERIC_ID) NameType name_type_ : 2; // Discriminator for name_ union (03, see NameType enum)
bool is_retry : 1; // True if this is a retry timeout bool is_retry : 1; // True if this is a retry timeout
// 3 bits padding // 3 bits padding
#endif #endif
@@ -206,6 +237,12 @@ class Scheduler {
name_type_ = NameType::NUMERIC_ID; name_type_ = NameType::NUMERIC_ID;
} }
// Helper to set an internal numeric ID (separate namespace from NUMERIC_ID)
void set_internal_id(uint32_t id) {
name_.hash_or_id = id;
name_type_ = NameType::NUMERIC_ID_INTERNAL;
}
static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b); static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
// Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility. // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility.
@@ -231,11 +268,14 @@ class Scheduler {
uint32_t hash_or_id, uint32_t delay, std::function<void()> func, bool is_retry = false, uint32_t hash_or_id, uint32_t delay, std::function<void()> func, bool is_retry = false,
bool skip_cancel = false); bool skip_cancel = false);
// Common implementation for retry // Common implementation for retry - Remove before 2026.8.0
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
void set_retry_common_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, void set_retry_common_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id,
uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> func, uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> func,
float backoff_increase_factor); float backoff_increase_factor);
#pragma GCC diagnostic pop
// Common implementation for cancel_retry // Common implementation for cancel_retry
bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);

View File

@@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["setuptools==80.10.2", "wheel>=0.43,<0.47"] requires = ["setuptools==82.0.0", "wheel>=0.43,<0.47"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]

View File

@@ -11,8 +11,8 @@ pyserial==3.5
platformio==6.1.19 platformio==6.1.19
esptool==5.1.0 esptool==5.1.0
click==8.1.7 click==8.1.7
esphome-dashboard==20260110.0 esphome-dashboard==20260210.0
aioesphomeapi==43.14.0 aioesphomeapi==44.0.0
zeroconf==0.148.0 zeroconf==0.148.0
puremagic==1.30 puremagic==1.30
ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml==0.19.1 # dashboard_import

View File

@@ -2277,6 +2277,12 @@ ifdefs: dict[str, str] = {}
# Track messages with no fields (empty messages) for parameter elision # Track messages with no fields (empty messages) for parameter elision
EMPTY_MESSAGES: set[str] = set() EMPTY_MESSAGES: set[str] = set()
# Track empty SOURCE_CLIENT messages that don't need class generation
# These messages have no fields and are only received (never sent), so the
# class definition (vtable, dump_to, message_name, ESTIMATED_SIZE) is dead code
# that the compiler compiles but the linker strips away.
SKIP_CLASS_GENERATION: set[str] = set()
def get_opt( def get_opt(
desc: descriptor.DescriptorProto, desc: descriptor.DescriptorProto,
@@ -2527,7 +2533,11 @@ def build_service_message_type(
case += "#endif\n" case += "#endif\n"
case += f"this->{func}({'msg' if not is_empty else ''});\n" case += f"this->{func}({'msg' if not is_empty else ''});\n"
case += "break;" case += "break;"
RECEIVE_CASES[id_] = (case, ifdef, mt.name) if mt.name in SKIP_CLASS_GENERATION:
case_label = f"{id_} /* {mt.name} is empty */"
else:
case_label = f"{mt.name}::MESSAGE_TYPE"
RECEIVE_CASES[id_] = (case, ifdef, case_label)
# Only close ifdef if we opened it # Only close ifdef if we opened it
if ifdef is not None: if ifdef is not None:
@@ -2723,6 +2733,19 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
mt = file.message_type mt = file.message_type
# Identify empty SOURCE_CLIENT messages that don't need class generation
for m in mt:
if m.options.deprecated:
continue
if not m.options.HasExtension(pb.id):
continue
source = message_source_map.get(m.name)
if source != SOURCE_CLIENT:
continue
has_fields = any(not field.options.deprecated for field in m.field)
if not has_fields:
SKIP_CLASS_GENERATION.add(m.name)
# Collect messages by base class # Collect messages by base class
base_class_groups = collect_messages_by_base_class(mt) base_class_groups = collect_messages_by_base_class(mt)
@@ -2755,6 +2778,10 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
if m.name not in used_messages and not m.options.HasExtension(pb.id): if m.name not in used_messages and not m.options.HasExtension(pb.id):
continue continue
# Skip class generation for empty SOURCE_CLIENT messages
if m.name in SKIP_CLASS_GENERATION:
continue
s, c, dc = build_message_type(m, base_class_fields, message_source_map) s, c, dc = build_message_type(m, base_class_fields, message_source_map)
msg_ifdef = message_ifdef_map.get(m.name) msg_ifdef = message_ifdef_map.get(m.name)
@@ -2881,33 +2908,8 @@ static const char *const TAG = "api.service";
cases = list(RECEIVE_CASES.items()) cases = list(RECEIVE_CASES.items())
cases.sort() cases.sort()
hpp += " protected:\n"
hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n"
out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n"
out += " switch (msg_type) {\n"
for i, (case, ifdef, message_name) in cases:
if ifdef is not None:
out += f"#ifdef {ifdef}\n"
c = f" case {message_name}::MESSAGE_TYPE: {{\n"
c += indent(case, " ") + "\n"
c += " }"
out += c + "\n"
if ifdef is not None:
out += "#endif\n"
out += " default:\n"
out += " break;\n"
out += " }\n"
out += "}\n"
cpp += out
hpp += "};\n"
serv = file.service[0] serv = file.service[0]
class_name = "APIServerConnection"
hpp += "\n"
hpp += f"class {class_name} : public {class_name}Base {{\n"
hpp_protected = ""
cpp += "\n"
# Build a mapping of message input types to their authentication requirements # Build a mapping of message input types to their authentication requirements
message_auth_map: dict[str, bool] = {} message_auth_map: dict[str, bool] = {}
@@ -2922,67 +2924,90 @@ static const char *const TAG = "api.service";
message_auth_map[inp] = needs_auth message_auth_map[inp] = needs_auth
message_conn_map[inp] = needs_conn message_conn_map[inp] = needs_conn
# Generate optimized read_message with authentication checking
# Categorize messages by their authentication requirements # Categorize messages by their authentication requirements
no_conn_ids: set[int] = set() no_conn_ids: set[int] = set()
conn_only_ids: set[int] = set() conn_only_ids: set[int] = set()
for id_, (_, _, case_msg_name) in cases: # Build a reverse lookup from message id to message name for auth lookups
if case_msg_name in message_auth_map: id_to_msg_name: dict[int, str] = {}
needs_auth = message_auth_map[case_msg_name] for mt in file.message_type:
needs_conn = message_conn_map[case_msg_name] id_ = get_opt(mt, pb.id)
if id_ is not None and not mt.options.deprecated:
id_to_msg_name[id_] = mt.name
for id_, (_, _, case_label) in cases:
msg_name = id_to_msg_name.get(id_, "")
if msg_name in message_auth_map:
needs_auth = message_auth_map[msg_name]
needs_conn = message_conn_map[msg_name]
if not needs_conn: if not needs_conn:
no_conn_ids.add(id_) no_conn_ids.add(id_)
elif not needs_auth: elif not needs_auth:
conn_only_ids.add(id_) conn_only_ids.add(id_)
# Generate override if we have messages that skip checks # Helper to generate case statements with ifdefs
if no_conn_ids or conn_only_ids: def generate_cases(ids: set[int], comment: str) -> str:
# Helper to generate case statements with ifdefs result = ""
def generate_cases(ids: set[int], comment: str) -> str: for id_ in sorted(ids):
result = "" _, ifdef, case_label = RECEIVE_CASES[id_]
for id_ in sorted(ids): if ifdef:
_, ifdef, msg_name = RECEIVE_CASES[id_] result += f"#ifdef {ifdef}\n"
if ifdef: result += f" case {case_label}: {comment}\n"
result += f"#ifdef {ifdef}\n" if ifdef:
result += f" case {msg_name}::MESSAGE_TYPE: {comment}\n" result += "#endif\n"
if ifdef: return result
result += "#endif\n"
return result
hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n"
cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n"
cpp += " // Check authentication/connection requirements for messages\n"
cpp += " switch (msg_type) {\n"
# Messages that don't need any checks
if no_conn_ids:
cpp += generate_cases(no_conn_ids, "// No setup required")
cpp += " break; // Skip all checks for these messages\n"
# Messages that only need connection setup
if conn_only_ids:
cpp += generate_cases(conn_only_ids, "// Connection setup only")
cpp += " if (!this->check_connection_setup_()) {\n"
cpp += " return; // Connection not setup\n"
cpp += " }\n"
cpp += " break;\n"
cpp += " default:\n"
cpp += " // All other messages require authentication (which includes connection check)\n"
cpp += " if (!this->check_authenticated_()) {\n"
cpp += " return; // Authentication failed\n"
cpp += " }\n"
cpp += " break;\n"
cpp += " }\n\n"
cpp += " // Call base implementation to process the message\n"
cpp += f" {class_name}Base::read_message(msg_size, msg_type, msg_data);\n"
cpp += "}\n"
# Generate read_message with auth check before dispatch
hpp += " protected:\n" hpp += " protected:\n"
hpp += hpp_protected hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n"
out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n"
# Auth check block before dispatch switch
out += " // Check authentication/connection requirements\n"
if no_conn_ids or conn_only_ids:
out += " switch (msg_type) {\n"
if no_conn_ids:
out += generate_cases(no_conn_ids, "// No setup required")
out += " break;\n"
if conn_only_ids:
out += generate_cases(conn_only_ids, "// Connection setup only")
out += " if (!this->check_connection_setup_()) {\n"
out += " return;\n"
out += " }\n"
out += " break;\n"
out += " default:\n"
out += " if (!this->check_authenticated_()) {\n"
out += " return;\n"
out += " }\n"
out += " break;\n"
out += " }\n"
else:
out += " if (!this->check_authenticated_()) {\n"
out += " return;\n"
out += " }\n"
# Dispatch switch
out += " switch (msg_type) {\n"
for i, (case, ifdef, case_label) in cases:
if ifdef is not None:
out += f"#ifdef {ifdef}\n"
c = f" case {case_label}: {{\n"
c += indent(case, " ") + "\n"
c += " }"
out += c + "\n"
if ifdef is not None:
out += "#endif\n"
out += " default:\n"
out += " break;\n"
out += " }\n"
out += "}\n"
cpp += out
hpp += "};\n" hpp += "};\n"
hpp += """\ hpp += """\

View File

@@ -756,6 +756,53 @@ def lint_no_sprintf(fname, match):
) )
@lint_re_check(
# Match std::to_string() or unqualified to_string() calls
# The esphome namespace has "using std::to_string;" so unqualified calls resolve to std::to_string
# Use negative lookbehind for unqualified calls to avoid matching:
# - Function definitions: "const char *to_string(" or "std::string to_string("
# - Method definitions: "Class::to_string("
# - Method calls: ".to_string(" or "->to_string("
# - Other identifiers: "_to_string("
# Also explicitly match std::to_string since : is in the lookbehind
r"(?:(?<![*&.\w>:])to_string|std\s*::\s*to_string)\s*\(" + CPP_RE_EOL,
include=cpp_include,
exclude=[
# Vendored library
"esphome/components/http_request/httplib.h",
# Deprecated helpers that return std::string
"esphome/core/helpers.cpp",
# The using declaration itself
"esphome/core/helpers.h",
# Test fixtures - not production embedded code
"tests/integration/fixtures/*",
],
)
def lint_no_std_to_string(fname, match):
return (
f"{highlight('std::to_string()')} (including unqualified {highlight('to_string()')}) "
f"allocates heap memory. On long-running embedded devices, repeated heap allocations "
f"fragment memory over time.\n"
f"Please use {highlight('snprintf()')} with a stack buffer instead.\n"
f"\n"
f"Buffer sizes and format specifiers (sizes include sign and null terminator):\n"
f" uint8_t: 4 chars - %u (or PRIu8)\n"
f" int8_t: 5 chars - %d (or PRId8)\n"
f" uint16_t: 6 chars - %u (or PRIu16)\n"
f" int16_t: 7 chars - %d (or PRId16)\n"
f" uint32_t: 11 chars - %" + "PRIu32\n"
" int32_t: 12 chars - %" + "PRId32\n"
" uint64_t: 21 chars - %" + "PRIu64\n"
" int64_t: 21 chars - %" + "PRId64\n"
f" float/double: 24 chars - %.8g (15 digits + sign + decimal + e+XXX)\n"
f" 317 chars - %f (for DBL_MAX: 309 int digits + decimal + 6 frac + sign)\n"
f"\n"
f"For sensor values, use value_accuracy_to_buf() from helpers.h.\n"
f'Example: char buf[11]; snprintf(buf, sizeof(buf), "%" PRIu32, value);\n'
f"(If strictly necessary, add `{highlight('// NOLINT')}` to the end of the line)"
)
@lint_re_check( @lint_re_check(
# Match scanf family functions: scanf, sscanf, fscanf, vscanf, vsscanf, vfscanf # Match scanf family functions: scanf, sscanf, fscanf, vscanf, vsscanf, vfscanf
# Also match std:: prefixed versions # Also match std:: prefixed versions

View File

@@ -1,5 +1,8 @@
ld2450: ld2450:
- id: ld2450_radar - id: ld2450_radar
on_data:
then:
- logger.log: "LD2450 Radar Data Received"
button: button:
- platform: ld2450 - platform: ld2450

View File

@@ -0,0 +1,52 @@
display:
- platform: mipi_rgb
spi_id: spi_bus
model: ZX2D10GE01R-V4848
update_interval: 1s
color_order: BGR
draw_rounding: 2
pixel_mode: 18bit
invert_colors: false
use_axis_flips: true
pclk_frequency: 15000000.0
pclk_inverted: true
byte_order: big_endian
hsync_pulse_width: 10
hsync_back_porch: 10
hsync_front_porch: 10
vsync_pulse_width: 2
vsync_back_porch: 12
vsync_front_porch: 14
data_pins:
red:
- number: 10
- number: 16
- number: 9
- number: 15
- number: 46
green:
- number: 8
- number: 13
- number: 18
- number: 12
- number: 11
- number: 17
blue:
- number: 47
- number: 1
- number: 0
- number: 42
- number: 14
de_pin:
number: 39
pclk_pin:
number: 45
hsync_pin:
number: 38
vsync_pin:
number: 48
data_rate: 1000000.0
spi_mode: MODE0
cs_pin:
number: 21
show_test_card: true

View File

@@ -0,0 +1,6 @@
packages:
spi: !include ../../test_build_components/common/spi/esp32-p4-idf.yaml
psram:
<<: !include common.yaml

View File

@@ -4,58 +4,4 @@ packages:
psram: psram:
mode: octal mode: octal
display: <<: !include common.yaml
- platform: mipi_rgb
spi_id: spi_bus
model: ZX2D10GE01R-V4848
update_interval: 1s
color_order: BGR
draw_rounding: 2
pixel_mode: 18bit
invert_colors: false
use_axis_flips: true
pclk_frequency: 15000000.0
pclk_inverted: true
byte_order: big_endian
hsync_pulse_width: 10
hsync_back_porch: 10
hsync_front_porch: 10
vsync_pulse_width: 2
vsync_back_porch: 12
vsync_front_porch: 14
data_pins:
red:
- number: 10
- number: 16
- number: 9
- number: 15
- number: 46
ignore_strapping_warning: true
green:
- number: 8
- number: 13
- number: 18
- number: 12
- number: 11
- number: 17
blue:
- number: 47
- number: 1
- number: 0
ignore_strapping_warning: true
- number: 42
- number: 14
de_pin:
number: 39
pclk_pin:
number: 45
ignore_strapping_warning: true
hsync_pin:
number: 38
vsync_pin:
number: 48
data_rate: 1000000.0
spi_mode: MODE0
cs_pin:
number: 21
show_test_card: true

View File

@@ -13,6 +13,8 @@ esphome:
id: template_water_heater id: template_water_heater
target_temperature: 50.0 target_temperature: 50.0
mode: ECO mode: ECO
away: false
is_on: true
# Templated # Templated
- water_heater.template.publish: - water_heater.template.publish:
@@ -20,6 +22,8 @@ esphome:
current_temperature: !lambda "return 45.0;" current_temperature: !lambda "return 45.0;"
target_temperature: !lambda "return 55.0;" target_temperature: !lambda "return 55.0;"
mode: !lambda "return water_heater::WATER_HEATER_MODE_GAS;" mode: !lambda "return water_heater::WATER_HEATER_MODE_GAS;"
away: !lambda "return true;"
is_on: !lambda "return false;"
# Test C++ API: set_template() with stateless lambda (no captures) # Test C++ API: set_template() with stateless lambda (no captures)
# NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break. # NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break.
@@ -414,6 +418,8 @@ water_heater:
current_temperature: !lambda "return 42.0f;" current_temperature: !lambda "return 42.0f;"
target_temperature: !lambda "return 60.0f;" target_temperature: !lambda "return 60.0f;"
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;" mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
away: !lambda "return false;"
is_on: !lambda "return true;"
supported_modes: supported_modes:
- "OFF" - "OFF"
- ECO - ECO

View File

@@ -29,7 +29,7 @@ class MockUARTComponent : public UARTComponent {
MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override));
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
MOCK_METHOD(int, available, (), (override)); MOCK_METHOD(size_t, available, (), (override));
MOCK_METHOD(void, flush, (), (override)); MOCK_METHOD(void, flush, (), (override));
MOCK_METHOD(void, check_logger_conflict, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override));
}; };

View File

@@ -68,3 +68,24 @@ voice_assistant:
- logger.log: - logger.log:
format: "Voice assistant error - code %s, message: %s" format: "Voice assistant error - code %s, message: %s"
args: [code.c_str(), message.c_str()] args: [code.c_str(), message.c_str()]
on_timer_started:
- logger.log:
format: "Timer started: %s"
args: [timer.id.c_str()]
on_timer_updated:
- logger.log:
format: "Timer updated: %s"
args: [timer.id.c_str()]
on_timer_cancelled:
- logger.log:
format: "Timer cancelled: %s"
args: [timer.id.c_str()]
on_timer_finished:
- logger.log:
format: "Timer finished: %s"
args: [timer.id.c_str()]
on_timer_tick:
- lambda: |-
for (auto &timer : timers) {
ESP_LOGD("timer", "Timer %s: %" PRIu32 "s left", timer.name.c_str(), timer.seconds_left);
}

View File

@@ -58,3 +58,24 @@ voice_assistant:
- logger.log: - logger.log:
format: "Voice assistant error - code %s, message: %s" format: "Voice assistant error - code %s, message: %s"
args: [code.c_str(), message.c_str()] args: [code.c_str(), message.c_str()]
on_timer_started:
- logger.log:
format: "Timer started: %s"
args: [timer.id.c_str()]
on_timer_updated:
- logger.log:
format: "Timer updated: %s"
args: [timer.id.c_str()]
on_timer_cancelled:
- logger.log:
format: "Timer cancelled: %s"
args: [timer.id.c_str()]
on_timer_finished:
- logger.log:
format: "Timer finished: %s"
args: [timer.id.c_str()]
on_timer_tick:
- lambda: |-
for (auto &timer : timers) {
ESP_LOGD("timer", "Timer %s: %" PRIu32 "s left", timer.name.c_str(), timer.seconds_left);
}

Some files were not shown because too many files have changed in this diff Show More