Compare commits

..

39 Commits

Author SHA1 Message Date
Jesse Hills ded0936b2a Merge pull request #15587 from esphome/bump-2026.4.0b1
2026.4.0b1
2026-04-09 13:40:37 +12:00
Jesse Hills ff5ba99d16 Bump version to 2026.4.0b1 2026-04-09 10:39:13 +12:00
Clyde Stubbs 14ec82084b [rpi_dpi_rgb][qspi_dbi] Add deprecation warnings (#15583) 2026-04-09 10:35:09 +12:00
J. Nick Koston 8e02d0a20e [fan] Store preset mode vector on Fan entity to eliminate heap allocation (#15209) 2026-04-09 10:25:37 +12:00
J. Nick Koston faa05031a7 [climate] Store custom mode vectors on Climate entity to eliminate heap allocation (#15206) 2026-04-09 10:25:29 +12:00
J. Nick Koston d4cce142c5 [api] Fix batch messages stuck in Nagle buffer (#15581) 2026-04-08 21:11:31 +00:00
J. Nick Koston 576d89a82a [api] Peel first write iteration, inline socket writes, zero-gap batch encoding (#15063) 2026-04-08 11:05:53 -10:00
J. Nick Koston 4a18ef87d7 [codegen] Fix templatable float type to use cg.float_ (#15568) 2026-04-08 20:23:36 +00:00
Jonathan Swoboda 2cd92a311b [esp32] Capture both cores' backtraces in crash handler (#15559)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-08 20:14:18 +00:00
J. Nick Koston 94f1e48d95 [esp32] Preserve crash data across OTA rollback reboots (#15578) 2026-04-08 16:09:43 -04:00
Jonathan Swoboda 19c8f0ac7a [zephyr] Fix user overlay only emitting first property (#15560) 2026-04-08 09:46:36 -10:00
J. Nick Koston 312dea7ddb [json] Fix heap buffer overflow in SerializationBuffer truncation path (#15566) 2026-04-08 19:46:16 +00:00
Jonathan Swoboda fb0033947c [qspi_dbi] Connect _validate to CONFIG_SCHEMA (#15563) 2026-04-08 09:45:43 -10:00
Jonathan Swoboda 4b8f99ed10 [modbus_controller] Fix output missing address validation and text_sensor division (#15561) 2026-04-08 09:44:19 -10:00
Jonathan Swoboda 4a764ae1e3 [spi] Fix IndexError on invalid RP2040 CLK pin (#15562) 2026-04-08 09:42:47 -10:00
J. Nick Koston 5b840c1662 [codegen] Fix templatable bool type to use cg.bool_ (#15569) 2026-04-08 19:39:12 +00:00
dependabot[bot] 62d84db5a4 Bump CodSpeedHQ/action from 4.13.0 to 4.13.1 (#15577)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 09:38:17 -10:00
J. Nick Koston 019d415bbd [codegen] Fix templatable int type to use cg.int_ (#15571) 2026-04-08 19:37:11 +00:00
Clyde Stubbs 7de060ed55 [lvgl] Fix args for lambda in set_rotation action (#15555) 2026-04-09 07:22:24 +12:00
J. Nick Koston cfa41b3467 [codegen] Add cg.int8 type and fix templatable int8 types (#15573) 2026-04-09 07:20:16 +12:00
J. Nick Koston 0a42a11f1c [at581x] Fix non-templated frequency/power_consumption constants for TemplatableFn (#15576) 2026-04-08 09:10:46 -10:00
J. Nick Koston 063a8ce666 [codegen] Fix templatable uint32 type to use cg.uint32 (#15574) 2026-04-08 09:03:25 -10:00
J. Nick Koston a2bd83382b [codegen] Fix templatable uint8 type to use cg.uint8 (#15572) 2026-04-08 09:00:59 -10:00
J. Nick Koston 869cace2f3 [web_server] Truncate update entity summary to 256 characters (#15570) 2026-04-08 08:59:49 -10:00
J. Nick Koston b83edf6c17 [script] Resolve IncludeFile objects in component config merge (#15575) 2026-04-08 08:57:56 -10:00
J. Nick Koston e1aa92b983 [rotary_encoder] Fix templatable value type to use cg.int32 (#15567) 2026-04-08 14:13:37 -04:00
J. Nick Koston a72609e640 [yaml] Resolve top-level IncludeFile in load_yaml (#15557) 2026-04-08 08:39:14 -04:00
J. Nick Koston a8b7c7a4ac [core] Add TemplatableFn for 4-byte function-pointer templatable storage (#15545)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-08 08:38:00 -04:00
Jonathan Swoboda 9bf53e0ab8 [esp32_hosted] Add SPI transport and SDIO 1-bit bus width support (#15551) 2026-04-08 03:17:58 +00:00
dependabot[bot] 51f3f5c774 Bump esphome-dashboard from 20260210.0 to 20260408.1 (#15552)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 03:08:28 +00:00
Szewcson 313b9fd5bf [gdk101] Retry reset on interval for slow-booting sensor MCU (#11750)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-07 17:05:18 -10:00
J. Nick Koston e658a8559e [ethernet] Add W6100 and W6300 support for RP2040 (#15543) 2026-04-07 16:57:05 -10:00
J. Nick Koston 4db82877af [yaml] Add IncludeFile representer to ESPHomeDumper (#15549) 2026-04-07 16:27:11 -10:00
dependabot[bot] 2e3ff4e215 Bump cryptography from 46.0.6 to 46.0.7 (#15550)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 02:11:51 +00:00
Jonathan Swoboda 8ffe0f5e31 [core] Fix ANSI codes for secret text hiding (#15521) 2026-04-07 22:02:36 -04:00
J. Nick Koston c7513b9262 [ci] Add lint check for test package key matching bus directory (#15547) 2026-04-07 16:01:18 -10:00
J. Nick Koston de7f081799 [emontx] Fix uart package name in tests (#15546) 2026-04-07 21:52:37 -04:00
Clyde Stubbs 88f4067dd6 [lvgl] Implement rotation with PPA (#15453) 2026-04-08 13:19:29 +12:00
Javier Peletier d20d613c1d [substitutions] !include ${filename}, Substitutions in include filename paths (package refactor part 5) (#12213)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-07 15:12:55 -10:00
190 changed files with 3114 additions and 1077 deletions
+1 -1
View File
@@ -339,7 +339,7 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@d872884a306dd4853acf0f584f4b706cf0cc72a2 # v4
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30 # v4
with:
run: ${{ steps.build.outputs.binary }}
mode: simulation
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.4.0-dev
PROJECT_NUMBER = 2026.4.0b1
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -1083,7 +1083,7 @@ def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
# add the console decoration so the front-end can hide the secrets
if not args.show_secrets:
output = re.sub(
r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output
r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[8m\2\\033[28m", output
)
if not CORE.quiet:
safe_print(output)
+1
View File
@@ -79,6 +79,7 @@ from esphome.cpp_types import ( # noqa: F401
float_,
global_ns,
gpio_Flags,
int8,
int16,
int32,
int64,
+1 -1
View File
@@ -97,7 +97,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
async def ags10newi2caddress_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
address = await cg.templatable(config[CONF_ADDRESS], args, int)
address = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
cg.add(var.set_new_address(address))
return var
+1 -1
View File
@@ -43,7 +43,7 @@ async def aic3204_set_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config.get(CONF_MODE), args, int)
template_ = await cg.templatable(config.get(CONF_MODE), args, cg.uint8)
cg.add(var.set_auto_mute_mode(template_))
return var
@@ -19,8 +19,8 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
protected:
sensor::Sensor *sensor_{nullptr};
TemplatableValue<float> upper_threshold_{};
TemplatableValue<float> lower_threshold_{};
TemplatableFn<float> upper_threshold_{};
TemplatableFn<float> lower_threshold_{};
bool raw_state_{false}; // Pre-filter state for hysteresis logic
};
@@ -40,10 +40,10 @@ async def to_code(config):
cg.add(var.set_sensor(sens))
if isinstance(config[CONF_THRESHOLD], dict):
lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], float)
upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], float)
lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], cg.float_)
upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], cg.float_)
else:
lower = await cg.templatable(config[CONF_THRESHOLD], [], float)
lower = await cg.templatable(config[CONF_THRESHOLD], [], cg.float_)
upper = lower
cg.add(var.set_upper_threshold(upper))
cg.add(var.set_lower_threshold(lower))
+38 -59
View File
@@ -315,6 +315,8 @@ void APIConnection::process_active_iterator_() {
this->destroy_active_iterator_();
if (this->flags_.state_subscription) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
} else {
this->finalize_iterator_sync_();
}
} else {
this->process_iterator_batch_(this->iterator_storage_.list_entities);
@@ -322,21 +324,27 @@ void APIConnection::process_active_iterator_() {
} else { // INITIAL_STATE
if (this->iterator_storage_.initial_state.completed()) {
this->destroy_active_iterator_();
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// Now that everything is sent, enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
this->finalize_iterator_sync_();
} else {
this->process_iterator_batch_(this->iterator_storage_.initial_state);
}
}
}
void APIConnection::finalize_iterator_sync_() {
// Flush any remaining batched messages immediately so clients
// receive completion responses (e.g. ListEntitiesDoneResponse)
// without waiting for the batch timer.
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// Enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
}
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
size_t initial_size = this->deferred_batch_.size();
size_t max_batch = this->get_max_batch_size_();
@@ -406,7 +414,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp
#ifdef USE_DEVICES
msg.device_id = entity->get_device_id();
#endif
return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
return encode_to_buffer_slow(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
}
uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
@@ -2005,48 +2013,12 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
}
// Encodes a message to the buffer and returns the total number of bytes used,
// including header and footer overhead. Returns 0 if the message doesn't fit.
uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
APIConnection *conn, uint32_t remaining_size) {
#ifdef HAS_PROTO_MESSAGE_DUMP
if (conn->flags_.log_only_mode) {
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
DumpBuffer dump_buf;
conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
return 1;
}
#endif
// Cache frame sizes to avoid repeated virtual calls
const uint8_t header_padding = conn->helper_->frame_header_padding();
const uint8_t footer_size = conn->helper_->frame_footer_size();
// encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE)
// Calculate total size with padding for buffer allocation
size_t total_calculated_size = calculated_size + header_padding + footer_size;
// Check if it fits
if (total_calculated_size > remaining_size)
return 0; // Doesn't fit
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
size_t to_add;
if (conn->flags_.batch_first_message) {
// First message - buffer already prepared by caller, just clear flag
conn->flags_.batch_first_message = false;
to_add = calculated_size;
} else {
// Batch message second or later
// Reserve for full message, resize to include footer gap + header padding + payload
to_add = total_calculated_size;
}
shared_buf.resize(shared_buf.size() + to_add);
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
// Return total size (header + payload + footer)
return static_cast<uint16_t>(total_calculated_size);
// Noinline version for cold paths — single shared copy
uint16_t APIConnection::encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
APIConnection *conn, uint32_t remaining_size) {
return encode_to_buffer(calculated_size, encode_fn, msg, conn, remaining_size);
}
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
@@ -2173,17 +2145,15 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
"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
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
MessageInfo *message_info = reinterpret_cast<MessageInfo *>(message_info_storage);
size_t items_processed = 0;
uint16_t remaining_size = std::numeric_limits<uint16_t>::max();
// Track where each message's header padding begins in the buffer
// For plaintext: this is where the 6-byte header padding starts
// For noise: this is where the 7-byte header padding starts
// The actual message data follows after the header padding
// Track where each message's header begins in the buffer
// First message: offset 0 (max padding, may have unused leading bytes)
// Subsequent messages: offset points to exact header start (no gaps)
uint32_t current_offset = 0;
// Process items and encode directly to buffer (up to our limit)
@@ -2199,13 +2169,14 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
}
// Message was encoded successfully
// payload_size is header_padding + actual payload size + footer_size
uint16_t proto_payload_size = payload_size - frame_overhead;
// payload_size = header_size + proto_payload_size + footer_size
uint16_t proto_payload_size = payload_size - this->batch_header_size_ - footer_size;
// Use placement new to construct MessageInfo in pre-allocated stack array
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
// Explicit destruction is not needed because MessageInfo is trivially destructible,
// as ensured by the static_assert in its definition.
new (&message_info[items_processed++]) MessageInfo(item.message_type, current_offset, proto_payload_size);
new (&message_info[items_processed++])
MessageInfo(item.message_type, current_offset, proto_payload_size, this->batch_header_size_);
// After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation
if (items_processed == 1) {
remaining_size = MAX_BATCH_PACKET_SIZE;
@@ -2222,6 +2193,13 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
shared_buf.resize(shared_buf.size() + footer_size);
}
// Ensure TCP_NODELAY is on before writing batch data.
// Log messages enable Nagle (NODELAY off) to coalesce small packets.
// Without this, batch data written to the socket sits in LWIP's Nagle
// buffer — the remote won't ACK until it sends its own data (e.g. a
// ping), which can take 20+ seconds.
this->helper_->set_nodelay_for_message(false);
// Send all collected messages
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
@@ -2255,6 +2233,7 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size,
bool batch_first) {
this->flags_.batch_first_message = batch_first;
this->batch_message_type_ = item.message_type;
#ifdef USE_EVENT
// Events need aux_data_index to look up event type from entity
if (item.message_type == EventResponse::MESSAGE_TYPE) {
+58 -8
View File
@@ -276,6 +276,7 @@ class APIConnection final : public APIServerConnectionBase {
App.schedule_dump_config();
#ifdef USE_ESP32_CRASH_HANDLER
esp32::crash_handler_log();
esp32::crash_handler_clear();
#endif
#ifdef USE_RP2040_CRASH_HANDLER
rp2040::crash_handler_log();
@@ -410,16 +411,59 @@ class APIConnection final : public APIServerConnectionBase {
// Non-template buffer management for send_message
bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
// Non-template buffer management for batch encoding
static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
APIConnection *conn, uint32_t remaining_size);
// Core batch encoding logic. Computes header size, checks fit, resizes buffer, encodes.
// ALWAYS_INLINE so the compiler can devirtualize encode_fn at hot call sites.
static inline uint16_t ESPHOME_ALWAYS_INLINE encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn,
const void *msg, APIConnection *conn,
uint32_t remaining_size) {
#ifdef HAS_PROTO_MESSAGE_DUMP
if (conn->flags_.log_only_mode) {
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
DumpBuffer dump_buf;
conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
return 1;
}
#endif
const uint8_t footer_size = conn->helper_->frame_footer_size();
// Thin template wrapper — computes size, delegates buffer work to non-template helper
// First message uses max padding (already in buffer), subsequent use exact header size
size_t to_add;
if (conn->flags_.batch_first_message) {
conn->flags_.batch_first_message = false;
conn->batch_header_size_ = conn->helper_->frame_header_padding();
to_add = calculated_size;
} else {
conn->batch_header_size_ = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_);
to_add = calculated_size + conn->batch_header_size_ + footer_size;
}
// Check if it fits (using actual header size, not max padding)
uint16_t total_calculated_size = calculated_size + conn->batch_header_size_ + footer_size;
if (total_calculated_size > remaining_size)
return 0;
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
shared_buf.resize(shared_buf.size() + to_add);
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
return total_calculated_size;
}
// Noinline version of encode_to_buffer for cold paths (entity info, zero-payload messages).
// All cold callers share this single copy instead of each getting an ALWAYS_INLINE expansion.
static uint16_t encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
APIConnection *conn, uint32_t remaining_size);
// Thin template wrapper — uses noinline encode_to_buffer_slow since
// encode_message_to_buffer callers are cold paths (zero-payload control messages).
// Hot paths (state/info) go through fill_and_encode_entity_state/info instead.
// batch_message_type_ is already set by dispatch_message_ before reaching here.
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
if constexpr (T::ESTIMATED_SIZE == 0) {
return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size);
return encode_to_buffer_slow(0, &encode_msg_noop, &msg, conn, remaining_size);
} else {
return encode_to_buffer(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
return encode_to_buffer_slow(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
}
}
@@ -618,6 +662,7 @@ class APIConnection final : public APIServerConnectionBase {
// Helper methods for iterator lifecycle management
void destroy_active_iterator_();
void begin_iterator_(ActiveIterator type);
void finalize_iterator_sync_();
#ifdef USE_CAMERA
std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif
@@ -734,9 +779,14 @@ class APIConnection final : public APIServerConnectionBase {
// 2-byte types immediately after flags_ (no padding between them)
uint16_t client_api_version_major_{0};
uint16_t client_api_version_minor_{0};
// 1-byte type to fill padding
// 1-byte types to fill remaining space before next 4-byte boundary
ActiveIterator active_iterator_{ActiveIterator::NONE};
// Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary
uint8_t batch_message_type_{0}; // Current message type during batch encoding
// Total: 2 (flags) + 2 + 2 + 1 + 1 = 8 bytes, aligned to 4-byte boundary
// Actual header size used by encode_to_buffer for the current message.
// Read by process_batch_multi_ to pass into MessageInfo.
uint8_t batch_header_size_{0};
uint32_t get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); }
// Message will use 8 more bytes than the minimum size, and typical
+49 -29
View File
@@ -100,10 +100,17 @@ const LogString *api_error_to_logstr(APIError err) {
return LOG_STR("UNKNOWN");
}
#ifdef HELPER_LOG_PACKETS
void APIFrameHelper::log_packet_sending_(const void *data, uint16_t len) {
LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
}
#endif
APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
if (err != EWOULDBLOCK && err != EAGAIN) {
this->state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
@@ -111,45 +118,58 @@ APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
return APIError::OK;
}
// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full.
// Returns OK if all data was sent or successfully queued.
// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED).
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
// Single-buffer write path: wraps in iovec and delegates.
APIError APIFrameHelper::write_raw_buf_(const void *data, uint16_t len, ssize_t sent) {
struct iovec iov = {const_cast<void *>(data), len};
APIError err = this->write_raw_iov_(&iov, 1, len, sent);
#ifdef HELPER_LOG_PACKETS
for (int i = 0; i < iovcnt; i++) {
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
}
// Log after write/enqueue so re-entrant log sends can't corrupt data before it's sent
if (err == APIError::OK)
LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
#endif
return err;
}
uint16_t skip = 0;
// Drain any existing backlog first
if (!this->overflow_buf_.empty()) [[unlikely]] {
APIError err = this->drain_overflow_and_handle_errors_();
if (err != APIError::OK)
return err;
}
// If backlog is clear, try direct send
if (this->overflow_buf_.empty()) [[likely]] {
ssize_t sent =
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
if (sent == -1) [[unlikely]] {
// Handles partial writes, errors, and overflow buffering.
// Called when the inline fast path couldn't complete the write,
// or directly from cold paths (handshake, error handling).
APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent) {
if (sent <= 0) {
if (sent == WRITE_NOT_ATTEMPTED) {
// Cold path: no write attempted yet, drain overflow and try
if (!this->overflow_buf_.empty()) {
APIError err = this->drain_overflow_and_handle_errors_();
if (err != APIError::OK)
return err;
}
if (this->overflow_buf_.empty()) {
sent = this->write_iov_to_socket_(iov, iovcnt);
if (sent == static_cast<ssize_t>(total_write_len))
return APIError::OK;
// Partial write or -1: fall through to error check / enqueue below
} else {
// Overflow backlog remains after drain; skip socket write, enqueue everything
sent = 0;
}
}
// WRITE_FAILED (-1): fast path or retry write returned -1, check errno
if (sent == WRITE_FAILED) {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
if (err != EWOULDBLOCK && err != EAGAIN) {
this->state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
} else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] {
return APIError::OK;
} else {
skip = static_cast<uint16_t>(sent);
sent = 0; // Treat WOULD_BLOCK as zero bytes sent
}
}
// Full write completed (possible when called directly, not via write_raw_fast_buf_)
if (sent == static_cast<ssize_t>(total_write_len))
return APIError::OK;
// Queue unsent data into overflow buffer
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) {
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent))) {
HELPER_LOG("Overflow buffer full, dropping connection");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
+63 -22
View File
@@ -49,12 +49,17 @@ struct ReadPacketBuffer {
};
// Packed message info structure to minimize memory usage
// Note: message_type is uint8_t — all current protobuf message types fit in 8 bits.
// The noise wire format encodes types as 16-bit, but the high byte is always 0.
// If message types ever exceed 255, this and encrypt_noise_message_ must be updated.
struct MessageInfo {
uint16_t offset; // Offset in buffer where message starts
uint16_t payload_size; // Size of the message payload
uint8_t message_type; // Message type (0-255)
uint8_t header_size; // Actual header size used (avoids recomputation in write path)
MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
MessageInfo(uint8_t type, uint16_t off, uint16_t size, uint8_t hdr)
: offset(off), payload_size(size), message_type(type), header_size(hdr) {}
};
enum class APIError : uint16_t {
@@ -161,20 +166,33 @@ class APIFrameHelper {
this->nodelay_counter_ = 0;
}
}
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize buffer to include footer space if needed (e.g. Noise MAC)
if (frame_footer_size_)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
MessageInfo msg{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
}
// Write multiple protobuf messages in a single operation
// messages contains (message_type, offset, length) for each message in the buffer
// The buffer contains all messages with appropriate padding before each
// Write a single protobuf message - the hot path (87-100% of all writes).
// Caller must ensure state is DATA before calling.
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf messages in a single batched operation.
// Caller must ensure state is DATA and messages is not empty.
// messages contains (message_type, offset, length) for each message in the buffer.
// The buffer contains all messages with appropriate padding before each.
virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0;
// Get the frame header padding required by this protocol
// Get the maximum frame header padding required by this protocol (worst case)
uint8_t frame_header_padding() const { return frame_header_padding_; }
// Get the actual frame header size for a specific message.
// For noise: always returns frame_header_padding_ (fixed 7-byte header).
// For plaintext: computes actual size from varint lengths (3-6 bytes).
// Distinguishes protocols via frame_footer_size_ (noise always has a non-zero MAC
// footer, plaintext has footer=0). If a protocol with a plaintext footer is ever
// added, this should become a virtual method.
uint8_t frame_header_size(uint16_t payload_size, uint8_t message_type) const {
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
return this->frame_footer_size_
? this->frame_header_padding_
: static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
#elif defined(USE_API_NOISE)
return this->frame_header_padding_;
#else // USE_API_PLAINTEXT only
return static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
#endif
}
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() const { return frame_footer_size_; }
// Check if socket has data ready to read
@@ -196,18 +214,41 @@ class APIFrameHelper {
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
APIError drain_overflow_and_handle_errors_();
// Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
// Sentinel values for the sent parameter in write_raw_ methods
static constexpr ssize_t WRITE_FAILED = -1; // Fast path: write()/writev() returned -1
static constexpr ssize_t WRITE_NOT_ATTEMPTED = -2; // Cold path: no write attempted yet
// Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN).
// Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors.
APIError check_socket_write_err_(int err) {
if (err == EWOULDBLOCK || err == EAGAIN)
return APIError::WOULD_BLOCK;
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
// Dispatch to write() or writev() based on iovec count
inline ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt) {
return (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
}
// Inlined write methods — used by hot paths (write_protobuf_packet, write_protobuf_messages)
// These inline the fast path (overflow empty + full write) and tail-call the out-of-line
// slow path only on failure/partial write.
inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) {
if (this->overflow_buf_.empty()) [[likely]] {
ssize_t sent = this->socket_->write(data, len);
if (sent == static_cast<ssize_t>(len)) [[likely]] {
#ifdef HELPER_LOG_PACKETS
this->log_packet_sending_(data, len);
#endif
return APIError::OK;
}
// sent is -1 (WRITE_FAILED) or partial write count
return this->write_raw_buf_(data, len, sent);
}
return this->write_raw_buf_(data, len, WRITE_NOT_ATTEMPTED);
}
// Out-of-line write paths: handle partial writes, errors, overflow buffering
// sent: WRITE_NOT_ATTEMPTED (cold path), WRITE_FAILED (fast path write returned -1), or bytes sent (partial write)
APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent = WRITE_NOT_ATTEMPTED);
APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
ssize_t sent = WRITE_NOT_ATTEMPTED);
#ifdef HELPER_LOG_PACKETS
void log_packet_sending_(const void *data, uint16_t len);
#endif
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;
@@ -47,15 +47,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
format_hex_pretty_to(hex_buf_, (buffer).data(), \
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_SENDING(data, len) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Sending raw: %s", \
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
} while (0)
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
#endif
/// Convert a noise error code to a readable error
@@ -464,65 +457,83 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = type;
return APIError::OK;
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
APIError aerr = this->check_data_state_();
// Encrypt a single noise message in place and return the encrypted frame length.
// Returns APIError::OK on success.
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
uint16_t &encrypted_len_out) {
// Write noise header
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set after encryption
// Write message header (to be encrypted)
constexpr uint8_t msg_offset = 3;
buf_start[msg_offset] = static_cast<uint8_t>(message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(payload_size >> 8); // data_len high byte
buf_start[msg_offset + 3] = static_cast<uint8_t>(payload_size); // data_len low byte
// payload data is already in the buffer starting at offset + 7
// Encrypt the message in place
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + payload_size, 4 + payload_size + this->frame_footer_size_);
int err = noise_cipherstate_encrypt(this->send_cipher_, &mbuf);
APIError aerr =
this->handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED);
if (aerr != APIError::OK)
return aerr;
if (messages.empty()) {
return APIError::OK;
}
// Fill in the encrypted size
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
encrypted_len_out = static_cast<uint16_t>(3 + mbuf.size); // indicator + size + encrypted data
return APIError::OK;
}
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
#ifdef ESPHOME_DEBUG_API
assert(this->state_ == State::DATA);
#endif
// Resize buffer to include footer space for Noise MAC
if (this->frame_footer_size_)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + this->frame_footer_size_);
uint16_t payload_size =
static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING - this->frame_footer_size_);
uint8_t *buf_start = buffer.get_buffer()->data();
uint16_t encrypted_len;
APIError aerr = this->encrypt_noise_message_(buf_start, payload_size, type, encrypted_len);
if (aerr != APIError::OK)
return aerr;
return this->write_raw_fast_buf_(buf_start, encrypted_len);
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
#ifdef ESPHOME_DEBUG_API
assert(this->state_ == State::DATA);
assert(!messages.empty());
#endif
// Noise messages are already contiguous in the buffer:
// HEADER_PADDING (7) exactly matches the fixed header size, and
// footer space (16) is consumed by the encryption MAC.
uint8_t *buffer_data = buffer.get_buffer()->data();
// Stack-allocated iovec array - no heap allocation
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
uint8_t *write_start = buffer_data + messages[0].offset;
uint16_t total_write_len = 0;
// We need to encrypt each message in place
for (const auto &msg : messages) {
// The buffer already has padding at offset
uint8_t *buf_start = buffer_data + msg.offset;
// Write noise header
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set after encryption
// Write message header (to be encrypted)
constexpr uint8_t msg_offset = 3;
buf_start[msg_offset] = static_cast<uint8_t>(msg.message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(msg.message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(msg.payload_size >> 8); // data_len high byte
buf_start[msg_offset + 3] = static_cast<uint8_t>(msg.payload_size); // data_len low byte
// payload data is already in the buffer starting at offset + 7
// Make sure we have space for MAC
// The buffer should already have been sized appropriately
// Encrypt the message in place
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + msg.payload_size,
4 + msg.payload_size + frame_footer_size_);
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
APIError aerr =
handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED);
uint16_t encrypted_len;
APIError aerr = this->encrypt_noise_message_(buf_start, msg.payload_size, msg.message_type, encrypted_len);
if (aerr != APIError::OK)
return aerr;
// Fill in the encrypted size
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
// Add iovec for this encrypted message
size_t msg_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
iovs.push_back({buf_start, msg_len});
total_write_len += msg_len;
total_write_len += encrypted_len;
}
// Send all encrypted messages in one writev call
return this->write_raw_(iovs.data(), iovs.size(), total_write_len);
return this->write_raw_fast_buf_(write_start, total_write_len);
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
@@ -531,16 +542,16 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
header[1] = (uint8_t) (len >> 8);
header[2] = (uint8_t) len;
if (len == 0) {
return this->write_raw_buf_(header, 3);
}
struct iovec iov[2];
iov[0].iov_base = header;
iov[0].iov_len = 3;
if (len == 0) {
return this->write_raw_(iov, 1, 3); // Just header
}
iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len;
return this->write_raw_(iov, 2, 3 + len); // Header + data
return this->write_raw_iov_(iov, 2, 3 + len);
}
/** Initiate the data structures for the handshake.
@@ -606,7 +617,7 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
if (aerr != APIError::OK)
return aerr;
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
@@ -9,19 +9,22 @@ namespace esphome::api {
class APINoiseFrameHelper final : public APIFrameHelper {
public:
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
: APIFrameHelper(std::move(socket)), ctx_(ctx) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
frame_header_padding_ = 7;
frame_header_padding_ = HEADER_PADDING;
}
~APINoiseFrameHelper() override;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
@@ -33,6 +36,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
APIError state_action_handshake_write_();
APIError try_read_frame_();
APIError write_frame_(const uint8_t *data, uint16_t len);
APIError encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
uint16_t &encrypted_len_out);
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const LogString *reason);
@@ -39,15 +39,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
format_hex_pretty_to(hex_buf_, (buffer).data(), \
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_SENDING(data, len) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Sending raw: %s", \
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
} while (0)
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
#endif
/// Initialize the frame helper, returns OK if successful.
@@ -205,7 +198,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
// Make sure to tell the remote that we don't
// understand the indicator byte so it knows
// we do not support it.
struct iovec iov[1];
// The \x00 first byte is the marker for plaintext.
//
// The remote will know how to handle the indicator byte,
@@ -220,14 +212,12 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
"Bad indicator byte";
char msg[INDICATOR_MSG_SIZE];
memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE);
iov[0].iov_base = (void *) msg;
this->write_raw_buf_(msg, INDICATOR_MSG_SIZE);
#else
static const char MSG[] = "\x00"
"Bad indicator byte";
iov[0].iov_base = (void *) MSG;
this->write_raw_buf_(MSG, INDICATOR_MSG_SIZE);
#endif
iov[0].iov_len = INDICATOR_MSG_SIZE;
this->write_raw_(iov, 1, INDICATOR_MSG_SIZE);
}
return aerr;
}
@@ -237,73 +227,101 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = this->rx_header_parsed_type_;
return APIError::OK;
}
// Encode a 16-bit varint (1-3 bytes) using pre-computed length.
ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_t varint_len, uint8_t *p) {
if (varint_len >= 2) {
*p++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
if (varint_len == 3) {
*p++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
}
}
*p = static_cast<uint8_t>(value);
}
// Encode an 8-bit varint (1-2 bytes) using pre-computed length.
ESPHOME_ALWAYS_INLINE static inline void encode_varint_8(uint8_t value, uint8_t varint_len, uint8_t *p) {
if (varint_len == 2) {
*p++ = static_cast<uint8_t>(value | 0x80);
*p = static_cast<uint8_t>(value >> 7);
} else {
*p = value;
}
}
// Write plaintext header into pre-allocated padding before payload.
// padding_size: bytes reserved before payload (HEADER_PADDING for first/single msg,
// actual header size for contiguous batch messages).
// Returns the total header length (indicator + varints).
ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, uint16_t payload_size,
uint8_t message_type, uint8_t padding_size) {
uint8_t size_varint_len = ProtoSize::varint16(payload_size);
uint8_t type_varint_len = ProtoSize::varint8(message_type);
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// The header is right-justified within the padding so it sits immediately before payload.
//
// Single/first message (padding_size = HEADER_PADDING = 6):
// Example (small, header=3): [0-2] unused | [3] 0x00 | [4] size | [5] type | [6...] payload
// Example (medium, header=4): [0-1] unused | [2] 0x00 | [3-4] size | [5] type | [6...] payload
// Example (large, header=6): [0] 0x00 | [1-3] size | [4-5] type | [6...] payload
//
// Batch messages 2+ (padding_size = actual header size, no unused bytes):
// Example (small, header=3): [0] 0x00 | [1] size | [2] type | [3...] payload
// Example (medium, header=4): [0] 0x00 | [1-2] size | [3] type | [4...] payload
#ifdef ESPHOME_DEBUG_API
assert(padding_size >= total_header_len);
#endif
uint32_t header_offset = padding_size - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode varints directly into buffer using pre-computed lengths
encode_varint_16(payload_size, size_varint_len, buf_start + header_offset + 1);
encode_varint_8(message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len);
return total_header_len;
}
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
#ifdef ESPHOME_DEBUG_API
assert(this->state_ == State::DATA);
#endif
uint16_t payload_size = static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING);
uint8_t *buffer_data = buffer.get_buffer()->data();
uint8_t header_len = write_plaintext_header(buffer_data, payload_size, type, HEADER_PADDING);
return this->write_raw_fast_buf_(buffer_data + HEADER_PADDING - header_len,
static_cast<uint16_t>(header_len + payload_size));
}
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
std::span<const MessageInfo> messages) {
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
if (messages.empty()) {
return APIError::OK;
}
#ifdef ESPHOME_DEBUG_API
assert(this->state_ == State::DATA);
assert(!messages.empty());
#endif
uint8_t *buffer_data = buffer.get_buffer()->data();
// Stack-allocated iovec array - no heap allocation
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
uint16_t total_write_len = 0;
// First message has max padding (header_size = HEADER_PADDING), may have unused leading bytes.
// Subsequent messages were encoded with exact header sizes (header_size = actual header len).
// write_plaintext_header right-justifies the header within header_size bytes of padding.
const auto &first = messages[0];
uint8_t *first_start = buffer_data + first.offset;
uint8_t header_len = write_plaintext_header(first_start, first.payload_size, first.message_type, HEADER_PADDING);
uint8_t *write_start = first_start + HEADER_PADDING - header_len;
uint16_t total_len = header_len + first.payload_size;
for (const auto &msg : messages) {
// Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead
uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE
? 1
: (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3);
uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2;
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// Calculate where to start writing the header
// The header starts at the latest possible position to minimize unused padding
//
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
// [0-2] - Unused padding
// [3] - 0x00 indicator byte
// [4] - Payload size varint (1 byte, for sizes 0-127)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
// [0-1] - Unused padding
// [2] - 0x00 indicator byte
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
// [0] - 0x00 indicator byte
// [1-3] - Payload size varint (3 bytes, for sizes 16384-65535)
// [4-5] - Message type varint (2 bytes, for types 128-16383)
// [6...] - Actual payload data
//
// The message starts at offset + frame_header_padding_
// So we write the header starting at offset + frame_header_padding_ - total_header_len
uint8_t *buf_start = buffer_data + msg.offset;
uint32_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode varints directly into buffer
encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
// Add iovec for this message (header + payload)
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
iovs.push_back({buf_start + header_offset, msg_len});
total_write_len += msg_len;
for (size_t i = 1; i < messages.size(); i++) {
const auto &msg = messages[i];
header_len = write_plaintext_header(buffer_data + msg.offset, msg.payload_size, msg.message_type, msg.header_size);
total_len += header_len + msg.payload_size;
}
// Send all messages in one writev call
return write_raw_(iovs.data(), iovs.size(), total_write_len);
return this->write_raw_fast_buf_(write_start, total_len);
}
} // namespace esphome::api
@@ -7,18 +7,21 @@ namespace esphome::api {
class APIPlaintextFrameHelper final : public APIFrameHelper {
public:
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
static constexpr uint8_t HEADER_PADDING = 1 + 3 + 2; // indicator + size varint + type varint
explicit APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
frame_header_padding_ = 6;
frame_header_padding_ = HEADER_PADDING;
}
~APIPlaintextFrameHelper() override = default;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
+11
View File
@@ -645,6 +645,17 @@ class ProtoSize {
static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152
static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456
// Varint encoded length for a 16-bit value (1, 2, or 3 bytes).
// Fully inline — no slow path call for values >= 128.
static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint16(uint16_t value) {
return value < VARINT_THRESHOLD_1_BYTE ? 1 : (value < VARINT_THRESHOLD_2_BYTE ? 2 : 3);
}
// Varint encoded length for an 8-bit value (1 or 2 bytes).
static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint8(uint8_t value) {
return value < VARINT_THRESHOLD_1_BYTE ? 1 : 2;
}
/**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
*
+1 -1
View File
@@ -275,7 +275,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
protected:
APIServer *parent_;
TemplatableValue<bool, Ts...> success_{true};
TemplatableFn<bool, Ts...> success_{[](Ts...) -> bool { return true; }};
TemplatableValue<std::string, Ts...> error_message_{""};
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
std::function<void(Ts..., JsonObject)> json_builder_;
+9 -11
View File
@@ -169,18 +169,17 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
# Radar configuration
if frontend_reset := config.get(CONF_HW_FRONTEND_RESET):
template_ = await cg.templatable(frontend_reset, args, int)
template_ = await cg.templatable(frontend_reset, args, cg.int8)
cg.add(var.set_hw_frontend_reset(template_))
if freq := config.get(CONF_FREQUENCY):
if cg.is_template(freq):
template_ = await cg.templatable(freq, args, cg.int32)
else:
template_ = int(freq / 1000000)
if not cg.is_template(freq):
freq = int(freq / 1000000)
template_ = await cg.templatable(freq, args, cg.int_)
cg.add(var.set_frequency(template_))
if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None:
template_ = await cg.templatable(sens_dist, args, int)
template_ = await cg.templatable(sens_dist, args, cg.int_)
cg.add(var.set_sensing_distance(template_))
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
@@ -200,14 +199,13 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
cg.add(var.set_trigger_keep(template_))
if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None:
template_ = await cg.templatable(stage_gain, args, int)
template_ = await cg.templatable(stage_gain, args, cg.int_)
cg.add(var.set_stage_gain(template_))
if power := config.get(CONF_POWER_CONSUMPTION):
if cg.is_template(power):
template_ = await cg.templatable(power, args, cg.int32)
else:
template_ = int(power * 1000000)
if not cg.is_template(power):
power = int(power * 1000000)
template_ = await cg.templatable(power, args, cg.int_)
cg.add(var.set_power_consumption(template_))
return var
+1 -1
View File
@@ -32,7 +32,7 @@ async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config.get(CONF_MIC_GAIN), args, float)
template_ = await cg.templatable(config.get(CONF_MIC_GAIN), args, cg.float_)
cg.add(var.set_mic_gain(template_))
return var
+1 -1
View File
@@ -52,7 +52,7 @@ async def audio_dac_set_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config.get(CONF_VOLUME), args, float)
template_ = await cg.templatable(config.get(CONF_VOLUME), args, cg.float_)
cg.add(var.set_volume(template_))
return var
@@ -61,6 +61,15 @@ void BedJetClimate::dump_config() {
}
void BedJetClimate::setup() {
// Set custom modes once during setup — stored on Climate base class, wired via get_traits()
this->set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
this->set_supported_custom_presets({
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
"M1",
"M2",
"M3",
});
// restore set points
auto restore = this->restore_state_();
if (restore.has_value()) {
@@ -42,21 +42,14 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
climate::CLIMATE_MODE_DRY,
});
// It would be better if we had a slider for the fan modes.
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
traits.set_supported_presets({
// If we support NONE, then have to decide what happens if the user switches to it (turn off?)
// climate::CLIMATE_PRESET_NONE,
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
climate::CLIMATE_PRESET_BOOST,
});
// String literals are stored in rodata and valid for program lifetime
traits.set_supported_custom_presets({
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
"M1",
"M2",
"M3",
});
// Custom fan modes and presets are set once in setup(), stored on Climate base class,
// and wired automatically via get_traits()
traits.set_visual_min_temperature(19.0);
traits.set_visual_max_temperature(43.0);
traits.set_visual_temperature_step(1.0);
+6 -6
View File
@@ -36,7 +36,7 @@ class TimeoutFilter : public Filter, public Component {
template<typename T> void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; }
protected:
TemplatableValue<uint32_t> timeout_delay_{};
TemplatableFn<uint32_t> timeout_delay_{};
};
class DelayedOnOffFilter final : public Filter, public Component {
@@ -49,8 +49,8 @@ class DelayedOnOffFilter final : public Filter, public Component {
template<typename T> void set_off_delay(T delay) { this->off_delay_ = delay; }
protected:
TemplatableValue<uint32_t> on_delay_{};
TemplatableValue<uint32_t> off_delay_{};
TemplatableFn<uint32_t> on_delay_{};
TemplatableFn<uint32_t> off_delay_{};
};
class DelayedOnFilter : public Filter, public Component {
@@ -62,7 +62,7 @@ class DelayedOnFilter : public Filter, public Component {
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected:
TemplatableValue<uint32_t> delay_{};
TemplatableFn<uint32_t> delay_{};
};
class DelayedOffFilter : public Filter, public Component {
@@ -74,7 +74,7 @@ class DelayedOffFilter : public Filter, public Component {
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected:
TemplatableValue<uint32_t> delay_{};
TemplatableFn<uint32_t> delay_{};
};
class InvertFilter : public Filter {
@@ -155,7 +155,7 @@ class SettleFilter : public Filter, public Component {
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected:
TemplatableValue<uint32_t> delay_{};
TemplatableFn<uint32_t> delay_{};
bool steady_{true};
};
+4 -5
View File
@@ -423,11 +423,10 @@ def _register_setter_actions():
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
data = config[CONF_VALUE]
if cg.is_template(data):
templ_ = await cg.templatable(data, args, _type)
cg.add(getattr(var, _setter)(templ_))
else:
cg.add(getattr(var, _setter)(_map[data] if _map else data))
if _map and not cg.is_template(data):
data = _map[data]
templ_ = await cg.templatable(data, args, _type)
cg.add(getattr(var, _setter)(templ_))
return var
automation.register_action(
+4 -4
View File
@@ -488,16 +488,16 @@ async def climate_control_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(mode, args, ClimateMode)
cg.add(var.set_mode(template_))
if (target_temp := config.get(CONF_TARGET_TEMPERATURE)) is not None:
template_ = await cg.templatable(target_temp, args, float)
template_ = await cg.templatable(target_temp, args, cg.float_)
cg.add(var.set_target_temperature(template_))
if (target_temp_low := config.get(CONF_TARGET_TEMPERATURE_LOW)) is not None:
template_ = await cg.templatable(target_temp_low, args, float)
template_ = await cg.templatable(target_temp_low, args, cg.float_)
cg.add(var.set_target_temperature_low(template_))
if (target_temp_high := config.get(CONF_TARGET_TEMPERATURE_HIGH)) is not None:
template_ = await cg.templatable(target_temp_high, args, float)
template_ = await cg.templatable(target_temp_high, args, cg.float_)
cg.add(var.set_target_temperature_high(template_))
if (target_humidity := config.get(CONF_TARGET_HUMIDITY)) is not None:
template_ = await cg.templatable(target_humidity, args, float)
template_ = await cg.templatable(target_humidity, args, cg.float_)
cg.add(var.set_target_humidity(template_))
if (fan_mode := config.get(CONF_FAN_MODE)) is not None:
template_ = await cg.templatable(fan_mode, args, ClimateFanMode)
+16 -5
View File
@@ -484,6 +484,11 @@ void Climate::publish_state() {
ClimateTraits Climate::get_traits() {
auto traits = this->traits();
// Wire custom mode pointers from Climate-owned storage
if (this->supported_custom_fan_modes_)
traits.set_supported_custom_fan_modes_(this->supported_custom_fan_modes_);
if (this->supported_custom_presets_)
traits.set_supported_custom_presets_(this->supported_custom_presets_);
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
if (!std::isnan(this->visual_min_temperature_override_)) {
traits.set_visual_min_temperature(this->visual_min_temperature_override_);
@@ -681,9 +686,8 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) {
}
bool Climate::set_custom_fan_mode_(const char *mode, size_t len) {
auto traits = this->get_traits();
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode,
traits.find_custom_fan_mode_(mode, len), this->has_custom_fan_mode());
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, this->find_custom_fan_mode_(mode, len),
this->has_custom_fan_mode());
}
void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; }
@@ -691,8 +695,7 @@ void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; }
bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); }
bool Climate::set_custom_preset_(const char *preset, size_t len) {
auto traits = this->get_traits();
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset, len),
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, this->find_custom_preset_(preset, len),
this->has_custom_preset());
}
@@ -703,6 +706,10 @@ const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
}
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode, size_t len) {
if (this->supported_custom_fan_modes_) {
return vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len);
}
// Fallback for deprecated path: external components may set modes on ClimateTraits directly
return this->get_traits().find_custom_fan_mode_(custom_fan_mode, len);
}
@@ -711,6 +718,10 @@ const char *Climate::find_custom_preset_(const char *custom_preset) {
}
const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) {
if (this->supported_custom_presets_) {
return vector_find(*this->supported_custom_presets_, custom_preset, len);
}
// Fallback for deprecated path: external components may set modes on ClimateTraits directly
return this->get_traits().find_custom_preset_(custom_preset, len);
}
+44 -3
View File
@@ -1,5 +1,6 @@
#pragma once
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
@@ -234,6 +235,28 @@ class Climate : public EntityBase {
void set_visual_max_humidity_override(float visual_max_humidity_override);
#endif
/// Set the supported custom fan modes (stored on Climate, referenced by ClimateTraits).
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
this->ensure_custom_fan_modes_().assign(modes.begin(), modes.end());
}
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
this->ensure_custom_fan_modes_() = modes;
}
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->ensure_custom_fan_modes_().assign(modes, modes + N);
}
/// Set the supported custom presets (stored on Climate, referenced by ClimateTraits).
void set_supported_custom_presets(std::initializer_list<const char *> presets) {
this->ensure_custom_presets_().assign(presets.begin(), presets.end());
}
void set_supported_custom_presets(const std::vector<const char *> &presets) {
this->ensure_custom_presets_() = presets;
}
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->ensure_custom_presets_().assign(presets, presets + N);
}
/// Check if a custom fan mode is currently active.
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
@@ -336,13 +359,14 @@ class Climate : public EntityBase {
* called from publish_state()
*/
void save_state_(const ClimateTraits &traits);
void save_state_() { this->save_state_(this->traits()); }
void save_state_() { this->save_state_(this->get_traits()); }
void dump_traits_(const char *tag);
LazyCallbackManager<void(Climate &)> state_callback_{};
LazyCallbackManager<void(ClimateCall &)> control_callback_{};
ESPPreferenceObject rtc_;
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
float visual_min_temperature_override_{NAN};
float visual_max_temperature_override_{NAN};
@@ -353,16 +377,33 @@ class Climate : public EntityBase {
#endif
private:
/// Lazy-allocate custom mode vectors (never freed — entity lives forever).
std::vector<const char *> &ensure_custom_fan_modes_() {
if (!this->supported_custom_fan_modes_) {
this->supported_custom_fan_modes_ = new std::vector<const char *>(); // NOLINT
}
return *this->supported_custom_fan_modes_;
}
std::vector<const char *> &ensure_custom_presets_() {
if (!this->supported_custom_presets_) {
this->supported_custom_presets_ = new std::vector<const char *>(); // NOLINT
}
return *this->supported_custom_presets_;
}
std::vector<const char *> *supported_custom_fan_modes_{nullptr};
std::vector<const char *> *supported_custom_presets_{nullptr};
/** The active custom fan mode (private - enforces use of safe setters).
*
* Points to an entry in traits.supported_custom_fan_modes_ or nullptr.
* Points to an entry in supported_custom_fan_modes_ or nullptr.
* Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify.
*/
const char *custom_fan_mode_{nullptr};
/** The active custom preset (private - enforces use of safe setters).
*
* Points to an entry in traits.supported_custom_presets_ or nullptr.
* Points to an entry in supported_custom_presets_ or nullptr.
* Use get_custom_preset() to read, set_custom_preset_() to modify.
*/
const char *custom_preset_{nullptr};
@@ -2,6 +2,33 @@
namespace esphome::climate {
// Compat: shared empty vector for getters when no custom modes are set.
// Remove in 2026.11.0 when deprecated ClimateTraits setters are removed
// and getters can return const vector * instead of const vector &.
static const std::vector<const char *> EMPTY_CUSTOM_MODES; // NOLINT
const std::vector<const char *> &ClimateTraits::get_supported_custom_fan_modes() const {
if (this->supported_custom_fan_modes_) {
return *this->supported_custom_fan_modes_;
}
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0.
if (!this->compat_custom_fan_modes_.empty()) {
return this->compat_custom_fan_modes_;
}
return EMPTY_CUSTOM_MODES;
}
const std::vector<const char *> &ClimateTraits::get_supported_custom_presets() const {
if (this->supported_custom_presets_) {
return *this->supported_custom_presets_;
}
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0.
if (!this->compat_custom_presets_.empty()) {
return this->compat_custom_presets_;
}
return EMPTY_CUSTOM_MODES;
}
int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const {
return step_to_accuracy_decimals(this->visual_target_temperature_step_);
}
+67 -23
View File
@@ -147,27 +147,45 @@ class ClimateTraits {
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const {
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
if (!this->supported_fan_modes_.empty()) {
return true;
}
// Same precedence as get_supported_custom_fan_modes() getter
if (this->supported_custom_fan_modes_) {
return !this->supported_custom_fan_modes_->empty();
}
return !this->compat_custom_fan_modes_.empty(); // Compat: remove in 2026.11.0
}
const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
this->supported_custom_fan_modes_ = modes;
// Compat: store in owned vector. Copies copy the vector (deprecated path still copies this vector).
this->compat_custom_fan_modes_ = modes;
}
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
this->supported_custom_fan_modes_ = modes;
this->compat_custom_fan_modes_ = modes;
}
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->supported_custom_fan_modes_.assign(modes, modes + N);
// Remove before 2026.11.0
template<size_t N>
ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->compat_custom_fan_modes_.assign(modes, modes + N);
}
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_custom_fan_modes(const std::vector<std::string> &modes) = delete;
void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) = delete;
const std::vector<const char *> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
const std::vector<const char *> &get_supported_custom_fan_modes() const;
bool supports_custom_fan_mode(const char *custom_fan_mode) const {
return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
return (this->supported_custom_fan_modes_ &&
vector_contains(*this->supported_custom_fan_modes_, custom_fan_mode)) ||
vector_contains(this->compat_custom_fan_modes_, custom_fan_mode); // Compat: remove in 2026.11.0
}
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return this->supports_custom_fan_mode(custom_fan_mode.c_str());
@@ -179,23 +197,32 @@ class ClimateTraits {
bool get_supports_presets() const { return !this->supported_presets_.empty(); }
const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_presets(std::initializer_list<const char *> presets) {
this->supported_custom_presets_ = presets;
this->compat_custom_presets_ = presets;
}
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_presets(const std::vector<const char *> &presets) {
this->supported_custom_presets_ = presets;
this->compat_custom_presets_ = presets;
}
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->supported_custom_presets_.assign(presets, presets + N);
// Remove before 2026.11.0
template<size_t N>
ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_custom_presets(const char *const (&presets)[N]) {
this->compat_custom_presets_.assign(presets, presets + N);
}
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_custom_presets(const std::vector<std::string> &presets) = delete;
void set_supported_custom_presets(std::initializer_list<std::string> presets) = delete;
const std::vector<const char *> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
const std::vector<const char *> &get_supported_custom_presets() const;
bool supports_custom_preset(const char *custom_preset) const {
return vector_contains(this->supported_custom_presets_, custom_preset);
return (this->supported_custom_presets_ && vector_contains(*this->supported_custom_presets_, custom_preset)) ||
vector_contains(this->compat_custom_presets_, custom_preset); // Compat: remove in 2026.11.0
}
bool supports_custom_preset(const std::string &custom_preset) const {
return this->supports_custom_preset(custom_preset.c_str());
@@ -258,13 +285,25 @@ class ClimateTraits {
}
}
/// Set custom mode pointers (only Climate::get_traits() should call these).
void set_supported_custom_fan_modes_(const std::vector<const char *> *modes) {
this->supported_custom_fan_modes_ = modes;
}
void set_supported_custom_presets_(const std::vector<const char *> *presets) {
this->supported_custom_presets_ = presets;
}
/// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead
const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len);
if (this->supported_custom_fan_modes_) {
return vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len);
}
// Compat: check owned vector from deprecated setters. Remove in 2026.11.0.
return vector_find(this->compat_custom_fan_modes_, custom_fan_mode, len);
}
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
@@ -273,7 +312,11 @@ class ClimateTraits {
return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *find_custom_preset_(const char *custom_preset, size_t len) const {
return vector_find(this->supported_custom_presets_, custom_preset, len);
if (this->supported_custom_presets_) {
return vector_find(*this->supported_custom_presets_, custom_preset, len);
}
// Compat: check owned vector from deprecated setters. Remove in 2026.11.0.
return vector_find(this->compat_custom_presets_, custom_preset, len);
}
uint32_t feature_flags_{0};
@@ -289,16 +332,17 @@ class ClimateTraits {
climate::ClimateSwingModeMask supported_swing_modes_;
climate::ClimatePresetMask supported_presets_;
/** Custom mode storage using const char* pointers to eliminate std::string overhead.
/** Custom mode storage - pointers to vectors owned by the Climate base class.
*
* Pointers must remain valid for the ClimateTraits lifetime. Safe patterns:
* - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"})
* - Static const data: static const char* MODE = "Eco";
*
* Climate class setters validate pointers are from these vectors before storing.
* ClimateTraits does not own this data; Climate stores the vectors and
* get_traits() wires these pointers automatically.
*/
std::vector<const char *> supported_custom_fan_modes_;
std::vector<const char *> supported_custom_presets_;
const std::vector<const char *> *supported_custom_fan_modes_{nullptr};
const std::vector<const char *> *supported_custom_presets_{nullptr};
// Compat: owned storage for deprecated setters. Copies copy the vector (copies include this vector).
// Remove in 2026.11.0.
std::vector<const char *> compat_custom_fan_modes_;
std::vector<const char *> compat_custom_presets_;
};
} // namespace esphome::climate
+8 -1
View File
@@ -7,6 +7,12 @@ namespace copy {
static const char *const TAG = "copy.fan";
void CopyFan::setup() {
// Copy preset modes once from source fan — stored on Fan base class
auto source_traits = source_->get_traits();
if (source_traits.supports_preset_modes()) {
this->set_supported_preset_modes(source_traits.supported_preset_modes());
}
source_->add_on_state_callback([this]() {
this->copy_state_from_source_();
this->publish_state();
@@ -39,7 +45,8 @@ fan::FanTraits CopyFan::get_traits() {
traits.set_speed(base.supports_speed());
traits.set_supported_speed_count(base.supported_speed_count());
traits.set_direction(base.supports_direction());
traits.set_supported_preset_modes(base.supported_preset_modes());
// Preset modes are set once in setup() and wired via wire_preset_modes_()
this->wire_preset_modes_(traits);
return traits;
}
+4 -4
View File
@@ -300,16 +300,16 @@ async def cover_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if (stop := config.get(CONF_STOP)) is not None:
template_ = await cg.templatable(stop, args, bool)
template_ = await cg.templatable(stop, args, cg.bool_)
cg.add(var.set_stop(template_))
if (state := config.get(CONF_STATE)) is not None:
template_ = await cg.templatable(state, args, float)
template_ = await cg.templatable(state, args, cg.float_)
cg.add(var.set_position(template_))
if (position := config.get(CONF_POSITION)) is not None:
template_ = await cg.templatable(position, args, float)
template_ = await cg.templatable(position, args, cg.float_)
cg.add(var.set_position(template_))
if (tilt := config.get(CONF_TILT)) is not None:
template_ = await cg.templatable(tilt, args, float)
template_ = await cg.templatable(tilt, args, cg.float_)
cg.add(var.set_tilt(template_))
return var
+6 -3
View File
@@ -204,7 +204,8 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args):
("month", date_config[CONF_MONTH]),
("year", date_config[CONF_YEAR]),
)
cg.add(action_var.set_date(date_struct))
template_ = await cg.templatable(date_struct, args, cg.ESPTime)
cg.add(action_var.set_date(template_))
return action_var
@@ -236,7 +237,8 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args):
("minute", time_config[CONF_MINUTE]),
("hour", time_config[CONF_HOUR]),
)
cg.add(action_var.set_time(time_struct))
template_ = await cg.templatable(time_struct, args, cg.ESPTime)
cg.add(action_var.set_time(template_))
return action_var
@@ -271,5 +273,6 @@ async def datetime_datetime_set_to_code(config, action_id, template_arg, args):
("month", datetime_config[CONF_MONTH]),
("year", datetime_config[CONF_YEAR]),
)
cg.add(action_var.set_datetime(datetime_struct))
template_ = await cg.templatable(datetime_struct, args, cg.ESPTime)
cg.add(action_var.set_datetime(template_))
return action_var
+15 -3
View File
@@ -16,6 +16,19 @@ class DemoClimate : public climate::Climate, public Component {
public:
void set_type(DemoClimateType type) { type_ = type; }
void setup() override {
// Set custom modes once during setup — stored on Climate base class, wired via get_traits()
switch (type_) {
case DemoClimateType::TYPE_1:
break;
case DemoClimateType::TYPE_2:
this->set_supported_custom_fan_modes({"Auto Low", "Auto High"});
this->set_supported_custom_presets({"My Preset"});
break;
case DemoClimateType::TYPE_3:
this->set_supported_custom_fan_modes({"Auto Low", "Auto High"});
break;
}
// Set initial state
switch (type_) {
case DemoClimateType::TYPE_1:
this->current_temperature = 20.0;
@@ -105,14 +118,13 @@ class DemoClimate : public climate::Climate, public Component {
climate::CLIMATE_FAN_DIFFUSE,
climate::CLIMATE_FAN_QUIET,
});
traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"});
// Custom fan modes and presets are set once in setup()
traits.set_supported_swing_modes({
climate::CLIMATE_SWING_OFF,
climate::CLIMATE_SWING_BOTH,
climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL,
});
traits.set_supported_custom_presets({"My Preset"});
break;
case DemoClimateType::TYPE_3:
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
@@ -123,7 +135,7 @@ class DemoClimate : public climate::Climate, public Component {
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_HEAT_COOL,
});
traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"});
// Custom fan modes are set once in setup()
traits.set_supported_swing_modes({
climate::CLIMATE_SWING_OFF,
climate::CLIMATE_SWING_HORIZONTAL,
+10 -10
View File
@@ -159,31 +159,31 @@ async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args
await cg.register_parented(var, config[CONF_ID])
if factory_reset_config := config.get(CONF_FACTORY_RESET):
template_ = await cg.templatable(factory_reset_config, args, int)
template_ = await cg.templatable(factory_reset_config, args, cg.int8)
cg.add(var.set_factory_reset(template_))
if CONF_DETECTION_SEGMENTS in config:
segments = config[CONF_DETECTION_SEGMENTS]
if len(segments) >= 2:
template_ = await cg.templatable(segments[0], args, float)
template_ = await cg.templatable(segments[0], args, cg.float_)
cg.add(var.set_det_min1(template_))
template_ = await cg.templatable(segments[1], args, float)
template_ = await cg.templatable(segments[1], args, cg.float_)
cg.add(var.set_det_max1(template_))
if len(segments) >= 4:
template_ = await cg.templatable(segments[2], args, float)
template_ = await cg.templatable(segments[2], args, cg.float_)
cg.add(var.set_det_min2(template_))
template_ = await cg.templatable(segments[3], args, float)
template_ = await cg.templatable(segments[3], args, cg.float_)
cg.add(var.set_det_max2(template_))
if len(segments) >= 6:
template_ = await cg.templatable(segments[4], args, float)
template_ = await cg.templatable(segments[4], args, cg.float_)
cg.add(var.set_det_min3(template_))
template_ = await cg.templatable(segments[5], args, float)
template_ = await cg.templatable(segments[5], args, cg.float_)
cg.add(var.set_det_max3(template_))
if len(segments) >= 8:
template_ = await cg.templatable(segments[6], args, float)
template_ = await cg.templatable(segments[6], args, cg.float_)
cg.add(var.set_det_min4(template_))
template_ = await cg.templatable(segments[7], args, float)
template_ = await cg.templatable(segments[7], args, cg.float_)
cg.add(var.set_det_max4(template_))
if CONF_OUTPUT_LATENCY in config:
template_ = await cg.templatable(
@@ -200,7 +200,7 @@ async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args
template_ = template_.total_milliseconds / 1000
cg.add(var.set_delay_after_disappear(template_))
if CONF_SENSITIVITY in config:
template_ = await cg.templatable(config[CONF_SENSITIVITY], args, int)
template_ = await cg.templatable(config[CONF_SENSITIVITY], args, cg.int8)
cg.add(var.set_sensitivity(template_))
return var
+2 -1
View File
@@ -207,7 +207,8 @@ async def display_page_show_to_code(config, action_id, template_arg, args):
cg.add(var.set_page(template_))
else:
paren = await cg.get_variable(config[CONF_ID])
cg.add(var.set_page(paren))
template_ = await cg.templatable(paren, args, DisplayPagePtr)
cg.add(var.set_page(template_))
return var
+158 -72
View File
@@ -59,6 +59,59 @@ static inline bool is_return_addr(uint32_t addr) {
}
#endif
// --- Architecture-specific backtrace helpers ---
// These run from IRAM during panic (no flash access).
#if CONFIG_IDF_TARGET_ARCH_XTENSA
// Walk Xtensa backtrace from an exception frame, writing PCs to out[].
// Returns number of entries written.
static uint8_t IRAM_ATTR walk_xtensa_backtrace(XtExcFrame *frame, uint32_t *out, uint8_t max) {
esp_backtrace_frame_t bt_frame = {
.pc = (uint32_t) frame->pc,
.sp = (uint32_t) frame->a1,
.next_pc = (uint32_t) frame->a0,
.exc_frame = frame,
};
uint8_t count = 0;
uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc);
if (is_code_addr(first_pc)) {
out[count++] = first_pc;
}
while (count < max && bt_frame.next_pc != 0) {
if (!esp_backtrace_get_next_frame(&bt_frame))
break;
uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc);
if (is_code_addr(pc)) {
out[count++] = pc;
}
}
return count;
}
#endif
#if CONFIG_IDF_TARGET_ARCH_RISCV
// Capture RISC-V backtrace: MEPC + RA from registers, then stack scan.
// Returns total count; *reg_count receives number of register-sourced entries.
static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *out, uint8_t max, uint8_t *reg_count) {
uint8_t count = 0;
if (is_code_addr(frame->mepc)) {
out[count++] = frame->mepc;
}
if (is_code_addr(frame->ra) && frame->ra != frame->mepc) {
out[count++] = frame->ra;
}
*reg_count = count;
auto *scan_start = (uint32_t *) frame->sp;
for (uint32_t i = 0; i < 64 && count < max; i++) {
uint32_t val = scan_start[i];
if (is_code_addr(val) && val != frame->mepc && val != frame->ra) {
out[count++] = val;
}
}
return count;
}
#endif
// Raw crash data written by the panic handler wrapper.
// Lives in .noinit so it survives software reset but contains garbage after power cycle.
// Validated by magic marker. Static linkage since it's only used within this file.
@@ -66,7 +119,7 @@ static inline bool is_return_addr(uint32_t addr) {
// Magic is second to validate the data. Remaining fields can change between versions.
// Version is uint32_t because it would be padded to 4 bytes anyway before the next
// uint32_t field, so we use the full width rather than wasting 3 bytes of padding.
static constexpr uint32_t CRASH_DATA_VERSION = 1;
static constexpr uint32_t CRASH_DATA_VERSION = 2;
struct RawCrashData {
uint32_t version;
uint32_t magic;
@@ -77,6 +130,13 @@ struct RawCrashData {
uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic)
uint32_t backtrace[MAX_BACKTRACE];
uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V)
uint8_t crashed_core;
#if SOC_CPU_CORES_NUM > 1
static_assert(SOC_CPU_CORES_NUM == 2, "Dual-core logic assumes exactly 2 cores");
uint8_t other_backtrace_count;
uint8_t other_reg_frame_count;
uint32_t other_backtrace[MAX_BACKTRACE];
#endif
};
static RawCrashData __attribute__((section(".noinit")))
s_raw_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -100,13 +160,28 @@ void crash_handler_read_and_clear() {
s_raw_crash_data.exception = 4; // Default to PANIC_EXCEPTION_FAULT
if (s_raw_crash_data.pseudo_excause > 1)
s_raw_crash_data.pseudo_excause = 0;
if (s_raw_crash_data.crashed_core >= SOC_CPU_CORES_NUM)
s_raw_crash_data.crashed_core = 0;
#if SOC_CPU_CORES_NUM > 1
if (s_raw_crash_data.other_backtrace_count > MAX_BACKTRACE)
s_raw_crash_data.other_backtrace_count = MAX_BACKTRACE;
if (s_raw_crash_data.other_reg_frame_count > s_raw_crash_data.other_backtrace_count)
s_raw_crash_data.other_reg_frame_count = s_raw_crash_data.other_backtrace_count;
#endif
}
// Clear magic regardless so we don't re-report on next normal reboot
s_raw_crash_data.magic = 0;
// Don't clear magic here — crash data must survive OTA rollback reboots.
// Magic is cleared by crash_handler_clear() after an API client receives the data.
}
bool crash_handler_has_data() { return s_crash_data_valid; }
void crash_handler_clear() {
// Only clear the magic so data doesn't survive the next reboot.
// Keep s_crash_data_valid so crash_handler_log() still works for
// additional API clients connecting during this boot session.
s_raw_crash_data.magic = 0;
}
// Look up the exception cause as a human-readable string.
// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays
// not exposed via any public API.
@@ -212,6 +287,36 @@ static const char *get_exception_type() {
return "Unknown";
}
// Log backtrace entries, filtering stack-scanned addresses on RISC-V.
static void log_backtrace(const uint32_t *addrs, uint8_t count, uint8_t reg_frame_count) {
uint8_t bt_num = 0;
for (uint8_t i = 0; i < count; i++) {
uint32_t addr = addrs[i];
#if CONFIG_IDF_TARGET_ARCH_RISCV
if (i >= reg_frame_count && !is_return_addr(addr))
continue;
const char *source = (i < reg_frame_count) ? "backtrace" : "stack scan";
#else
const char *source = "backtrace";
#endif
ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source);
}
}
// Append backtrace addresses to the addr2line hint buffer.
static int append_addrs_to_hint(char *buf, int size, int pos, const uint32_t *addrs, uint8_t count,
uint8_t reg_frame_count) {
for (uint8_t i = 0; i < count && pos < size - 12; i++) {
uint32_t addr = addrs[i];
#if CONFIG_IDF_TARGET_ARCH_RISCV
if (i >= reg_frame_count && !is_return_addr(addr))
continue;
#endif
pos += snprintf(buf + pos, size - pos, " 0x%08" PRIX32, addr);
}
return pos;
}
// Intentionally uses separate ESP_LOGE calls per line instead of combining into
// one multi-line log message. This ensures each address appears as its own line
// on the serial console, making it possible to see partial output if the device
@@ -228,33 +333,28 @@ void crash_handler_log() {
} else {
ESP_LOGE(TAG, " Reason: %s", get_exception_type());
}
ESP_LOGE(TAG, " Crashed core: %d", s_raw_crash_data.crashed_core);
ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc);
uint8_t bt_num = 0;
for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count; i++) {
uint32_t addr = s_raw_crash_data.backtrace[i];
#if CONFIG_IDF_TARGET_ARCH_RISCV
// Register-sourced entries (MEPC/RA) are trusted; only filter stack-scanned ones.
if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr))
continue;
#endif
#if CONFIG_IDF_TARGET_ARCH_RISCV
const char *source = (i < s_raw_crash_data.reg_frame_count) ? "backtrace" : "stack scan";
#else
const char *source = "backtrace";
#endif
ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source);
log_backtrace(s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, s_raw_crash_data.reg_frame_count);
#if SOC_CPU_CORES_NUM > 1
if (s_raw_crash_data.other_backtrace_count > 0) {
int other_core = 1 - s_raw_crash_data.crashed_core;
ESP_LOGE(TAG, " Other core (%d) backtrace:", other_core);
log_backtrace(s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count,
s_raw_crash_data.other_reg_frame_count);
}
#endif
// Build addr2line hint with all captured addresses for easy copy-paste
char hint[256];
int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc);
for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count && pos < (int) sizeof(hint) - 12; i++) {
uint32_t addr = s_raw_crash_data.backtrace[i];
#if CONFIG_IDF_TARGET_ARCH_RISCV
if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr))
continue;
pos = append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count,
s_raw_crash_data.reg_frame_count);
#if SOC_CPU_CORES_NUM > 1
append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace,
s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count);
#endif
pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr);
}
ESP_LOGE(TAG, "%s", hint);
}
@@ -276,68 +376,54 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
s_raw_crash_data.reg_frame_count = 0;
s_raw_crash_data.exception = (uint8_t) info->exception;
s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0;
s_raw_crash_data.crashed_core = (uint8_t) info->core;
#if SOC_CPU_CORES_NUM > 1
s_raw_crash_data.other_backtrace_count = 0;
s_raw_crash_data.other_reg_frame_count = 0;
#endif
#if CONFIG_IDF_TARGET_ARCH_XTENSA
// Xtensa: walk the backtrace using the public API
if (info->frame != nullptr) {
auto *xt_frame = (XtExcFrame *) info->frame;
s_raw_crash_data.cause = xt_frame->exccause;
esp_backtrace_frame_t bt_frame = {
.pc = (uint32_t) xt_frame->pc,
.sp = (uint32_t) xt_frame->a1,
.next_pc = (uint32_t) xt_frame->a0,
.exc_frame = xt_frame,
};
uint8_t count = 0;
// First frame PC
uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc);
if (is_code_addr(first_pc)) {
s_raw_crash_data.backtrace[count++] = first_pc;
}
// Walk remaining frames
while (count < MAX_BACKTRACE && bt_frame.next_pc != 0) {
if (!esp_backtrace_get_next_frame(&bt_frame)) {
break;
}
uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc);
if (is_code_addr(pc)) {
s_raw_crash_data.backtrace[count++] = pc;
}
}
s_raw_crash_data.backtrace_count = count;
s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE);
}
#if SOC_CPU_CORES_NUM > 1
// Capture the other core's backtrace from the global frame array.
// Both cores save their frames to g_exc_frames[] before esp_panic_handler
// is called, so the other core's frame is available here.
if (info->core >= 0 && info->core < SOC_CPU_CORES_NUM) {
int other_core = 1 - info->core;
auto *other_frame = (XtExcFrame *) g_exc_frames[other_core];
if (other_frame != nullptr) {
s_raw_crash_data.other_backtrace_count =
walk_xtensa_backtrace(other_frame, s_raw_crash_data.other_backtrace, MAX_BACKTRACE);
}
}
#endif
#elif CONFIG_IDF_TARGET_ARCH_RISCV
// RISC-V: capture MEPC + RA, then scan stack for code addresses
if (info->frame != nullptr) {
auto *rv_frame = (RvExcFrame *) info->frame;
s_raw_crash_data.cause = rv_frame->mcause;
uint8_t count = 0;
// Save MEPC (fault PC) and RA (return address)
if (is_code_addr(rv_frame->mepc)) {
s_raw_crash_data.backtrace[count++] = rv_frame->mepc;
}
if (is_code_addr(rv_frame->ra) && rv_frame->ra != rv_frame->mepc) {
s_raw_crash_data.backtrace[count++] = rv_frame->ra;
}
// Track how many entries came from registers (MEPC/RA) so we can
// skip return-address validation for them at log time.
s_raw_crash_data.reg_frame_count = count;
// Scan stack for code addresses — captures broadly during panic,
// filtered by is_return_addr() at log time when flash is accessible.
auto *scan_start = (uint32_t *) rv_frame->sp;
for (uint32_t i = 0; i < 64 && count < MAX_BACKTRACE; i++) {
uint32_t val = scan_start[i];
if (is_code_addr(val) && val != rv_frame->mepc && val != rv_frame->ra) {
s_raw_crash_data.backtrace[count++] = val;
}
}
s_raw_crash_data.backtrace_count = count;
s_raw_crash_data.backtrace_count =
capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count);
}
#if SOC_CPU_CORES_NUM > 1
// Capture the other core's backtrace from the global frame array.
if (info->core >= 0 && info->core < SOC_CPU_CORES_NUM) {
int other_core = 1 - info->core;
auto *other_frame = (RvExcFrame *) g_exc_frames[other_core];
if (other_frame != nullptr) {
s_raw_crash_data.other_backtrace_count = capture_riscv_backtrace(
other_frame, s_raw_crash_data.other_backtrace, MAX_BACKTRACE, &s_raw_crash_data.other_reg_frame_count);
}
}
#endif
#endif
// Write version and magic last — ensures all data is written before we mark it valid
+7 -1
View File
@@ -4,12 +4,18 @@
namespace esphome::esp32 {
/// Read crash data from NOINIT memory and clear the magic marker.
/// Read and validate crash data from NOINIT memory.
/// Does not clear the magic marker — call crash_handler_clear() after
/// the data has been delivered to an API client so it survives OTA rollback reboots.
void crash_handler_read_and_clear();
/// Log crash data if a crash was detected on previous boot.
void crash_handler_log();
/// Clear the magic marker and mark crash data as consumed.
/// Call after the data has been delivered to an API client.
void crash_handler_clear();
/// Returns true if crash data was found this boot.
bool crash_handler_has_data();
@@ -378,7 +378,8 @@ async def esp32_ble_tracker_start_scan_action_to_code(
):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
cg.add(var.set_continuous(config[CONF_CONTINUOUS]))
template_ = await cg.templatable(config[CONF_CONTINUOUS], args, cg.bool_)
cg.add(var.set_continuous(template_))
return var
+207 -57
View File
@@ -4,95 +4,245 @@ from pathlib import Path
from esphome import pins
from esphome.components import esp32
import esphome.config_validation as cv
from esphome.const import CONF_CLK_PIN, CONF_RESET_PIN, CONF_VARIANT
from esphome.const import (
CONF_CLK_PIN,
CONF_CS_PIN,
CONF_FREQUENCY,
CONF_MISO_PIN,
CONF_MOSI_PIN,
CONF_RESET_PIN,
CONF_TYPE,
CONF_VARIANT,
)
from esphome.cpp_generator import add_define
CODEOWNERS = ["@swoboda1337"]
CONF_ACTIVE_HIGH = "active_high"
CONF_BUS_WIDTH = "bus_width"
CONF_CMD_PIN = "cmd_pin"
CONF_D0_PIN = "d0_pin"
CONF_D1_PIN = "d1_pin"
CONF_D2_PIN = "d2_pin"
CONF_D3_PIN = "d3_pin"
CONF_SLOT = "slot"
CONF_DATA_READY_ACTIVE_HIGH = "data_ready_active_high"
CONF_DATA_READY_PIN = "data_ready_pin"
CONF_HANDSHAKE_ACTIVE_HIGH = "handshake_active_high"
CONF_HANDSHAKE_PIN = "handshake_pin"
CONF_SDIO_FREQUENCY = "sdio_frequency"
CONF_SLOT = "slot"
CONF_SPI_MODE = "spi_mode"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True),
cv.Required(CONF_ACTIVE_HIGH): cv.boolean,
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_D1_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_D2_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1),
cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All(
cv.frequency, cv.Range(min=400e3, max=50e6)
),
}
),
# Shared fields for both transport modes
BASE_SCHEMA = cv.Schema(
{
cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True),
cv.Required(CONF_ACTIVE_HIGH): cv.boolean,
cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
}
)
SDIO_SCHEMA = BASE_SCHEMA.extend(
{
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_D1_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_D2_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_D3_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_BUS_WIDTH, default=4): cv.one_of(1, 4, int=True),
cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1),
cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All(
cv.frequency, cv.Range(min=400e3, max=50e6)
),
}
)
async def to_code(config):
add_define("USE_ESP32_HOSTED")
if config[CONF_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH",
True,
def _validate_sdio(config):
if config[CONF_BUS_WIDTH] == 4:
for pin in (CONF_D1_PIN, CONF_D2_PIN, CONF_D3_PIN):
if pin not in config:
raise cv.Invalid(
f"{pin} is required when bus_width is 4",
path=[pin],
)
return config
# SPI variant-dependent defaults and limits
_SPI_VARIANT_DEFAULTS = {
"ESP32": {"spi_mode": 2, "frequency": 10, "max_frequency": 10},
"ESP32C6": {"spi_mode": 3, "frequency": 26, "max_frequency": 40},
}
_SPI_DEFAULT = {"spi_mode": 3, "frequency": 40, "max_frequency": 40}
SPI_SCHEMA = BASE_SCHEMA.extend(
{
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_HANDSHAKE_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_DATA_READY_PIN): pins.internal_gpio_input_pin_number,
cv.Optional(CONF_SPI_MODE): cv.int_range(min=0, max=3),
cv.Optional(CONF_FREQUENCY): cv.All(cv.frequency, cv.Range(min=1e6, max=40e6)),
cv.Optional(CONF_HANDSHAKE_ACTIVE_HIGH, default=True): cv.boolean,
cv.Optional(CONF_DATA_READY_ACTIVE_HIGH, default=True): cv.boolean,
}
)
def _validate_spi(config):
variant = config[CONF_VARIANT]
defaults = _SPI_VARIANT_DEFAULTS.get(variant, _SPI_DEFAULT)
if CONF_SPI_MODE not in config:
config[CONF_SPI_MODE] = defaults["spi_mode"]
if CONF_FREQUENCY not in config:
config[CONF_FREQUENCY] = float(defaults["frequency"] * 1e6)
freq_mhz = int(config[CONF_FREQUENCY] // 1e6)
if freq_mhz > defaults["max_frequency"]:
raise cv.Invalid(
f"SPI frequency {freq_mhz}MHz exceeds maximum {defaults['max_frequency']}MHz for {variant}",
path=[CONF_FREQUENCY],
)
return config
CONFIG_SCHEMA = cv.typed_schema(
{
"sdio": cv.All(SDIO_SCHEMA, _validate_sdio),
"spi": cv.All(SPI_SCHEMA, _validate_spi),
},
default_type="sdio",
)
def _configure_sdio(config):
slot = config[CONF_SLOT]
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_SDIO_SLOT_{slot}",
True,
)
if config[CONF_BUS_WIDTH] == 1:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SDIO_1_BIT_BUS", True)
else:
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_LOW",
True,
)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SDIO_4_BIT_BUS", True)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE", # NOLINT
config[CONF_RESET_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}", # NOLINT
True,
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_SDIO_SLOT_{config[CONF_SLOT]}",
True,
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{config[CONF_SLOT]}",
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{slot}",
config[CONF_CLK_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{config[CONF_SLOT]}",
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{slot}",
config[CONF_CMD_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{config[CONF_SLOT]}",
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{slot}",
config[CONF_D0_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{config[CONF_SLOT]}",
config[CONF_D1_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{config[CONF_SLOT]}",
config[CONF_D2_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{config[CONF_SLOT]}",
config[CONF_D3_PIN],
)
if config[CONF_BUS_WIDTH] == 4:
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{slot}",
config[CONF_D1_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{slot}",
config[CONF_D2_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{slot}",
config[CONF_D3_PIN],
)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ",
int(config[CONF_SDIO_FREQUENCY] // 1000),
)
def _configure_spi(config):
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_HOST_INTERFACE", True)
# SPI mode is set via per-variant choice options
variant = config[CONF_VARIANT]
mode = config[CONF_SPI_MODE]
suffix = "ESP32" if variant == "ESP32" else "ESP32XX"
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_SPI_PRIV_MODE_{mode}_{suffix}",
True,
)
# Frequency is set via per-variant options
freq_mhz = int(config[CONF_FREQUENCY] // 1e6)
if variant == "ESP32":
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_FREQ_ESP32", freq_mhz)
elif variant == "ESP32C6":
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_FREQ_ESP32C6", freq_mhz)
else:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_FREQ_ESP32XX", freq_mhz)
# Pin configuration (use HSPI variant as P4/H2 hosts don't have VSPI)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_MOSI", config[CONF_MOSI_PIN]
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_MISO", config[CONF_MISO_PIN]
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_CLK", config[CONF_CLK_PIN]
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_CS", config[CONF_CS_PIN]
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SPI_GPIO_HANDSHAKE", config[CONF_HANDSHAKE_PIN]
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SPI_GPIO_DATA_READY", config[CONF_DATA_READY_PIN]
)
# Handshake and data_ready polarity
if config[CONF_HANDSHAKE_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_HS_ACTIVE_HIGH", True)
else:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_HS_ACTIVE_LOW", True)
if config[CONF_DATA_READY_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_DR_ACTIVE_HIGH", True)
else:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_DR_ACTIVE_LOW", True)
async def to_code(config):
add_define("USE_ESP32_HOSTED")
transport = config[CONF_TYPE]
transport_prefix = "SDIO" if transport == "sdio" else "SPI"
# Reset polarity
if config[CONF_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_{transport_prefix}_RESET_ACTIVE_HIGH", True
)
else:
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_{transport_prefix}_RESET_ACTIVE_LOW", True
)
# Reset GPIO
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_{transport_prefix}_GPIO_RESET_SLAVE", # NOLINT
config[CONF_RESET_PIN],
)
# Slave variant # NOLINT
esp32.add_idf_sdkconfig_option(
f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}", # NOLINT
True,
)
# Transport-specific configuration
if transport == "sdio":
_configure_sdio(config)
else:
_configure_spi(config)
# Library versions
idf_ver = esp32.idf_version()
os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}"
if idf_ver >= cv.Version(5, 5, 0):
+1 -1
View File
@@ -62,6 +62,6 @@ async def to_code(config) -> None:
async def esp8266_set_frequency_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, float)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_)
cg.add(var.set_frequency(template_))
return var
+12 -4
View File
@@ -123,6 +123,8 @@ ETHERNET_TYPES = {
"DM9051": EthernetType.ETHERNET_TYPE_DM9051,
"LAN8670": EthernetType.ETHERNET_TYPE_LAN8670,
"ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60,
"W6100": EthernetType.ETHERNET_TYPE_W6100,
"W6300": EthernetType.ETHERNET_TYPE_W6300,
}
# PHY types that need compile-time defines for conditional compilation
@@ -140,6 +142,8 @@ _PHY_TYPE_TO_DEFINE = {
"DM9051": "USE_ETHERNET_DM9051",
"LAN8670": "USE_ETHERNET_LAN8670",
"ENC28J60": "USE_ETHERNET_ENC28J60",
"W6100": "USE_ETHERNET_W6100",
"W6300": "USE_ETHERNET_W6300",
}
@@ -170,12 +174,14 @@ _ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"}
# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver)
SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60"}
# RP2040-supported SPI ethernet types
RP2040_SPI_ETHERNET_TYPES = {"W5100", "W5500", "ENC28J60"}
# RP2040-supported ethernet types (SPI and PIO QSPI)
RP2040_ETHERNET_TYPES = {"W5100", "W5500", "W6100", "W6300", "ENC28J60"}
_RP2040_SPI_LIBRARIES = {
"W5100": "lwIP_w5100",
"W5500": "lwIP_w5500",
"ENC28J60": "lwIP_enc28j60",
"W6100": "lwIP_w6100",
"W6300": "lwIP_w6300",
}
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10)
@@ -328,9 +334,9 @@ def _validate(config):
f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported "
f"on ESP32 classic and ESP32-P4, not {variant}"
)
elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_SPI_ETHERNET_TYPES:
elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_ETHERNET_TYPES:
raise cv.Invalid(
f"Only {', '.join(sorted(RP2040_SPI_ETHERNET_TYPES))} are supported on RP2040, "
f"Only {', '.join(sorted(RP2040_ETHERNET_TYPES))} are supported on RP2040, "
f"not {config[CONF_TYPE]}"
)
return config
@@ -427,6 +433,8 @@ CONFIG_SCHEMA = cv.All(
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
"DM9051": SPI_SCHEMA,
"ENC28J60": SPI_SCHEMA,
"W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"LAN8670": RMII_SCHEMA,
},
upper=True,
@@ -30,6 +30,20 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void);
#include <W5500lwIP.h>
#elif defined(USE_ETHERNET_W5100)
#include <W5100lwIP.h>
#elif defined(USE_ETHERNET_W6100)
#include <W6100lwIP.h>
#elif defined(USE_ETHERNET_W6300)
#include <W6300lwIP.h>
// W6300 uses PIO QSPI, not Arduino SPI. The upstream Wiznet6300 class
// incorrectly returns needsSPI()=true, causing LwipIntfDev::begin() to
// call SPI.begin() which claims GPIOs that PIO QSPI needs.
// This wrapper hides needsSPI() with a version returning false.
class Wiznet6300NoSPI : public Wiznet6300 {
public:
using Wiznet6300::Wiznet6300;
constexpr bool needsSPI() const { return false; }
};
using Wiznet6300lwIPFixed = LwipIntfDev<Wiznet6300NoSPI>;
#elif defined(USE_ETHERNET_ENC28J60)
#include <ENC28J60lwIP.h>
#else
@@ -70,6 +84,8 @@ enum EthernetType : uint8_t {
ETHERNET_TYPE_DM9051,
ETHERNET_TYPE_LAN8670,
ETHERNET_TYPE_ENC28J60,
ETHERNET_TYPE_W6100,
ETHERNET_TYPE_W6300,
};
struct ManualIP {
@@ -232,6 +248,8 @@ class EthernetComponent final : public Component {
static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls
#if defined(USE_ETHERNET_W5100)
static constexpr uint32_t RESET_DELAY_MS = 150; // W5100S PLL lock time
#elif defined(USE_ETHERNET_W6300)
static constexpr uint32_t RESET_DELAY_MS = 100; // W6300 needs 100ms after hardware reset
#else
static constexpr uint32_t RESET_DELAY_MS = 10;
#endif
@@ -239,6 +257,10 @@ class EthernetComponent final : public Component {
Wiznet5500lwIP *eth_{nullptr};
#elif defined(USE_ETHERNET_W5100)
Wiznet5100lwIP *eth_{nullptr};
#elif defined(USE_ETHERNET_W6100)
Wiznet6100lwIP *eth_{nullptr};
#elif defined(USE_ETHERNET_W6300)
Wiznet6300lwIPFixed *eth_{nullptr};
#elif defined(USE_ETHERNET_ENC28J60)
ENC28J60lwIP *eth_{nullptr};
#else
@@ -18,9 +18,14 @@ static const char *const TAG = "ethernet";
void EthernetComponent::setup() {
// Configure SPI pins
#if !defined(USE_ETHERNET_W6300)
SPI.setRX(this->miso_pin_);
SPI.setTX(this->mosi_pin_);
SPI.setSCK(this->clk_pin_);
#endif
// W6300 uses PIO QSPI with hardcoded pins, not Arduino SPI.
// SPI pin config is skipped; Wiznet6300lwIPFixed (needsSPI()=false)
// prevents LwipIntfDev::begin() from calling SPI.begin().
// Toggle reset pin if configured
if (this->reset_pin_ >= 0) {
@@ -40,6 +45,10 @@ void EthernetComponent::setup() {
this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_W5100)
this->eth_ = new Wiznet5100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_W6100)
this->eth_ = new Wiznet6100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_W6300)
this->eth_ = new Wiznet6300lwIPFixed(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_ENC28J60)
this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#endif
@@ -183,9 +192,23 @@ void EthernetComponent::dump_config() {
type_str = "W5500";
#elif defined(USE_ETHERNET_W5100)
type_str = "W5100";
#elif defined(USE_ETHERNET_W6100)
type_str = "W6100";
#elif defined(USE_ETHERNET_W6300)
type_str = "W6300";
#elif defined(USE_ETHERNET_ENC28J60)
type_str = "ENC28J60";
#endif
#if defined(USE_ETHERNET_W6300)
// W6300 uses PIO QSPI with hardcoded pins — SPI pin fields are not used
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Type: %s (PIO QSPI)\n"
" Connected: %s\n"
" IRQ Pin: %d\n"
" Reset Pin: %d",
type_str, YESNO(this->is_connected()), this->interrupt_pin_, this->reset_pin_);
#else
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Type: %s\n"
@@ -198,6 +221,7 @@ void EthernetComponent::dump_config() {
" Reset Pin: %d",
type_str, YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
this->interrupt_pin_, this->reset_pin_);
#endif
this->dump_connect_params_();
}
+2 -2
View File
@@ -202,7 +202,7 @@ async def ezo_pmp_dose_volume_over_time_to_code(config, action_id, template_arg,
template_ = await cg.templatable(config[CONF_VOLUME], args, cg.double)
cg.add(var.set_volume(template_))
template_ = await cg.templatable(config[CONF_DURATION], args, int)
template_ = await cg.templatable(config[CONF_DURATION], args, cg.int_)
cg.add(var.set_duration(template_))
return var
@@ -236,7 +236,7 @@ async def ezo_pmp_dose_with_constant_flow_rate_to_code(
template_ = await cg.templatable(config[CONF_VOLUME_PER_MINUTE], args, cg.double)
cg.add(var.set_volume(template_))
template_ = await cg.templatable(config[CONF_DURATION], args, int)
template_ = await cg.templatable(config[CONF_DURATION], args, cg.int_)
cg.add(var.set_duration(template_))
return var
+3 -3
View File
@@ -345,10 +345,10 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if (oscillating := config.get(CONF_OSCILLATING)) is not None:
template_ = await cg.templatable(oscillating, args, bool)
template_ = await cg.templatable(oscillating, args, cg.bool_)
cg.add(var.set_oscillating(template_))
if (speed := config.get(CONF_SPEED)) is not None:
template_ = await cg.templatable(speed, args, int)
template_ = await cg.templatable(speed, args, cg.int_)
cg.add(var.set_speed(template_))
if (direction := config.get(CONF_DIRECTION)) is not None:
template_ = await cg.templatable(direction, args, FanDirection)
@@ -370,7 +370,7 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args):
async def fan_cycle_speed_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_OFF_SPEED_CYCLE], args, bool)
template_ = await cg.templatable(config[CONF_OFF_SPEED_CYCLE], args, cg.bool_)
cg.add(var.set_no_off_cycle(template_))
return var
+47 -8
View File
@@ -9,6 +9,22 @@ namespace fan {
static const char *const TAG = "fan";
// Compat: shared empty vector for getter when no preset modes are set.
// Remove in 2026.11.0 when deprecated FanTraits setters are removed
// and getter can return const vector * instead of const vector &.
static const std::vector<const char *> EMPTY_PRESET_MODES; // NOLINT
const std::vector<const char *> &FanTraits::supported_preset_modes() const {
if (this->preset_modes_) {
return *this->preset_modes_;
}
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0 (change return to const vector *).
if (!this->compat_preset_modes_.empty()) {
return this->compat_preset_modes_;
}
return EMPTY_PRESET_MODES;
}
// Fan direction strings indexed by FanDirection enum (0-1): FORWARD, REVERSE, plus UNKNOWN
PROGMEM_STRING_TABLE(FanDirectionStrings, "FORWARD", "REVERSE", "UNKNOWN");
@@ -148,6 +164,18 @@ const char *Fan::find_preset_mode_(const char *preset_mode) {
}
const char *Fan::find_preset_mode_(const char *preset_mode, size_t len) {
if (preset_mode == nullptr || len == 0) {
return nullptr;
}
if (this->supported_preset_modes_) {
for (const char *mode : *this->supported_preset_modes_) {
if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') {
return mode;
}
}
return nullptr;
}
// Fallback for deprecated path: external components may set modes on FanTraits directly
return this->get_traits().find_preset_mode(preset_mode, len);
}
@@ -261,8 +289,6 @@ void Fan::save_state_() {
return;
}
auto traits = this->get_traits();
FanRestoreState state{};
state.state = this->state;
state.oscillating = this->oscillating;
@@ -271,12 +297,25 @@ void Fan::save_state_() {
state.preset_mode = FanRestoreState::NO_PRESET;
if (this->has_preset_mode()) {
const auto &preset_modes = traits.supported_preset_modes();
// Find index of current preset mode (pointer comparison is safe since preset is from traits)
for (size_t i = 0; i < preset_modes.size(); i++) {
if (preset_modes[i] == this->preset_mode_) {
state.preset_mode = i;
break;
if (this->supported_preset_modes_) {
// New path: search Fan-owned vector directly
for (size_t i = 0; i < this->supported_preset_modes_->size(); i++) {
if ((*this->supported_preset_modes_)[i] == this->preset_mode_) {
state.preset_mode = i;
break;
}
}
} else {
// Compat: fall back to traits for deprecated path. Remove in 2026.11.0.
// Pointer comparison works because preset_mode_ and the compat vector both
// hold pointers to string literals in .rodata (stable addresses).
auto traits = this->get_traits();
const auto &preset_modes = traits.supported_preset_modes();
for (size_t i = 0; i < preset_modes.size(); i++) {
if (preset_modes[i] == this->preset_mode_) {
state.preset_mode = i;
break;
}
}
}
}
+24
View File
@@ -130,6 +130,14 @@ class Fan : public EntityBase {
virtual FanTraits get_traits() = 0;
/// Set the supported preset modes (stored on Fan, referenced by FanTraits via pointer).
void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) {
this->ensure_preset_modes_().assign(preset_modes.begin(), preset_modes.end());
}
void set_supported_preset_modes(const std::vector<const char *> &preset_modes) {
this->ensure_preset_modes_() = preset_modes;
}
/// Set the restore mode of this fan.
void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
@@ -167,11 +175,27 @@ class Fan : public EntityBase {
const char *find_preset_mode_(const char *preset_mode);
const char *find_preset_mode_(const char *preset_mode, size_t len);
/// Wire the Fan-owned preset modes pointer into the given traits object.
void wire_preset_modes_(FanTraits &traits) {
if (this->supported_preset_modes_) {
traits.set_supported_preset_modes_(this->supported_preset_modes_);
}
}
LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;
FanRestoreMode restore_mode_;
private:
/// Lazy-allocate preset modes vector (never freed — entity lives forever).
std::vector<const char *> &ensure_preset_modes_() {
if (!this->supported_preset_modes_) {
this->supported_preset_modes_ = new std::vector<const char *>(); // NOLINT
}
return *this->supported_preset_modes_;
}
std::vector<const char *> *supported_preset_modes_{nullptr};
const char *preset_mode_{nullptr};
};
+38 -11
View File
@@ -3,12 +3,17 @@
#include <cstring>
#include <vector>
#include <initializer_list>
#include "esphome/core/helpers.h"
namespace esphome {
namespace fan {
class Fan; // Forward declaration
class FanTraits {
friend class Fan; // Allow Fan to access protected pointer setter
public:
FanTraits() = default;
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
@@ -30,42 +35,64 @@ class FanTraits {
bool supports_direction() const { return this->direction_; }
/// Set whether this fan supports changing direction
void set_direction(bool direction) { this->direction_ = direction; }
/// Return the preset modes supported by the fan.
const std::vector<const char *> &supported_preset_modes() const { return this->preset_modes_; }
/// Set the preset modes supported by the fan (from initializer list).
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
const std::vector<const char *> &supported_preset_modes() const;
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_preset_modes() on the Fan entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) {
this->preset_modes_ = preset_modes;
// Compat: store in owned vector. Copies copy the vector (deprecated path still copies this vector).
this->compat_preset_modes_ = preset_modes;
}
// Remove before 2026.11.0
ESPDEPRECATED("Call set_supported_preset_modes() on the Fan entity instead. Removed in 2026.11.0", "2026.5.0")
void set_supported_preset_modes(const std::vector<const char *> &preset_modes) {
this->compat_preset_modes_ = preset_modes;
}
/// Set the preset modes supported by the fan (from vector).
void set_supported_preset_modes(const std::vector<const char *> &preset_modes) { this->preset_modes_ = preset_modes; }
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_preset_modes(const std::vector<std::string> &preset_modes) = delete;
void set_supported_preset_modes(std::initializer_list<std::string> preset_modes) = delete;
/// Return if preset modes are supported
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
bool supports_preset_modes() const {
// Same precedence as supported_preset_modes() getter
if (this->preset_modes_) {
return !this->preset_modes_->empty();
}
return !this->compat_preset_modes_.empty();
}
/// Find and return the matching preset mode pointer from supported modes, or nullptr if not found.
const char *find_preset_mode(const char *preset_mode) const {
return this->find_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0);
}
const char *find_preset_mode(const char *preset_mode, size_t len) const {
if (preset_mode == nullptr || len == 0)
if (preset_mode == nullptr || len == 0) {
return nullptr;
for (const char *mode : this->preset_modes_) {
}
// Check pointer-based storage (new path) then compat owned vector (deprecated path)
const auto &modes = this->preset_modes_ ? *this->preset_modes_ : this->compat_preset_modes_;
for (const char *mode : modes) {
if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') {
return mode; // Return pointer from traits
return mode;
}
}
return nullptr;
}
protected:
/// Set the preset modes pointer (only Fan::wire_preset_modes_() should call this).
void set_supported_preset_modes_(const std::vector<const char *> *preset_modes) {
this->preset_modes_ = preset_modes;
}
bool oscillation_{false};
bool speed_{false};
bool direction_{false};
int speed_count_{};
std::vector<const char *> preset_modes_{};
const std::vector<const char *> *preset_modes_{nullptr};
// Compat: owned storage for deprecated setters. Copies copy the vector (copies include this vector).
// Remove in 2026.11.0.
std::vector<const char *> compat_preset_modes_;
};
} // namespace fan
+40 -20
View File
@@ -6,9 +6,15 @@ namespace esphome {
namespace gdk101 {
static const char *const TAG = "gdk101";
static const uint8_t NUMBER_OF_READ_RETRIES = 5;
static constexpr uint8_t NUMBER_OF_READ_RETRIES = 5;
static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 10;
static constexpr uint32_t RESET_INTERVAL_ID = 0;
static constexpr uint32_t RESET_INTERVAL_MS = 1000;
void GDK101Component::update() {
if (!this->reset_complete_)
return;
uint8_t data[2];
if (!this->read_dose_1m_(data)) {
this->status_set_warning(LOG_STR("Failed to read dose 1m"));
@@ -33,26 +39,45 @@ void GDK101Component::update() {
}
void GDK101Component::setup() {
uint8_t data[2];
// first, reset the sensor
if (!this->try_reset_()) {
// Sensor MCU boots slowly after power cycle — retry on a short interval
this->reset_retries_remaining_ = NUMBER_OF_RESET_RETRIES;
this->set_interval(RESET_INTERVAL_ID, RESET_INTERVAL_MS, [this]() {
if (this->try_reset_()) {
if (this->reset_complete_) {
this->update();
}
return;
}
if (--this->reset_retries_remaining_ == 0) {
this->cancel_interval(RESET_INTERVAL_ID);
this->mark_failed(LOG_STR("Reset failed after retries"));
}
});
}
}
/// Attempt to reset the sensor and read firmware version. Returns true on success or hard failure.
bool GDK101Component::try_reset_() {
uint8_t data[2] = {0};
if (!this->reset_sensor_(data)) {
this->status_set_error(LOG_STR("Reset failed!"));
this->mark_failed();
return;
this->status_set_warning(LOG_STR("Sensor not answering reset, will retry"));
return false;
}
// sensor should acknowledge success of the reset procedure
if (data[0] != 1) {
this->status_set_error(LOG_STR("Reset not acknowledged!"));
this->mark_failed();
return;
this->status_set_warning(LOG_STR("Reset not acknowledged, will retry"));
return false;
}
delay(10);
// read firmware version
if (!this->read_fw_version_(data)) {
this->status_set_error(LOG_STR("Failed to read firmware version"));
this->mark_failed();
return;
this->cancel_interval(RESET_INTERVAL_ID);
this->mark_failed(LOG_STR("Failed to read firmware version"));
return true;
}
this->reset_complete_ = true;
this->status_clear_warning();
this->cancel_interval(RESET_INTERVAL_ID);
return true;
}
void GDK101Component::dump_config() {
@@ -92,12 +117,7 @@ bool GDK101Component::reset_sensor_(uint8_t *data) {
// After sending reset command it looks that sensor start performing reset and is unresponsible during read
// after a while we can send another reset command and read "0x01" as confirmation
// Documentation not going in to such details unfortunately
if (!this->read_bytes_with_retry_(GDK101_REG_RESET, data, 2)) {
ESP_LOGE(TAG, "Updating GDK101 failed!");
return false;
}
return true;
return this->read_bytes_with_retry_(GDK101_REG_RESET, data, 2);
}
bool GDK101Component::read_dose_1m_(uint8_t *data) {
+3
View File
@@ -44,12 +44,15 @@ class GDK101Component : public PollingComponent, public i2c::I2CDevice {
protected:
bool read_bytes_with_retry_(uint8_t a_register, uint8_t *data, uint8_t len);
bool try_reset_();
bool reset_sensor_(uint8_t *data);
bool read_dose_1m_(uint8_t *data);
bool read_dose_10m_(uint8_t *data);
bool read_status_(uint8_t *data);
bool read_fw_version_(uint8_t *data);
bool read_measurement_duration_(uint8_t *data);
bool reset_complete_{false};
uint8_t reset_retries_remaining_{0};
};
} // namespace gdk101
+1 -1
View File
@@ -109,7 +109,7 @@ async def globals_set_to_code(config, action_id, template_arg, args):
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
var = cg.new_Pvariable(action_id, template_arg, paren)
templ = await cg.templatable(
config[CONF_VALUE], args, None, to_exp=cg.RawExpression
config[CONF_VALUE], args, None, to_exp=cg.RawExpression, wrap_constant=True
)
cg.add(var.set_value(templ))
return var
@@ -78,7 +78,7 @@ async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_channel = await cg.templatable(config[CONF_CHANNEL], args, int)
template_channel = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8)
template_speed = await cg.templatable(config[CONF_SPEED], args, cg.uint16)
cg.add(var.set_channel(template_channel))
cg.add(var.set_speed(template_speed))
@@ -101,7 +101,7 @@ async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_channel = await cg.templatable(config[CONF_CHANNEL], args, int)
template_channel = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8)
cg.add(var.set_channel(template_channel))
return var
@@ -121,7 +121,7 @@ async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_channel = await cg.templatable(config[CONF_CHANNEL], args, int)
template_channel = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8)
cg.add(var.set_channel(template_channel))
return var
@@ -175,6 +175,6 @@ async def grove_tb6612fng_change_address_to_code(config, action_id, template_arg
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_channel = await cg.templatable(config[CONF_ADDRESS], args, int)
template_channel = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
cg.add(var.set_address(template_channel))
return var
@@ -30,7 +30,6 @@ fan::FanCall HBridgeFan::brake() {
void HBridgeFan::setup() {
// Construct traits before restore so preset modes can be looked up by index
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
this->traits_.set_supported_preset_modes(this->preset_modes_);
auto restore = this->restore_state_();
if (restore.has_value()) {
+5 -3
View File
@@ -20,11 +20,14 @@ class HBridgeFan : public Component, public fan::Fan {
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
void set_preset_modes(std::initializer_list<const char *> presets) { preset_modes_ = presets; }
void set_preset_modes(std::initializer_list<const char *> presets) { this->set_supported_preset_modes(presets); }
void setup() override;
void dump_config() override;
fan::FanTraits get_traits() override { return this->traits_; }
fan::FanTraits get_traits() override {
this->wire_preset_modes_(this->traits_);
return this->traits_;
}
fan::FanCall brake();
@@ -36,7 +39,6 @@ class HBridgeFan : public Component, public fan::Fan {
int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};
void control(const fan::FanCall &call) override;
void write_state_();
+4 -2
View File
@@ -302,11 +302,13 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_))
cg.add(var.set_method(config[CONF_METHOD]))
template_ = await cg.templatable(config[CONF_METHOD], args, cg.const_char_ptr)
cg.add(var.set_method(template_))
capture_response = config[CONF_CAPTURE_RESPONSE]
if capture_response:
cg.add(var.set_capture_response(capture_response))
template_ = await cg.templatable(capture_response, args, cg.bool_)
cg.add(var.set_capture_response(template_))
cg.add_define("USE_HTTP_REQUEST_RESPONSE")
cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE]))
@@ -457,7 +457,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
#endif
void init_request_headers(size_t count) { this->request_headers_.init(count); }
void add_request_header(const char *key, TemplatableValue<const char *, Ts...> value) {
void add_request_header(const char *key, TemplatableFn<const char *, Ts...> value) {
this->request_headers_.push_back({key, value});
}
@@ -560,7 +560,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
}
}
HttpRequestComponent *parent_;
FixedVector<std::pair<const char *, TemplatableValue<const char *, Ts...>>> request_headers_{};
FixedVector<std::pair<const char *, TemplatableFn<const char *, Ts...>>> request_headers_{};
std::vector<std::string> lower_case_collect_headers_{"content-type", "content-length"};
FixedVector<std::pair<const char *, TemplatableValue<std::string, Ts...>>> json_{};
std::function<void(Ts..., JsonObject)> json_func_{nullptr};
+2 -2
View File
@@ -98,7 +98,7 @@ async def to_code(config):
async def set_heater_level_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
level_ = await cg.templatable(config[CONF_LEVEL], args, int)
level_ = await cg.templatable(config[CONF_LEVEL], args, cg.uint8)
cg.add(var.set_level(level_))
return var
@@ -118,6 +118,6 @@ async def set_heater_level_to_code(config, action_id, template_arg, args):
async def set_heater_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
status_ = await cg.templatable(config[CONF_STATUS], args, bool)
status_ = await cg.templatable(config[CONF_STATUS], args, cg.bool_)
cg.add(var.set_status(status_))
return var
+1 -1
View File
@@ -133,6 +133,6 @@ async def sensor_integration_reset_to_code(config, action_id, template_arg, args
async def sensor_integration_set_value_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_VALUE], args, float)
template_ = await cg.templatable(config[CONF_VALUE], args, cg.float_)
cg.add(var.set_value(template_))
return var
+5 -2
View File
@@ -140,8 +140,11 @@ SerializationBuffer<> JsonBuilder::serialize() {
heap_size *= 2;
}
// Payload exceeds 5120 bytes - return truncated result
ESP_LOGW(TAG, "JSON payload too large, truncated to %zu bytes", size);
result.set_size_(size);
// heap_size was doubled after the last iteration, so the actual allocated
// buffer capacity is heap_size/2. Clamp to avoid writing past the buffer.
size_t max_content = heap_size / 2 - 1;
ESP_LOGW(TAG, "JSON payload too large, truncated to %zu bytes", max_content);
result.set_size_(max_content);
return result;
}
+1 -1
View File
@@ -82,6 +82,6 @@ async def to_code(config):
async def ledc_set_frequency_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, float)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_)
cg.add(var.set_frequency(template_))
return var
+1 -1
View File
@@ -43,6 +43,6 @@ async def to_code(config):
async def libretiny_pwm_set_frequency_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, float)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_)
cg.add(var.set_frequency(template_))
return var
+42 -33
View File
@@ -24,51 +24,60 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
LightState *state_;
};
/// Compact light control action — each field is a function pointer (nullptr = unset).
/// Codegen wraps constants in stateless lambdas. 72 bytes vs 128 with TemplatableValue.
template<typename... Ts> class LightControlAction : public Action<Ts...> {
public:
explicit LightControlAction(LightState *parent) : parent_(parent) {}
#define LIGHT_CONTROL_FIELDS(X) \
X(ColorMode, color_mode) \
X(bool, state) \
X(uint32_t, transition_length) \
X(uint32_t, flash_length) \
X(float, brightness) \
X(float, color_brightness) \
X(float, red) \
X(float, green) \
X(float, blue) \
X(float, white) \
X(float, color_temperature) \
X(float, cold_white) \
X(float, warm_white) \
X(uint32_t, effect)
#define LIGHT_FIELD_SETTER_(type, name) \
void set_##name(type (*f)(Ts...)) { this->name##_ = f; }
#define LIGHT_FIELD_APPLY_(type, name) \
if (this->name##_) \
call.set_##name(this->name##_(x...));
#define LIGHT_FIELD_DECL_(type, name) type (*name##_)(Ts...){nullptr};
LIGHT_CONTROL_FIELDS(LIGHT_FIELD_SETTER_)
TEMPLATABLE_VALUE(ColorMode, color_mode)
TEMPLATABLE_VALUE(bool, state)
TEMPLATABLE_VALUE(uint32_t, transition_length)
TEMPLATABLE_VALUE(uint32_t, flash_length)
TEMPLATABLE_VALUE(float, brightness)
TEMPLATABLE_VALUE(float, color_brightness)
TEMPLATABLE_VALUE(float, red)
TEMPLATABLE_VALUE(float, green)
TEMPLATABLE_VALUE(float, blue)
TEMPLATABLE_VALUE(float, white)
TEMPLATABLE_VALUE(float, color_temperature)
TEMPLATABLE_VALUE(float, cold_white)
TEMPLATABLE_VALUE(float, warm_white)
TEMPLATABLE_VALUE(uint32_t, effect)
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
LIGHT_CONTROL_FIELDS(LIGHT_FIELD_APPLY_)
if (this->color_mode_.has_value())
call.set_color_mode(this->color_mode_.value(x...));
if (this->state_.has_value())
call.set_state(this->state_.value(x...));
if (this->transition_length_.has_value())
call.set_transition_length(this->transition_length_.value(x...));
if (this->flash_length_.has_value())
call.set_flash_length(this->flash_length_.value(x...));
if (this->brightness_.has_value())
call.set_brightness(this->brightness_.value(x...));
if (this->color_brightness_.has_value())
call.set_color_brightness(this->color_brightness_.value(x...));
if (this->red_.has_value())
call.set_red(this->red_.value(x...));
if (this->green_.has_value())
call.set_green(this->green_.value(x...));
if (this->blue_.has_value())
call.set_blue(this->blue_.value(x...));
if (this->white_.has_value())
call.set_white(this->white_.value(x...));
if (this->color_temperature_.has_value())
call.set_color_temperature(this->color_temperature_.value(x...));
if (this->cold_white_.has_value())
call.set_cold_white(this->cold_white_.value(x...));
if (this->warm_white_.has_value())
call.set_warm_white(this->warm_white_.value(x...));
if (this->effect_.has_value())
call.set_effect(this->effect_.value(x...));
call.perform();
}
protected:
LightState *parent_;
LIGHT_CONTROL_FIELDS(LIGHT_FIELD_DECL_)
#undef LIGHT_FIELD_DECL_
#undef LIGHT_FIELD_APPLY_
#undef LIGHT_FIELD_SETTER_
#undef LIGHT_CONTROL_FIELDS
};
template<typename... Ts> class DimRelativeAction : public Action<Ts...> {
+17 -43
View File
@@ -1,5 +1,3 @@
from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.config import path_context
@@ -30,7 +28,7 @@ from esphome.const import (
)
from esphome.core import CORE, EsphomeError, Lambda
from esphome.cpp_generator import LambdaExpression
from esphome.types import ConfigType, SafeExpType
from esphome.types import ConfigType
from .types import (
COLOR_MODES,
@@ -143,28 +141,6 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id(
)
async def _as_lambda(
value: Any,
args: list[tuple[SafeExpType, str]],
output_type: SafeExpType,
) -> LambdaExpression:
"""Return a stateless lambda expression for a templatable value.
If value is already a lambda, process it normally. Otherwise wrap
the constant in a ``[](...) -> T { return <value>; }`` expression
so that LightControlAction can store every field as a plain
function pointer.
"""
if cg.is_template(value):
return await cg.process_lambda(value, args, return_type=output_type)
return LambdaExpression(
f"return {cg.safe_exp(value)};",
args,
capture="",
return_type=output_type,
)
def _resolve_effect_index(config: ConfigType) -> int:
"""Resolve a static effect name to its 1-based index at codegen time.
@@ -207,24 +183,23 @@ async def light_control_to_code(config, action_id, template_arg, args):
# (config_key, setter_name, c++ type)
FIELDS = (
(CONF_COLOR_MODE, "set_color_mode", ColorMode),
(CONF_STATE, "set_state", bool),
(CONF_STATE, "set_state", cg.bool_),
(CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32),
(CONF_FLASH_LENGTH, "set_flash_length", cg.uint32),
(CONF_BRIGHTNESS, "set_brightness", float),
(CONF_COLOR_BRIGHTNESS, "set_color_brightness", float),
(CONF_RED, "set_red", float),
(CONF_GREEN, "set_green", float),
(CONF_BLUE, "set_blue", float),
(CONF_WHITE, "set_white", float),
(CONF_COLOR_TEMPERATURE, "set_color_temperature", float),
(CONF_COLD_WHITE, "set_cold_white", float),
(CONF_WARM_WHITE, "set_warm_white", float),
(CONF_BRIGHTNESS, "set_brightness", cg.float_),
(CONF_COLOR_BRIGHTNESS, "set_color_brightness", cg.float_),
(CONF_RED, "set_red", cg.float_),
(CONF_GREEN, "set_green", cg.float_),
(CONF_BLUE, "set_blue", cg.float_),
(CONF_WHITE, "set_white", cg.float_),
(CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_),
(CONF_COLD_WHITE, "set_cold_white", cg.float_),
(CONF_WARM_WHITE, "set_warm_white", cg.float_),
)
for conf_key, setter, type_ in FIELDS:
if conf_key in config:
cg.add(
getattr(var, setter)(await _as_lambda(config[conf_key], args, type_))
)
template_ = await cg.templatable(config[conf_key], args, type_)
cg.add(getattr(var, setter)(template_))
if CONF_EFFECT in config:
if isinstance(config[CONF_EFFECT], Lambda):
@@ -248,11 +223,10 @@ async def light_control_to_code(config, action_id, template_arg, args):
cg.add(var.set_effect(wrapper))
else:
# Static string — resolve effect name to index at codegen time
cg.add(
var.set_effect(
await _as_lambda(_resolve_effect_index(config), args, cg.uint32)
)
template_ = await cg.templatable(
_resolve_effect_index(config), args, cg.uint32
)
cg.add(var.set_effect(template_))
return var
@@ -288,7 +262,7 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema(
async def light_dim_relative_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
templ = await cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, float)
templ = await cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, cg.float_)
cg.add(var.set_relative_brightness(templ))
if CONF_TRANSITION_LENGTH in config:
templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32)
+7 -9
View File
@@ -61,15 +61,13 @@ async def send_raw_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
repeats = await cg.templatable(config[CONF_REPEAT], args, int)
inverted = await cg.templatable(config[CONF_INVERTED], args, bool)
pulse_length = await cg.templatable(config[CONF_PULSE_LENGTH], args, int)
code = config[CONF_CODE]
cg.add(var.set_repeats(repeats))
cg.add(var.set_inverted(inverted))
cg.add(var.set_pulse_length(pulse_length))
cg.add(var.set_data(code))
template_ = await cg.templatable(config[CONF_REPEAT], args, cg.int_)
cg.add(var.set_repeat(template_))
template_ = await cg.templatable(config[CONF_INVERTED], args, cg.int_)
cg.add(var.set_inverted(template_))
template_ = await cg.templatable(config[CONF_PULSE_LENGTH], args, cg.int_)
cg.add(var.set_pulse_length(template_))
cg.add(var.set_code(config[CONF_CODE]))
return var
+1 -5
View File
@@ -45,11 +45,7 @@ template<typename... Ts> class SendRawAction : public Action<Ts...> {
TEMPLATABLE_VALUE(int, inverted);
TEMPLATABLE_VALUE(int, pulse_length);
TEMPLATABLE_VALUE(std::vector<uint8_t>, code);
void set_repeats(const int &data) { repeat_ = data; }
void set_inverted(const int &data) { inverted_ = data; }
void set_pulse_length(const int &data) { pulse_length_ = data; }
void set_data(const std::vector<uint8_t> &data) { code_ = data; }
void set_code(std::initializer_list<uint8_t> data) { this->code_ = std::vector<uint8_t>(data); }
void play(const Ts &...x) {
int repeats = this->repeat_.value(x...);
+8 -5
View File
@@ -187,7 +187,6 @@ def final_validation(config_list):
for config in config_list:
if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages):
raise cv.Invalid("At least one page must not be skipped")
uses_rotation = CONF_ROTATION in config
for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1]
display = global_config.get_config_for_path(path)
@@ -196,9 +195,9 @@ def final_validation(config_list):
"Using lambda: or pages: in display config is not compatible with LVGL"
)
# treating 0 as false is intended here.
if uses_rotation and display.get(CONF_ROTATION):
df.LOGGER.warning(
"use of 'rotation' in both LVGL and the display config is not recommended"
if display.get(CONF_ROTATION):
raise cv.Invalid(
"use of 'rotation' in the display config is not compatible with LVGL, please set rotation in the LVGL config instead"
)
if display.get(CONF_AUTO_CLEAR_ENABLED) is True:
raise cv.Invalid(
@@ -262,6 +261,7 @@ async def to_code(configs):
df.add_define("LV_USE_STDLIB_SPRINTF", "LV_STDLIB_CLIB")
df.add_define("LV_USE_STDLIB_STRING", "LV_STDLIB_CLIB")
df.add_define("LV_USE_STDLIB_MALLOC", "LV_STDLIB_CUSTOM")
df.add_define("LV_DEF_REFR_PERIOD", "16")
cg.add_define("USE_LVGL")
# suppress default enabling of extra widgets
# cg.add_define("LV_KCONFIG_PRESENT")
@@ -341,7 +341,10 @@ async def to_code(configs):
df.LOGGER.info("LVGL will use hardware rotation via display driver")
else:
rotation_type = RotationType.ROTATION_SOFTWARE
df.LOGGER.info("LVGL will use software rotation")
if get_esp32_variant() == VARIANT_ESP32P4:
df.LOGGER.info("LVGL will use software rotation (PPA accelerated)")
else:
df.LOGGER.info("LVGL will use software rotation")
lv_component = cg.new_Pvariable(
config[CONF_ID],
displays,
+3 -3
View File
@@ -2,6 +2,7 @@ from collections.abc import Callable
from typing import Any
from esphome import automation
from esphome.automation import StatelessLambdaAction
import esphome.codegen as cg
from esphome.components.display import validate_rotation
import esphome.config_validation as cv
@@ -201,7 +202,7 @@ def _validate_rotation(value):
@automation.register_action(
"lvgl.display.set_rotation",
ObjUpdateAction,
StatelessLambdaAction,
cv.maybe_simple_value(
LVGL_SCHEMA.extend(
{
@@ -214,8 +215,7 @@ def _validate_rotation(value):
)
async def lvgl_set_rotation(config, action_id, template_arg, args):
lv_comp = await cg.get_variable(config[CONF_LVGL_ID])
async with LambdaContext() as context:
add_line_marks(where=action_id)
async with LambdaContext(args, where=action_id) as context:
lv_add(lv_comp.set_rotation(config[CONF_ROTATION]))
return cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
+125 -21
View File
@@ -158,8 +158,15 @@ void LvglComponent::dump_config() {
" Draw rounding: %d",
this->width_, this->height_, 100 / this->buffer_frac_, this->rotation_, (int) this->draw_rounding);
if (this->rotation_type_ != ROTATION_UNUSED) {
ESP_LOGCONFIG(TAG, " Rotation type: %s",
this->rotation_type_ == RotationType::ROTATION_SOFTWARE ? "software" : "hardware via display driver");
const char *rot_type = "hardware via display driver";
if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) {
#ifdef USE_ESP32_VARIANT_ESP32P4
rot_type = this->ppa_client_ != nullptr ? "software (PPA accelerated)" : "software";
#else
rot_type = "software";
#endif
}
ESP_LOGCONFIG(TAG, " Rotation type: %s", rot_type);
}
}
@@ -252,21 +259,120 @@ void LvglComponent::show_prev_page(lv_screen_load_anim_t anim, uint32_t time) {
size_t LvglComponent::get_current_page() const { return this->current_page_; }
bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; }
#ifdef USE_ESP32_VARIANT_ESP32P4
bool LvglComponent::ppa_rotate_(const lv_color_data *src, lv_color_data *dst, uint16_t width, uint16_t height,
uint32_t height_rounded) {
ppa_srm_rotation_angle_t angle;
uint16_t out_w, out_h;
// Map ESPHome clockwise display rotation to PPA counter-clockwise angles
switch (this->rotation_) {
case display::DISPLAY_ROTATION_90_DEGREES:
angle = PPA_SRM_ROTATION_ANGLE_270; // 270° CCW = 90° CW
out_w = height_rounded;
out_h = width;
break;
case display::DISPLAY_ROTATION_180_DEGREES:
angle = PPA_SRM_ROTATION_ANGLE_180;
out_w = width;
out_h = height;
break;
case display::DISPLAY_ROTATION_270_DEGREES:
angle = PPA_SRM_ROTATION_ANGLE_90; // 90° CCW = 270° CW
out_w = height_rounded;
out_h = width;
break;
default:
return false; // No rotation needed
}
// Align buffer size to cache line (LV_DRAW_BUF_ALIGN) as required by PPA DMA
// the underlying buffer will be large enough as the size is also padded when allocating.
size_t out_buf_size = out_w * out_h * sizeof(lv_color_data);
out_buf_size = LV_ROUND_UP(out_buf_size, LV_DRAW_BUF_ALIGN);
ppa_srm_oper_config_t srm_config{};
srm_config.in.buffer = src;
srm_config.in.pic_w = width;
srm_config.in.pic_h = height;
srm_config.in.block_w = width;
srm_config.in.block_h = height;
#if LV_COLOR_DEPTH == 16
srm_config.in.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
#elif LV_COLOR_DEPTH == 32
srm_config.in.srm_cm = PPA_SRM_COLOR_MODE_ARGB8888;
#endif
srm_config.out.buffer = dst;
srm_config.out.buffer_size = out_buf_size;
srm_config.out.pic_w = out_w;
srm_config.out.pic_h = out_h;
#if LV_COLOR_DEPTH == 16
srm_config.out.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
#elif LV_COLOR_DEPTH == 32
srm_config.out.srm_cm = PPA_SRM_COLOR_MODE_ARGB8888;
#endif
srm_config.rotation_angle = angle;
srm_config.scale_x = 1.0f;
srm_config.scale_y = 1.0f;
srm_config.mode = PPA_TRANS_MODE_BLOCKING;
esp_err_t ret = ppa_do_scale_rotate_mirror(this->ppa_client_, &srm_config);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "PPA rotation failed: %s", esp_err_to_name(ret));
ESP_LOGW(TAG, "PPA SRM: in=%ux%u src=%p, out=%ux%u dst=%p size=%zu, angle=%d", width, height, src, out_w, out_h,
dst, out_buf_size, (int) angle);
return false;
}
return true;
}
#endif // USE_ESP32_VARIANT_ESP32P4
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) {
auto width = lv_area_get_width(area);
auto height = lv_area_get_height(area);
auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding;
auto x1 = area->x1;
auto y1 = area->y1;
if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) {
if (this->rotation_type_ == ROTATION_SOFTWARE) {
lv_color_data *dst = reinterpret_cast<lv_color_data *>(this->rotate_buf_);
#ifdef USE_ESP32_VARIANT_ESP32P4
bool ppa_done = this->ppa_client_ != nullptr && this->ppa_rotate_(ptr, dst, width, height, height_rounded);
if (!ppa_done)
#endif
{
switch (this->rotation_) {
case display::DISPLAY_ROTATION_90_DEGREES:
for (lv_coord_t x = height; x-- != 0;) {
for (lv_coord_t y = 0; y != width; y++) {
dst[y * height_rounded + x] = *ptr++;
}
}
break;
case display::DISPLAY_ROTATION_180_DEGREES:
for (lv_coord_t y = height; y-- != 0;) {
for (lv_coord_t x = width; x-- != 0;) {
dst[y * width + x] = *ptr++;
}
}
break;
case display::DISPLAY_ROTATION_270_DEGREES:
for (lv_coord_t x = 0; x != height; x++) {
for (lv_coord_t y = width; y-- != 0;) {
dst[y * height_rounded + x] = *ptr++;
}
}
break;
default:
dst = ptr;
break;
}
}
// Coordinate adjustments apply regardless of PPA or SW rotation
switch (this->rotation_) {
case display::DISPLAY_ROTATION_90_DEGREES:
for (lv_coord_t x = height; x-- != 0;) {
for (lv_coord_t y = 0; y != width; y++) {
dst[y * height_rounded + x] = *ptr++;
}
}
y1 = x1;
x1 = this->width_ - area->y1 - height;
height = width;
@@ -274,21 +380,11 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) {
break;
case display::DISPLAY_ROTATION_180_DEGREES:
for (lv_coord_t y = height; y-- != 0;) {
for (lv_coord_t x = width; x-- != 0;) {
dst[y * width + x] = *ptr++;
}
}
x1 = this->width_ - x1 - width;
y1 = this->height_ - y1 - height;
break;
case display::DISPLAY_ROTATION_270_DEGREES:
for (lv_coord_t x = 0; x != height; x++) {
for (lv_coord_t y = width; y-- != 0;) {
dst[y * height_rounded + x] = *ptr++;
}
}
x1 = y1;
y1 = this->height_ - area->x1 - width;
height = width;
@@ -296,7 +392,6 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) {
break;
default:
dst = ptr;
break;
}
ptr = dst;
@@ -317,7 +412,7 @@ void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uin
lv_display_flush_ready(disp_drv);
}
IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout) : timeout_(std::move(timeout)) {
IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableFn<uint32_t> timeout) : timeout_(timeout) {
parent->add_on_idle_callback([this](uint32_t idle_time) {
if (!this->is_idle_ && idle_time > this->timeout_.value()) {
this->is_idle_ = true;
@@ -664,6 +759,15 @@ void LvglComponent::setup() {
this->mark_failed();
return;
}
#ifdef USE_ESP32_VARIANT_ESP32P4
ppa_client_config_t ppa_config{};
ppa_config.oper_type = PPA_OPERATION_SRM;
ppa_config.max_pending_trans_num = 1;
if (ppa_register_client(&ppa_config, &this->ppa_client_) != ESP_OK) {
ESP_LOGW(TAG, "PPA client registration failed, using software rotation");
this->ppa_client_ = nullptr;
}
#endif
}
if (this->draw_start_callback_ != nullptr) {
lv_display_add_event_cb(this->disp_, render_start_cb, LV_EVENT_RENDER_START, this);
@@ -804,7 +908,7 @@ static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; // NOLINT
static void *lv_alloc_draw_buf(size_t size, bool internal) {
void *buffer;
size = ((size + LV_DRAW_BUF_ALIGN - 1) / LV_DRAW_BUF_ALIGN) * LV_DRAW_BUF_ALIGN;
size = LV_ROUND_UP(size, LV_DRAW_BUF_ALIGN);
buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT
if (buffer == nullptr)
ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : "");
+16 -2
View File
@@ -26,6 +26,10 @@
#include <utility>
#include <vector>
#ifdef USE_ESP32_VARIANT_ESP32P4
#include "driver/ppa.h"
#endif
#ifdef USE_FONT
#include "esphome/components/font/font.h"
#endif // USE_LVGL_FONT
@@ -229,6 +233,9 @@ class LvglComponent : public PollingComponent {
display::DisplayRotation get_rotation() const { return this->rotation_; }
void rotate_coordinates(int32_t &x, int32_t &y) const;
uint16_t get_width() const { return lv_display_get_horizontal_resolution(this->disp_); }
uint16_t get_height() const { return lv_display_get_vertical_resolution(this->disp_); }
protected:
void set_resolution_() const;
void draw_end_();
@@ -238,6 +245,10 @@ class LvglComponent : public PollingComponent {
void write_random_();
void draw_buffer_(const lv_area_t *area, lv_color_data *ptr);
#ifdef USE_ESP32_VARIANT_ESP32P4
bool ppa_rotate_(const lv_color_data *src, lv_color_data *dst, uint16_t width, uint16_t height,
uint32_t height_rounded);
#endif
void flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p);
std::vector<display::Display *> displays_{};
@@ -266,14 +277,17 @@ class LvglComponent : public PollingComponent {
void *rotate_buf_{};
display::DisplayRotation rotation_{display::DISPLAY_ROTATION_0_DEGREES};
RotationType rotation_type_;
#ifdef USE_ESP32_VARIANT_ESP32P4
ppa_client_handle_t ppa_client_{};
#endif
};
class IdleTrigger : public Trigger<> {
public:
explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout);
explicit IdleTrigger(LvglComponent *parent, TemplatableFn<uint32_t> timeout);
protected:
TemplatableValue<uint32_t> timeout_;
TemplatableFn<uint32_t> timeout_;
bool is_idle_{};
};
+2 -1
View File
@@ -2,6 +2,7 @@ from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_BUTTON,
CONF_ID,
CONF_INDEX,
CONF_ITEMS,
@@ -73,7 +74,7 @@ class TabviewType(WidgetType):
)
def get_uses(self):
return CONF_BUTTONMATRIX, TYPE_FLEX
return CONF_BUTTONMATRIX, TYPE_FLEX, CONF_BUTTON
async def to_code(self, w: Widget, config: dict):
await w.set_property(
-1
View File
@@ -15,7 +15,6 @@ from esphome.const import (
CODEOWNERS = ["@looping40"]
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["gpio_expander"]
MULTI_CONF = True
CONF_BRIGHTNESS_MODE = "brightness_mode"
+5 -43
View File
@@ -40,52 +40,14 @@ void MAX6956::setup() {
ESP_LOGD(TAG, "setup reg[0x%.2X]=0x%.2X", MAX6956_CONFIGURATION, configuration);
}
void MAX6956::loop() {
// Invalidate cache at the start of each loop
this->reset_pin_cache_();
}
bool MAX6956::digital_read_hw(uint8_t pin) {
// MAX6956 pins start at MAX6956_MIN
if (pin < MAX6956_MIN || pin > MAX6956_MAX) {
return false;
}
// Calculate bank index based on the base class view (no offset adjustment)
uint8_t bank_index = pin / MAX6956_BANK_SIZE;
static constexpr uint8_t BANK_REGS[4] = {
MAX6956_4PORTS_4_7, // Bank 0: 4 ports 4-7 (bits D0-D3, D4-D7 read as 0)
MAX6956_8PORTS_8_15, // Bank 1: 8 ports 8-15 (bits D0-D7)
MAX6956_8PORTS_16_23, // Bank 2: 8 ports 16-23 (bits D0-D7)
MAX6956_8PORTS_24_31, // Bank 3: 8 ports 24-31 (bits D0-D7)
};
// Read the appropriate register
bool MAX6956::digital_read(uint8_t pin) {
uint8_t reg_addr = MAX6956_1PORT_VALUE_START + pin;
uint8_t value = 0;
if (!this->read_reg_(BANK_REGS[bank_index], &value)) {
return false;
}
// Store in cache with proper alignment
if (bank_index == 0) {
// Special case for bank 0: pins 4-7 are in bits D0-D3, shift them to bits 4-7
this->input_banks_[0] = value << MAX6956_BANK0_SHIFT;
} else {
// Banks 1-3 map directly
this->input_banks_[bank_index] = value;
}
return true;
this->read_reg_(reg_addr, &value);
return (value & MASK_1PORT_VALUE);
}
bool MAX6956::digital_read_cache(uint8_t pin) {
uint8_t bank_index = pin / MAX6956_BANK_SIZE;
uint8_t bit_position = pin % MAX6956_BANK_SIZE;
return (this->input_banks_[bank_index] & (1 << bit_position)) != 0;
}
void MAX6956::digital_write_hw(uint8_t pin, bool value) {
void MAX6956::digital_write(uint8_t pin, bool value) {
uint8_t reg_addr = MAX6956_1PORT_VALUE_START + pin;
this->write_reg_(reg_addr, value);
}
+7 -32
View File
@@ -3,7 +3,6 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
namespace esphome {
namespace max6956 {
@@ -22,41 +21,29 @@ enum MAX6956GPIORange : uint8_t {
MAX6956_MAX = 31,
};
/// Bank configuration for MAX6956
static constexpr uint8_t MAX6956_BANK_SIZE = 8;
static constexpr uint8_t MAX6956_TOTAL_PINS = 32; // Includes pins 0-3 (unused) for cache alignment
static constexpr uint8_t MAX6956_BANK0_SHIFT = 4;
static_assert(MAX6956_TOTAL_PINS % MAX6956_BANK_SIZE == 0,
"MAX6956_TOTAL_PINS must be an exact multiple of MAX6956_BANK_SIZE for CachedGpioExpander");
enum MAX6956GPIORegisters {
MAX6956_GLOBAL_CURRENT = 0x02,
MAX6956_CONFIGURATION = 0x04,
MAX6956_TRANSITION_DETECT_MASK = 0x06,
MAX6956_DISPLAY_TEST = 0x07,
MAX6956_PORT_CONFIG_START = 0x09, // Port Configuration P7, P6, P5, P4
MAX6956_CURRENT_START = 0x12, // Current054
MAX6956_1PORT_VALUE_START = 0x20, // Port 0 only (virtual port, no action)
// 8-port bulk read registers aligned with base class banks
MAX6956_4PORTS_4_7 = 0x40, // 4 ports 4-7 (bits D0-D3, D4-D7 read as 0)
MAX6956_8PORTS_8_15 = 0x48, // 8 ports 8-15 (bits D0-D7)
MAX6956_8PORTS_16_23 = 0x50, // 8 ports 16-23 (bits D0-D7)
MAX6956_8PORTS_24_31 = 0x58, // 8 ports 24-31 (bits D0-D7)
MAX6956_PORT_CONFIG_START = 0x09, // Port Configuration P7, P6, P5, P4
MAX6956_CURRENT_START = 0x12, // Current054
MAX6956_1PORT_VALUE_START = 0x20, // Port 0 only (virtual port, no action)
MAX6956_8PORTS_VALUE_START = 0x44, // 8 ports 4-11 (data bits D0-D7)
};
enum MAX6956GPIOFlag { FLAG_LED = 0x20 };
enum MAX6956CURRENTMODE { GLOBAL = 0x00, SEGMENT = 0x01 };
class MAX6956 : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, MAX6956_TOTAL_PINS> {
class MAX6956 : public Component, public i2c::I2CDevice {
public:
MAX6956() = default;
void setup() override;
void loop() override;
bool digital_read(uint8_t pin);
void digital_write(uint8_t pin, bool value);
void pin_mode(uint8_t pin, gpio::Flags flags);
void pin_mode(uint8_t pin, max6956::MAX6956GPIOFlag flags);
@@ -71,11 +58,6 @@ class MAX6956 : public Component,
void write_brightness_global();
void write_brightness_mode();
// CachedGpioExpander implementation
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
protected:
// read a given register
bool read_reg_(uint8_t reg, uint8_t *value);
@@ -84,13 +66,6 @@ class MAX6956 : public Component,
max6956::MAX6956CURRENTMODE brightness_mode_{};
uint8_t global_brightness_{0};
// Cache for the 4 banks of 8 pins each (aligned with base class view)
// Bank 0: bits 0-7 (bits 0-3 unused as MAX6956 pins start at 4, bits 4-7 = pins 4-7)
// Bank 1: bits 8-15 (pins 8-15)
// Bank 2: bits 16-23 (pins 16-23)
// Bank 3: bits 24-31 (pins 24-31)
uint8_t input_banks_[4] = {0, 0, 0, 0};
private:
int8_t prev_bright_[28] = {0};
};
+6 -3
View File
@@ -147,7 +147,8 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id(
async def max7219digit_invert_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_state(config[CONF_STATE]))
template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_)
cg.add(var.set_state(template_))
return var
@@ -166,7 +167,8 @@ async def max7219digit_invert_to_code(config, action_id, template_arg, args):
async def max7219digit_visible_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_state(config[CONF_STATE]))
template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_)
cg.add(var.set_state(template_))
return var
@@ -185,7 +187,8 @@ async def max7219digit_visible_to_code(config, action_id, template_arg, args):
async def max7219digit_reverse_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_state(config[CONF_STATE]))
template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_)
cg.add(var.set_state(template_))
return var
+7
View File
@@ -13,6 +13,7 @@ from esphome.const import (
)
from esphome.core import CORE, Lambda, coroutine_with_priority
from esphome.coroutine import CoroPriority
from esphome.cpp_generator import LambdaExpression
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
@@ -131,6 +132,12 @@ def mdns_service(
Returns:
A StructInitializer representing a MDNSService struct
"""
# Wrap port in a stateless lambda for TemplatableFn storage.
# Can't use cg.templatable() here because this is a sync function.
if not isinstance(port, LambdaExpression):
port = LambdaExpression(
f"return {cg.safe_exp(port)};", [], capture="", return_type=cg.uint16
)
return cg.StructInitializer(
MDNSService,
("service_type", cg.RawExpression(f"MDNS_STR({cg.safe_exp(service)})")),
+6 -6
View File
@@ -57,7 +57,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &service = services.emplace_next();
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
service.proto = MDNS_STR(SERVICE_TCP);
service.port = api::global_api_server->get_port();
service.port = []() -> uint16_t { return api::global_api_server->get_port(); };
const auto &friendly_name = App.get_friendly_name();
bool friendly_name_empty = friendly_name.empty();
@@ -151,7 +151,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &prom_service = services.emplace_next();
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
prom_service.proto = MDNS_STR(SERVICE_TCP);
prom_service.port = USE_WEBSERVER_PORT;
prom_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
#endif
#ifdef USE_SENDSPIN
@@ -162,7 +162,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &sendspin_service = services.emplace_next();
sendspin_service.service_type = MDNS_STR(SERVICE_SENDSPIN);
sendspin_service.proto = MDNS_STR(SERVICE_TCP);
sendspin_service.port = USE_SENDSPIN_PORT;
sendspin_service.port = []() -> uint16_t { return USE_SENDSPIN_PORT; };
sendspin_service.txt_records = {{MDNS_STR(TXT_SENDSPIN_PATH), MDNS_STR(VALUE_SENDSPIN_PATH)}};
#endif
@@ -172,7 +172,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &web_service = services.emplace_next();
web_service.service_type = MDNS_STR(SERVICE_HTTP);
web_service.proto = MDNS_STR(SERVICE_TCP);
web_service.port = USE_WEBSERVER_PORT;
web_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
#endif
#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_SENDSPIN) && !defined(USE_WEBSERVER) && \
@@ -185,7 +185,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
auto &fallback_service = services.emplace_next();
fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
fallback_service.proto = MDNS_STR(SERVICE_TCP);
fallback_service.port = USE_WEBSERVER_PORT;
fallback_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}};
#endif
}
@@ -199,7 +199,7 @@ void MDNSComponent::dump_config() {
ESP_LOGV(TAG, " Services:");
for (const auto &service : this->services_) {
ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto),
const_cast<TemplatableValue<uint16_t> &>(service.port).value());
service.port.value());
for (const auto &record : service.txt_records) {
ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
}
+1 -1
View File
@@ -36,7 +36,7 @@ struct MDNSService {
// second label indicating protocol _including_ underscore character prefix
// as defined in RFC6763 Section 7, like "_tcp" or "_udp"
const MDNSString *proto;
TemplatableValue<uint16_t> port;
TemplatableFn<uint16_t> port;
FixedVector<MDNSTXTRecord> txt_records;
};
+1 -1
View File
@@ -37,7 +37,7 @@ static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_S
txt_records.get()[i].key = MDNS_STR_ARG(record.key);
txt_records.get()[i].value = MDNS_STR_ARG(record.value);
}
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
uint16_t port = service.port.value();
err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
txt_records.get(), service.txt_records.size());
+1 -1
View File
@@ -27,7 +27,7 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
while (progmem_read_byte((const uint8_t *) service_type) == '_') {
service_type++;
}
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
uint16_t port = service.port.value();
MDNS.addService(FPSTR(service_type), FPSTR(proto), port);
for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)),
+1 -1
View File
@@ -27,7 +27,7 @@ static void register_libretiny(MDNSComponent *, StaticVector<MDNSService, MDNS_S
while (*service_type == '_') {
service_type++;
}
uint16_t port_ = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
uint16_t port_ = service.port.value();
MDNS.addService(service_type, proto, port_);
for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
+1 -1
View File
@@ -32,7 +32,7 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
while (*service_type == '_') {
service_type++;
}
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
uint16_t port = service.port.value();
MDNS.addService(service_type, proto, port);
for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
+1 -1
View File
@@ -305,7 +305,7 @@ _register_state_conditions()
async def media_player_volume_set_action(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
volume = await cg.templatable(config[CONF_VOLUME], args, float)
volume = await cg.templatable(config[CONF_VOLUME], args, cg.float_)
cg.add(var.set_volume(volume))
return var
+2 -2
View File
@@ -168,8 +168,8 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST);
if (capabilities.supportEcoPreset())
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO);
if (capabilities.supportFrostProtectionPreset())
traits.set_supported_custom_presets({Constants::FREEZE_PROTECTION});
// Frost protection custom preset is handled by AirConditioner directly
// since custom presets are stored on the Climate base class
}
} // namespace ac
+22 -5
View File
@@ -24,6 +24,25 @@ template<typename T> void update_property(T &property, const T &value, bool &fla
}
void AirConditioner::on_status_change() {
// Add frost protection custom preset once when autoconf completes
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK &&
this->base_.getCapabilities().supportFrostProtectionPreset() && !this->frost_protection_set_) {
// Read existing presets (set by codegen), append frost protection, write back
const auto &existing = this->get_traits().get_supported_custom_presets();
bool found = false;
for (const char *p : existing) {
if (strcmp(p, Constants::FREEZE_PROTECTION) == 0) {
found = true;
break;
}
}
if (!found) {
std::vector<const char *> merged(existing.begin(), existing.end());
merged.push_back(Constants::FREEZE_PROTECTION);
this->set_supported_custom_presets(merged);
}
this->frost_protection_set_ = true;
}
bool need_publish = false;
update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish);
update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish);
@@ -91,17 +110,15 @@ ClimateTraits AirConditioner::traits() {
traits.set_supported_modes(this->supported_modes_);
traits.set_supported_swing_modes(this->supported_swing_modes_);
traits.set_supported_presets(this->supported_presets_);
if (!this->supported_custom_presets_.empty())
traits.set_supported_custom_presets(this->supported_custom_presets_);
if (!this->supported_custom_fan_modes_.empty())
traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
// Custom fan modes and presets are stored on Climate base class and wired via get_traits()
/* + MINIMAL SET OF CAPABILITIES */
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH);
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK)
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) {
Converters::to_climate_traits(traits, this->base_.getCapabilities());
}
if (!traits.get_supported_modes().empty())
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF);
if (!traits.get_supported_swing_modes().empty())
+3 -4
View File
@@ -46,8 +46,8 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
void set_custom_presets(std::initializer_list<const char *> presets) { this->supported_custom_presets_ = presets; }
void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->supported_custom_fan_modes_ = modes; }
void set_custom_presets(std::initializer_list<const char *> presets) { this->set_supported_custom_presets(presets); }
void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->set_supported_custom_fan_modes(modes); }
protected:
void control(const ClimateCall &call) override;
@@ -55,8 +55,7 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
ClimateModeMask supported_modes_{};
ClimateSwingModeMask supported_swing_modes_{};
ClimatePresetMask supported_presets_{};
std::vector<const char *> supported_custom_presets_{};
std::vector<const char *> supported_custom_fan_modes_{};
bool frost_protection_set_{false};
Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr};
@@ -11,6 +11,7 @@ from .. import (
modbus_controller_ns,
)
from ..const import (
CONF_CUSTOM_COMMAND,
CONF_MODBUS_CONTROLLER_ID,
CONF_REGISTER_TYPE,
CONF_USE_WRITE_MULTIPLE,
@@ -35,6 +36,10 @@ CONFIG_SCHEMA = cv.typed_schema(
"coil": output.BINARY_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend(
{
cv.GenerateID(): cv.declare_id(ModbusBinaryOutput),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid(
"custom_command is not supported for outputs"
),
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
}
@@ -42,6 +47,10 @@ CONFIG_SCHEMA = cv.typed_schema(
"holding": output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend(
{
cv.GenerateID(): cv.declare_id(ModbusFloatOutput),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid(
"custom_command is not supported for outputs"
),
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(
SENSOR_VALUE_TYPE
),
@@ -61,7 +61,7 @@ async def to_code(config):
response_size = config[CONF_RESPONSE_SIZE]
reg_count = config[CONF_REGISTER_COUNT]
if reg_count == 0:
reg_count = response_size / 2
reg_count = response_size // 2
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_REGISTER_TYPE],
+2 -2
View File
@@ -504,7 +504,7 @@ async def mqtt_publish_action_to_code(config, action_id, template_arg, args):
cg.add(var.set_payload(template_))
template_ = await cg.templatable(config[CONF_QOS], args, cg.uint8)
cg.add(var.set_qos(template_))
template_ = await cg.templatable(config[CONF_RETAIN], args, bool)
template_ = await cg.templatable(config[CONF_RETAIN], args, cg.bool_)
cg.add(var.set_retain(template_))
return var
@@ -537,7 +537,7 @@ async def mqtt_publish_json_action_to_code(config, action_id, template_arg, args
cg.add(var.set_payload(lambda_))
template_ = await cg.templatable(config[CONF_QOS], args, cg.uint8)
cg.add(var.set_qos(template_))
template_ = await cg.templatable(config[CONF_RETAIN], args, bool)
template_ = await cg.templatable(config[CONF_RETAIN], args, cg.bool_)
cg.add(var.set_retain(template_))
return var
@@ -76,13 +76,13 @@ async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, bool)
template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_)
cg.add(var.set_state(template_))
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, bool)
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_)
cg.add(var.set_publish_state(template_))
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, bool)
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_)
cg.add(var.set_send_to_nextion(template_))
return var
+1 -1
View File
@@ -148,7 +148,7 @@ async def nextion_set_brightness_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float)
template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.float_)
cg.add(var.set_brightness(template_))
return var
@@ -116,13 +116,13 @@ async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, float)
template_ = await cg.templatable(config[CONF_STATE], args, cg.float_)
cg.add(var.set_state(template_))
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, bool)
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_)
cg.add(var.set_publish_state(template_))
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, bool)
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_)
cg.add(var.set_send_to_nextion(template_))
return var
@@ -58,13 +58,13 @@ async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_STATE], args, bool)
template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_)
cg.add(var.set_state(template_))
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, bool)
template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_)
cg.add(var.set_publish_state(template_))
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, bool)
template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_)
cg.add(var.set_send_to_nextion(template_))
return var
+14 -6
View File
@@ -257,10 +257,14 @@ async def _build_number_automations(var, config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await cg.register_component(trigger, conf)
if CONF_ABOVE in conf:
template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float)
template_ = await cg.templatable(
conf[CONF_ABOVE], [(float, "x")], cg.float_
)
cg.add(trigger.set_min(template_))
if CONF_BELOW in conf:
template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float)
template_ = await cg.templatable(
conf[CONF_BELOW], [(float, "x")], cg.float_
)
cg.add(trigger.set_max(template_))
await automation.build_automation(trigger, [(float, "x")], conf)
@@ -362,7 +366,7 @@ OPERATION_BASE_SCHEMA = cv.Schema(
async def number_set_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VALUE], args, float)
template_ = await cg.templatable(config[CONF_VALUE], args, cg.float_)
cg.add(var.set_value(template_))
return var
@@ -445,10 +449,14 @@ async def number_to_to_code(config, action_id, template_arg, args):
to_ = await cg.templatable(operation, args, NumberOperation)
cg.add(var.set_operation(to_))
if (cycle := config.get(CONF_CYCLE)) is not None:
template_ = await cg.templatable(cycle, args, bool)
template_ = await cg.templatable(cycle, args, cg.bool_)
cg.add(var.set_cycle(template_))
if (mode := config.get(CONF_MODE)) is not None:
cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[mode]))
template_ = await cg.templatable(
NUMBER_OPERATION_OPTIONS[mode], args, NumberOperation
)
cg.add(var.set_operation(template_))
if (cycle := config.get(CONF_CYCLE)) is not None:
cg.add(var.set_cycle(cycle))
template_ = await cg.templatable(cycle, args, cg.bool_)
cg.add(var.set_cycle(template_))
return var
+2 -2
View File
@@ -63,8 +63,8 @@ class ValueRangeTrigger : public Trigger<float>, public Component {
Number *parent_;
ESPPreferenceObject rtc_;
bool previous_in_range_{false};
TemplatableValue<float, float> min_{NAN};
TemplatableValue<float, float> max_{NAN};
TemplatableFn<float, float> min_{[](float) -> float { return NAN; }};
TemplatableFn<float, float> max_{[](float) -> float { return NAN; }};
};
template<typename... Ts> class NumberInRangeCondition : public Condition<Ts...> {
+1 -1
View File
@@ -100,7 +100,7 @@ async def online_image_action_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_))
if CONF_UPDATE in config:
template_ = await cg.templatable(config[CONF_UPDATE], args, bool)
template_ = await cg.templatable(config[CONF_UPDATE], args, cg.bool_)
cg.add(var.set_update(template_))
return var
+1 -1
View File
@@ -181,7 +181,7 @@ void OpenThreadSrpComponent::setup() {
memcpy(string, host_name.c_str(), host_name_len);
// Set port
entry->mService.mPort = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
entry->mService.mPort = service.port.value();
otDnsTxtEntry *txt_entries =
reinterpret_cast<otDnsTxtEntry *>(this->pool_alloc_(sizeof(otDnsTxtEntry) * service.txt_records.size()));
+3 -3
View File
@@ -104,7 +104,7 @@ async def output_turn_off_to_code(config, action_id, template_arg, args):
async def output_set_level_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_LEVEL], args, float)
template_ = await cg.templatable(config[CONF_LEVEL], args, cg.float_)
cg.add(var.set_level(template_))
return var
@@ -123,7 +123,7 @@ async def output_set_level_to_code(config, action_id, template_arg, args):
async def output_set_min_power_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_MIN_POWER], args, float)
template_ = await cg.templatable(config[CONF_MIN_POWER], args, cg.float_)
cg.add(var.set_min_power(template_))
return var
@@ -142,7 +142,7 @@ async def output_set_min_power_to_code(config, action_id, template_arg, args):
async def output_set_max_power_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_MAX_POWER], args, float)
template_ = await cg.templatable(config[CONF_MAX_POWER], args, cg.float_)
cg.add(var.set_max_power(template_))
return var
+51 -10
View File
@@ -6,7 +6,12 @@ from pathlib import Path
from typing import Any
from esphome import git, yaml_util
from esphome.components.substitutions import ContextVars, push_context, substitute
from esphome.components.substitutions import (
ContextVars,
push_context,
resolve_include,
substitute,
)
from esphome.components.substitutions.jinja import has_jinja
from esphome.config_helpers import Remove, merge_config
import esphome.config_validation as cv
@@ -31,6 +36,8 @@ from esphome.core import EsphomeError
_LOGGER = logging.getLogger(__name__)
DOMAIN = CONF_PACKAGES
# Guard against infinite include chains (e.g. A includes B includes A).
MAX_INCLUDE_DEPTH = 20
def is_remote_package(package_config: dict) -> bool:
@@ -59,8 +66,8 @@ def valid_package_contents(package_config: dict) -> dict:
for k, v in package_config.items():
if not isinstance(k, str):
raise cv.Invalid("Package content keys must be strings")
if isinstance(v, (dict, list, Remove)):
continue # e.g. script: [], psram: !remove, logger: {level: debug}
if isinstance(v, (dict, list, Remove, yaml_util.IncludeFile)):
continue # e.g. script: [], psram: !remove, logger: {level: debug}, switch: !include switches.yaml
if v is None:
continue # e.g. web_server:
if isinstance(v, str) and has_jinja(v):
@@ -160,6 +167,7 @@ REMOTE_PACKAGE_SCHEMA = cv.All(
PACKAGE_SCHEMA = cv.Any( # A package definition is either:
validate_source_shorthand, # A git URL shorthand string that expands to a remote package schema, or
REMOTE_PACKAGE_SCHEMA, # a valid remote package schema, or
yaml_util.IncludeFile, # isinstance check — passes IncludeFile objects through unchanged, or:
valid_package_contents, # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}}
# which will have to be fully validated later as per each component's schema.
)
@@ -396,16 +404,49 @@ class _PackageProcessor:
self.skip_update = skip_update
def resolve_package(
self, package_config: dict | str, context_vars: ContextVars | None
self,
package_config: dict | str | yaml_util.IncludeFile,
context_vars: ContextVars | None,
) -> dict:
"""Substitute variables in the definition and fetch remote packages.
"""Resolve a package definition to a concrete ``dict`` and fetch remote packages.
The input may be a ``str`` (git shorthand or Jinja expression) or a
``dict`` (remote or local package). After ``PACKAGE_SCHEMA`` validation
the result is always a ``dict``.
The input may be a ``str`` (git shorthand or Jinja expression), a
``dict`` (remote or local package), or an ``IncludeFile`` whose filename
may itself contain substitution expressions.
The loop handles the case where loading an ``IncludeFile`` yields another
``IncludeFile`` (e.g. a chain of deferred includes). Each iteration:
1. If the current value is an ``IncludeFile``, load it resolving any
substitutions in its filename first.
2. Substitute variables in the resulting value (for strings and remote
package dicts).
3. Validate against ``PACKAGE_SCHEMA``. If the result is a ``dict``,
the loop exits; otherwise another iteration is needed.
Raises ``cv.Invalid`` if the chain has not resolved to a ``dict`` after
``MAX_INCLUDE_DEPTH`` iterations.
"""
package_config = _substitute_package_definition(package_config, context_vars)
package_config = PACKAGE_SCHEMA(package_config)
for _ in range(MAX_INCLUDE_DEPTH):
if isinstance(package_config, yaml_util.IncludeFile):
package_config, _ = resolve_include(
package_config,
[],
context_vars or ContextVars(),
strict_undefined=False,
)
package_config = _substitute_package_definition(
package_config, context_vars
)
package_config = PACKAGE_SCHEMA(package_config)
if isinstance(package_config, dict):
break
else:
raise cv.Invalid(
f"Maximum include nesting depth ({MAX_INCLUDE_DEPTH}) exceeded"
)
if is_remote_package(package_config):
package_config = _process_remote_package(package_config, self.skip_update)
return package_config
+3 -3
View File
@@ -189,13 +189,13 @@ async def set_control_parameters(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
kp_template_ = await cg.templatable(config[CONF_KP], args, float)
kp_template_ = await cg.templatable(config[CONF_KP], args, cg.float_)
cg.add(var.set_kp(kp_template_))
ki_template_ = await cg.templatable(config[CONF_KI], args, float)
ki_template_ = await cg.templatable(config[CONF_KI], args, cg.float_)
cg.add(var.set_ki(ki_template_))
kd_template_ = await cg.templatable(config[CONF_KD], args, float)
kd_template_ = await cg.templatable(config[CONF_KD], args, cg.float_)
cg.add(var.set_kd(kd_template_))
return var

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