Compare commits

..
269 changed files with 1233 additions and 5387 deletions
+1 -1
View File
@@ -1 +1 @@
10c432ae818f9ed7fd4a0176a04467b1f2634363f5ec985045a6d72747f60b90
f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6
+1 -1
View File
@@ -339,7 +339,7 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30 # v4
uses: CodSpeedHQ/action@d872884a306dd4853acf0f584f4b706cf0cc72a2 # 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.2
PROJECT_NUMBER = 2026.4.0-dev
# 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 -8
View File
@@ -750,15 +750,8 @@ def upload_using_esptool(
platformio_api.FlashImage(
path=idedata.firmware_bin_path, offset=firmware_offset
),
*idedata.extra_flash_images,
]
for image in idedata.extra_flash_images:
if not image.path.is_file():
_LOGGER.warning(
"Skipping missing flash image declared by platform: %s",
image.path,
)
continue
flash_images.append(image)
mcu = "esp8266"
if CORE.is_esp32:
+6 -72
View File
@@ -151,8 +151,8 @@ class ConfigBundleCreator:
def __init__(self, config: dict[str, Any]) -> None:
self._config = config
self._config_dir = Path(CORE.config_dir).resolve()
self._config_path = Path(CORE.config_path).resolve()
self._config_dir = CORE.config_dir
self._config_path = CORE.config_path
self._files: list[BundleFile] = []
self._seen_paths: set[Path] = set()
self._secrets_paths: set[Path] = set()
@@ -258,36 +258,21 @@ class ConfigBundleCreator:
def _discover_yaml_includes(self) -> None:
"""Discover YAML files loaded during config parsing.
Deliberately uses a fresh re-parse and force-loads every deferred
``IncludeFile`` to include *all* potentially-reachable includes,
even branches not selected by the local substitutions. Bundles are
meant to be compiled on another system where command-line
substitution overrides may choose a different branch — e.g.
``!include network/${eth_model}/config.yaml`` must ship every
candidate so the remote build can pick any one.
Entries with unresolved substitution variables in the filename
path are skipped with a warning (they cannot be resolved without
the substitution pass).
We track files by wrapping _load_yaml_internal. The config has already
been loaded at this point (bundle is a POST_CONFIG_ACTION), so we
re-load just to discover the file list.
Secrets files are tracked separately so we can filter them to
only include the keys this config actually references.
"""
# Must be a fresh parse: IncludeFile.load() caches its result in
# _content, and we discover files by listening for loader calls. On
# an already-parsed tree the cache is populated, .load() returns
# without calling the loader, the listener never fires, and the
# referenced files would be silently dropped from the bundle.
with yaml_util.track_yaml_loads() as loaded_files:
try:
data = yaml_util.load_yaml(self._config_path)
yaml_util.load_yaml(self._config_path)
except EsphomeError:
_LOGGER.debug(
"Bundle: re-loading YAML for include discovery failed, "
"proceeding with partial file list"
)
else:
_force_load_include_files(data)
for fpath in loaded_files:
if fpath == self._config_path.resolve():
@@ -623,57 +608,6 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
tar.addfile(info, io.BytesIO(data))
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
resolved during the substitution pass. During bundle discovery we need
the referenced files to actually load so the ``track_yaml_loads``
listener fires for them.
``IncludeFile`` instances with unresolved substitution variables in the
filename cannot be loaded — we skip and warn about those.
"""
if _seen is None:
_seen = set()
if isinstance(obj, yaml_util.IncludeFile):
if id(obj) in _seen:
return
_seen.add(id(obj))
if obj.has_unresolved_expressions():
_LOGGER.warning(
"Bundle: cannot resolve !include %s (referenced from %s) "
"with substitutions in path",
obj.file,
obj.parent_file,
)
return
try:
loaded = obj.load()
except EsphomeError as err:
_LOGGER.warning(
"Bundle: failed to load !include %s (referenced from %s): %s",
obj.file,
obj.parent_file,
err,
)
return
_force_load_include_files(loaded, _seen)
elif isinstance(obj, dict):
if id(obj) in _seen:
return
_seen.add(id(obj))
for value in obj.values():
_force_load_include_files(value, _seen)
elif isinstance(obj, (list, tuple)):
if id(obj) in _seen:
return
_seen.add(id(obj))
for item in obj:
_force_load_include_files(item, _seen)
def _resolve_include_path(include_path: Any) -> Path | None:
"""Resolve an include path to absolute, skipping system includes."""
if isinstance(include_path, str) and include_path.startswith("<"):
-1
View File
@@ -79,7 +79,6 @@ from esphome.cpp_types import ( # noqa: F401
float_,
global_ns,
gpio_Flags,
int8,
int16,
int32,
int64,
+1 -14
View File
@@ -2,11 +2,7 @@ import logging
import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import (
get_esp32_variant,
include_builtin_idf_component,
require_adc_oneshot_iram,
)
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
from esphome.components.zephyr import (
zephyr_add_overlay,
@@ -28,7 +24,6 @@ from esphome.const import (
PlatformFramework,
)
from esphome.core import CORE
from esphome.types import ConfigType
from . import (
ATTENUATION_MODES,
@@ -70,13 +65,6 @@ def validate_config(config):
return config
def _require_adc_iram(config: ConfigType) -> ConfigType:
"""Register ADC oneshot IRAM requirement during config validation."""
if CORE.is_esp32:
require_adc_oneshot_iram()
return config
ADCSensor = adc_ns.class_(
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
)
@@ -107,7 +95,6 @@ CONFIG_SCHEMA = cv.All(
)
.extend(cv.polling_component_schema("60s")),
validate_config,
_require_adc_iram,
)
CONF_ADC_CHANNEL_ID = "adc_channel_id"
+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, cg.uint8)
address = await cg.templatable(config[CONF_ADDRESS], args, cg.int32)
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, cg.uint8)
template_ = await cg.templatable(config.get(CONF_MODE), args, cg.int32)
cg.add(var.set_auto_mute_mode(template_))
return var
@@ -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], [], cg.float_)
upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], cg.float_)
lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], float)
upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], float)
else:
lower = await cg.templatable(config[CONF_THRESHOLD], [], cg.float_)
lower = await cg.templatable(config[CONF_THRESHOLD], [], float)
upper = lower
cg.add(var.set_upper_threshold(upper))
cg.add(var.set_lower_threshold(lower))
+2 -6
View File
@@ -671,7 +671,6 @@ message SensorStateResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SENSOR";
option (no_delay) = true;
option (speed_optimized) = true;
fixed32 key = 1 [(force) = true];
float state = 2;
@@ -778,10 +777,9 @@ message SubscribeLogsResponse {
option (source) = SOURCE_SERVER;
option (log) = false;
option (no_delay) = false;
option (speed_optimized) = true;
LogLevel level = 1 [(force) = true];
bytes message = 3 [(force) = true];
LogLevel level = 1;
bytes message = 3;
}
// ==================== NOISE ENCRYPTION ====================
@@ -1627,7 +1625,6 @@ message BluetoothLEAdvertisementResponse {
}
message BluetoothLERawAdvertisement {
option (inline_encode) = true;
uint64 address = 1 [(force) = true];
sint32 rssi = 2 [(force) = true];
uint32 address_type = 3 [(max_value) = 4];
@@ -1640,7 +1637,6 @@ message BluetoothLERawAdvertisementsResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BLUETOOTH_PROXY";
option (no_delay) = true;
option (speed_optimized) = true;
repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"];
}
+64 -55
View File
@@ -52,11 +52,11 @@
namespace esphome::api {
// Maximum messages to read per loop iteration to prevent starving other components.
// Read a maximum of 5 messages per loop iteration to prevent starving other components.
// This is a balance between API responsiveness and allowing other components to run.
// Since each message could contain multiple protobuf messages when using packet batching,
// this limits the number of messages processed, not the number of TCP packets.
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 10;
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5;
static constexpr uint8_t MAX_PING_RETRIES = 60;
static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
@@ -220,17 +220,10 @@ void APIConnection::loop() {
}
const uint32_t now = App.get_loop_component_start_time();
// Check if socket has data ready before attempting to read.
// Also try reading if we hit the message limit last time — LWIP's rcvevent
// (used by is_socket_ready) tracks pbuf dequeues, not bytes. When multiple
// messages share a TCP segment, the last message's data stays in LWIP's
// lastdata cache after rcvevent hits 0, making is_socket_ready() return false
// even though data remains.
if (this->helper_->is_socket_ready() || this->flags_.may_have_remaining_data) {
this->flags_.may_have_remaining_data = false;
// Check if socket has data ready before attempting to read
if (this->helper_->is_socket_ready()) {
// Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput
uint8_t message_count = 0;
for (; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
ReadPacketBuffer buffer;
err = this->helper_->read_packet(&buffer);
if (err == APIError::WOULD_BLOCK) {
@@ -252,11 +245,6 @@ void APIConnection::loop() {
return;
}
}
// If we hit the limit, there may be more data remaining in LWIP's
// lastdata cache that rcvevent doesn't account for.
if (message_count == MAX_MESSAGES_PER_LOOP) {
this->flags_.may_have_remaining_data = true;
}
}
// Process deferred batch if scheduled and timer has expired
@@ -327,8 +315,6 @@ 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);
@@ -336,27 +322,21 @@ void APIConnection::process_active_iterator_() {
} else { // INITIAL_STATE
if (this->iterator_storage_.initial_state.completed()) {
this->destroy_active_iterator_();
this->finalize_iterator_sync_();
// 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();
} 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_();
@@ -426,7 +406,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_slow(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
}
uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
@@ -2025,12 +2005,48 @@ 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);
}
// encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE)
// 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();
// 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);
// 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);
}
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
@@ -2098,13 +2114,6 @@ void APIConnection::process_batch_() {
return;
}
// Ensure TCP_NODELAY is on before draining overflow and writing batch data.
// Log messages enable Nagle (NODELAY off) to coalesce small packets.
// If Nagle is still on when we try to drain, LWIP holds data in the
// Nagle buffer, the TCP send buffer stays full, and the overflow
// buffer can never drain — blocking the batch write indefinitely.
this->helper_->set_nodelay_for_message(false);
// Try to clear buffer first
if (!this->try_to_clear_buffer(true)) {
// Can't write now, we'll try again later
@@ -2164,15 +2173,17 @@ 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 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)
// 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
uint32_t current_offset = 0;
// Process items and encode directly to buffer (up to our limit)
@@ -2188,14 +2199,13 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
}
// Message was encoded successfully
// payload_size = header_size + proto_payload_size + footer_size
uint16_t proto_payload_size = payload_size - this->batch_header_size_ - footer_size;
// payload_size is header_padding + actual payload size + footer_size
uint16_t proto_payload_size = payload_size - frame_overhead;
// 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, this->batch_header_size_);
new (&message_info[items_processed++]) MessageInfo(item.message_type, current_offset, proto_payload_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;
@@ -2245,7 +2255,6 @@ 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) {
+8 -59
View File
@@ -276,7 +276,6 @@ 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();
@@ -411,59 +410,16 @@ 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);
// 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();
// 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);
// 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.
// Thin template wrapper — computes size, delegates buffer work to non-template helper
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_slow(0, &encode_msg_noop, &msg, conn, remaining_size);
return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size);
} else {
return encode_to_buffer_slow(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
return encode_to_buffer(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
}
}
@@ -662,7 +618,6 @@ 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
@@ -771,7 +726,6 @@ class APIConnection final : public APIServerConnectionBase {
uint8_t batch_scheduled : 1;
uint8_t batch_first_message : 1; // For batch buffer allocation
uint8_t should_try_send_immediately : 1; // True after initial states are sent
uint8_t may_have_remaining_data : 1; // Read loop hit limit, retry without ready check
#ifdef HAS_PROTO_MESSAGE_DUMP
uint8_t log_only_mode : 1;
#endif
@@ -780,14 +734,9 @@ 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 types to fill remaining space before next 4-byte boundary
// 1-byte type to fill padding
ActiveIterator active_iterator_{ActiveIterator::NONE};
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};
// Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary
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
+29 -49
View File
@@ -100,17 +100,10 @@ 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 (err != EWOULDBLOCK && err != EAGAIN) {
this->state_ = State::FAILED;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
@@ -118,58 +111,45 @@ APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
return APIError::OK;
}
// 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);
// 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) {
#ifdef HELPER_LOG_PACKETS
// 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);
for (int i = 0; i < iovcnt; i++) {
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
}
#endif
return err;
}
// 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) {
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]] {
int err = errno;
if (err != EWOULDBLOCK && err != EAGAIN) {
this->state_ = State::FAILED;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
sent = 0; // Treat WOULD_BLOCK as zero bytes sent
} else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] {
return APIError::OK;
} else {
skip = static_cast<uint16_t>(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, static_cast<uint16_t>(sent))) {
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) {
HELPER_LOG("Overflow buffer full, dropping connection");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
+23 -64
View File
@@ -49,17 +49,12 @@ 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, uint8_t hdr)
: offset(off), payload_size(size), message_type(type), header_size(hdr) {}
MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
};
enum class APIError : uint16_t {
@@ -166,33 +161,20 @@ class APIFrameHelper {
this->nodelay_counter_ = 0;
}
}
// 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 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
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
virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0;
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() const { return frame_header_padding_; }
// 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
@@ -214,41 +196,18 @@ class APIFrameHelper {
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
APIError drain_overflow_and_handle_errors_();
// 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
// Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
// 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);
// 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;
}
// 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,8 +47,15 @@ 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
@@ -457,83 +464,65 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = type;
return APIError::OK;
}
// 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;
// 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
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
// 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();
uint8_t *write_start = buffer_data + messages[0].offset;
uint16_t total_write_len = 0;
for (const auto &msg : messages) {
uint8_t *buf_start = buffer_data + msg.offset;
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;
total_write_len += encrypted_len;
if (messages.empty()) {
return APIError::OK;
}
return this->write_raw_fast_buf_(write_start, total_write_len);
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;
// 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);
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;
}
// Send all encrypted messages in one writev call
return this->write_raw_(iovs.data(), iovs.size(), total_write_len);
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
@@ -542,16 +531,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_(iov, 2, 3 + len);
return this->write_raw_(iov, 2, 3 + len); // Header + data
}
/** Initiate the data structures for the handshake.
@@ -617,7 +606,7 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
if (aerr != APIError::OK)
return aerr;
this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
@@ -9,22 +9,19 @@ 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) {
frame_header_padding_ = HEADER_PADDING;
// 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;
}
~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:
@@ -36,8 +33,6 @@ 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,8 +39,15 @@ 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.
@@ -198,6 +205,7 @@ 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,
@@ -212,12 +220,14 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
"Bad indicator byte";
char msg[INDICATOR_MSG_SIZE];
memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE);
this->write_raw_buf_(msg, INDICATOR_MSG_SIZE);
iov[0].iov_base = (void *) msg;
#else
static const char MSG[] = "\x00"
"Bad indicator byte";
this->write_raw_buf_(MSG, INDICATOR_MSG_SIZE);
iov[0].iov_base = (void *) MSG;
#endif
iov[0].iov_len = INDICATOR_MSG_SIZE;
this->write_raw_(iov, 1, INDICATOR_MSG_SIZE);
}
return aerr;
}
@@ -227,101 +237,73 @@ 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) {
#ifdef ESPHOME_DEBUG_API
assert(this->state_ == State::DATA);
assert(!messages.empty());
#endif
uint8_t *buffer_data = buffer.get_buffer()->data();
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
// 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 (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;
if (messages.empty()) {
return APIError::OK;
}
return this->write_raw_fast_buf_(write_start, total_len);
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;
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;
}
// Send all messages in one writev call
return write_raw_(iovs.data(), iovs.size(), total_write_len);
}
} // namespace esphome::api
@@ -7,21 +7,18 @@ 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)) {
frame_header_padding_ = HEADER_PADDING;
// 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;
}
~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:
-2
View File
@@ -22,8 +22,6 @@ extend google.protobuf.MessageOptions {
optional bool log = 1039 [default=true];
optional bool no_delay = 1040 [default=false];
optional string base_class = 1041;
optional bool inline_encode = 1042 [default=false];
optional bool speed_optimized = 1043 [default=false];
}
extend google.protobuf.FieldOptions {
+35 -46
View File
@@ -745,9 +745,7 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const {
#endif
return size;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key);
ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state);
@@ -757,9 +755,7 @@ SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) c
#endif
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
SensorStateResponse::calculate_size() const {
uint32_t SensorStateResponse::calculate_size() const {
uint32_t size = 0;
size += 5;
size += ProtoSize::calc_float(1, this->state);
@@ -916,22 +912,16 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t
}
return true;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level), true);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26);
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_len_);
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_ptr_, this->message_len_);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level));
ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_);
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
SubscribeLogsResponse::calculate_size() const {
uint32_t SubscribeLogsResponse::calculate_size() const {
uint32_t size = 0;
size += 2;
size += ProtoSize::calc_length_force(1, this->message_len_);
size += this->level ? 2 : 0;
size += ProtoSize::calc_length(1, this->message_len_);
return size;
}
#ifdef USE_API_NOISE
@@ -2338,41 +2328,40 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
}
return true;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, this->address);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(this->rssi));
if (this->address_type) {
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24);
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->address_type);
}
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(this->data_len));
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->data, this->data_len);
return pos;
}
uint32_t BluetoothLERawAdvertisement::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_uint64_force(1, this->address);
size += ProtoSize::calc_sint32_force(1, this->rssi);
size += this->address_type ? 2 : 0;
size += 2 + this->data_len;
return size;
}
uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
for (uint16_t i = 0; i < this->advertisements_len; i++) {
auto &sub_msg = this->advertisements[i];
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 10);
uint8_t *len_pos = pos;
ProtoEncode::reserve_byte(pos PROTO_ENCODE_DEBUG_ARG);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(sub_msg.rssi));
if (sub_msg.address_type) {
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24);
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address_type);
}
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(sub_msg.data_len));
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.data, sub_msg.data_len);
*len_pos = static_cast<uint8_t>(pos - len_pos - 1);
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 1, this->advertisements[i]);
}
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
BluetoothLERawAdvertisementsResponse::calculate_size() const {
uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
uint32_t size = 0;
for (uint16_t i = 0; i < this->advertisements_len; i++) {
auto &sub_msg = this->advertisements[i];
size += 2;
size += ProtoSize::calc_uint64_force(1, sub_msg.address);
size += ProtoSize::calc_sint32_force(1, sub_msg.rssi);
size += sub_msg.address_type ? 2 : 0;
size += 2 + sub_msg.data_len;
size += ProtoSize::calc_message_force(1, this->advertisements[i].calculate_size());
}
return size;
}
+2
View File
@@ -1888,6 +1888,8 @@ class BluetoothLERawAdvertisement final : public ProtoMessage {
uint32_t address_type{0};
uint8_t data[62]{};
uint8_t data_len{0};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
-17
View File
@@ -352,12 +352,6 @@ class ProtoEncode {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = b;
}
/// Reserve one byte for later backpatch (e.g., sub-message length).
/// Advances pos past the reserved byte without writing a value.
static inline void ESPHOME_ALWAYS_INLINE reserve_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM) {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
pos++;
}
/// Write raw bytes to the buffer (no tag, no length prefix).
static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
const void *data, size_t len) {
@@ -651,17 +645,6 @@ 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
*
+11 -9
View File
@@ -169,17 +169,18 @@ 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, cg.int8)
template_ = await cg.templatable(frontend_reset, args, cg.int32)
cg.add(var.set_hw_frontend_reset(template_))
if freq := config.get(CONF_FREQUENCY):
if not cg.is_template(freq):
freq = int(freq / 1000000)
template_ = await cg.templatable(freq, args, cg.int_)
if cg.is_template(freq):
template_ = await cg.templatable(freq, args, cg.int32)
else:
template_ = int(freq / 1000000)
cg.add(var.set_frequency(template_))
if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None:
template_ = await cg.templatable(sens_dist, args, cg.int_)
template_ = await cg.templatable(sens_dist, args, cg.int32)
cg.add(var.set_sensing_distance(template_))
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
@@ -199,13 +200,14 @@ 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, cg.int_)
template_ = await cg.templatable(stage_gain, args, cg.int32)
cg.add(var.set_stage_gain(template_))
if power := config.get(CONF_POWER_CONSUMPTION):
if not cg.is_template(power):
power = int(power * 1000000)
template_ = await cg.templatable(power, args, cg.int_)
if cg.is_template(power):
template_ = await cg.templatable(power, args, cg.int32)
else:
template_ = int(power * 1000000)
cg.add(var.set_power_consumption(template_))
return var
+2 -2
View File
@@ -111,14 +111,14 @@ class ATM90E32Component : public PollingComponent,
#endif
float get_reference_voltage(uint8_t phase) {
#ifdef USE_NUMBER
return (phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
#else
return 120.0; // Default voltage
#endif
}
float get_reference_current(uint8_t phase) {
#ifdef USE_NUMBER
return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
#else
return 5.0f; // Default current
#endif
+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, cg.float_)
template_ = await cg.templatable(config.get(CONF_MIC_GAIN), args, 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, cg.float_)
template_ = await cg.templatable(config.get(CONF_VOLUME), args, float)
cg.add(var.set_volume(template_))
return var
@@ -61,15 +61,6 @@ 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,14 +42,21 @@ 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,
});
// Custom fan modes and presets are set once in setup(), stored on Climate base class,
// and wired automatically via get_traits()
// 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",
});
traits.set_visual_min_temperature(19.0);
traits.set_visual_max_temperature(43.0);
traits.set_visual_temperature_step(1.0);
+1 -1
View File
@@ -63,7 +63,7 @@ void BM8563::read_time() {
rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second);
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
+1
View File
@@ -162,6 +162,7 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
await cg.register_parented(var, config[CONF_CANBUS_ID])
if (can_id := config.get(CONF_CAN_ID)) is not None:
can_id = await cg.templatable(can_id, args, cg.uint32)
cg.add(var.set_can_id(can_id))
cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID]))
+1 -17
View File
@@ -102,8 +102,6 @@ CC1101Component::CC1101Component() {
memset(this->pa_table_, 0, sizeof(this->pa_table_));
}
void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
void CC1101Component::setup() {
this->spi_setup();
this->cs_->digital_write(true);
@@ -150,12 +148,7 @@ void CC1101Component::setup() {
// Defer pin mode setup until after all components have completed setup()
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
if (this->gdo0_pin_ != nullptr) {
this->defer([this]() {
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
}
});
this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); });
}
}
@@ -167,7 +160,6 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
}
void CC1101Component::loop() {
this->disable_loop();
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
!this->gdo0_pin_->digital_read()) {
return;
@@ -248,7 +240,6 @@ void CC1101Component::begin_tx() {
this->write_(Register::PKTCTRL0, 0x32);
ESP_LOGV(TAG, "Beginning TX sequence");
if (this->gdo0_pin_ != nullptr) {
this->gdo0_pin_->detach_interrupt();
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
}
// Transition through IDLE to bypass CCA (Clear Channel Assessment) which can
@@ -678,13 +669,6 @@ void CC1101Component::set_packet_mode(bool value) {
this->state_.GDO0_CFG = 0x0D;
}
if (this->initialized_) {
if (this->gdo0_pin_ != nullptr) {
if (value) {
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
} else {
this->gdo0_pin_->detach_interrupt();
}
}
this->write_(Register::PKTCTRL0);
this->write_(Register::PKTCTRL1);
this->write_(Register::IOCFG0);
-1
View File
@@ -93,7 +93,6 @@ class CC1101Component : public Component,
// GDO pin for packet reception
InternalGPIOPin *gdo0_pin_{nullptr};
static void IRAM_ATTR gpio_intr(CC1101Component *arg);
// Packet handling
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
+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, cg.float_)
template_ = await cg.templatable(target_temp, args, 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, cg.float_)
template_ = await cg.templatable(target_temp_low, args, 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, cg.float_)
template_ = await cg.templatable(target_temp_high, args, 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, cg.float_)
template_ = await cg.templatable(target_humidity, args, 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)
+5 -16
View File
@@ -484,11 +484,6 @@ 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_);
@@ -686,8 +681,9 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) {
}
bool Climate::set_custom_fan_mode_(const char *mode, size_t len) {
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, this->find_custom_fan_mode_(mode, len),
this->has_custom_fan_mode());
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());
}
void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; }
@@ -695,7 +691,8 @@ 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) {
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, this->find_custom_preset_(preset, len),
auto traits = this->get_traits();
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset, len),
this->has_custom_preset());
}
@@ -706,10 +703,6 @@ 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);
}
@@ -718,10 +711,6 @@ 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);
}
+3 -44
View File
@@ -1,6 +1,5 @@
#pragma once
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
@@ -235,28 +234,6 @@ 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; }
@@ -359,14 +336,13 @@ class Climate : public EntityBase {
* called from publish_state()
*/
void save_state_(const ClimateTraits &traits);
void save_state_() { this->save_state_(this->get_traits()); }
void save_state_() { this->save_state_(this->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};
@@ -377,33 +353,16 @@ 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 supported_custom_fan_modes_ or nullptr.
* Points to an entry in traits.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 supported_custom_presets_ or nullptr.
* Points to an entry in traits.supported_custom_presets_ or nullptr.
* Use get_custom_preset() to read, set_custom_preset_() to modify.
*/
const char *custom_preset_{nullptr};
@@ -2,33 +2,6 @@
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_);
}
+23 -67
View File
@@ -147,45 +147,27 @@ 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 {
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
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
}
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) {
// Compat: store in owned vector. Copies copy the vector (deprecated path still copies this vector).
this->compat_custom_fan_modes_ = modes;
this->supported_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->compat_custom_fan_modes_ = modes;
this->supported_custom_fan_modes_ = modes;
}
// 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);
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->supported_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;
// 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;
const std::vector<const char *> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
bool supports_custom_fan_mode(const char *custom_fan_mode) const {
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
return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
}
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return this->supports_custom_fan_mode(custom_fan_mode.c_str());
@@ -197,32 +179,23 @@ 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->compat_custom_presets_ = presets;
this->supported_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->compat_custom_presets_ = presets;
this->supported_custom_presets_ = presets;
}
// 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);
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->supported_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;
// 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;
const std::vector<const char *> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
bool supports_custom_preset(const char *custom_preset) const {
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
return vector_contains(this->supported_custom_presets_, custom_preset);
}
bool supports_custom_preset(const std::string &custom_preset) const {
return this->supports_custom_preset(custom_preset.c_str());
@@ -285,25 +258,13 @@ 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 {
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);
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len);
}
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
@@ -312,11 +273,7 @@ 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 {
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);
return vector_find(this->supported_custom_presets_, custom_preset, len);
}
uint32_t feature_flags_{0};
@@ -332,17 +289,16 @@ class ClimateTraits {
climate::ClimateSwingModeMask supported_swing_modes_;
climate::ClimatePresetMask supported_presets_;
/** Custom mode storage - pointers to vectors owned by the Climate base class.
/** Custom mode storage using const char* pointers to eliminate std::string overhead.
*
* ClimateTraits does not own this data; Climate stores the vectors and
* get_traits() wires these pointers automatically.
* 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.
*/
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_;
std::vector<const char *> supported_custom_fan_modes_;
std::vector<const char *> supported_custom_presets_;
};
} // namespace esphome::climate
+1 -8
View File
@@ -7,12 +7,6 @@ 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();
@@ -45,8 +39,7 @@ 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());
// Preset modes are set once in setup() and wired via wire_preset_modes_()
this->wire_preset_modes_(traits);
traits.set_supported_preset_modes(base.supported_preset_modes());
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, cg.bool_)
template_ = await cg.templatable(stop, args, bool)
cg.add(var.set_stop(template_))
if (state := config.get(CONF_STATE)) is not None:
template_ = await cg.templatable(state, args, cg.float_)
template_ = await cg.templatable(state, args, float)
cg.add(var.set_position(template_))
if (position := config.get(CONF_POSITION)) is not None:
template_ = await cg.templatable(position, args, cg.float_)
template_ = await cg.templatable(position, args, float)
cg.add(var.set_position(template_))
if (tilt := config.get(CONF_TILT)) is not None:
template_ = await cg.templatable(tilt, args, cg.float_)
template_ = await cg.templatable(tilt, args, float)
cg.add(var.set_tilt(template_))
return var
+3 -15
View File
@@ -16,19 +16,6 @@ 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;
@@ -118,13 +105,14 @@ class DemoClimate : public climate::Climate, public Component {
climate::CLIMATE_FAN_DIFFUSE,
climate::CLIMATE_FAN_QUIET,
});
// Custom fan modes and presets are set once in setup()
traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"});
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 |
@@ -135,7 +123,7 @@ class DemoClimate : public climate::Climate, public Component {
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_HEAT_COOL,
});
// Custom fan modes are set once in setup()
traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"});
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, cg.int8)
template_ = await cg.templatable(factory_reset_config, args, cg.int32)
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, cg.float_)
template_ = await cg.templatable(segments[0], args, float)
cg.add(var.set_det_min1(template_))
template_ = await cg.templatable(segments[1], args, cg.float_)
template_ = await cg.templatable(segments[1], args, float)
cg.add(var.set_det_max1(template_))
if len(segments) >= 4:
template_ = await cg.templatable(segments[2], args, cg.float_)
template_ = await cg.templatable(segments[2], args, float)
cg.add(var.set_det_min2(template_))
template_ = await cg.templatable(segments[3], args, cg.float_)
template_ = await cg.templatable(segments[3], args, float)
cg.add(var.set_det_max2(template_))
if len(segments) >= 6:
template_ = await cg.templatable(segments[4], args, cg.float_)
template_ = await cg.templatable(segments[4], args, float)
cg.add(var.set_det_min3(template_))
template_ = await cg.templatable(segments[5], args, cg.float_)
template_ = await cg.templatable(segments[5], args, float)
cg.add(var.set_det_max3(template_))
if len(segments) >= 8:
template_ = await cg.templatable(segments[6], args, cg.float_)
template_ = await cg.templatable(segments[6], args, float)
cg.add(var.set_det_min4(template_))
template_ = await cg.templatable(segments[7], args, cg.float_)
template_ = await cg.templatable(segments[7], args, 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, cg.int8)
template_ = await cg.templatable(config[CONF_SENSITIVITY], args, cg.int32)
cg.add(var.set_sensitivity(template_))
return var
+1 -1
View File
@@ -44,7 +44,7 @@ void DS1307Component::read_time() {
.year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
+31 -110
View File
@@ -128,30 +128,23 @@ ASSERTION_LEVELS = {
SIGNING_SCHEMES = {
"rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME",
"ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME",
"ecdsa_v1": "CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME",
}
# Chip variants that only support one V2 signing scheme.
# Chip variants that only support one signing scheme for Secure Boot V2.
# Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h.
# Variants not listed in either set support both RSA and ECDSA V2
# Variants not listed in either set support both RSA and ECDSA
# (e.g. C5, C6, H2, P4). New variants should be added to the
# appropriate set if they only support one scheme.
# Note: VARIANT_ESP32 is not listed here because it supports V2 RSA only
# when minimum_chip_revision >= 3.0, which requires special handling.
SIGNED_OTA_V2_RSA_ONLY_VARIANTS = {
SIGNED_OTA_RSA_ONLY_VARIANTS = {
VARIANT_ESP32,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C3,
}
SIGNED_OTA_V2_ECC_ONLY_VARIANTS = {
SIGNED_OTA_ECC_ONLY_VARIANTS = {
VARIANT_ESP32C2,
VARIANT_ESP32C61,
}
# V1 ECDSA (Secure Boot V1) is only supported on the original ESP32.
# Based on SOC_SECURE_BOOT_V1 in soc_caps.h.
SIGNED_OTA_V1_ECDSA_VARIANTS = {
VARIANT_ESP32,
}
COMPILER_OPTIMIZATIONS = {
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
@@ -678,12 +671,11 @@ def _is_framework_url(source: str) -> bool:
# The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 3, 8),
"latest": cv.Version(3, 3, 8),
"dev": cv.Version(3, 3, 8),
"recommended": cv.Version(3, 3, 7),
"latest": cv.Version(3, 3, 7),
"dev": cv.Version(3, 3, 7),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"),
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
@@ -703,7 +695,6 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# These versions correspond to pioarduino/esp-idf releases
# See: https://github.com/pioarduino/esp-idf/releases
ARDUINO_IDF_VERSION_LOOKUP = {
cv.Version(3, 3, 8): cv.Version(5, 5, 4),
cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"),
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
@@ -723,15 +714,17 @@ ARDUINO_IDF_VERSION_LOOKUP = {
# The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(5, 5, 4),
"latest": cv.Version(5, 5, 4),
"recommended": cv.Version(5, 5, 3, "1"),
"latest": cv.Version(5, 5, 3, "1"),
"dev": cv.Version(5, 5, 4),
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(
6, 0, 0
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
cv.Version(5, 5, 4): cv.Version(55, 3, 38, "1"),
cv.Version(
5, 5, 4
): "https://github.com/pioarduino/platform-espressif32.git#develop",
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
cv.Version(5, 5, 3): cv.Version(55, 3, 37),
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
@@ -751,8 +744,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 38, "1"),
"latest": cv.Version(55, 3, 38, "1"),
"recommended": cv.Version(55, 3, 37),
"latest": cv.Version(55, 3, 37),
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
}
@@ -998,73 +991,25 @@ def final_validate(config):
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
scheme = signed_ota[CONF_SIGNING_SCHEME]
variant = config[CONF_VARIANT]
min_rev = advanced.get(CONF_MINIMUM_CHIP_REVISION)
scheme_path = [
CONF_FRAMEWORK,
CONF_ADVANCED,
CONF_SIGNED_OTA_VERIFICATION,
CONF_SIGNING_SCHEME,
]
# V1 ECDSA is only available on the original ESP32
if scheme == "ecdsa_v1" and variant not in SIGNED_OTA_V1_ECDSA_VARIANTS:
scheme_variant_conflicts = {
"ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"),
"rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"),
}
if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[
0
]:
errs.append(
cv.Invalid(
f"Signing scheme 'ecdsa_v1' is only supported on "
f"{VARIANT_FRIENDLY[VARIANT_ESP32]}. "
f"Use 'rsa3072' or 'ecdsa256' instead.",
path=scheme_path,
f"Signing scheme '{scheme}' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
path=[
CONF_FRAMEWORK,
CONF_ADVANCED,
CONF_SIGNED_OTA_VERIFICATION,
CONF_SIGNING_SCHEME,
],
)
)
elif variant == VARIANT_ESP32:
# On ESP32, V2 RSA requires minimum_chip_revision >= 3.0
# Note: string comparison works here because cv.one_of constrains
# min_rev to known ESP32_CHIP_REVISIONS values ("0.0".."3.1").
if scheme == "rsa3072" and (min_rev is None or min_rev < "3.0"):
errs.append(
cv.Invalid(
f"Signing scheme 'rsa3072' on {VARIANT_FRIENDLY[variant]} "
f"requires minimum_chip_revision: '3.0' or higher "
f"(Secure Boot V2 RSA needs chip revision 3.0+). "
f"For older chip revisions, use 'ecdsa_v1' instead.",
path=scheme_path,
)
)
# ESP32 does not support V2 ECDSA (no SOC_SECURE_BOOT_V2_ECC)
elif scheme == "ecdsa256":
errs.append(
cv.Invalid(
f"Signing scheme 'ecdsa256' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use 'rsa3072' (with "
f"minimum_chip_revision: '3.0') or 'ecdsa_v1' instead.",
path=scheme_path,
)
)
# V1 on rev 3.0+ -- suggest V2 RSA for stronger security
elif scheme == "ecdsa_v1" and min_rev is not None and min_rev >= "3.0":
_LOGGER.info(
"Using Secure Boot V1 ECDSA on %s rev %s. "
"Consider using 'rsa3072' (Secure Boot V2 RSA) for "
"stronger security on chip revision 3.0+.",
VARIANT_FRIENDLY[variant],
min_rev,
)
else:
# Non-ESP32 variants: check V2 scheme-variant compatibility
scheme_variant_conflicts = {
"ecdsa256": (SIGNED_OTA_V2_RSA_ONLY_VARIANTS, "rsa3072"),
"rsa3072": (SIGNED_OTA_V2_ECC_ONLY_VARIANTS, "ecdsa256"),
}
if (
conflict := scheme_variant_conflicts.get(scheme)
) and variant in conflict[0]:
errs.append(
cv.Invalid(
f"Signing scheme '{scheme}' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
path=scheme_path,
)
)
if CONF_OTA not in full_config:
_LOGGER.warning(
"Signed OTA verification is enabled but no OTA component is configured. "
@@ -1113,7 +1058,6 @@ CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
CONF_DISABLE_FATFS = "disable_fatfs"
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
# VFS requirement tracking
# Components that need VFS features can call require_vfs_*() functions
@@ -1127,7 +1071,6 @@ KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
KEY_FATFS_REQUIRED = "fatfs_required"
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
def require_vfs_select() -> None:
@@ -1225,17 +1168,6 @@ def require_fatfs() -> None:
CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True
def require_adc_oneshot_iram() -> None:
"""Mark that ADC oneshot IRAM safety is required by a component.
Call this from components that use the ADC oneshot driver. When flash cache is
disabled (e.g., during NVS writes by WiFi, BLE, Zigbee, or power management),
the ADC oneshot read function must be in IRAM to avoid crashes.
This sets CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM.
"""
CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_REQUIRED] = True
def _parse_idf_component(value: str) -> ConfigType:
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
# Match operator followed by version-like string (digit or *)
@@ -1277,7 +1209,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean,
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean,
cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of(
*ESP32_CHIP_REVISIONS, string=True
*ESP32_CHIP_REVISIONS
),
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
@@ -1336,7 +1268,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
}
),
@@ -2137,16 +2068,6 @@ async def to_code(config):
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False)
# Place ADC oneshot control functions in IRAM for cache safety
# When flash cache is disabled (during NVS writes by WiFi, BLE, Zigbee, Thread,
# power management, etc.), ADC reads will crash if these functions are in flash.
# Components using ADC call require_adc_oneshot_iram() to force this.
if (
CORE.data[KEY_ESP32].get(KEY_ADC_ONESHOT_IRAM_REQUIRED, False)
or advanced[CONF_ADC_ONESHOT_IN_IRAM]
):
add_idf_sdkconfig_option("CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM", True)
# Disable FATFS support
# Components that need FATFS (SD card, etc.) can call require_fatfs()
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
-16
View File
@@ -1960,10 +1960,6 @@ BOARDS = {
"name": "Hornbill ESP32 Minima",
"variant": VARIANT_ESP32,
},
"huidu_hd_wf1": {
"name": "Huidu HD-WF1",
"variant": VARIANT_ESP32S2,
},
"huidu_hd_wf2": {
"name": "Huidu HD-WF2",
"variant": VARIANT_ESP32S3,
@@ -2032,10 +2028,6 @@ BOARDS = {
"name": "LilyGo T-Display-S3",
"variant": VARIANT_ESP32S3,
},
"lilygo-t-energy-s3": {
"name": "LilyGo T-Energy-S3",
"variant": VARIANT_ESP32S3,
},
"lilygo-t3-s3": {
"name": "LilyGo T3-S3",
"variant": VARIANT_ESP32S3,
@@ -2297,18 +2289,10 @@ BOARDS = {
"name": "S.ODI Ultra v1",
"variant": VARIANT_ESP32,
},
"seeed_xiao_esp32_s3_plus": {
"name": "Seeed Studio XIAO ESP32S3 Plus",
"variant": VARIANT_ESP32S3,
},
"seeed_xiao_esp32c3": {
"name": "Seeed Studio XIAO ESP32C3",
"variant": VARIANT_ESP32C3,
},
"seeed_xiao_esp32c5": {
"name": "Seeed Studio XIAO ESP32C5",
"variant": VARIANT_ESP32C5,
},
"seeed_xiao_esp32c6": {
"name": "Seeed Studio XIAO ESP32C6",
"variant": VARIANT_ESP32C6,
+68 -154
View File
@@ -59,59 +59,6 @@ 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.
@@ -119,7 +66,7 @@ static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *ou
// 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 = 2;
static constexpr uint32_t CRASH_DATA_VERSION = 1;
struct RawCrashData {
uint32_t version;
uint32_t magic;
@@ -130,13 +77,6 @@ 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)
@@ -160,28 +100,13 @@ 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
}
// 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.
// Clear magic regardless so we don't re-report on next normal reboot
s_raw_crash_data.magic = 0;
}
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.
@@ -287,36 +212,6 @@ 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
@@ -333,28 +228,33 @@ 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);
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);
}
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);
}
// 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);
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);
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;
#endif
pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr);
}
ESP_LOGE(TAG, "%s", hint);
}
@@ -376,54 +276,68 @@ 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;
s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE);
}
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,
};
#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);
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;
}
#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;
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);
}
uint8_t count = 0;
#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);
// 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;
}
#endif
#endif
// Write version and magic last — ensures all data is written before we mark it valid
+1 -7
View File
@@ -4,18 +4,12 @@
namespace esphome::esp32 {
/// 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.
/// Read crash data from NOINIT memory and clear the magic marker.
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();
+3 -9
View File
@@ -172,16 +172,10 @@ def validate_gpio_pin(pin):
exc,
)
else:
# `ignore_pin_validation_error` only suppresses an error raised by the
# variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin
# numbers). If that didn't raise, the option is a no-op -- warn so the
# user can clean it up, but don't block the build.
# Throw an exception if used for a pin that would not have resulted
# in a validation error anyway!
if ignore_pin_validation_warning:
_LOGGER.warning(
"GPIO%d has no validation errors to ignore; "
"remove `ignore_pin_validation_error: true` from this pin.",
pin[CONF_NUMBER],
)
raise cv.Invalid(f"GPIO{pin[CONF_NUMBER]} is not a reserved pin")
return pin
+3 -120
View File
@@ -5,7 +5,6 @@ import json # noqa: E402
import os # noqa: E402
import pathlib # noqa: E402
import shutil # noqa: E402
import subprocess # noqa: E402
from glob import glob # noqa: E402
@@ -26,114 +25,6 @@ def _parse_sdkconfig(sdkconfig_path):
return options
def _generate_v1_verification_key(env):
"""Generate the V1 ECDSA verification key binary and assembly source file.
Secure Boot V1 embeds the public verification key directly in the app binary
as a compiled object (via a .S assembly file). The ESP-IDF CMake build generates
these files via custom commands, but PlatformIO's SCons bridge does not execute
them. This function replicates that logic:
1. Extracts the raw public key from the PEM signing key using espsecure.
2. Generates the .S assembly source that embeds the key bytes.
"""
build_dir = pathlib.Path(env.subst("$BUILD_DIR"))
project_dir = pathlib.Path(env.subst("$PROJECT_DIR"))
pioenv = env.subst("$PIOENV")
sdkconfig = _parse_sdkconfig(project_dir / f"sdkconfig.{pioenv}")
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") != "y":
return
bin_path = build_dir / "signature_verification_key.bin"
asm_path = build_dir / "signature_verification_key.bin.S"
# Determine the source of the verification key
if sdkconfig.get("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES") == "y":
# Extract public key from the signing key
signing_key = sdkconfig.get("CONFIG_SECURE_BOOT_SIGNING_KEY")
if not signing_key:
return
signing_key_path = pathlib.Path(signing_key)
if not signing_key_path.exists():
print(f"Error: V1 ECDSA signing key not found: {signing_key_path}")
env.Exit(1)
return
if not bin_path.exists() or bin_path.stat().st_mtime < signing_key_path.stat().st_mtime:
python_exe = env.subst("$PYTHONEXE")
result = subprocess.run(
[python_exe, "-m", "espsecure", "extract_public_key",
"--keyfile", str(signing_key_path), str(bin_path)],
capture_output=True, text=True,
)
if result.returncode != 0:
print(f"Error extracting V1 verification key: {result.stderr}")
env.Exit(1)
return
print(f"Extracted V1 ECDSA verification key from {signing_key_path.name}")
else:
# User-provided verification key -- should already be a raw binary file
verification_key = sdkconfig.get("CONFIG_SECURE_BOOT_VERIFICATION_KEY")
if not verification_key:
return
verification_key_path = pathlib.Path(verification_key)
if not verification_key_path.exists():
print(f"Error: Verification key not found: {verification_key_path}")
env.Exit(1)
return
shutil.copyfile(str(verification_key_path), str(bin_path))
if not bin_path.exists():
return
# Generate the .S assembly file from the binary key data.
# Replicates ESP-IDF's data_file_embed_asm.cmake with RENAME_TO=signature_verification_key_bin.
# The file is needed in both the app build dir and the bootloader build dir, since
# the bootloader also embeds the verification key when CONFIG_SECURE_SIGNED_ON_BOOT_NO_SECURE_BOOT
# is enabled. PlatformIO's SCons bridge does not execute the CMake custom commands that
# normally generate these files.
data = bin_path.read_bytes()
varname = "signature_verification_key_bin"
lines = []
lines.append(f"/* Data converted from {bin_path.name} */")
lines.append(".data")
lines.append("#if !defined (__APPLE__) && !defined (__linux__)")
lines.append(".section .rodata.embedded")
lines.append("#endif")
lines.append(f"\n.global {varname}")
lines.append(f"{varname}:")
lines.append(f"\n.global _binary_{varname}_start")
lines.append(f"_binary_{varname}_start: /* for objcopy compatibility */")
# Format binary data as .byte lines (16 bytes per line)
for i in range(0, len(data), 16):
chunk = data[i:i + 16]
hex_bytes = ", ".join(f"0x{b:02x}" for b in chunk)
lines.append(f".byte {hex_bytes}")
lines.append(f"\n.global _binary_{varname}_end")
lines.append(f"_binary_{varname}_end: /* for objcopy compatibility */")
lines.append(f"\n.global {varname}_length")
lines.append(f"{varname}_length:")
lines.append(f".long {len(data)}")
lines.append("")
lines.append('#if defined (__linux__)')
lines.append('.section .note.GNU-stack,"",@progbits')
lines.append("#endif")
asm_content = "\n".join(lines) + "\n"
# Write to app build dir and bootloader build dir
asm_path.write_text(asm_content)
bootloader_dir = build_dir / "bootloader"
if bootloader_dir.is_dir():
bootloader_bin = bootloader_dir / "signature_verification_key.bin"
bootloader_asm = bootloader_dir / "signature_verification_key.bin.S"
shutil.copyfile(str(bin_path), str(bootloader_bin))
bootloader_asm.write_text(asm_content)
def sign_firmware(source, target, env):
"""
Sign the firmware binary using espsecure.py if signed OTA verification is enabled.
@@ -164,12 +55,9 @@ def sign_firmware(source, target, env):
env.Exit(1)
return
# Determine espsecure signature version from the signing scheme:
# V1 ECDSA (Secure Boot V1) uses --version 1, V2 RSA/ECDSA use --version 2.
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") == "y":
sign_version = "1"
else:
sign_version = "2"
# ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes),
# so the espsecure signature version is always 2.
sign_version = "2"
firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin"
firmware_path = build_dir / firmware_name
@@ -329,11 +217,6 @@ def esp32_copy_ota_bin(source, target, env):
print(f"Copied firmware to {new_file_name}")
# Generate V1 ECDSA verification key files before build starts.
# Workaround for PlatformIO not executing CMake custom commands that extract
# the public key and generate the .S assembly file for Secure Boot V1.
_generate_v1_verification_key(env) # noqa: F821
# Run signing first, then merge, then ota copy
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821
+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, cg.float_)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, float)
cg.add(var.set_frequency(template_))
return var
@@ -221,7 +221,7 @@ class EthernetComponent final : public Component {
int reset_pin_{-1};
int phy_addr_spi_{-1};
int clock_speed_;
spi_host_device_t interface_{SPI2_HOST};
spi_host_device_t interface_{SPI3_HOST};
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
uint32_t polling_interval_{0};
#endif
+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, cg.int_)
template_ = await cg.templatable(config[CONF_DURATION], args, cg.int32)
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, cg.int_)
template_ = await cg.templatable(config[CONF_DURATION], args, cg.int32)
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, cg.bool_)
template_ = await cg.templatable(oscillating, args, bool)
cg.add(var.set_oscillating(template_))
if (speed := config.get(CONF_SPEED)) is not None:
template_ = await cg.templatable(speed, args, cg.int_)
template_ = await cg.templatable(speed, args, cg.int32)
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, cg.bool_)
template_ = await cg.templatable(config[CONF_OFF_SPEED_CYCLE], args, bool)
cg.add(var.set_no_off_cycle(template_))
return var
+8 -47
View File
@@ -9,22 +9,6 @@ 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");
@@ -164,18 +148,6 @@ 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);
}
@@ -289,6 +261,8 @@ void Fan::save_state_() {
return;
}
auto traits = this->get_traits();
FanRestoreState state{};
state.state = this->state;
state.oscillating = this->oscillating;
@@ -297,25 +271,12 @@ void Fan::save_state_() {
state.preset_mode = FanRestoreState::NO_PRESET;
if (this->has_preset_mode()) {
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;
}
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;
}
}
}
-24
View File
@@ -130,14 +130,6 @@ 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; }
@@ -175,27 +167,11 @@ 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};
};
+11 -38
View File
@@ -3,17 +3,12 @@
#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)
@@ -35,64 +30,42 @@ class FanTraits {
bool supports_direction() const { return this->direction_; }
/// Set whether this fan supports changing direction
void set_direction(bool direction) { this->direction_ = direction; }
// 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")
/// 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).
void set_supported_preset_modes(std::initializer_list<const char *> 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;
this->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 {
// Same precedence as supported_preset_modes() getter
if (this->preset_modes_) {
return !this->preset_modes_->empty();
}
return !this->compat_preset_modes_.empty();
}
bool supports_preset_modes() const { return !this->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;
}
// 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) {
for (const char *mode : this->preset_modes_) {
if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') {
return mode;
return mode; // Return pointer from traits
}
}
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_{};
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_;
std::vector<const char *> preset_modes_{};
};
} // namespace fan
+1 -1
View File
@@ -7,7 +7,7 @@ namespace gdk101 {
static const char *const TAG = "gdk101";
static constexpr uint8_t NUMBER_OF_READ_RETRIES = 5;
static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 30;
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;
+1 -6
View File
@@ -108,13 +108,8 @@ async def globals_set_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
var = cg.new_Pvariable(action_id, template_arg, paren)
# Use the global's value_type alias as the lambda return type so
# TemplatableFn stores a direct function pointer instead of going through
# the deprecated converting trampoline when the value expression deduces
# to a different type (e.g. int literal assigned to a float global).
value_type = cg.RawExpression(f"{full_id.type}::value_type")
templ = await cg.templatable(
config[CONF_VALUE], args, value_type, 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, cg.uint8)
template_channel = await cg.templatable(config[CONF_CHANNEL], args, cg.int32)
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, cg.uint8)
template_channel = await cg.templatable(config[CONF_CHANNEL], args, cg.int32)
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, cg.uint8)
template_channel = await cg.templatable(config[CONF_CHANNEL], args, cg.int32)
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, cg.uint8)
template_channel = await cg.templatable(config[CONF_ADDRESS], args, cg.int32)
cg.add(var.set_address(template_channel))
return var
@@ -30,6 +30,7 @@ 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()) {
+3 -5
View File
@@ -20,14 +20,11 @@ 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) { this->set_supported_preset_modes(presets); }
void set_preset_modes(std::initializer_list<const char *> presets) { preset_modes_ = presets; }
void setup() override;
void dump_config() override;
fan::FanTraits get_traits() override {
this->wire_preset_modes_(this->traits_);
return this->traits_;
}
fan::FanTraits get_traits() override { return this->traits_; }
fan::FanCall brake();
@@ -39,6 +36,7 @@ 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_();
+1 -1
View File
@@ -8,7 +8,7 @@ from .. import hbridge_ns
CODEOWNERS = ["@DotNetDann"]
HBridgeLightOutput = hbridge_ns.class_(
"HBridgeLightOutput", cg.Component, light.LightOutput
"HBridgeLightOutput", cg.PollingComponent, light.LightOutput
)
CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
@@ -1,17 +1,20 @@
#pragma once
#include "esphome/components/light/light_output.h"
#include "esphome/components/output/float_output.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/light/light_output.h"
#include "esphome/core/log.h"
namespace esphome {
namespace hbridge {
class HBridgeLightOutput : public Component, public light::LightOutput {
// Using PollingComponent as the updates are more consistent and reduces flickering
class HBridgeLightOutput : public PollingComponent, public light::LightOutput {
public:
void set_pina_pin(output::FloatOutput *pina_pin) { this->pina_pin_ = pina_pin; }
void set_pinb_pin(output::FloatOutput *pinb_pin) { this->pinb_pin_ = pinb_pin; }
HBridgeLightOutput() : PollingComponent(1) {}
void set_pina_pin(output::FloatOutput *pina_pin) { pina_pin_ = pina_pin; }
void set_pinb_pin(output::FloatOutput *pinb_pin) { pinb_pin_ = pinb_pin; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
@@ -21,16 +24,16 @@ class HBridgeLightOutput : public Component, public light::LightOutput {
return traits;
}
void setup() override { this->disable_loop(); }
void setup() override { this->forward_direction_ = false; }
void loop() override {
// Only called when both channels are active — alternate H-bridge direction
// each iteration to multiplex cold and warm white.
if (!this->forward_direction_) {
void update() override {
// This method runs around 60 times per second
// We cannot do the PWM ourselves so we are reliant on the hardware PWM
if (!this->forward_direction_) { // First LED Direction
this->pina_pin_->set_level(this->pina_duty_);
this->pinb_pin_->set_level(0);
this->forward_direction_ = true;
} else {
} else { // Second LED Direction
this->pina_pin_->set_level(0);
this->pinb_pin_->set_level(this->pinb_duty_);
this->forward_direction_ = false;
@@ -40,32 +43,15 @@ class HBridgeLightOutput : public Component, public light::LightOutput {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void write_state(light::LightState *state) override {
float new_pina, new_pinb;
state->current_values_as_cwww(&new_pina, &new_pinb, false);
this->pina_duty_ = new_pina;
this->pinb_duty_ = new_pinb;
if (new_pina != 0.0f && new_pinb != 0.0f) {
// Both channels active — need loop to alternate H-bridge direction
this->high_freq_.start();
this->enable_loop();
} else {
// Zero or one channel active — drive pins directly, no multiplexing needed
this->high_freq_.stop();
this->disable_loop();
this->pina_pin_->set_level(new_pina);
this->pinb_pin_->set_level(new_pinb);
}
state->current_values_as_cwww(&this->pina_duty_, &this->pinb_duty_, false);
}
protected:
output::FloatOutput *pina_pin_;
output::FloatOutput *pinb_pin_;
float pina_duty_{0};
float pinb_duty_{0};
bool forward_direction_{false};
HighFrequencyLoopRequester high_freq_;
float pina_duty_ = 0;
float pinb_duty_ = 0;
bool forward_direction_ = false;
};
} // namespace hbridge
+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, cg.uint8)
level_ = await cg.templatable(config[CONF_LEVEL], args, cg.int32)
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, cg.bool_)
status_ = await cg.templatable(config[CONF_STATUS], args, bool)
cg.add(var.set_status(status_))
return var
@@ -36,7 +36,7 @@ I2SAudioMicrophone = i2s_audio_ns.class_(
)
INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32]
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3, esp32.VARIANT_ESP32P4]
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3]
def _validate_esp32_variant(config):
@@ -1,7 +1,5 @@
#pragma once
#include <cstdint>
namespace esphome {
namespace ili9xxx {
@@ -229,10 +229,6 @@ void ILI9XXXDisplay::update() {
}
void ILI9XXXDisplay::display_() {
// buffer may be null if allocation failed
if (this->buffer_ == nullptr) {
return;
}
// check if something was displayed
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) {
return;
+8 -11
View File
@@ -28,6 +28,7 @@ from esphome.const import (
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.final_validate import full_config
_LOGGER = logging.getLogger(__name__)
@@ -675,16 +676,12 @@ def _final_validate(config):
:param config:
:return:
"""
config = config.copy()
for c in config:
if byte_order := c.get(CONF_BYTE_ORDER):
if byte_order == "BIG_ENDIAN":
_LOGGER.warning(
"The image '%s' is configured with big-endian byte order, little-endian is expected",
c.get(CONF_FILE),
)
else:
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
fv = full_config.get()
if "lvgl" in fv and not all(CONF_BYTE_ORDER in x for x in config):
config = config.copy()
for c in config:
if not c.get(CONF_BYTE_ORDER):
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
return config
@@ -756,7 +753,7 @@ async def write_image(config, all_frames=False):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
encoder.end_image()
encoder.end_image()
rhs = [HexInt(x) for x in encoder.data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
+1 -1
View File
@@ -189,7 +189,7 @@ Color Image::get_rgb_pixel_(int x, int y) const {
}
Color Image::get_rgb565_pixel_(int x, int y) const {
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + 1), progmem_read_byte(pos));
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;
+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, cg.float_)
template_ = await cg.templatable(config[CONF_VALUE], args, float)
cg.add(var.set_value(template_))
return var
+2 -5
View File
@@ -140,11 +140,8 @@ SerializationBuffer<> JsonBuilder::serialize() {
heap_size *= 2;
}
// Payload exceeds 5120 bytes - return truncated result
// 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);
ESP_LOGW(TAG, "JSON payload too large, truncated to %zu bytes", size);
result.set_size_(size);
return result;
}
+18 -24
View File
@@ -766,38 +766,32 @@ void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY
void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); }
void LD2412Component::set_basic_config() {
uint8_t min_gate = 1;
uint8_t max_gate = TOTAL_GATES;
uint16_t timeout = DEFAULT_PRESENCE_TIMEOUT;
uint8_t out_pin_level = 0x01;
#ifdef USE_NUMBER
if (this->min_distance_gate_number_ != nullptr) {
if (!this->min_distance_gate_number_->has_state())
return;
min_gate = static_cast<int>(this->min_distance_gate_number_->state);
}
if (this->max_distance_gate_number_ != nullptr) {
if (!this->max_distance_gate_number_->has_state())
return;
max_gate = static_cast<int>(this->max_distance_gate_number_->state) + 1;
}
if (this->timeout_number_ != nullptr) {
if (!this->timeout_number_->has_state())
return;
timeout = static_cast<int>(this->timeout_number_->state);
if (!this->min_distance_gate_number_->has_state() || !this->max_distance_gate_number_->has_state() ||
!this->timeout_number_->has_state()) {
return;
}
#endif
#ifdef USE_SELECT
if (this->out_pin_level_select_ != nullptr) {
if (!this->out_pin_level_select_->has_state())
return;
out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
if (!this->out_pin_level_select_->has_state()) {
return;
}
#endif
uint8_t value[5] = {
lowbyte(min_gate), lowbyte(max_gate), lowbyte(timeout), highbyte(timeout), out_pin_level,
#ifdef USE_NUMBER
lowbyte(static_cast<int>(this->min_distance_gate_number_->state)),
lowbyte(static_cast<int>(this->max_distance_gate_number_->state) + 1),
lowbyte(static_cast<int>(this->timeout_number_->state)),
highbyte(static_cast<int>(this->timeout_number_->state)),
#else
1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0,
#endif
#ifdef USE_SELECT
find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()),
#else
0x01, // Default value if not using select
#endif
};
this->set_config_mode_(true);
this->send_command_(CMD_BASIC_CONF, value, sizeof(value));
+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, cg.float_)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, 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, cg.float_)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, float)
cg.add(var.set_frequency(template_))
return var
+6 -57
View File
@@ -58,12 +58,6 @@ void AddressableLightTransformer::start() {
// our transition will handle brightness, disable brightness in correction.
this->light_.correction_.set_local_brightness(255);
this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
// Uniformity scan is deferred to the first apply() call. start() can run before the underlying
// LED output's setup() has allocated its frame buffer (e.g. on_boot at priority > HARDWARE
// triggering a transition), and reading through ESPColorView would deref a null buffer.
this->uniform_start_scanned_ = false;
this->uniform_start_is_uniform_ = false;
}
inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) {
@@ -103,57 +97,12 @@ optional<LightColorValues> AddressableLightTransformer::apply() {
// non-linear when applying small deltas.
if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) {
// Lazy uniformity scan: deferred from start() so the LED output's setup() has run and the
// frame buffer is valid. When every LED already has the same color (the common case: plain
// turn_on/turn_off on a uniform strip), interpolate math-only against a single start color.
// Avoiding the per-step read-back through the 8-bit stored byte prevents gamma round-trip
// quantization from stalling the fade at low values (e.g. gamma 2.8 pre-gamma values <27
// round to stored 0, freezing progress).
if (!this->uniform_start_scanned_) {
this->uniform_start_scanned_ = true;
if (this->light_.size() > 0) {
Color first = this->light_[0].get();
bool uniform = true;
for (int32_t i = 1; i < this->light_.size(); i++) {
if (this->light_[i].get() != first) {
uniform = false;
break;
}
}
if (uniform) {
this->uniform_start_color_ = first;
this->uniform_start_is_uniform_ = true;
}
}
}
if (this->uniform_start_is_uniform_) {
// All LEDs started at the same color: compute the interpolated value once and write it to
// every LED. No read-back, so each LED's stored byte advances through every gamma threshold
// as smoothed_progress crosses it, instead of stalling at 0 for low pre-gamma values.
//
// Trade-off: any mid-transition writes to individual LEDs (e.g. from a user lambda) will be
// overwritten on the next apply() here. The fallback path below would have respected them
// via its read-back. Concurrent per-LED mutation during a transition isn't a pattern we
// support, so this is acceptable.
// lerp(start, target, progress) via existing helper: target - (target-start)*(1-progress).
const Color &start = this->uniform_start_color_;
int32_t remaining = int32_t(256.f * (1.f - smoothed_progress));
uint8_t r = subtract_scaled_difference(this->target_color_.red, start.red, remaining);
uint8_t g = subtract_scaled_difference(this->target_color_.green, start.green, remaining);
uint8_t b = subtract_scaled_difference(this->target_color_.blue, start.blue, remaining);
uint8_t w = subtract_scaled_difference(this->target_color_.white, start.white, remaining);
for (auto led : this->light_) {
led.set_rgbw(r, g, b, w);
}
} else {
int32_t scale =
int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
for (auto led : this->light_) {
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
}
int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
for (auto led : this->light_) {
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
}
this->last_transition_progress_ = smoothed_progress;
this->light_.schedule_show();
@@ -115,9 +115,6 @@ class AddressableLightTransformer : public LightTransformer {
AddressableLight &light_;
float last_transition_progress_{0.0f};
Color target_color_{};
Color uniform_start_color_{};
bool uniform_start_scanned_{false};
bool uniform_start_is_uniform_{false};
};
} // namespace esphome::light
+11 -11
View File
@@ -183,18 +183,18 @@ 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", cg.bool_),
(CONF_STATE, "set_state", bool),
(CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32),
(CONF_FLASH_LENGTH, "set_flash_length", cg.uint32),
(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_),
(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),
)
for conf_key, setter, type_ in FIELDS:
if conf_key in config:
@@ -262,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, cg.float_)
templ = await cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, 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)
+2 -4
View File
@@ -44,7 +44,6 @@ from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import MockObj
from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed
from esphome.writer import clean_build
from esphome.yaml_util import load_yaml
from . import defines as df, helpers, lv_validation as lvalid, widgets
@@ -342,7 +341,7 @@ async def to_code(configs):
df.LOGGER.info("LVGL will use hardware rotation via display driver")
else:
rotation_type = RotationType.ROTATION_SOFTWARE
if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32P4:
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")
@@ -452,8 +451,7 @@ async def to_code(configs):
df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1")
lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME)
if write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()):
clean_build(clear_pio_cache=False)
write_file_if_changed(lv_conf_h_file, generate_lv_conf_h())
cg.add_build_flag("-DLV_CONF_H=1")
# handle windows paths in a way that doesn't break the generated C++
lv_conf_h_path = Path(lv_conf_h_file).as_posix()
+3 -3
View File
@@ -2,7 +2,6 @@ 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
@@ -202,7 +201,7 @@ def _validate_rotation(value):
@automation.register_action(
"lvgl.display.set_rotation",
StatelessLambdaAction,
ObjUpdateAction,
cv.maybe_simple_value(
LVGL_SCHEMA.extend(
{
@@ -215,7 +214,8 @@ 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(args, where=action_id) as context:
async with LambdaContext() as context:
add_line_marks(where=action_id)
lv_add(lv_comp.set_rotation(config[CONF_ROTATION]))
return cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
+1 -3
View File
@@ -89,12 +89,10 @@
id: hello_world_label_
text: "Hello World!"
align: center
- container:
- obj:
id: hello_world_qrcode_
outline_width: 0
border_width: 0
height: 100
width: 100
hidden: !lambda |-
return lv_obj_get_width(lv_screen_active()) < 300 && lv_obj_get_height(lv_screen_active()) < 400;
widgets:
+9 -11
View File
@@ -642,28 +642,26 @@ void LvglComponent::write_random_() {
int iterations = 6 - lv_display_get_inactive_time(this->disp_) / 60000;
if (iterations <= 0)
iterations = 1;
int16_t width = lv_display_get_horizontal_resolution(this->disp_);
int16_t height = lv_display_get_vertical_resolution(this->disp_);
while (iterations-- != 0) {
int32_t col = random_uint32() % width;
int32_t col = random_uint32() % this->width_;
col = col / this->draw_rounding * this->draw_rounding;
int32_t row = random_uint32() % height;
int32_t row = random_uint32() % this->height_;
row = row / this->draw_rounding * this->draw_rounding;
// size will be between 8 and 32, and a multiple of draw_rounding
int32_t size = (random_uint32() % 25 + 8) / this->draw_rounding * this->draw_rounding;
lv_area_t area{.x1 = col, .y1 = row, .x2 = col + size - 1, .y2 = row + size - 1};
lv_area_t area{col, row, col + size - 1, row + size - 1};
// clip to display bounds just in case
if (area.x2 >= width)
area.x2 = width - 1;
if (area.y2 >= height)
area.y2 = height - 1;
if (area.x2 >= this->width_)
area.x2 = this->width_ - 1;
if (area.y2 >= this->height_)
area.y2 = this->height_ - 1;
// line_len can't exceed 1024, and minimum buffer size is 2048, so this won't overflow the buffer
size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2;
for (size_t i = 0; i != line_len; i++) {
reinterpret_cast<uint32_t *>(this->draw_buf_)[i] = random_uint32();
((uint32_t *) (this->draw_buf_))[i] = random_uint32();
}
this->draw_buffer_(&area, reinterpret_cast<lv_color_data *>(this->draw_buf_));
this->draw_buffer_(&area, (lv_color_data *) this->draw_buf_);
}
}
+5 -12
View File
@@ -76,23 +76,16 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
}
#endif
#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE)
#if LV_USE_IMAGE
// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda.
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); }
#endif // LV_USE_IMAGE
// Shortcut / overload, so that the source of an image can easily be updated
// from within a lambda.
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { lv_image_set_src(obj, image->get_lv_image_dsc()); }
inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
}
inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
::lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
}
inline void lv_style_set_bg_image_src(lv_style_t *style, image::Image *image) {
::lv_style_set_bg_image_src(style, image->get_lv_image_dsc());
}
inline void lv_style_set_bitmap_mask_src(lv_style_t *style, image::Image *image) {
::lv_style_set_bitmap_mask_src(style, image->get_lv_image_dsc());
lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
}
#endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ANIMIMG
+2 -5
View File
@@ -77,11 +77,8 @@ class ArcType(NumberType):
# start_angle and end_angle are mapped to bg_start_angle and bg_end_angle
prop = str(prop)
if prop.endswith("_angle"):
await w.set_property(
"bg_" + prop, await validator.process(config.get(prop))
)
else:
await w.set_property(prop, config, processor=validator)
prop = "bg_" + prop
await w.set_property(prop, config, processor=validator)
if CONF_ADJUSTABLE in config:
if not config[CONF_ADJUSTABLE]:
lv_obj.remove_style(w.obj, nullptr, LV_PART.KNOB)
+11 -15
View File
@@ -52,23 +52,19 @@ class KeyboardType(WidgetType):
if mode := config.get(CONF_MODE):
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode))
if textarea := config.get(CONF_TEXTAREA):
if not is_widget_completed(textarea):
# Can only happen for an initial config, where the keyboard is configured before the
# textarea, so it's ok to always emit into the global context
async def add_textarea():
async with LvContext():
await w.set_property(
CONF_TEXTAREA,
(await get_widgets(config, CONF_TEXTAREA))[0].obj,
)
# If a textarea is configured, it must be generated before the keyboard can attach it.
# If not yet configured, defer the attachment code.
CORE.add_job(add_textarea)
async def add_textarea():
async with LvContext():
await w.set_property(
CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj
)
if is_widget_completed(textarea):
await add_textarea()
else:
# Handles updates in automations, and properly ordered initial config. Code is generated
# into the enclosing context (main or lambda)
await w.set_property(
CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj
)
CORE.add_job(add_textarea)
keyboard_spec = KeyboardType()
+1 -2
View File
@@ -2,7 +2,6 @@ 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,
@@ -74,7 +73,7 @@ class TabviewType(WidgetType):
)
def get_uses(self):
return CONF_BUTTONMATRIX, TYPE_FLEX, CONF_BUTTON
return CONF_BUTTONMATRIX, TYPE_FLEX
async def to_code(self, w: Widget, config: dict):
await w.set_property(
-4
View File
@@ -5,7 +5,6 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INTERRUPT_PIN,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
@@ -25,7 +24,6 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(MCP23016),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -37,8 +35,6 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
def validate_mode(value):
-17
View File
@@ -24,25 +24,11 @@ void MCP23016::setup() {
// all pins input
this->write_reg_(MCP23016_IODIR1, 0xFFFF);
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&MCP23016::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
this->disable_loop();
}
void IRAM_ATTR MCP23016::gpio_intr(MCP23016 *arg) { arg->enable_loop_soon_any_context(); }
void MCP23016::loop() {
// Invalidate cache at the start of each loop
this->reset_pin_cache_();
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
// stale state forever; keep looping until the line is released so we self-heal.
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
this->disable_loop();
}
}
bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); }
@@ -51,9 +37,6 @@ void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin
void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
this->update_reg_(pin, true, MCP23016_IODIR1);
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_reg_(pin, false, MCP23016_IODIR1);
}
-4
View File
@@ -35,10 +35,7 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::
float get_setup_priority() const override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(MCP23016 *arg);
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
@@ -54,7 +51,6 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::
uint16_t olat_{0x0000};
// Cache for input values (16-bit combined for both banks)
uint16_t input_mask_{0x0000};
InternalGPIOPin *interrupt_pin_{nullptr};
};
class MCP23016GPIOPin : public GPIOPin {
@@ -21,10 +21,7 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
void loop() override {
this->reset_pin_cache_();
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
// stale state forever; keep looping until the line is released so we self-heal.
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
if (this->interrupt_pin_ != nullptr) {
this->disable_loop();
}
}
+1 -1
View File
@@ -170,7 +170,7 @@ async def to_code(config):
cg.add_library("LEAmDNS", None)
if CORE.is_esp32:
add_idf_component(name="espressif/mdns", ref="1.11.0")
add_idf_component(name="espressif/mdns", ref="1.10.0")
cg.add_define("USE_MDNS")
+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, cg.float_)
volume = await cg.templatable(config[CONF_VOLUME], args, float)
cg.add(var.set_volume(volume))
return var
@@ -451,8 +451,6 @@ async def to_code(config):
ota.request_ota_state_listeners()
esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1")
# Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn)
esp32.add_idf_component(name="espressif/esp-nn", ref="1.1.2")
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
@@ -29,6 +29,14 @@ void VADModel::log_model_config() {
bool StreamingModel::load_model_() {
RAMAllocator<uint8_t> arena_allocator;
if (this->tensor_arena_ == nullptr) {
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
if (this->tensor_arena_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
return false;
}
}
if (this->var_arena_ == nullptr) {
this->var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE);
if (this->var_arena_ == nullptr) {
@@ -45,26 +53,6 @@ bool StreamingModel::load_model_() {
return false;
}
// Probe for the actual required tensor arena size if not yet determined
if (!this->tensor_arena_size_probed_) {
size_t probed_size = this->probe_arena_size_();
if (probed_size > 0) {
ESP_LOGD(TAG, "Probed tensor arena size: %zu bytes", probed_size);
this->tensor_arena_size_ = probed_size;
} else {
ESP_LOGW(TAG, "Arena size probe failed, using manifest size: %zu bytes", this->tensor_arena_size_);
}
this->tensor_arena_size_probed_ = true;
}
if (this->tensor_arena_ == nullptr) {
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
if (this->tensor_arena_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
return false;
}
}
if (this->interpreter_ == nullptr) {
this->interpreter_ =
make_unique<tflite::MicroInterpreter>(tflite::GetModel(this->model_start_), this->streaming_op_resolver_,
@@ -106,70 +94,6 @@ bool StreamingModel::load_model_() {
return true;
}
size_t StreamingModel::probe_arena_size_() {
RAMAllocator<uint8_t> arena_allocator;
// Try with the manifest size first, then escalates to 1.5, then 2x if it fails. Different platforms and different
// versions of the esp-nn library require different amounts of memory, so the manifest size may not always be correct,
// and probing allows us to find the actual required size for the current build and platform. Aligns test sizes to 16
// bytes.
size_t attempt_sizes[] = {(this->tensor_arena_size_ + 15) & ~15, (this->tensor_arena_size_ * 3 / 2 + 15) & ~15,
(this->tensor_arena_size_ * 2 + 15) & ~15};
for (size_t attempt_size : attempt_sizes) {
uint8_t *probe_arena = arena_allocator.allocate(attempt_size);
if (probe_arena == nullptr) {
continue;
}
// Verify the model works at all with this arena size
auto probe_interpreter = make_unique<tflite::MicroInterpreter>(
tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, attempt_size, this->mrv_);
if (probe_interpreter->AllocateTensors() != kTfLiteOk) {
probe_interpreter.reset();
arena_allocator.deallocate(probe_arena, attempt_size);
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
continue;
}
// Try to shrink the arena. Start with arena_used_bytes() + 16 (rounded to 16-byte alignment).
// If that works, use it. Otherwise, try midpoints between that and the full size until one succeeds.
size_t lower = (probe_interpreter->arena_used_bytes() + 16 + 15) & ~15;
probe_interpreter.reset();
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
size_t upper = attempt_size;
while (lower < upper) {
auto test_interpreter = make_unique<tflite::MicroInterpreter>(
tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, lower, this->mrv_);
bool ok = test_interpreter->AllocateTensors() == kTfLiteOk;
test_interpreter.reset();
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
if (ok) {
// Found a working size smaller than the full arena
upper = lower + 16; // Pad by 16 bytes to be safe for future allocations
break;
}
// Try the midpoint between current attempt and full size
lower = ((lower + upper) / 2 + 15) & ~15;
}
arena_allocator.deallocate(probe_arena, attempt_size);
return upper;
}
return 0;
}
void StreamingModel::unload_model() {
this->interpreter_.reset();
@@ -63,10 +63,6 @@ class StreamingModel {
/// @brief Allocates tensor and variable arenas and sets up the model interpreter
/// @return True if successful, false otherwise
bool load_model_();
/// @brief Probes the actual required tensor arena size by trial allocation.
/// Tries the manifest size first, then 2x if that fails.
/// @return The required arena size rounded up to 16-byte alignment, or 0 on failure.
size_t probe_arena_size_();
/// @brief Returns true if successfully registered the streaming model's TensorFlow operations
bool register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver);
@@ -74,7 +70,6 @@ class StreamingModel {
bool loaded_{false};
bool enabled_{true};
bool tensor_arena_size_probed_{false};
bool unprocessed_probability_status_{false};
uint8_t current_stride_step_{0};
int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION};
+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);
// Frost protection custom preset is handled by AirConditioner directly
// since custom presets are stored on the Climate base class
if (capabilities.supportFrostProtectionPreset())
traits.set_supported_custom_presets({Constants::FREEZE_PROTECTION});
}
} // namespace ac
+5 -23
View File
@@ -24,26 +24,6 @@ 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
auto traits = this->get_traits();
const auto &existing = 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);
@@ -111,15 +91,17 @@ 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_);
// Custom fan modes and presets are stored on Climate base class and wired via get_traits()
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_);
/* + 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())
+4 -3
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->set_supported_custom_presets(presets); }
void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->set_supported_custom_fan_modes(modes); }
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; }
protected:
void control(const ClimateCall &call) override;
@@ -55,7 +55,8 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
ClimateModeMask supported_modes_{};
ClimateSwingModeMask supported_swing_modes_{};
ClimatePresetMask supported_presets_{};
bool frost_protection_set_{false};
std::vector<const char *> supported_custom_presets_{};
std::vector<const char *> supported_custom_fan_modes_{};
Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr};
+3 -3
View File
@@ -195,7 +195,7 @@ def model_schema(config):
"big_endian", "little_endian", lower=True
),
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
model.option(CONF_DRAW_ROUNDING, 1): power_of_two,
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of(
*pixel_modes, lower=True
),
@@ -297,9 +297,9 @@ def _final_validate(config):
buffer_size = color_depth // 8 * width * height // frac
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
fraction = min(20000.0, buffer_size // 4) / buffer_size
fraction = min(20000.0, buffer_size // 16) / buffer_size
config[CONF_BUFFER_SIZE] = 1.0 / next(
(x for x in range(2, 8) if fraction >= 1 / x), 8
x for x in range(2, 17) if fraction >= 1 / x
)
+11 -11
View File
@@ -234,9 +234,9 @@ class MipiSpi : public display::Display,
}
void dump_config() override {
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT,
(uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_,
this->cs_, this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL,
this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_,
this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
HAS_HARDWARE_ROTATION);
}
@@ -305,7 +305,7 @@ class MipiSpi : public display::Display,
this->write_command_(BRIGHTNESS, this->brightness_.value());
// calculate new madctl value from base value adjusted for rotation
uint8_t madctl = (uint8_t) MADCTL; // lower 8 bits only
uint8_t madctl = MADCTL; // lower 8 bits only
constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0;
constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX;
constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY;
@@ -546,12 +546,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
}
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
// the display height,
auto increment = (this->get_height_internal() / FRACTION / ROUNDING) * ROUNDING;
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); this->start_line_ = this->end_line_) {
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal();
this->start_line_ += this->get_height_internal() / FRACTION) {
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
auto lap = millis();
#endif
this->end_line_ = clamp_at_most(this->start_line_ + increment, this->get_height_internal());
this->end_line_ =
clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal());
if (this->auto_clear_enabled_) {
this->clear();
}
@@ -573,13 +574,12 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
// Some chips require that the drawing window be aligned on certain boundaries
this->x_low_ = this->x_low_ / ROUNDING * ROUNDING;
this->y_low_ = this->y_low_ / ROUNDING * ROUNDING;
this->x_high_ = round_buffer(this->x_high_ + 1) - 1;
this->y_high_ = clamp_at_most(round_buffer(this->y_high_ + 1) - 1, this->end_line_ - 1);
this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
this->y_low_ - this->start_line_,
round_buffer(this->get_width_internal()) - w - this->x_low_);
this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w);
// invalidate watermarks
this->x_low_ = this->get_width_internal();
this->y_low_ = this->get_height_internal();
@@ -7,7 +7,7 @@ namespace esphome::mitsubishi_cn105 {
static const char *const TAG = "mitsubishi_cn105.climate";
static constexpr std::array MODE_MAP{
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_HEAT_COOL},
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO},
std::pair{MitsubishiCN105::Mode::HEAT, climate::CLIMATE_MODE_HEAT},
std::pair{MitsubishiCN105::Mode::DRY, climate::CLIMATE_MODE_DRY},
std::pair{MitsubishiCN105::Mode::COOL, climate::CLIMATE_MODE_COOL},
@@ -76,13 +76,23 @@ void MitsubishiCN105Climate::loop() {
climate::ClimateTraits MitsubishiCN105Climate::traits() {
climate::ClimateTraits traits;
for (const auto &p : MODE_MAP) {
traits.add_supported_mode(p.second);
}
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_DRY,
climate::CLIMATE_MODE_FAN_ONLY,
climate::CLIMATE_MODE_AUTO,
});
for (const auto &p : FAN_MODE_MAP) {
traits.add_supported_fan_mode(p.second);
}
traits.set_supported_fan_modes({
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_FAN_QUIET,
climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_MIDDLE,
climate::CLIMATE_FAN_HIGH,
});
traits.set_visual_min_temperature(16.0f);
traits.set_visual_max_temperature(31.0f);

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