Compare commits

..

9 Commits

Author SHA1 Message Date
J. Nick Koston
974f0a3548 Apply suggestion from @swoboda1337
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-11 12:43:40 -06:00
J. Nick Koston
242d152edc Use ESPHOME_DEBUG_API guard instead of DEBUG
CI compile tests also define DEBUG, so a dedicated guard is needed.
Enable ESPHOME_DEBUG_API in integration tests via conftest.py.
2026-02-11 11:34:14 -06:00
J. Nick Koston
e63308aa33 Use DEBUG guard for asserts instead of custom define
Integration tests already pass -DDEBUG, so no custom define needed.
2026-02-11 11:30:51 -06:00
J. Nick Koston
93ff39709a Enable ESPHOME_DEBUG_API in API integration tests 2026-02-11 11:28:08 -06:00
J. Nick Koston
d3e47a5d80 Fix encode_bool to encode actual value when force=true 2026-02-11 11:20:41 -06:00
J. Nick Koston
77d8fc80be Guard asserts with ESPHOME_DEBUG_API to avoid flash overhead 2026-02-11 11:18:50 -06:00
J. Nick Koston
fb1c495506 tweaks 2026-02-11 11:12:46 -06:00
J. Nick Koston
dc2085cc25 tweaks 2026-02-11 11:11:22 -06:00
J. Nick Koston
25baec48ee [api] Optimize varint encoding to eliminate uint64_t widening
Replace ProtoVarInt temporaries with direct uint32_t encoding functions
to avoid unnecessary 64-bit widening on 32-bit architectures (ESP32 Xtensa).

- Add encode_varint_to_buffer() free function for direct buffer encoding
- Replace encode_varint_raw(ProtoVarInt) with uint32_t-native implementation
- Add encode_varint_raw_64() for the few call sites needing 64-bit (BLE)
- Remove unused ProtoVarInt::encode() and encode_to_buffer_unchecked()
- Remove dead null checks from ProtoVarInt::parse()

Net savings: -152 bytes flash on ESP32 IDF.
2026-02-11 11:02:03 -06:00
14 changed files with 109 additions and 281 deletions

View File

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

View File

@@ -548,8 +548,8 @@ class APIConnection final : public APIServerConnectionBase {
batch_start_time = 0;
}
// Remove processed items from the front — noinline to keep memmove out of warm callers
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
// Remove processed items from the front
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
@@ -621,8 +621,6 @@ class APIConnection final : public APIServerConnectionBase {
bool schedule_batch_();
void process_batch_();
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) __attribute__((noinline));
void clear_batch_() {
this->deferred_batch_.clear();
this->flags_.batch_scheduled = false;

View File

@@ -295,9 +295,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
buf_start[header_offset] = 0x00; // indicator
// Encode varints directly into buffer
ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
ProtoVarInt(msg.message_type)
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
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);

View File

@@ -57,6 +57,16 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
return count;
}
/// Encode a varint directly into a pre-allocated buffer.
/// Caller must ensure buffer has space (use ProtoSize::varint() to calculate).
inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) {
while (val > 0x7F) {
*buffer++ = static_cast<uint8_t>(val | 0x80);
val >>= 7;
}
*buffer = static_cast<uint8_t>(val);
}
/*
* StringRef Ownership Model for API Protocol Messages
* ===================================================
@@ -93,17 +103,17 @@ class ProtoVarInt {
ProtoVarInt() : value_(0) {}
explicit ProtoVarInt(uint64_t value) : value_(value) {}
/// Parse a varint from buffer. consumed must be a valid pointer (not null).
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
if (len == 0) {
if (consumed != nullptr)
*consumed = 0;
#ifdef ESPHOME_DEBUG_API
assert(consumed != nullptr);
#endif
if (len == 0)
return {};
}
// Most common case: single-byte varint (values 0-127)
if ((buffer[0] & 0x80) == 0) {
if (consumed != nullptr)
*consumed = 1;
*consumed = 1;
return ProtoVarInt(buffer[0]);
}
@@ -122,14 +132,11 @@ class ProtoVarInt {
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7;
if ((val & 0x80) == 0) {
if (consumed != nullptr)
*consumed = i + 1;
*consumed = i + 1;
return ProtoVarInt(result);
}
}
if (consumed != nullptr)
*consumed = 0;
return {}; // Incomplete or invalid varint
}
@@ -153,50 +160,6 @@ class ProtoVarInt {
// with ZigZag encoding
return decode_zigzag64(this->value_);
}
/**
* Encode the varint value to a pre-allocated buffer without bounds checking.
*
* @param buffer The pre-allocated buffer to write the encoded varint to
* @param len The size of the buffer in bytes
*
* @note The caller is responsible for ensuring the buffer is large enough
* to hold the encoded value. Use ProtoSize::varint() to calculate
* the exact size needed before calling this method.
* @note No bounds checking is performed for performance reasons.
*/
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
uint64_t val = this->value_;
if (val <= 0x7F) {
buffer[0] = val;
return;
}
size_t i = 0;
while (val && i < len) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
buffer[i++] = temp | 0x80;
} else {
buffer[i++] = temp;
}
}
}
void encode(std::vector<uint8_t> &out) {
uint64_t val = this->value_;
if (val <= 0x7F) {
out.push_back(val);
return;
}
while (val) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
out.push_back(temp | 0x80);
} else {
out.push_back(temp);
}
}
}
protected:
uint64_t value_;
@@ -256,8 +219,20 @@ class ProtoWriteBuffer {
public:
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
void write(uint8_t value) { this->buffer_->push_back(value); }
void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
void encode_varint_raw(uint32_t value) {
while (value > 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
value >>= 7;
}
this->buffer_->push_back(static_cast<uint8_t>(value));
}
void encode_varint_raw_64(uint64_t value) {
while (value > 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
value >>= 7;
}
this->buffer_->push_back(static_cast<uint8_t>(value));
}
/**
* Encode a field key (tag/wire type combination).
*
@@ -307,13 +282,13 @@ class ProtoWriteBuffer {
if (value == 0 && !force)
return;
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
this->encode_varint_raw(ProtoVarInt(value));
this->encode_varint_raw_64(value);
}
void encode_bool(uint32_t field_id, bool value, bool force = false) {
if (!value && !force)
return;
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
this->write(0x01);
this->buffer_->push_back(value ? 0x01 : 0x00);
}
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force)
@@ -938,13 +913,15 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
// Write the length varint directly
ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes);
encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin);
// Now encode the message content - it will append to the buffer
value.encode(*this);
#ifdef ESPHOME_DEBUG_API
// Verify that the encoded size matches what we calculated
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
#endif
}
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined

View File

@@ -11,7 +11,6 @@ from esphome.const import (
CONF_ICON,
CONF_ID,
CONF_MQTT_ID,
CONF_MQTT_JSON_STATE_PAYLOAD,
CONF_ON_IDLE,
CONF_ON_OPEN,
CONF_POSITION,
@@ -120,9 +119,6 @@ _COVER_SCHEMA = (
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
cv.requires_component("mqtt"), cv.boolean
),
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.subscribe_topic
@@ -152,22 +148,6 @@ _COVER_SCHEMA = (
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
def _validate_mqtt_state_topics(config):
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
if CONF_POSITION_STATE_TOPIC in config:
raise cv.Invalid(
f"'{CONF_POSITION_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'"
)
if CONF_TILT_STATE_TOPIC in config:
raise cv.Invalid(
f"'{CONF_TILT_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'"
)
return config
_COVER_SCHEMA.add_extra(_validate_mqtt_state_topics)
def cover_schema(
class_: MockObjClass,
*,
@@ -215,9 +195,6 @@ async def setup_cover_core_(var, config):
position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC)
) is not None:
cg.add(mqtt_.set_custom_position_command_topic(position_command_topic))
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
cg.add_define("USE_MQTT_COVER_JSON")
cg.add(mqtt_.set_use_json_format(True))
if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None:
cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic))
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:

View File

@@ -124,11 +124,14 @@ class ESP32Preferences : public ESPPreferences {
return true;
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
// goal try write all pending saves even if one fails
int cached = 0, written = 0, failed = 0;
esp_err_t last_err = ESP_OK;
uint32_t last_key = 0;
for (const auto &save : s_pending_save) {
// go through vector from back to front (makes erase easier/more efficient)
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
const auto &save = s_pending_save[i];
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
@@ -147,9 +150,8 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
}
s_pending_save.clear();
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed);
if (failed > 0) {

View File

@@ -114,11 +114,14 @@ class LibreTinyPreferences : public ESPPreferences {
return true;
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
// goal try write all pending saves even if one fails
int cached = 0, written = 0, failed = 0;
fdb_err_t last_err = FDB_NO_ERR;
uint32_t last_key = 0;
for (const auto &save : s_pending_save) {
// go through vector from back to front (makes erase easier/more efficient)
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
const auto &save = s_pending_save[i];
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
@@ -138,9 +141,8 @@ class LibreTinyPreferences : public ESPPreferences {
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
}
s_pending_save.clear();
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed);
if (failed > 0) {

View File

@@ -67,26 +67,17 @@ void MQTTCoverComponent::dump_config() {
auto traits = this->cover_->get_traits();
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
LOG_MQTT_COMPONENT(true, has_command_topic);
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
#ifdef USE_MQTT_COVER_JSON
if (this->use_json_format_) {
ESP_LOGCONFIG(TAG, " JSON State Payload: YES");
} else {
#endif
if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic_to(topic_buf).c_str());
}
if (traits.get_supports_tilt()) {
ESP_LOGCONFIG(TAG, " Tilt State Topic: '%s'", this->get_tilt_state_topic_to(topic_buf).c_str());
}
#ifdef USE_MQTT_COVER_JSON
}
#endif
if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic_to(topic_buf).c_str());
ESP_LOGCONFIG(TAG,
" Position State Topic: '%s'\n"
" Position Command Topic: '%s'",
this->get_position_state_topic().c_str(), this->get_position_command_topic().c_str());
}
if (traits.get_supports_tilt()) {
ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic_to(topic_buf).c_str());
ESP_LOGCONFIG(TAG,
" Tilt State Topic: '%s'\n"
" Tilt Command Topic: '%s'",
this->get_tilt_state_topic().c_str(), this->get_tilt_command_topic().c_str());
}
}
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
@@ -101,33 +92,13 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
if (traits.get_is_assumed_state()) {
root[MQTT_OPTIMISTIC] = true;
}
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
#ifdef USE_MQTT_COVER_JSON
if (this->use_json_format_) {
// JSON mode: all state published to state_topic as JSON, use templates to extract
root[MQTT_VALUE_TEMPLATE] = ESPHOME_F("{{ value_json.state }}");
if (traits.get_supports_position()) {
root[MQTT_POSITION_TOPIC] = this->get_state_topic_to_(topic_buf);
root[MQTT_POSITION_TEMPLATE] = ESPHOME_F("{{ value_json.position }}");
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf);
}
if (traits.get_supports_tilt()) {
root[MQTT_TILT_STATUS_TOPIC] = this->get_state_topic_to_(topic_buf);
root[MQTT_TILT_STATUS_TEMPLATE] = ESPHOME_F("{{ value_json.tilt }}");
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
}
} else
#endif
{
// Standard mode: separate topics for position and tilt
if (traits.get_supports_position()) {
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic_to(topic_buf);
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf);
}
if (traits.get_supports_tilt()) {
root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic_to(topic_buf);
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
}
if (traits.get_supports_position()) {
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic();
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic();
}
if (traits.get_supports_tilt()) {
root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic();
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic();
}
if (traits.get_supports_tilt() && !traits.get_supports_position()) {
config.command_topic = false;
@@ -140,24 +111,8 @@ const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_;
bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
bool MQTTCoverComponent::publish_state() {
auto traits = this->cover_->get_traits();
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
#ifdef USE_MQTT_COVER_JSON
if (this->use_json_format_) {
return this->publish_json(this->get_state_topic_to_(topic_buf), [this, traits](JsonObject root) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("state")] = cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
traits.get_supports_position());
if (traits.get_supports_position()) {
root[ESPHOME_F("position")] = static_cast<int>(roundf(this->cover_->position * 100));
}
if (traits.get_supports_tilt()) {
root[ESPHOME_F("tilt")] = static_cast<int>(roundf(this->cover_->tilt * 100));
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
});
}
#endif
bool success = true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (traits.get_supports_position()) {
char pos[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0);

View File

@@ -27,18 +27,12 @@ class MQTTCoverComponent : public mqtt::MQTTComponent {
bool publish_state();
void dump_config() override;
#ifdef USE_MQTT_COVER_JSON
void set_use_json_format(bool use_json_format) { this->use_json_format_ = use_json_format; }
#endif
protected:
const char *component_type() const override;
const EntityBase *get_entity() const override;
cover::Cover *cover_;
#ifdef USE_MQTT_COVER_JSON
bool use_json_format_{false};
#endif
};
} // namespace esphome::mqtt

View File

@@ -352,26 +352,7 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
reinterpret_cast<const uint8_t *>(user_info), user_info_len);
// Constant-time comparison to avoid timing side channels.
// No early return on length mismatch — the length difference is folded
// into the accumulator so any mismatch is rejected.
const char *provided = auth_str + auth_prefix_len;
size_t digest_len = out; // length from esp_crypto_base64_encode
// Derive provided_len from the already-sized std::string rather than
// rescanning with strlen (avoids attacker-controlled scan length).
size_t provided_len = auth.value().size() - auth_prefix_len;
// Use full-width XOR so any bit difference in the lengths is preserved
// (uint8_t truncation would miss differences in higher bytes, e.g.
// digest_len vs digest_len + 256).
volatile size_t result = digest_len ^ provided_len;
// Iterate over the expected digest length only — the full-width length
// XOR above already rejects any length mismatch, and bounding the loop
// prevents a long Authorization header from forcing extra work.
for (size_t i = 0; i < digest_len; i++) {
char provided_ch = (i < provided_len) ? provided[i] : 0;
result |= static_cast<uint8_t>(digest[i] ^ provided_ch);
}
return result == 0;
return strcmp(digest, auth_str + auth_prefix_len) == 0;
}
void AsyncWebServerRequest::requestAuthentication(const char *realm) const {

View File

@@ -639,7 +639,6 @@ CONF_MOVEMENT_COUNTER = "movement_counter"
CONF_MOVING_DISTANCE = "moving_distance"
CONF_MQTT = "mqtt"
CONF_MQTT_ID = "mqtt_id"
CONF_MQTT_JSON_STATE_PAYLOAD = "mqtt_json_state_payload"
CONF_MULTIPLE = "multiple"
CONF_MULTIPLEXER = "multiplexer"
CONF_MULTIPLY = "multiply"

View File

@@ -14,6 +14,7 @@
#define ESPHOME_PROJECT_VERSION_30 "v2"
#define ESPHOME_VARIANT "ESP32"
#define ESPHOME_DEBUG_SCHEDULER
#define ESPHOME_DEBUG_API
// Default threading model for static analysis (ESP32 is multi-threaded with atomics)
#define ESPHOME_THREAD_MULTI_ATOMICS
@@ -145,7 +146,6 @@
#define USE_MD5
#define USE_SHA256
#define USE_MQTT
#define USE_MQTT_COVER_JSON
#define USE_NETWORK
#define USE_ONLINE_IMAGE_BMP_SUPPORT
#define USE_ONLINE_IMAGE_PNG_SUPPORT

View File

@@ -219,7 +219,6 @@ cover:
name: Template Cover
state_topic: some/topic/cover
qos: 2
mqtt_json_state_payload: true
lambda: |-
if (id(some_binary_sensor).state) {
return COVER_OPEN;
@@ -232,53 +231,6 @@ cover:
stop_action:
- logger.log: stop_action
optimistic: true
- platform: template
name: Template Cover with Position and Tilt
state_topic: some/topic/cover_pt
position_state_topic: some/topic/cover_pt/position
position_command_topic: some/topic/cover_pt/position/set
tilt_state_topic: some/topic/cover_pt/tilt
tilt_command_topic: some/topic/cover_pt/tilt/set
qos: 2
has_position: true
lambda: |-
if (id(some_binary_sensor).state) {
return COVER_OPEN;
}
return COVER_CLOSED;
position_action:
- logger.log: position_action
tilt_action:
- logger.log: tilt_action
open_action:
- logger.log: open_action
close_action:
- logger.log: close_action
stop_action:
- logger.log: stop_action
optimistic: true
- platform: template
name: Template Cover with Position and Tilt JSON
state_topic: some/topic/cover_pt_json
qos: 2
mqtt_json_state_payload: true
has_position: true
lambda: |-
if (id(some_binary_sensor).state) {
return COVER_OPEN;
}
return COVER_CLOSED;
position_action:
- logger.log: position_action
tilt_action:
- logger.log: tilt_action
open_action:
- logger.log: open_action
close_action:
- logger.log: close_action
stop_action:
- logger.log: stop_action
optimistic: true
datetime:
- platform: template

View File

@@ -197,6 +197,7 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s
" platformio_options:\n"
" build_flags:\n"
' - "-DDEBUG" # Enable assert() statements\n'
' - "-DESPHOME_DEBUG_API" # Enable API protocol asserts\n'
' - "-g" # Add debug symbols',
)