Merge branch 'rwalker777-network-priority' of https://github.com/rwalker777/esphome into rwalker777-network-priority

This commit is contained in:
Roy Walker
2026-02-26 11:31:10 -06:00
82 changed files with 6309 additions and 5495 deletions
+13 -6
View File
@@ -199,12 +199,19 @@ void AcDimmer::setup() {
setTimer1Callback(&timer_interrupt);
#endif
#ifdef USE_ESP32
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
if (dimmer_timer == nullptr) {
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
if (dimmer_timer == nullptr) {
ESP_LOGE(TAG, "Failed to create GPTimer for AC dimmer");
this->mark_failed();
return;
}
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
}
#endif
}
-1
View File
@@ -2,7 +2,6 @@
// See script/api_protobuf/api_protobuf.py
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/string_ref.h"
#include "proto.h"
+12
View File
@@ -0,0 +1,12 @@
// This file was automatically generated with a tool.
// See script/api_protobuf/api_protobuf.py
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BLUETOOTH_PROXY
#ifndef USE_API_VARINT64
#define USE_API_VARINT64
#endif
#endif
namespace esphome::api {} // namespace esphome::api
+10 -10
View File
@@ -433,8 +433,8 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper to add subscription (reduces duplication)
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
bool once) {
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
std::function<void(StringRef)> &&f, bool once) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
@@ -443,7 +443,7 @@ void APIServer::add_state_subscription_(const char *entity_id, const char *attri
// Helper to add subscription with heap-allocated strings (reduces duplication)
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f, bool once) {
std::function<void(StringRef)> &&f, bool once) {
HomeAssistantStateSubscription sub;
// Allocate heap storage for the strings
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
@@ -463,29 +463,29 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
// New const char* overload (for internal components - zero allocation)
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
}
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
}
// std::string overload with StringRef callback (zero-allocation callback)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
// Legacy helper: wraps std::string callback and delegates to StringRef version
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once) {
std::function<void(const std::string &)> &&f, bool once) {
// Wrap callback to convert StringRef -> std::string, then delegate
this->add_state_subscription_(std::move(entity_id), std::move(attribute),
std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
@@ -494,12 +494,12 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
std::function<void(const std::string &)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
std::function<void(const std::string &)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
+10 -10
View File
@@ -201,20 +201,20 @@ class APIServer : public Component,
};
// New const char* overload (for internal components - zero allocation)
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f);
// std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f);
std::function<void(StringRef)> &&f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f);
std::function<void(StringRef)> &&f);
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
std::function<void(const std::string &)> &&f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
std::function<void(const std::string &)> &&f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
#endif
@@ -241,13 +241,13 @@ class APIServer : public Component,
#endif // USE_API_NOISE
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper methods to reduce code duplication
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute, std::function<void(StringRef)> f,
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> &&f, bool once);
// Legacy helper: wraps std::string callback and delegates to StringRef version
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once);
std::function<void(const std::string &)> &&f, bool once);
#endif // USE_API_HOMEASSISTANT_STATES
// No explicit close() needed — listen sockets have no active connections on
// failure/shutdown. Destructor handles fd cleanup (close or abort per platform).
+1 -1
View File
@@ -15,7 +15,7 @@ class APIConnection;
return this->client_->schedule_message_(entity, ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \
}
class ListEntitiesIterator : public ComponentIterator {
class ListEntitiesIterator final : public ComponentIterator {
public:
ListEntitiesIterator(APIConnection *client);
#ifdef USE_BINARY_SENSOR
+17
View File
@@ -7,6 +7,23 @@ namespace esphome::api {
static const char *const TAG = "api.proto";
#ifdef USE_API_VARINT64
optional<ProtoVarInt> ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed,
uint32_t result32) {
uint64_t result64 = result32;
uint32_t limit = std::min(len, uint32_t(10));
for (uint32_t i = 4; i < limit; i++) {
uint8_t val = buffer[i];
result64 |= uint64_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
*consumed = i + 1;
return ProtoVarInt(result64);
}
}
return {};
}
#endif
uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
uint32_t count = 0;
const uint8_t *ptr = buffer;
+45 -25
View File
@@ -1,5 +1,6 @@
#pragma once
#include "api_pb2_defines.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -110,59 +111,78 @@ class ProtoVarInt {
#endif
if (len == 0)
return {};
// Most common case: single-byte varint (values 0-127)
// Fast path: single-byte varints (0-127) are the most common case
// (booleans, small enums, field tags). Avoid loop overhead entirely.
if ((buffer[0] & 0x80) == 0) {
*consumed = 1;
return ProtoVarInt(buffer[0]);
}
// General case for multi-byte varints
// Since we know buffer[0]'s high bit is set, initialize with its value
uint64_t result = buffer[0] & 0x7F;
uint8_t bitpos = 7;
// A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings
// to avoid undefined behavior from shifting uint64_t by >= 64 bits.
uint32_t max_len = std::min(len, uint32_t(10));
// Start from the second byte since we've already processed the first
for (uint32_t i = 1; i < max_len; i++) {
// 32-bit phase: process remaining bytes with native 32-bit shifts.
// Without USE_API_VARINT64: cover bytes 1-4 (shifts 7, 14, 21, 28) — the uint32_t
// shift at byte 4 (shift by 28) may lose bits 32-34, but those are always zero for valid uint32 values.
// With USE_API_VARINT64: cover bytes 1-3 (shifts 7, 14, 21) so parse_wide handles
// byte 4+ with full 64-bit arithmetic (avoids truncating values > UINT32_MAX).
uint32_t result32 = buffer[0] & 0x7F;
#ifdef USE_API_VARINT64
uint32_t limit = std::min(len, uint32_t(4));
#else
uint32_t limit = std::min(len, uint32_t(5));
#endif
for (uint32_t i = 1; i < limit; i++) {
uint8_t val = buffer[i];
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7;
result32 |= uint32_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
*consumed = i + 1;
return ProtoVarInt(result);
return ProtoVarInt(result32);
}
}
return {}; // Incomplete or invalid varint
// 64-bit phase for remaining bytes (BLE addresses etc.)
#ifdef USE_API_VARINT64
return parse_wide(buffer, len, consumed, result32);
#else
return {};
#endif
}
#ifdef USE_API_VARINT64
protected:
/// Continue parsing varint bytes 4-9 with 64-bit arithmetic.
/// Separated to keep 64-bit shift code (__ashldi3 on 32-bit platforms) out of the common path.
static optional<ProtoVarInt> parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32)
__attribute__((noinline));
public:
#endif
constexpr uint16_t as_uint16() const { return this->value_; }
constexpr uint32_t as_uint32() const { return this->value_; }
constexpr uint64_t as_uint64() const { return this->value_; }
constexpr bool as_bool() const { return this->value_; }
constexpr int32_t as_int32() const {
// Not ZigZag encoded
return static_cast<int32_t>(this->as_int64());
}
constexpr int64_t as_int64() const {
// Not ZigZag encoded
return static_cast<int64_t>(this->value_);
return static_cast<int32_t>(this->value_);
}
constexpr int32_t as_sint32() const {
// with ZigZag encoding
return decode_zigzag32(static_cast<uint32_t>(this->value_));
}
#ifdef USE_API_VARINT64
constexpr uint64_t as_uint64() const { return this->value_; }
constexpr int64_t as_int64() const {
// Not ZigZag encoded
return static_cast<int64_t>(this->value_);
}
constexpr int64_t as_sint64() const {
// with ZigZag encoding
return decode_zigzag64(this->value_);
}
#endif
protected:
#ifdef USE_API_VARINT64
uint64_t value_;
#else
uint32_t value_;
#endif
};
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32
+1 -1
View File
@@ -16,7 +16,7 @@ class APIConnection;
return this->client_->send_##entity_type##_state(entity); \
}
class InitialStateIterator : public ComponentIterator {
class InitialStateIterator final : public ComponentIterator {
public:
InitialStateIterator(APIConnection *client);
#ifdef USE_BINARY_SENSOR
+1 -1
View File
@@ -230,7 +230,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; }
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void set_data(std::function<void(Ts..., JsonObject)> func) {
void set_data(std::function<void(Ts..., JsonObject)> &&func) {
this->json_builder_ = std::move(func);
this->has_data_ = true;
}
+4 -4
View File
@@ -52,12 +52,12 @@ void BL0942::loop() {
return;
}
if (avail < sizeof(buffer)) {
if (!this->rx_start_) {
if (!this->rx_start_.has_value()) {
this->rx_start_ = millis();
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
} else if (millis() - *this->rx_start_ > PKT_TIMEOUT_MS) {
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail);
this->read_array((uint8_t *) &buffer, avail);
this->rx_start_ = 0;
this->rx_start_.reset();
}
return;
}
@@ -67,7 +67,7 @@ void BL0942::loop() {
this->received_package_(&buffer);
}
}
this->rx_start_ = 0;
this->rx_start_.reset();
}
bool BL0942::validate_checksum_(DataPacket *data) {
+1 -1
View File
@@ -140,7 +140,7 @@ class BL0942 : public PollingComponent, public uart::UARTDevice {
uint8_t address_ = 0;
bool reset_ = false;
LineFrequency line_freq_ = LINE_FREQUENCY_50HZ;
uint32_t rx_start_ = 0;
optional<uint32_t> rx_start_{};
uint32_t prev_cf_cnt_ = 0;
bool validate_checksum_(DataPacket *data);
@@ -101,7 +101,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
}
void loop() override {
if (this->found_ && this->last_seen_ + this->timeout_ < millis())
if (this->found_ && millis() - this->last_seen_ > this->timeout_)
this->set_found_(false);
}
void dump_config() override;
@@ -6,7 +6,7 @@
namespace esphome::captive_portal {
#ifdef USE_CAPTIVE_PORTAL_GZIP
constexpr uint8_t INDEX_GZ[] PROGMEM = {
const uint8_t INDEX_GZ[] PROGMEM = {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e,
0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36,
0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf,
@@ -86,7 +86,7 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = {
0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00};
#else // Brotli (default, smaller)
constexpr uint8_t INDEX_BR[] PROGMEM = {
const uint8_t INDEX_BR[] PROGMEM = {
0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b,
0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48,
0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78,
+1 -3
View File
@@ -1,7 +1,5 @@
#include "dlms_meter.h"
#include <cmath>
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
#include <bearssl/bearssl.h>
#elif defined(USE_ESP32)
@@ -410,7 +408,7 @@ void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_lengt
if (current_position + 1 < message_length) {
int8_t scaler = static_cast<int8_t>(plaintext[current_position + 1]);
if (scaler != 0) {
value *= powf(10.0f, scaler);
value *= pow10_int(scaler);
}
}
@@ -24,6 +24,7 @@ from esphome.const import (
__version__ as ESPHOME_VERSION,
)
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.schema_extractors import SCHEMA_EXTRACT
AUTO_LOAD = ["esp32_ble", "bytebuffer"]
@@ -42,6 +43,7 @@ CONF_FIRMWARE_VERSION = "firmware_version"
CONF_INDICATE = "indicate"
CONF_MANUFACTURER = "manufacturer"
CONF_MANUFACTURER_DATA = "manufacturer_data"
CONF_MAX_CLIENTS = "max_clients"
CONF_ON_WRITE = "on_write"
CONF_READ = "read"
CONF_STRING = "string"
@@ -287,6 +289,22 @@ def create_device_information_service(config):
def final_validate_config(config):
# Validate max_clients does not exceed esp32_ble max_connections
max_clients = config[CONF_MAX_CLIENTS]
if max_clients > 1:
full_config = fv.full_config.get()
ble_config = full_config.get("esp32_ble", {})
max_connections = ble_config.get(
"max_connections", esp32_ble.DEFAULT_MAX_CONNECTIONS
)
if max_clients > max_connections:
raise cv.Invalid(
f"'max_clients' ({max_clients}) cannot exceed esp32_ble "
f"'max_connections' ({max_connections}). "
f"Please set 'max_connections: {max_clients}' in the "
f"'esp32_ble' component."
)
# Check if all characteristics that require notifications have the notify property set
for char_id in CORE.data.get(DOMAIN, {}).get(KEY_NOTIFY_REQUIRED, set()):
# Look for the characteristic in the configuration
@@ -428,6 +446,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_MODEL): value_schema("string", templatable=False),
cv.Optional(CONF_FIRMWARE_VERSION): value_schema("string", templatable=False),
cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.uint8_t]),
cv.Optional(CONF_MAX_CLIENTS, default=1): cv.int_range(min=1, max=9),
cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA),
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
@@ -552,6 +571,7 @@ async def to_code(config):
esp32_ble.register_ble_status_event_handler(parent, var)
cg.add(var.set_parent(parent))
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS]))
if CONF_MANUFACTURER_DATA in config:
cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA]))
for service_config in config[CONF_SERVICES]:
@@ -175,6 +175,10 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
case ESP_GATTS_CONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client connected");
this->add_client_(param->connect.conn_id);
// Resume advertising so additional clients can discover and connect
if (this->client_count_ < this->max_clients_) {
this->parent_->advertising_start();
}
this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id);
break;
}
@@ -241,7 +245,12 @@ void BLEServer::ble_before_disabled_event_handler() {
float BLEServer::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH + 10; }
void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); }
void BLEServer::dump_config() {
ESP_LOGCONFIG(TAG,
"ESP32 BLE Server:\n"
" Max clients: %u",
this->max_clients_);
}
BLEServer *global_ble_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -39,6 +39,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
this->restart_advertising_();
}
void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; }
uint8_t get_max_clients() const { return this->max_clients_; }
BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15);
void remove_service(ESPBTUUID uuid, uint8_t inst_id = 0);
BLEService *get_service(ESPBTUUID uuid, uint8_t inst_id = 0);
@@ -95,6 +98,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
uint8_t client_count_{0};
uint8_t max_clients_{1};
std::vector<ServiceEntry> services_{};
std::vector<BLEService *> services_to_start_{};
BLEService *device_information_service_{};
+1 -1
View File
@@ -12,7 +12,7 @@
namespace esphome {
/// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
class ESPHomeOTAComponent : public ota::OTAComponent {
class ESPHomeOTAComponent final : public ota::OTAComponent {
public:
enum class OTAState : uint8_t {
IDLE,
+2 -2
View File
@@ -6,12 +6,12 @@
namespace esphome {
namespace gp8403 {
enum GP8403Voltage {
enum GP8403Voltage : uint8_t {
GP8403_VOLTAGE_5V = 0x00,
GP8403_VOLTAGE_10V = 0x11,
};
enum GP8403Model {
enum GP8403Model : uint8_t {
GP8403,
GP8413,
};
+1 -1
View File
@@ -95,7 +95,7 @@ void HMC5883LComponent::update() {
float mg_per_bit;
switch (this->range_) {
case HMC5883L_RANGE_88_UT:
mg_per_bit = 0.073f;
mg_per_bit = 0.73f;
break;
case HMC5883L_RANGE_130_UT:
mg_per_bit = 0.92f;
@@ -1,5 +1,7 @@
#include "ota_http_request.h"
#include <cctype>
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
@@ -209,6 +211,26 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
return ota::OTA_RESPONSE_OK;
}
// URL-encode characters that are not unreserved per RFC 3986 section 2.3.
// This is needed for embedding userinfo (username/password) in URLs safely.
static std::string url_encode(const std::string &str) {
std::string result;
result.reserve(str.size());
for (char c : str) {
if (std::isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '_' || c == '.' || c == '~') {
result += c;
} else {
result += '%';
result += format_hex_pretty_char((static_cast<uint8_t>(c) >> 4) & 0x0F);
result += format_hex_pretty_char(static_cast<uint8_t>(c) & 0x0F);
}
}
return result;
}
void OtaHttpRequestComponent::set_password(const std::string &password) { this->password_ = url_encode(password); }
void OtaHttpRequestComponent::set_username(const std::string &username) { this->username_ = url_encode(username); }
std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) {
if (this->username_.empty() || this->password_.empty()) {
return url;
@@ -22,16 +22,16 @@ enum OtaHttpRequestError : uint8_t {
OTA_CONNECTION_ERROR = 0x12,
};
class OtaHttpRequestComponent : public ota::OTAComponent, public Parented<HttpRequestComponent> {
class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<HttpRequestComponent> {
public:
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void set_md5_url(const std::string &md5_url);
void set_md5(const std::string &md5) { this->md5_expected_ = md5; }
void set_password(const std::string &password) { this->password_ = password; }
void set_password(const std::string &password);
void set_url(const std::string &url);
void set_username(const std::string &username) { this->username_ = username; }
void set_username(const std::string &username);
std::string md5_computed() { return this->md5_computed_; }
std::string md5_expected() { return this->md5_expected_; }
@@ -222,11 +222,11 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_
}
// Calculate exponent
float exponent = msg[6] & 0x3F;
int8_t exp_val = msg[6] & 0x3F;
if (msg[6] & 0x40) {
exponent = -exponent;
exp_val = -exp_val;
}
exponent = powf(10, exponent);
float exponent = pow10_int(exp_val);
if (msg[6] & 0x80) {
exponent = -exponent;
}
+1 -1
View File
@@ -45,7 +45,7 @@ void LCDDisplay::setup() {
// TODO dotsize
// Commands can only be sent 40ms after boot-up, so let's wait if we're close
const uint8_t now = millis();
const uint32_t now = millis();
if (now < 40)
delay(40u - now);
+21 -22
View File
@@ -560,8 +560,6 @@ void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND];
this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH];
uint8_t reg_element = 0;
uint8_t data_element = 0;
uint16_t data_pos = 0;
if (this->cmd_reply_.length > CMD_MAX_BYTES) {
ESP_LOGW(TAG, "Reply frame too long");
@@ -583,43 +581,44 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
case (CMD_DISABLE_CONF):
ESP_LOGV(TAG, "Set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result);
break;
case (CMD_READ_REGISTER):
case (CMD_READ_REGISTER): {
ESP_LOGV(TAG, "Read register: CMD = %2X %s", CMD_READ_REGISTER, result);
// TODO Read/Write register is not implemented yet, this will get flushed out to a proper header file
data_pos = 0x0A;
for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT
((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE));
index += CMD_REG_DATA_REPLY_SIZE) {
memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], sizeof(CMD_REG_DATA_REPLY_SIZE));
byteswap(this->cmd_reply_.data[reg_element]);
reg_element++;
uint16_t reg_count = std::min<uint16_t>((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE,
sizeof(this->cmd_reply_.data) / sizeof(this->cmd_reply_.data[0]));
for (uint16_t i = 0; i < reg_count; i++) {
memcpy(&this->cmd_reply_.data[i], &buffer[data_pos + i * CMD_REG_DATA_REPLY_SIZE], CMD_REG_DATA_REPLY_SIZE);
}
break;
}
case (CMD_WRITE_REGISTER):
ESP_LOGV(TAG, "Write register: CMD = %2X %s", CMD_WRITE_REGISTER, result);
break;
case (CMD_WRITE_ABD_PARAM):
ESP_LOGV(TAG, "Write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result);
break;
case (CMD_READ_ABD_PARAM):
case (CMD_READ_ABD_PARAM): {
ESP_LOGV(TAG, "Read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result);
data_pos = CMD_ABD_DATA_REPLY_START;
for (uint16_t index = 0; index < (CMD_ABD_DATA_REPLY_SIZE * // NOLINT
((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE));
index += CMD_ABD_DATA_REPLY_SIZE) {
memcpy(&this->cmd_reply_.data[data_element], &buffer[data_pos + index],
sizeof(this->cmd_reply_.data[data_element]));
byteswap(this->cmd_reply_.data[data_element]);
data_element++;
uint16_t abd_count = std::min<uint16_t>((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE,
sizeof(this->cmd_reply_.data) / sizeof(this->cmd_reply_.data[0]));
for (uint16_t i = 0; i < abd_count; i++) {
memcpy(&this->cmd_reply_.data[i], &buffer[data_pos + i * CMD_ABD_DATA_REPLY_SIZE],
sizeof(this->cmd_reply_.data[i]));
}
break;
}
case (CMD_WRITE_SYS_PARAM):
ESP_LOGV(TAG, "Set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result);
break;
case (CMD_READ_VERSION):
memcpy(this->firmware_ver_, &buffer[12], buffer[10]);
ESP_LOGV(TAG, "Firmware version: %7s %s", this->firmware_ver_, result);
case (CMD_READ_VERSION): {
uint8_t ver_len = std::min<uint8_t>(buffer[10], sizeof(this->firmware_ver_) - 1);
memcpy(this->firmware_ver_, &buffer[12], ver_len);
this->firmware_ver_[ver_len] = '\0';
ESP_LOGV(TAG, "Firmware version: %s %s", this->firmware_ver_, result);
break;
}
default:
break;
}
@@ -729,9 +728,9 @@ void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) {
cmd_frame.data_length = 0;
cmd_frame.header = CMD_FRAME_HEADER;
cmd_frame.command = CMD_WRITE_REGISTER;
memcpy(&cmd_frame.data[cmd_frame.data_length], &reg, sizeof(CMD_REG_DATA_REPLY_SIZE));
memcpy(&cmd_frame.data[cmd_frame.data_length], &reg, CMD_REG_DATA_REPLY_SIZE);
cmd_frame.data_length += 2;
memcpy(&cmd_frame.data[cmd_frame.data_length], &value, sizeof(CMD_REG_DATA_REPLY_SIZE));
memcpy(&cmd_frame.data[cmd_frame.data_length], &value, CMD_REG_DATA_REPLY_SIZE);
cmd_frame.data_length += 2;
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGV(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value);
+17 -14
View File
@@ -1,27 +1,30 @@
#include "light_color_values.h"
#include <cmath>
namespace esphome::light {
// Lightweight lerp: a + t * (b - a).
// Avoids std::lerp's NaN/infinity handling which Clang doesn't optimize out,
// adding ~200 bytes per call. Safe because all values are finite floats.
static float __attribute__((noinline)) lerp_fast(float a, float b, float t) { return a + t * (b - a); }
LightColorValues LightColorValues::lerp(const LightColorValues &start, const LightColorValues &end, float completion) {
// Directly interpolate the raw values to avoid getter/setter overhead.
// This is safe because:
// - All LightColorValues have their values clamped when set via the setters
// - std::lerp guarantees output is in the same range as inputs
// - All LightColorValues except color_temperature_ have their values clamped when set via the setters
// - lerp_fast output stays in range when inputs are in range and 0 <= completion <= 1
// - Therefore the output doesn't need clamping, so we can skip the setters
LightColorValues v;
v.color_mode_ = end.color_mode_;
v.state_ = std::lerp(start.state_, end.state_, completion);
v.brightness_ = std::lerp(start.brightness_, end.brightness_, completion);
v.color_brightness_ = std::lerp(start.color_brightness_, end.color_brightness_, completion);
v.red_ = std::lerp(start.red_, end.red_, completion);
v.green_ = std::lerp(start.green_, end.green_, completion);
v.blue_ = std::lerp(start.blue_, end.blue_, completion);
v.white_ = std::lerp(start.white_, end.white_, completion);
v.color_temperature_ = std::lerp(start.color_temperature_, end.color_temperature_, completion);
v.cold_white_ = std::lerp(start.cold_white_, end.cold_white_, completion);
v.warm_white_ = std::lerp(start.warm_white_, end.warm_white_, completion);
v.state_ = lerp_fast(start.state_, end.state_, completion);
v.brightness_ = lerp_fast(start.brightness_, end.brightness_, completion);
v.color_brightness_ = lerp_fast(start.color_brightness_, end.color_brightness_, completion);
v.red_ = lerp_fast(start.red_, end.red_, completion);
v.green_ = lerp_fast(start.green_, end.green_, completion);
v.blue_ = lerp_fast(start.blue_, end.blue_, completion);
v.white_ = lerp_fast(start.white_, end.white_, completion);
v.color_temperature_ = lerp_fast(start.color_temperature_, end.color_temperature_, completion);
v.cold_white_ = lerp_fast(start.cold_white_, end.cold_white_, completion);
v.warm_white_ = lerp_fast(start.warm_white_, end.warm_white_, completion);
return v;
}
+3 -4
View File
@@ -44,12 +44,11 @@ class LightTransformer {
/// The progress of this transition, on a scale of 0 to 1.
float get_progress_() {
uint32_t now = esphome::millis();
if (now < this->start_time_)
return 0.0f;
if (now >= this->start_time_ + this->length_)
uint32_t elapsed = now - this->start_time_;
if (elapsed >= this->length_)
return 1.0f;
return clamp((now - this->start_time_) / float(this->length_), 0.0f, 1.0f);
return clamp(elapsed / float(this->length_), 0.0f, 1.0f);
}
uint32_t start_time_;
+1 -1
View File
@@ -78,7 +78,7 @@ class LightFlashTransformer : public LightTransformer {
optional<LightColorValues> apply() override {
optional<LightColorValues> result = {};
if (this->transformer_ == nullptr && millis() > this->start_time_ + this->length_ - this->transition_length_) {
if (this->transformer_ == nullptr && millis() - this->start_time_ > this->length_ - this->transition_length_) {
// second transition back to start value
this->transformer_ = this->state_.get_output()->create_default_transition();
this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_);
@@ -31,13 +31,12 @@ void LightWaveRF::read_tx() {
void LightWaveRF::send_rx(const std::vector<uint8_t> &msg, uint8_t repeats, bool inverted, int u_sec) {
this->lwtx_.lwtx_setup(pin_tx_, repeats, inverted, u_sec);
uint32_t timeout = 0;
uint32_t timeout = millis();
if (this->lwtx_.lwtx_free()) {
this->lwtx_.lwtx_send(msg);
timeout = millis();
ESP_LOGD(TAG, "[%i] msg start", timeout);
}
while (!this->lwtx_.lwtx_free() && millis() < (timeout + 1000)) {
while (!this->lwtx_.lwtx_free() && millis() - timeout < 1000) {
delay(10);
}
timeout = millis() - timeout;
+2 -2
View File
@@ -141,7 +141,7 @@ enum UARTSelection : uint8_t {
* 2. Works with ESP-IDF's pthread implementation that uses a linked list for TLS variables
* 3. Avoids the limitations of the fixed FreeRTOS task local storage slots
*/
class Logger : public Component {
class Logger final : public Component {
public:
explicit Logger(uint32_t baud_rate);
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
@@ -481,7 +481,7 @@ class Logger : public Component {
};
extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
class LoggerMessageTrigger : public Trigger<uint8_t, const char *, const char *> {
class LoggerMessageTrigger final : public Trigger<uint8_t, const char *, const char *> {
public:
explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) {
parent->add_log_callback(this,
+2 -2
View File
@@ -133,8 +133,8 @@ uint8_t MCP2515::get_status_() {
canbus::Error MCP2515::set_mode_(const CanctrlReqopMode mode) {
modify_register_(MCP_CANCTRL, CANCTRL_REQOP, mode);
uint32_t end_time = millis() + 10;
while (millis() < end_time) {
uint32_t start_time = millis();
while (millis() - start_time < 10) {
if ((read_register_(MCP_CANSTAT) & CANSTAT_OPMOD) == mode)
return canbus::ERROR_OK;
}
+1 -1
View File
@@ -40,7 +40,7 @@ struct MDNSService {
FixedVector<MDNSTXTRecord> txt_records;
};
class MDNSComponent : public Component {
class MDNSComponent final : public Component {
public:
void setup() override;
void dump_config() override;
@@ -7,7 +7,7 @@
namespace esphome {
namespace ota {
class ArduinoLibreTinyOTABackend : public OTABackend {
class ArduinoLibreTinyOTABackend final : public OTABackend {
public:
OTAResponseTypes begin(size_t image_size) override;
void set_update_md5(const char *md5) override;
@@ -9,7 +9,7 @@
namespace esphome {
namespace ota {
class ArduinoRP2040OTABackend : public OTABackend {
class ArduinoRP2040OTABackend final : public OTABackend {
public:
OTAResponseTypes begin(size_t image_size) override;
void set_update_md5(const char *md5) override;
+1 -1
View File
@@ -12,7 +12,7 @@ namespace esphome::ota {
/// OTA backend for ESP8266 using native SDK functions.
/// This implementation bypasses the Arduino Updater library to save ~228 bytes of RAM
/// by not having a global Update object in .bss.
class ESP8266OTABackend : public OTABackend {
class ESP8266OTABackend final : public OTABackend {
public:
OTAResponseTypes begin(size_t image_size) override;
void set_update_md5(const char *md5) override;
+1 -1
View File
@@ -10,7 +10,7 @@
namespace esphome {
namespace ota {
class IDFOTABackend : public OTABackend {
class IDFOTABackend final : public OTABackend {
public:
OTAResponseTypes begin(size_t image_size) override;
void set_update_md5(const char *md5) override;
+1 -1
View File
@@ -7,7 +7,7 @@ namespace esphome::ota {
/// Stub OTA backend for host platform - allows compilation but does not implement OTA.
/// All operations return error codes immediately. This enables configurations with
/// OTA triggers to compile for host platform during development.
class HostOTABackend : public OTABackend {
class HostOTABackend final : public OTABackend {
public:
OTAResponseTypes begin(size_t image_size) override;
void set_update_md5(const char *md5) override;
+2 -2
View File
@@ -50,8 +50,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput),
cv.Optional(CONF_DEADBAND_PARAMETERS): cv.Schema(
{
cv.Required(CONF_THRESHOLD_HIGH): cv.temperature,
cv.Required(CONF_THRESHOLD_LOW): cv.temperature,
cv.Required(CONF_THRESHOLD_HIGH): cv.temperature_delta,
cv.Required(CONF_THRESHOLD_LOW): cv.temperature_delta,
cv.Optional(CONF_KP_MULTIPLIER, default=0.1): cv.float_,
cv.Optional(CONF_KI_MULTIPLIER, default=0.0): cv.float_,
cv.Optional(CONF_KD_MULTIPLIER, default=0.0): cv.float_,
+4 -4
View File
@@ -308,13 +308,13 @@ void PN532::send_nack_() {
enum PN532ReadReady PN532::read_ready_(bool block) {
if (this->rd_ready_ == READY) {
if (block) {
this->rd_start_time_ = 0;
this->rd_start_time_.reset();
this->rd_ready_ = WOULDBLOCK;
}
return READY;
}
if (!this->rd_start_time_) {
if (!this->rd_start_time_.has_value()) {
this->rd_start_time_ = millis();
}
@@ -324,7 +324,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) {
break;
}
if (millis() - this->rd_start_time_ > 100) {
if (millis() - *this->rd_start_time_ > 100) {
ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!");
this->rd_ready_ = TIMEOUT;
break;
@@ -340,7 +340,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) {
auto rdy = this->rd_ready_;
if (block || rdy == TIMEOUT) {
this->rd_start_time_ = 0;
this->rd_start_time_.reset();
this->rd_ready_ = WOULDBLOCK;
}
return rdy;
+1 -1
View File
@@ -99,7 +99,7 @@ class PN532 : public PollingComponent {
std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_;
nfc::NfcTagUid current_uid_;
nfc::NdefMessage *next_task_message_to_write_;
uint32_t rd_start_time_{0};
optional<uint32_t> rd_start_time_{};
enum PN532ReadReady rd_ready_ { WOULDBLOCK };
enum NfcTask {
READ = 0,
+11 -10
View File
@@ -139,9 +139,10 @@ void Rtttl::loop() {
x++;
}
if (x > 0) {
int send = this->speaker_->play((uint8_t *) (&sample), x * 2);
if (send != x * 4) {
this->samples_sent_ -= (x - (send / 2));
size_t bytes_to_send = x * sizeof(SpeakerSample);
size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send);
if (send != bytes_to_send) {
this->samples_sent_ -= (x - (send / sizeof(SpeakerSample)));
}
return;
}
@@ -201,9 +202,9 @@ void Rtttl::loop() {
bool need_note_gap = false;
if (note) {
auto note_index = (scale - 4) * 12 + note;
if (note_index < 0 || note_index >= (int) sizeof(NOTES)) {
if (note_index < 0 || note_index >= (int) (sizeof(NOTES) / sizeof(NOTES[0]))) {
ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index,
(int) sizeof(NOTES));
(int) (sizeof(NOTES) / sizeof(NOTES[0])));
this->finish_();
return;
}
@@ -221,7 +222,7 @@ void Rtttl::loop() {
#ifdef USE_OUTPUT
if (this->output_ != nullptr) {
if (need_note_gap) {
if (need_note_gap && this->note_duration_ > DOUBLE_NOTE_GAP_MS) {
this->output_->set_level(0.0);
delay(DOUBLE_NOTE_GAP_MS);
this->note_duration_ -= DOUBLE_NOTE_GAP_MS;
@@ -240,9 +241,9 @@ void Rtttl::loop() {
this->samples_sent_ = 0;
this->samples_gap_ = 0;
this->samples_per_wave_ = 0;
this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1600; //(ms);
this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1000;
if (need_note_gap) {
this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1600; //(ms);
this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1000;
}
if (this->output_freq_ != 0) {
// make sure there is enough samples to add a full last sinus.
@@ -279,7 +280,7 @@ void Rtttl::play(std::string rtttl) {
this->note_duration_ = 0;
int bpm = 63;
uint8_t num;
uint16_t num;
// Get name
this->position_ = this->rtttl_.find(':');
@@ -395,7 +396,7 @@ void Rtttl::finish_() {
sample[0].right = 0;
sample[1].left = 0;
sample[1].right = 0;
this->speaker_->play((uint8_t *) (&sample), 8);
this->speaker_->play((uint8_t *) (&sample), sizeof(sample));
this->speaker_->finish();
this->set_state_(State::STOPPING);
}
+3 -3
View File
@@ -46,8 +46,8 @@ class Rtttl : public Component {
}
protected:
inline uint8_t get_integer_() {
uint8_t ret = 0;
inline uint16_t get_integer_() {
uint16_t ret = 0;
while (isdigit(this->rtttl_[this->position_])) {
ret = (ret * 10) + (this->rtttl_[this->position_++] - '0');
}
@@ -87,7 +87,7 @@ class Rtttl : public Component {
#ifdef USE_OUTPUT
/// The output to write the sound to.
output::FloatOutput *output_;
output::FloatOutput *output_{nullptr};
#endif // USE_OUTPUT
#ifdef USE_SPEAKER
@@ -2,6 +2,7 @@
#include "image_decoder.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <algorithm>
#include <cstring>
#ifdef USE_RUNTIME_IMAGE_BMP
@@ -43,6 +44,14 @@ int RuntimeImage::resize(int width, int height) {
int target_width = this->fixed_width_ ? this->fixed_width_ : width;
int target_height = this->fixed_height_ ? this->fixed_height_ : height;
// When both fixed dimensions are set, scale uniformly to preserve aspect ratio
if (this->fixed_width_ && this->fixed_height_ && width > 0 && height > 0) {
float scale =
std::min(static_cast<float>(this->fixed_width_) / width, static_cast<float>(this->fixed_height_) / height);
target_width = static_cast<int>(width * scale);
target_height = static_cast<int>(height * scale);
}
size_t result = this->resize_buffer_(target_width, target_height);
if (result > 0 && this->progressive_display_) {
// Update display dimensions for progressive display
+1 -1
View File
@@ -8,7 +8,7 @@
namespace esphome::safe_mode {
class SafeModeTrigger : public Trigger<> {
class SafeModeTrigger final : public Trigger<> {
public:
explicit SafeModeTrigger(SafeModeComponent *parent) {
parent->add_on_safe_mode_callback([this]() { trigger(); });
+1 -1
View File
@@ -15,7 +15,7 @@ namespace esphome::safe_mode {
constexpr uint32_t RTC_KEY = 233825507UL;
/// SafeModeComponent provides a safe way to recover from repeated boot failures
class SafeModeComponent : public Component {
class SafeModeComponent final : public Component {
public:
bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after);
-9
View File
@@ -56,15 +56,6 @@ static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) {
}
}
// This function performs an in-place conversion of the provided buffer
// from uint16_t values to big endianness
static inline const char *sensirion_convert_to_string_in_place(uint16_t *array, size_t length) {
for (size_t i = 0; i < length; i++) {
array[i] = convert_big_endian(array[i]);
}
return reinterpret_cast<const char *>(array);
}
void SEN5XComponent::setup() {
// the sensor needs 1000 ms to enter the idle state
this->set_timeout(1000, [this]() {
@@ -21,6 +21,17 @@ class SensirionI2CDevice : public i2c::I2CDevice {
public:
enum CommandLen : uint8_t { ADDR_8_BIT = 1, ADDR_16_BIT = 2 };
/**
* This function performs an in-place conversion of the provided buffer
* from uint16_t values to big endianness. Useful for Sensirion strings in SEN5X and SEN6X
*/
static inline const char *sensirion_convert_to_string_in_place(uint16_t *array, size_t length) {
for (size_t i = 0; i < length; i++) {
array[i] = convert_big_endian(array[i]);
}
return reinterpret_cast<const char *>(array);
}
/** Read data words from I2C device.
* handles CRC check used by Sensirion sensors
* @param data pointer to raw result
+1 -1
View File
@@ -603,7 +603,7 @@ DELTA_SCHEMA = cv.Any(
def _get_delta(value):
if isinstance(value, str):
assert value.endswith("%")
return 0.0, float(value[:-1])
return 0.0, float(value[:-1]) / 100.0
return value, 0.0
@@ -149,7 +149,7 @@ stm32_err_t stm32_get_ack_timeout(const stm32_unique_ptr &stm, uint32_t timeout)
do {
yield();
if (!stream->available()) {
if (millis() < start_time + timeout)
if (millis() - start_time < timeout)
continue;
ESP_LOGD(TAG, "Failed to read ACK timeout=%i", timeout);
return STM32_ERR_UNKNOWN;
@@ -212,7 +212,7 @@ stm32_err_t stm32_resync(const stm32_unique_ptr &stm) {
static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes");
uint8_t ack;
while (t1 < t0 + STM32_RESYNC_TIMEOUT) {
while (t1 - t0 < STM32_RESYNC_TIMEOUT) {
stream->write_array(buf, BUFFER_SIZE);
stream->flush();
if (!stream->read_array(&ack, 1)) {
+6 -2
View File
@@ -134,6 +134,8 @@ def require_wake_loop_threadsafe() -> None:
IMPORTANT: This is for background thread context only, NOT ISR context.
Socket operations are not safe to call from ISR handlers.
On ESP32, FreeRTOS task notifications are used instead (no socket needed).
Example:
from esphome.components import socket
@@ -147,8 +149,10 @@ def require_wake_loop_threadsafe() -> None:
):
CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True
cg.add_define("USE_WAKE_LOOP_THREADSAFE")
# Consume 1 socket for the shared wake notification socket
consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({})
if not CORE.is_esp32:
# Only non-ESP32 platforms need a UDP socket for wake notifications.
# ESP32 uses FreeRTOS task notifications instead (no socket needed).
consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({})
CONFIG_SCHEMA = cv.Schema(
+1 -1
View File
@@ -71,7 +71,7 @@ class Socket {
int get_fd() const { return -1; }
#endif
/// Check if socket has data ready to read
/// Check if socket has data ready to read. Must only be called from the main loop thread.
/// For select()-based sockets: non-virtual, checks Application's select() results
/// For LWIP raw TCP sockets: virtual, checks internal buffer state
#ifdef USE_SOCKET_SELECT_SUPPORT
+41 -40
View File
@@ -84,32 +84,30 @@ SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler
: controller_(controller), valve_(valve) {}
void SprinklerValveOperator::loop() {
// Use wrapping subtraction so 32-bit millis() rollover is handled correctly:
// (now - start) yields the true elapsed time even across the 49.7-day boundary.
uint32_t now = App.get_loop_component_start_time();
if (now >= this->start_millis_) { // dummy check
switch (this->state_) {
case STARTING:
if (now > (this->start_millis_ + this->start_delay_)) {
this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state
}
break;
switch (this->state_) {
case STARTING:
if ((now - *this->start_millis_) > this->start_delay_) {
this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state
}
break;
case ACTIVE:
if (now > (this->start_millis_ + this->start_delay_ + this->run_duration_)) {
this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down
}
break;
case ACTIVE:
if ((now - *this->start_millis_) > (this->start_delay_ + this->run_duration_)) {
this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down
}
break;
case STOPPING:
if (now > (this->stop_millis_ + this->stop_delay_)) {
this->kill_(); // stop_delay_has been exceeded, ensure all valves are off
}
break;
case STOPPING:
if ((now - *this->stop_millis_) > this->stop_delay_) {
this->kill_(); // stop_delay_has been exceeded, ensure all valves are off
}
break;
default:
break;
}
} else { // perhaps millis() rolled over...or something else is horribly wrong!
this->stop(); // bail out (TODO: handle this highly unlikely situation better...)
default:
break;
}
}
@@ -124,11 +122,11 @@ void SprinklerValveOperator::set_valve(SprinklerValve *valve) {
if (this->state_ != IDLE) { // Only kill if not already idle
this->kill_(); // ensure everything is off before we let go!
}
this->state_ = IDLE; // reset state
this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it
this->start_millis_ = 0; // reset because (new) valve has not been started yet
this->stop_millis_ = 0; // reset because (new) valve has not been started yet
this->valve_ = valve; // finally, set the pointer to the new valve
this->state_ = IDLE; // reset state
this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it
this->start_millis_.reset(); // reset because (new) valve has not been started yet
this->stop_millis_.reset(); // reset because (new) valve has not been started yet
this->valve_ = valve; // finally, set the pointer to the new valve
}
}
@@ -162,7 +160,7 @@ void SprinklerValveOperator::start() {
} else {
this->run_(); // there is no start_delay_, so just start the pump and valve
}
this->stop_millis_ = 0;
this->stop_millis_.reset();
this->start_millis_ = millis(); // save the time the start request was made
}
@@ -189,22 +187,25 @@ void SprinklerValveOperator::stop() {
uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_ / 1000; }
uint32_t SprinklerValveOperator::time_remaining() {
if (this->start_millis_ == 0) {
if (!this->start_millis_.has_value()) {
return this->run_duration(); // hasn't been started yet
}
if (this->stop_millis_) {
if (this->stop_millis_ - this->start_millis_ >= this->start_delay_ + this->run_duration_) {
if (this->stop_millis_.has_value()) {
uint32_t elapsed = *this->stop_millis_ - *this->start_millis_;
if (elapsed >= this->start_delay_ + this->run_duration_) {
return 0; // valve was active for more than its configured duration, so we are done
} else {
// we're stopped; return time remaining
return (this->run_duration_ - (this->stop_millis_ - this->start_millis_)) / 1000;
}
if (elapsed <= this->start_delay_) {
return this->run_duration_ / 1000; // stopped during start delay, full run duration remains
}
return (this->run_duration_ - (elapsed - this->start_delay_)) / 1000;
}
auto completed_millis = this->start_millis_ + this->start_delay_ + this->run_duration_;
if (completed_millis > millis()) {
return (completed_millis - millis()) / 1000; // running now
uint32_t elapsed = millis() - *this->start_millis_;
uint32_t total_duration = this->start_delay_ + this->run_duration_;
if (elapsed < total_duration) {
return (total_duration - elapsed) / 1000; // running now
}
return 0; // run completed
}
@@ -593,7 +594,7 @@ void Sprinkler::set_repeat(optional<uint32_t> repeat) {
if (this->repeat_number_ == nullptr) {
return;
}
if (this->repeat_number_->state == repeat.value()) {
if (this->repeat_number_->state == repeat.value_or(0)) {
return;
}
auto call = this->repeat_number_->make_call();
@@ -793,7 +794,7 @@ void Sprinkler::start_single_valve(const optional<size_t> valve_number, optional
void Sprinkler::queue_valve(optional<size_t> valve_number, optional<uint32_t> run_duration) {
if (valve_number.has_value()) {
if (this->is_a_valid_valve(valve_number.value()) && (this->queued_valves_.size() < this->max_queue_size_)) {
SprinklerQueueItem item{valve_number.value(), run_duration.value()};
SprinklerQueueItem item{valve_number.value(), run_duration.value_or(0)};
this->queued_valves_.insert(this->queued_valves_.begin(), item);
ESP_LOGD(TAG, "Valve %zu placed into queue with run duration of %" PRIu32 " seconds", valve_number.value_or(0),
run_duration.value_or(0));
@@ -1080,7 +1081,7 @@ uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() {
}
}
if (incomplete_valve_count >= enabled_valve_count) {
if (incomplete_valve_count > 0 && incomplete_valve_count >= enabled_valve_count) {
incomplete_valve_count--;
}
if (incomplete_valve_count) {
+2 -2
View File
@@ -141,8 +141,8 @@ class SprinklerValveOperator {
uint32_t start_delay_{0};
uint32_t stop_delay_{0};
uint32_t run_duration_{0};
uint64_t start_millis_{0};
uint64_t stop_millis_{0};
optional<uint32_t> start_millis_{};
optional<uint32_t> stop_millis_{};
Sprinkler *controller_{nullptr};
SprinklerValve *valve_{nullptr};
SprinklerState state_{IDLE};
@@ -14,8 +14,6 @@ ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource
ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es)
: web_server_(ws), events_(es) {}
#endif
ListEntitiesIterator::~ListEntitiesIterator() {}
#ifdef USE_BINARY_SENSOR
bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *obj) {
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::binary_sensor_all_json_generator);
@@ -17,14 +17,13 @@ class DeferredUpdateEventSource;
#endif
class WebServer;
class ListEntitiesIterator : public ComponentIterator {
class ListEntitiesIterator final : public ComponentIterator {
public:
#ifdef USE_ESP32
ListEntitiesIterator(const WebServer *ws, esphome::web_server_idf::AsyncEventSource *es);
#elif defined(USE_ARDUINO)
ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es);
#endif
virtual ~ListEntitiesIterator();
#ifdef USE_BINARY_SENSOR
bool on_binary_sensor(binary_sensor::BinarySensor *obj) override;
#endif
@@ -9,7 +9,7 @@
namespace esphome::web_server {
class WebServerOTAComponent : public ota::OTAComponent {
class WebServerOTAComponent final : public ota::OTAComponent {
public:
void setup() override;
void dump_config() override;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -385,6 +385,7 @@ json::SerializationBuffer<> WebServer::get_config_json() {
#endif
root[ESPHOME_F("log")] = this->expose_log_;
root[ESPHOME_F("lang")] = "en";
root[ESPHOME_F("uptime")] = static_cast<uint32_t>(App.scheduler.millis_64() / 1000);
return builder.serialize();
}
@@ -411,7 +412,12 @@ void WebServer::setup() {
// doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly
// getting a lot of events
this->set_interval(10000, [this]() { this->events_.try_send_nodefer("", "ping", millis(), 30000); });
this->set_interval(10000, [this]() {
char buf[32];
auto uptime = static_cast<uint32_t>(App.scheduler.millis_64() / 1000);
buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%u}", uptime);
this->events_.try_send_nodefer(buf, "ping", millis(), 30000);
});
}
void WebServer::loop() { this->events_.loop(); }
+3 -3
View File
@@ -107,7 +107,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
using message_generator_t = json::SerializationBuffer<>(WebServer *, void *);
class DeferredUpdateEventSourceList;
class DeferredUpdateEventSource : public AsyncEventSource {
class DeferredUpdateEventSource final : public AsyncEventSource {
friend class DeferredUpdateEventSourceList;
/*
@@ -163,7 +163,7 @@ class DeferredUpdateEventSource : public AsyncEventSource {
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
};
class DeferredUpdateEventSourceList : public std::list<DeferredUpdateEventSource *> {
class DeferredUpdateEventSourceList final : public std::list<DeferredUpdateEventSource *> {
protected:
void on_client_connect_(DeferredUpdateEventSource *source);
void on_client_disconnect_(DeferredUpdateEventSource *source);
@@ -187,7 +187,7 @@ class DeferredUpdateEventSourceList : public std::list<DeferredUpdateEventSource
* under the '/light/...', '/sensor/...', ... URLs. A full documentation for this API
* can be found under https://esphome.io/web-api/.
*/
class WebServer : public Controller, public Component, public AsyncWebHandler {
class WebServer final : public Controller, public Component, public AsyncWebHandler {
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
friend class DeferredUpdateEventSourceList;
#endif
@@ -33,8 +33,8 @@ class MultipartReader {
~MultipartReader();
// Set callbacks for handling data
void set_data_callback(DataCallback callback) { data_callback_ = std::move(callback); }
void set_part_complete_callback(PartCompleteCallback callback) { part_complete_callback_ = std::move(callback); }
void set_data_callback(DataCallback &&callback) { data_callback_ = std::move(callback); }
void set_part_complete_callback(PartCompleteCallback &&callback) { part_complete_callback_ = std::move(callback); }
// Parse incoming data
size_t parse(const char *data, size_t len);
@@ -204,7 +204,7 @@ class AsyncWebServer {
~AsyncWebServer() { this->end(); }
// NOLINTNEXTLINE(readability-identifier-naming)
void onNotFound(std::function<void(AsyncWebServerRequest *request)> fn) { on_not_found_ = std::move(fn); }
void onNotFound(std::function<void(AsyncWebServerRequest *request)> &&fn) { on_not_found_ = std::move(fn); }
void begin();
void end();
@@ -327,7 +327,7 @@ class AsyncEventSource : public AsyncWebHandler {
// NOLINTNEXTLINE(readability-identifier-naming)
void handleRequest(AsyncWebServerRequest *request) override;
// NOLINTNEXTLINE(readability-identifier-naming)
void onConnect(connect_handler_t cb) { this->on_connect_ = std::move(cb); }
void onConnect(connect_handler_t &&cb) { this->on_connect_ = std::move(cb); }
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
+12 -9
View File
@@ -24,7 +24,7 @@ namespace wled {
// https://github.com/Aircoookie/WLED/wiki/UDP-Realtime-Control
enum Protocol { WLED_NOTIFIER = 0, WARLS = 1, DRGB = 2, DRGBW = 3, DNRGB = 4 };
const int DEFAULT_BLANK_TIME = 1000;
constexpr uint32_t DEFAULT_BLANK_TIME = 1000;
static const char *const TAG = "wled_light_effect";
@@ -34,9 +34,10 @@ void WLEDLightEffect::start() {
AddressableLightEffect::start();
if (this->blank_on_start_) {
this->blank_at_ = 0;
this->blank_start_ = millis();
this->blank_timeout_ = 0;
} else {
this->blank_at_ = UINT32_MAX;
this->blank_start_.reset();
}
}
@@ -81,10 +82,10 @@ void WLEDLightEffect::apply(light::AddressableLight &it, const Color &current_co
}
}
// FIXME: Use roll-over safe arithmetic
if (blank_at_ < millis()) {
if (this->blank_start_.has_value() && millis() - *this->blank_start_ >= this->blank_timeout_) {
blank_all_leds_(it);
blank_at_ = millis() + DEFAULT_BLANK_TIME;
this->blank_start_ = millis();
this->blank_timeout_ = DEFAULT_BLANK_TIME;
}
}
@@ -142,11 +143,13 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p
}
if (timeout == UINT8_MAX) {
blank_at_ = UINT32_MAX;
this->blank_start_.reset();
} else if (timeout > 0) {
blank_at_ = millis() + timeout * 1000;
this->blank_start_ = millis();
this->blank_timeout_ = timeout * 1000;
} else {
blank_at_ = millis() + DEFAULT_BLANK_TIME;
this->blank_start_ = millis();
this->blank_timeout_ = DEFAULT_BLANK_TIME;
}
it.schedule_show();
+2 -1
View File
@@ -35,7 +35,8 @@ class WLEDLightEffect : public light::AddressableLightEffect {
uint16_t port_{0};
std::unique_ptr<UDP> udp_;
uint32_t blank_at_{0};
optional<uint32_t> blank_start_{};
uint32_t blank_timeout_{0};
uint32_t dropped_{0};
uint8_t sync_group_mask_{0};
bool blank_on_start_{true};
+4 -1
View File
@@ -1640,7 +1640,10 @@ def dimensions(value):
if width <= 0 or height <= 0:
raise Invalid("Width and height must at least be 1")
return [width, height]
value = string(value)
if not isinstance(value, str):
raise Invalid(
"Dimensions must be a string (WIDTHxHEIGHT). Got a number instead, try quoting the value."
)
match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value)
if not match:
raise Invalid(
+77 -27
View File
@@ -9,6 +9,9 @@
#endif
#ifdef USE_ESP32
#include <esp_chip_info.h>
#include "esphome/core/lwip_fast_select.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#endif
#include "esphome/core/version.h"
#include "esphome/core/hal.h"
@@ -134,7 +137,7 @@ void Application::setup() {
this->after_loop_tasks_();
this->app_state_ = new_app_state;
yield();
} while (!component->can_proceed());
} while (!component->can_proceed() && !component->is_failed());
}
ESP_LOGI(TAG, "setup() finished successfully!");
@@ -144,8 +147,14 @@ void Application::setup() {
clear_setup_priority_overrides();
#endif
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
// Set up wake socket for waking main loop from tasks
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32)
// Initialize fast select: saves main loop task handle for xTaskNotifyGive wake.
// Always init on ESP32 — the fast path (rcvevent reads + ulTaskNotifyTake) is used
// unconditionally when USE_SOCKET_SELECT_SUPPORT is enabled.
esphome_lwip_fast_select_init();
#endif
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
// Set up wake socket for waking main loop from tasks (non-ESP32 only)
this->setup_wake_loop_threadsafe_();
#endif
@@ -523,7 +532,7 @@ void Application::enable_pending_loops_() {
}
void Application::before_loop_tasks_(uint32_t loop_start_time) {
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
// Drain wake notifications first to clear socket for next wake
this->drain_wake_notifications_();
#endif
@@ -576,11 +585,15 @@ bool Application::register_socket_fd(int fd) {
#endif
this->socket_fds_.push_back(fd);
#ifdef USE_ESP32
// Hook the socket's netconn callback for instant wake on receive events
esphome_lwip_hook_socket(fd);
#else
this->socket_fds_changed_ = true;
if (fd > this->max_fd_) {
this->max_fd_ = fd;
}
#endif
return true;
}
@@ -595,12 +608,14 @@ void Application::unregister_socket_fd(int fd) {
if (this->socket_fds_[i] != fd)
continue;
// Swap with last element and pop - O(1) removal since order doesn't matter
// Swap with last element and pop - O(1) removal since order doesn't matter.
// No need to unhook the netconn callback on ESP32 — all LwIP sockets share
// the same static event_callback, and the socket will be closed by the caller.
if (i < this->socket_fds_.size() - 1)
this->socket_fds_[i] = this->socket_fds_.back();
this->socket_fds_.pop_back();
#ifndef USE_ESP32
this->socket_fds_changed_ = true;
// Only recalculate max_fd if we removed the current max
if (fd == this->max_fd_) {
this->max_fd_ = -1;
@@ -609,6 +624,7 @@ void Application::unregister_socket_fd(int fd) {
this->max_fd_ = sock_fd;
}
}
#endif
return;
}
}
@@ -616,16 +632,41 @@ void Application::unregister_socket_fd(int fd) {
#endif
void Application::yield_with_select_(uint32_t delay_ms) {
// Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run
// since select() with 0 timeout only polls without yielding.
#ifdef USE_SOCKET_SELECT_SUPPORT
if (!this->socket_fds_.empty()) {
// Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run.
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32)
// ESP32 fast path: reads rcvevent directly via lwip_socket_dbg_get_socket() (~215 ns per socket).
// Safe because this runs on the main loop which owns socket lifetime (create, read, close).
if (delay_ms == 0) [[unlikely]] {
yield();
return;
}
// Check if any socket already has pending data before sleeping.
// If a socket still has unread data (rcvevent > 0) but the task notification was already
// consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency.
// This scan preserves select() semantics: return immediately when any fd is ready.
for (int fd : this->socket_fds_) {
if (esphome_lwip_socket_has_data(fd)) {
yield();
return;
}
}
// Sleep with instant wake via FreeRTOS task notification.
// Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout.
// Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task —
// background tasks won't call wake, so this degrades to a pure timeout (same as old select path).
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms));
#elif defined(USE_SOCKET_SELECT_SUPPORT)
// Non-ESP32 select() path (LibreTiny bk72xx/rtl87xx, host platform).
// ESP32 is excluded by the #if above — both BSD_SOCKETS and LWIP_SOCKETS on ESP32
// use LwIP under the hood, so the fast path handles all ESP32 socket implementations.
if (!this->socket_fds_.empty()) [[likely]] {
// Update fd_set if socket list has changed
if (this->socket_fds_changed_) {
if (this->socket_fds_changed_) [[unlikely]] {
FD_ZERO(&this->base_read_fds_);
// fd bounds are already validated in register_socket_fd() or guaranteed by platform design:
// - ESP32: LwIP guarantees fd < FD_SETSIZE by design (LWIP_SOCKET_OFFSET = FD_SETSIZE - CONFIG_LWIP_MAX_SOCKETS)
// - Other platforms: register_socket_fd() validates fd < FD_SETSIZE
// fd bounds are validated in register_socket_fd()
for (int fd : this->socket_fds_) {
FD_SET(fd, &this->base_read_fds_);
}
@@ -641,7 +682,7 @@ void Application::yield_with_select_(uint32_t delay_ms) {
tv.tv_usec = (delay_ms - tv.tv_sec * 1000) * 1000;
// Call select with timeout
#if defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || (defined(USE_ESP32) && defined(USE_SOCKET_IMPL_BSD_SOCKETS))
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
int ret = lwip_select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv);
#else
int ret = ::select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv);
@@ -651,19 +692,18 @@ void Application::yield_with_select_(uint32_t delay_ms) {
// ret < 0: error (except EINTR which is normal)
// ret > 0: socket(s) have data ready - normal and expected
// ret == 0: timeout occurred - normal and expected
if (ret < 0 && errno != EINTR) {
// Actual error - log and fall back to delay
ESP_LOGW(TAG, "select() failed with errno %d", errno);
delay(delay_ms);
if (ret >= 0 || errno == EINTR) [[likely]] {
// Yield if zero timeout since select(0) only polls without yielding
if (delay_ms == 0) [[unlikely]] {
yield();
}
return;
}
// When delay_ms is 0, we need to yield since select(0) doesn't yield
if (delay_ms == 0) {
yield();
}
} else {
// No sockets registered, use regular delay
delay(delay_ms);
// select() error - log and fall through to delay()
ESP_LOGW(TAG, "select() failed with errno %d", errno);
}
// No sockets registered or select() failed - use regular delay
delay(delay_ms);
#elif defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)
// No select support but can wake on socket activity via esp_schedule()
socket::socket_delay(delay_ms);
@@ -676,6 +716,14 @@ void Application::yield_with_select_(uint32_t delay_ms) {
Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
#ifdef USE_ESP32
void Application::wake_loop_threadsafe() {
// Direct FreeRTOS task notification — <1 us, task context only (NOT ISR-safe)
esphome_lwip_wake_main_loop();
}
#else // !USE_ESP32
void Application::setup_wake_loop_threadsafe_() {
// Create UDP socket for wake notifications
this->wake_socket_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
@@ -742,6 +790,8 @@ void Application::wake_loop_threadsafe() {
lwip_send(this->wake_socket_fd_, &dummy, 1, 0);
}
}
#endif // USE_ESP32
#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
void Application::get_build_time_string(std::span<char, BUILD_TIME_STR_SIZE> buffer) {
+26 -21
View File
@@ -24,10 +24,14 @@
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT
#ifdef USE_ESP32
#include "esphome/core/lwip_fast_select.h"
#else
#include <sys/select.h>
#ifdef USE_WAKE_LOOP_THREADSAFE
#include <lwip/sockets.h>
#endif
#endif
#endif // USE_SOCKET_SELECT_SUPPORT
#ifdef USE_BINARY_SENSOR
@@ -491,15 +495,12 @@ class Application {
/// @return true if registration was successful, false if fd exceeds limits
bool register_socket_fd(int fd);
void unregister_socket_fd(int fd);
/// Check if there's data available on a socket without blocking
/// This function is thread-safe for reading, but should be called after select() has run
/// The read_fds_ is only modified by select() in the main loop
bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); }
#ifdef USE_WAKE_LOOP_THREADSAFE
/// Wake the main event loop from a FreeRTOS task
/// Thread-safe, can be called from task context to immediately wake select()
/// IMPORTANT: NOT safe to call from ISR context (socket operations not ISR-safe)
/// Wake the main event loop from another FreeRTOS task.
/// Thread-safe, but must only be called from task context (NOT ISR-safe).
/// On ESP32: uses xTaskNotifyGive (<1 us)
/// On other platforms: uses UDP loopback socket
void wake_loop_threadsafe();
#endif
#endif
@@ -510,10 +511,14 @@ class Application {
#ifdef USE_SOCKET_SELECT_SUPPORT
/// Fast path for Socket::ready() via friendship - skips negative fd check.
/// Safe because: fd was validated in register_socket_fd() at registration time,
/// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded).
/// FD_ISSET may include its own upper bounds check depending on platform.
/// Main loop only — on ESP32, reads rcvevent via lwip_socket_dbg_get_socket()
/// which has no refcount; safe only because the main loop owns socket lifetime
/// (creates, reads, and closes sockets on the same thread).
#ifdef USE_ESP32
bool is_socket_ready_(int fd) const { return esphome_lwip_socket_has_data(fd); }
#else
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
#endif
#endif
void register_component_(Component *comp);
@@ -541,7 +546,7 @@ class Application {
/// Perform a delay while also monitoring socket file descriptors for readiness
void yield_with_select_(uint32_t delay_ms);
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
void setup_wake_loop_threadsafe_(); // Create wake notification socket
inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined)
#endif
@@ -571,7 +576,7 @@ class Application {
FixedVector<Component *> looping_components_{};
#ifdef USE_SOCKET_SELECT_SUPPORT
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
#ifdef USE_WAKE_LOOP_THREADSAFE
#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks
#endif
#endif
@@ -584,7 +589,7 @@ class Application {
uint32_t last_loop_{0};
uint32_t loop_component_start_time_{0};
#ifdef USE_SOCKET_SELECT_SUPPORT
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32)
int max_fd_{-1}; // Highest file descriptor number for select()
#endif
@@ -600,14 +605,14 @@ class Application {
bool in_loop_{false};
volatile bool has_pending_enable_loop_requests_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32)
bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT
// Variable-sized members
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32)
// Variable-sized members (not needed on ESP32 — is_socket_ready_ reads rcvevent directly)
fd_set read_fds_{}; // Working fd_set: populated by select()
fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes
fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_
#endif
// StaticVectors (largest members - contain actual array data inline)
@@ -694,7 +699,7 @@ class Application {
/// Global storage of Application pointer - only one Application can exist.
extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
// Inline implementations for hot-path functions
// drain_wake_notifications_() is called on every loop iteration
@@ -704,8 +709,8 @@ static constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16;
inline void Application::drain_wake_notifications_() {
// Called from main loop to drain any pending wake notifications
// Must check is_socket_ready() to avoid blocking on empty socket
if (this->wake_socket_fd_ >= 0 && this->is_socket_ready(this->wake_socket_fd_)) {
// Must check is_socket_ready_() to avoid blocking on empty socket
if (this->wake_socket_fd_ >= 0 && this->is_socket_ready_(this->wake_socket_fd_)) {
char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE];
// Drain all pending notifications with non-blocking reads
// Multiple wake events may have triggered multiple writes, so drain until EWOULDBLOCK
@@ -716,6 +721,6 @@ inline void Application::drain_wake_notifications_() {
}
}
}
#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
} // namespace esphome
+1
View File
@@ -144,6 +144,7 @@
#define USE_API_HOMEASSISTANT_SERVICES
#define USE_API_HOMEASSISTANT_STATES
#define USE_API_NOISE
#define USE_API_VARINT64
#define USE_API_PLAINTEXT
#define USE_API_USER_DEFINED_ACTIONS
#define USE_API_CUSTOM_SERVICES
+216
View File
@@ -0,0 +1,216 @@
// Fast socket monitoring for ESP32 (ESP-IDF LwIP)
// Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications.
//
// This must be a .c file (not .cpp) because:
// 1. lwip/priv/sockets_priv.h conflicts with C++ compilation units that include bootloader headers
// 2. The netconn callback is a C function pointer
//
// defines.h is force-included by the build system (-include flag), providing USE_ESP32 etc.
//
// Thread safety analysis
// ======================
// Three threads interact with this code:
// 1. Main loop task — calls init, has_data, hook
// 2. LwIP TCP/IP task — calls event_callback (reads s_original_callback; writes rcvevent
// via the original callback under SYS_ARCH_PROTECT/UNPROTECT mutex)
// 3. Background tasks — call wake_main_loop
//
// LwIP source references (ESP-IDF v5.5.2, commit 30aaf64524):
// sockets.c: https://github.com/espressif/esp-idf/blob/30aaf64524/components/lwip/lwip/src/api/sockets.c
// - event_callback (static, same for all sockets): L327
// - DEFAULT_SOCKET_EVENTCB = event_callback: L328
// - tryget_socket_unconn_nouse (direct array lookup): L450
// - lwip_socket_dbg_get_socket (thin wrapper): L461
// - All socket types use DEFAULT_SOCKET_EVENTCB: L1741, L1748, L1759
// - event_callback definition: L2538
// - SYS_ARCH_PROTECT before rcvevent switch: L2578
// - sock->rcvevent++ (NETCONN_EVT_RCVPLUS case): L2582
// - SYS_ARCH_UNPROTECT after switch: L2615
// sys.h: https://github.com/espressif/esp-idf/blob/30aaf64524/components/lwip/lwip/src/include/lwip/sys.h
// - SYS_ARCH_PROTECT calls sys_arch_protect(): L495
// - SYS_ARCH_UNPROTECT calls sys_arch_unprotect(): L506
// (ESP-IDF implements sys_arch_protect/unprotect as FreeRTOS mutex lock/unlock)
//
// Socket slot lifetime
// ====================
// This code reads struct lwip_sock fields without SYS_ARCH_PROTECT. The safety
// argument requires that the slot cannot be freed while we read it.
//
// In LwIP, the socket table is a static array and slots are only freed via:
// lwip_close() -> lwip_close_internal() -> free_socket_free_elements() -> free_socket()
// The TCP/IP thread does NOT call free_socket(). On link loss, RST, or timeout
// it frees the TCP PCB and signals the netconn (rcvevent++ to indicate EOF), but
// the netconn and lwip_sock slot remain allocated until the application calls
// lwip_close(). ESPHome removes the fd from the monitored set before calling
// lwip_close().
//
// Therefore lwip_socket_dbg_get_socket(fd) plus a volatile read of rcvevent
// (to prevent compiler reordering or caching) is safe as long as the application
// is single-writer for close. ESPHome guarantees this by design: all socket
// create/read/close happens on the main loop. fd numbers are not reused while
// the slot remains allocated, and the slot remains allocated until lwip_close().
// Any change in LwIP that allows free_socket() to be called outside lwip_close()
// would invalidate this assumption.
//
// LwIP source references for slot lifetime:
// sockets.c (same commit as above):
// - alloc_socket (slot allocation): L419
// - free_socket (slot deallocation): L384
// - free_socket_free_elements (called from lwip_close_internal): L393
// - lwip_close_internal (only caller of free_socket_free_elements): L2355
// - lwip_close (only caller of lwip_close_internal): L2450
//
// Shared state and safety rationale:
//
// s_main_loop_task (TaskHandle_t, 4 bytes):
// Written once by main loop in init(). Read by TCP/IP thread (in callback)
// and background tasks (in wake).
// Safe: write-once-then-read pattern. Socket hooks may run before init(),
// but the NULL check on s_main_loop_task in the callback provides correct
// degraded behavior — notifications are simply skipped until init() completes.
//
// s_original_callback (netconn_callback, 4-byte function pointer):
// Written by main loop in hook_socket() (only when NULL — set once).
// Read by TCP/IP thread in esphome_socket_event_callback().
// Safe: set-once pattern. The first hook_socket() captures the original callback.
// All subsequent hooks see it already set and skip the write. The TCP/IP thread
// only reads this after the callback pointer has been swapped (which happens after
// the write), so it always sees the initialized value.
//
// sock->conn->callback (netconn_callback, 4-byte function pointer):
// Written by main loop in hook_socket(). Never restored — all LwIP sockets share
// the same static event_callback (DEFAULT_SOCKET_EVENTCB), so the wrapper stays permanently.
// Read by TCP/IP thread when invoking the callback.
// Safe: 32-bit aligned pointer writes are atomic on Xtensa and RISC-V (ESP32).
// The TCP/IP thread will see either the old or new pointer atomically — never a
// torn value. Both the wrapper and original callbacks are valid at all times
// (the wrapper itself calls the original), so either value is correct.
//
// sock->rcvevent (s16_t, 2 bytes):
// Written by TCP/IP thread in event_callback under SYS_ARCH_PROTECT.
// Read by main loop in has_data() via volatile cast.
// Safe: SYS_ARCH_UNPROTECT releases a FreeRTOS mutex, which internally
// uses a critical section with memory barrier (rsync on dual-core Xtensa; on
// single-core builds the spinlock is compiled out, but cross-core visibility is
// not an issue). The volatile cast prevents the compiler
// from caching the read. Aligned 16-bit reads are single-instruction loads on
// Xtensa (L16SI) and RISC-V (LH), which cannot produce torn values.
//
// FreeRTOS task notification value:
// Written by TCP/IP thread (xTaskNotifyGive in callback) and background tasks
// (xTaskNotifyGive in wake_main_loop). Read by main loop (ulTaskNotifyTake).
// Safe: FreeRTOS notification APIs are thread-safe by design (use internal
// critical sections). Multiple concurrent xTaskNotifyGive calls are safe —
// the notification count simply increments.
#ifdef USE_ESP32
// LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc.
#include <lwip/api.h>
#include <lwip/priv/sockets_priv.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "esphome/core/lwip_fast_select.h"
#include <stddef.h>
// Compile-time verification of thread safety assumptions.
// On ESP32 (Xtensa/RISC-V), naturally-aligned reads/writes up to 32 bits are atomic.
// These asserts ensure our cross-thread shared state meets those requirements.
// Pointer types must fit in a single 32-bit store (atomic write)
_Static_assert(sizeof(TaskHandle_t) <= 4, "TaskHandle_t must be <= 4 bytes for atomic access");
_Static_assert(sizeof(netconn_callback) <= 4, "netconn_callback must be <= 4 bytes for atomic access");
// rcvevent must fit in a single atomic read
_Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) <= 4, "rcvevent must be <= 4 bytes for atomic access");
// Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V.
// Misaligned access would not be atomic even if the size is <= 4 bytes.
_Static_assert(offsetof(struct netconn, callback) % sizeof(netconn_callback) == 0,
"netconn.callback must be naturally aligned for atomic access");
_Static_assert(offsetof(struct lwip_sock, rcvevent) % sizeof(((struct lwip_sock *) 0)->rcvevent) == 0,
"lwip_sock.rcvevent must be naturally aligned for atomic access");
// Task handle for the main loop — written once in init(), read from TCP/IP and background tasks.
static TaskHandle_t s_main_loop_task = NULL;
// Saved original event_callback pointer — written once in first hook_socket(), read from TCP/IP task.
static netconn_callback s_original_callback = NULL;
// Wrapper callback: calls original event_callback + notifies main loop task.
// Called from LwIP's TCP/IP thread when socket events occur (task context, not ISR).
static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt evt, u16_t len) {
// Call original LwIP event_callback first — updates rcvevent/sendevent/errevent,
// signals any select() waiters. This preserves all LwIP behavior.
// s_original_callback is always valid here: hook_socket() sets it before swapping
// the callback pointer, so this wrapper cannot run until it's initialized.
s_original_callback(conn, evt, len);
// Wake the main loop task if sleeping in ulTaskNotifyTake().
// Only notify on receive events to avoid spurious wakeups from send-ready events.
// NETCONN_EVT_ERROR is deliberately omitted: LwIP signals errors via RCVPLUS
// (rcvevent++ with a NULL pbuf or error in recvmbox), so error conditions
// already wake the main loop through the RCVPLUS path.
if (evt == NETCONN_EVT_RCVPLUS) {
TaskHandle_t task = s_main_loop_task;
if (task != NULL) {
xTaskNotifyGive(task);
}
}
}
void esphome_lwip_fast_select_init(void) { s_main_loop_task = xTaskGetCurrentTaskHandle(); }
// lwip_socket_dbg_get_socket() is a thin wrapper around the static
// tryget_socket_unconn_nouse() — a direct array lookup without the refcount
// that get_socket()/done_socket() uses. This is safe because:
// 1. The only path to free_socket() is lwip_close(), called exclusively from the main loop
// 2. The TCP/IP thread never frees socket slots (see "Socket slot lifetime" above)
// 3. Both has_data() reads and lwip_close() run on the main loop — no concurrent free
// If lwip_socket_dbg_get_socket() were ever removed, we could fall back to lwip_select().
// Returns the sock only if both the sock and its netconn are valid, NULL otherwise.
static inline struct lwip_sock *get_sock(int fd) {
struct lwip_sock *sock = lwip_socket_dbg_get_socket(fd);
if (sock == NULL || sock->conn == NULL)
return NULL;
return sock;
}
bool esphome_lwip_socket_has_data(int fd) {
struct lwip_sock *sock = get_sock(fd);
if (sock == NULL)
return false;
// volatile prevents the compiler from caching/reordering this cross-thread read.
// The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a
// FreeRTOS mutex with a memory barrier (rsync on Xtensa), ensuring the value is
// visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH) on
// Xtensa/RISC-V and cannot produce torn values.
return *(volatile s16_t *) &sock->rcvevent > 0;
}
void esphome_lwip_hook_socket(int fd) {
struct lwip_sock *sock = get_sock(fd);
if (sock == NULL)
return;
// Save original callback once — all LwIP sockets share the same static event_callback
// (DEFAULT_SOCKET_EVENTCB in sockets.c, used for SOCK_RAW, SOCK_DGRAM, and SOCK_STREAM).
if (s_original_callback == NULL) {
s_original_callback = sock->conn->callback;
}
// Replace with our wrapper. Atomic on ESP32 (32-bit aligned pointer write).
// TCP/IP thread sees either old or new pointer — both are valid.
sock->conn->callback = esphome_socket_event_callback;
}
// Wake the main loop from another FreeRTOS task. NOT ISR-safe.
void esphome_lwip_wake_main_loop(void) {
TaskHandle_t task = s_main_loop_task;
if (task != NULL) {
xTaskNotifyGive(task);
}
}
#endif // USE_ESP32
+33
View File
@@ -0,0 +1,33 @@
#pragma once
// Fast socket monitoring for ESP32 (ESP-IDF LwIP)
// Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications.
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/// Initialize fast select — must be called from the main loop task during setup().
/// Saves the current task handle for xTaskNotifyGive() wake notifications.
void esphome_lwip_fast_select_init(void);
/// Check if a LwIP socket has data ready via direct rcvevent read (~215 ns per socket).
/// Uses lwip_socket_dbg_get_socket() — a direct array lookup without the refcount that
/// get_socket()/done_socket() uses. Safe because the caller owns the socket lifetime:
/// both has_data reads and socket close/unregister happen on the main loop thread.
bool esphome_lwip_socket_has_data(int fd);
/// Hook a socket's netconn callback to notify the main loop task on receive events.
/// Wraps the original event_callback with one that also calls xTaskNotifyGive().
/// Must be called from the main loop after socket creation.
void esphome_lwip_hook_socket(int fd);
/// Wake the main loop task from another FreeRTOS task — costs <1 us.
/// NOT ISR-safe — must only be called from task context.
void esphome_lwip_wake_main_loop(void);
#ifdef __cplusplus
}
#endif
+21 -16
View File
@@ -33,6 +33,11 @@ static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max()
// max delay to start an interval sequence
static constexpr uint32_t MAX_INTERVAL_DELAY = 5000;
// Prevent inlining of SchedulerItem deletion. On BK7231N (Thumb-1), GCC inlines
// ~unique_ptr<SchedulerItem> (~30 bytes each) at every destruction site. Defining
// the deleter in the .cpp file ensures a single copy of the destructor + operator delete.
void Scheduler::SchedulerItemDeleter::operator()(SchedulerItem *ptr) const noexcept { delete ptr; }
#if defined(ESPHOME_LOG_HAS_VERBOSE) || defined(ESPHOME_DEBUG_SCHEDULER)
// Helper struct for formatting scheduler item names consistently in logs
// Uses a stack buffer to avoid heap allocation
@@ -135,7 +140,7 @@ bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_t
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type,
const char *static_name, uint32_t hash_or_id, uint32_t delay,
std::function<void()> func, bool is_retry, bool skip_cancel) {
std::function<void()> &&func, bool is_retry, bool skip_cancel) {
if (delay == SCHEDULER_DONT_RUN) {
// Still need to cancel existing timer if we have a name/id
if (!skip_cancel) {
@@ -216,17 +221,18 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
target->push_back(std::move(item));
}
void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func) {
void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout,
std::function<void()> &&func) {
this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::STATIC_STRING, name, 0, timeout,
std::move(func));
}
void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout,
std::function<void()> func) {
std::function<void()> &&func) {
this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::HASHED_STRING, nullptr, fnv1a_hash(name),
timeout, std::move(func));
}
void HOT Scheduler::set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function<void()> func) {
void HOT Scheduler::set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function<void()> &&func) {
this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID, nullptr, id, timeout,
std::move(func));
}
@@ -240,17 +246,17 @@ bool HOT Scheduler::cancel_timeout(Component *component, uint32_t id) {
return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::TIMEOUT);
}
void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval,
std::function<void()> func) {
std::function<void()> &&func) {
this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::HASHED_STRING, nullptr, fnv1a_hash(name),
interval, std::move(func));
}
void HOT Scheduler::set_interval(Component *component, const char *name, uint32_t interval,
std::function<void()> func) {
std::function<void()> &&func) {
this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::STATIC_STRING, name, 0, interval,
std::move(func));
}
void HOT Scheduler::set_interval(Component *component, uint32_t id, uint32_t interval, std::function<void()> func) {
void HOT Scheduler::set_interval(Component *component, uint32_t id, uint32_t interval, std::function<void()> &&func) {
this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID, nullptr, id, interval,
std::move(func));
}
@@ -467,7 +473,7 @@ void HOT Scheduler::call(uint32_t now) {
if (now_64 - last_print > 2000) {
last_print = now_64;
std::vector<std::unique_ptr<SchedulerItem>> old_items;
std::vector<SchedulerItemPtr> old_items;
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed);
const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed);
@@ -480,7 +486,7 @@ void HOT Scheduler::call(uint32_t now) {
// Cleanup before debug output
this->cleanup_();
while (!this->items_.empty()) {
std::unique_ptr<SchedulerItem> item;
SchedulerItemPtr item;
{
LockGuard guard{this->lock_};
item = this->pop_raw_locked_();
@@ -641,7 +647,7 @@ size_t HOT Scheduler::cleanup_() {
}
return this->items_.size();
}
std::unique_ptr<Scheduler::SchedulerItem> HOT Scheduler::pop_raw_locked_() {
Scheduler::SchedulerItemPtr HOT Scheduler::pop_raw_locked_() {
std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
// Move the item out before popping - this is the item that was at the front of the heap
@@ -864,8 +870,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
#endif
}
bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
const std::unique_ptr<SchedulerItem> &b) {
bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) {
// High bits are almost always equal (change only on 32-bit rollover ~49 days)
// Optimize for common case: check low bits first when high bits are equal
return (a->next_execution_high_ == b->next_execution_high_) ? (a->next_execution_low_ > b->next_execution_low_)
@@ -876,7 +881,7 @@ bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
// This protects scheduler_item_pool_ from concurrent access by other threads
// that may be acquiring items from the pool in set_timer_common_().
void Scheduler::recycle_item_main_loop_(std::unique_ptr<SchedulerItem> item) {
void Scheduler::recycle_item_main_loop_(SchedulerItemPtr item) {
if (!item)
return;
@@ -919,8 +924,8 @@ void Scheduler::debug_log_timer_(const SchedulerItem *item, NameType name_type,
// Helper to get or create a scheduler item from the pool
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
std::unique_ptr<Scheduler::SchedulerItem> Scheduler::get_item_from_pool_locked_() {
std::unique_ptr<SchedulerItem> item;
Scheduler::SchedulerItemPtr Scheduler::get_item_from_pool_locked_() {
SchedulerItemPtr item;
if (!this->scheduler_item_pool_.empty()) {
item = std::move(this->scheduler_item_pool_.back());
this->scheduler_item_pool_.pop_back();
@@ -928,7 +933,7 @@ std::unique_ptr<Scheduler::SchedulerItem> Scheduler::get_item_from_pool_locked_(
ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size());
#endif
} else {
item = make_unique<SchedulerItem>();
item = SchedulerItemPtr(new SchedulerItem());
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Allocated new item (pool empty)");
#endif
+40 -26
View File
@@ -33,7 +33,7 @@ class Scheduler {
// std::string overload - deprecated, use const char* or uint32_t instead
// Remove before 2026.7.0
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function<void()> func);
void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function<void()> &&func);
/** Set a timeout with a const char* name.
*
@@ -43,11 +43,11 @@ class Scheduler {
* - A static const char* variable
* - A pointer with lifetime >= the scheduled task
*/
void set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func);
void set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> &&func);
/// Set a timeout with a numeric ID (zero heap allocation)
void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function<void()> func);
void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function<void()> &&func);
/// Set a timeout with an internal scheduler ID (separate namespace from component NUMERIC_ID)
void set_timeout(Component *component, InternalSchedulerID id, uint32_t timeout, std::function<void()> func) {
void set_timeout(Component *component, InternalSchedulerID id, uint32_t timeout, std::function<void()> &&func) {
this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID_INTERNAL, nullptr,
static_cast<uint32_t>(id), timeout, std::move(func));
}
@@ -62,7 +62,7 @@ class Scheduler {
}
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> func);
void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> &&func);
/** Set an interval with a const char* name.
*
@@ -72,11 +72,11 @@ class Scheduler {
* - A static const char* variable
* - A pointer with lifetime >= the scheduled task
*/
void set_interval(Component *component, const char *name, uint32_t interval, std::function<void()> func);
void set_interval(Component *component, const char *name, uint32_t interval, std::function<void()> &&func);
/// Set an interval with a numeric ID (zero heap allocation)
void set_interval(Component *component, uint32_t id, uint32_t interval, std::function<void()> func);
void set_interval(Component *component, uint32_t id, uint32_t interval, std::function<void()> &&func);
/// Set an interval with an internal scheduler ID (separate namespace from component NUMERIC_ID)
void set_interval(Component *component, InternalSchedulerID id, uint32_t interval, std::function<void()> func) {
void set_interval(Component *component, InternalSchedulerID id, uint32_t interval, std::function<void()> &&func) {
this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID_INTERNAL, nullptr,
static_cast<uint32_t>(id), interval, std::move(func));
}
@@ -142,6 +142,19 @@ class Scheduler {
};
protected:
struct SchedulerItem;
// Custom deleter for SchedulerItem unique_ptr that prevents the compiler from
// inlining the destructor at every destruction site. On BK7231N (Thumb-1), GCC
// inlines ~unique_ptr<SchedulerItem> (~30 bytes: null check + ~std::function +
// operator delete) at every destruction site, while ESP32/ESP8266/RTL8720CF outline
// it into a single helper. This noinline deleter ensures only one copy exists.
// operator() is defined in scheduler.cpp to prevent inlining.
struct SchedulerItemDeleter {
void operator()(SchedulerItem *ptr) const noexcept;
};
using SchedulerItemPtr = std::unique_ptr<SchedulerItem, SchedulerItemDeleter>;
struct SchedulerItem {
// Ordered by size to minimize padding
Component *component;
@@ -233,7 +246,7 @@ class Scheduler {
name_type_ = type;
}
static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
static bool cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b);
// Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility.
// The upper 16 bits of the 64-bit value are always zero, which is fine since
@@ -255,7 +268,7 @@ class Scheduler {
// Common implementation for both timeout and interval
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
void set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, const char *static_name,
uint32_t hash_or_id, uint32_t delay, std::function<void()> func, bool is_retry = false,
uint32_t hash_or_id, uint32_t delay, std::function<void()> &&func, bool is_retry = false,
bool skip_cancel = false);
// Common implementation for retry - Remove before 2026.8.0
@@ -276,10 +289,10 @@ class Scheduler {
size_t cleanup_();
// Remove and return the front item from the heap
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
std::unique_ptr<SchedulerItem> pop_raw_locked_();
SchedulerItemPtr pop_raw_locked_();
// Get or create a scheduler item from the pool
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
std::unique_ptr<SchedulerItem> get_item_from_pool_locked_();
SchedulerItemPtr get_item_from_pool_locked_();
private:
// Helper to cancel items - must be called with lock held
@@ -303,9 +316,9 @@ class Scheduler {
// Helper function to check if item matches criteria for cancellation
// name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id
// IMPORTANT: Must be called with scheduler lock held
inline bool HOT matches_item_locked_(const std::unique_ptr<SchedulerItem> &item, Component *component,
NameType name_type, const char *static_name, uint32_t hash_or_id,
SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const {
inline bool HOT matches_item_locked_(const SchedulerItemPtr &item, Component *component, NameType name_type,
const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type,
bool match_retry, bool skip_removed = true) const {
// THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded
// platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries.
// PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_()), but this check
@@ -340,7 +353,7 @@ class Scheduler {
// IMPORTANT: Only call from main loop context! Recycling clears the callback,
// so calling from another thread while the callback is executing causes use-after-free.
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
void recycle_item_main_loop_(std::unique_ptr<SchedulerItem> item);
void recycle_item_main_loop_(SchedulerItemPtr item);
// Helper to perform full cleanup when too many items are cancelled
void full_cleanup_removed_items_();
@@ -396,7 +409,7 @@ class Scheduler {
// Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total),
// recycle each item after re-acquiring the lock for the next iteration (N+1 total).
// The lock is held across: recycle → loop condition → move-out, then released for execution.
std::unique_ptr<SchedulerItem> item;
SchedulerItemPtr item;
this->lock_.lock();
while (this->defer_queue_front_ < defer_queue_end) {
@@ -496,9 +509,10 @@ class Scheduler {
// name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id
// Returns the number of items marked for removal
// IMPORTANT: Must be called with scheduler lock held
size_t mark_matching_items_removed_locked_(std::vector<std::unique_ptr<SchedulerItem>> &container,
Component *component, NameType name_type, const char *static_name,
uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) {
__attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector<SchedulerItemPtr> &container,
Component *component, NameType name_type,
const char *static_name, uint32_t hash_or_id,
SchedulerItem::Type type, bool match_retry) {
size_t count = 0;
for (auto &item : container) {
// Skip nullptr items (can happen in defer_queue_ when items are being processed)
@@ -514,15 +528,15 @@ class Scheduler {
}
Mutex lock_;
std::vector<std::unique_ptr<SchedulerItem>> items_;
std::vector<std::unique_ptr<SchedulerItem>> to_add_;
std::vector<SchedulerItemPtr> items_;
std::vector<SchedulerItemPtr> to_add_;
#ifndef ESPHOME_THREAD_SINGLE
// Single-core platforms don't need the defer queue and save ~32 bytes of RAM
// Using std::vector instead of std::deque avoids 512-byte chunked allocations
// Index tracking avoids O(n) erase() calls when draining the queue each loop
std::vector<std::unique_ptr<SchedulerItem>> defer_queue_; // FIFO queue for defer() calls
size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items)
#endif /* ESPHOME_THREAD_SINGLE */
std::vector<SchedulerItemPtr> defer_queue_; // FIFO queue for defer() calls
size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items)
#endif /* ESPHOME_THREAD_SINGLE */
uint32_t to_remove_{0};
// Memory pool for recycling SchedulerItem objects to reduce heap churn.
@@ -533,7 +547,7 @@ class Scheduler {
// - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation
// can stall the entire system, causing timing issues and dropped events for any components that need
// to synchronize between tasks (see https://github.com/esphome/backlog/issues/52)
std::vector<std::unique_ptr<SchedulerItem>> scheduler_item_pool_;
std::vector<SchedulerItemPtr> scheduler_item_pool_;
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
/*
+1 -1
View File
@@ -8,7 +8,7 @@ namespace esphome {
uint8_t days_in_month(uint8_t month, uint16_t year) {
static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (month == 2 && (year % 4 == 0))
if (month == 2 && (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0))
return 29;
return DAYS_IN_MONTH[month];
}
+3 -3
View File
@@ -70,14 +70,14 @@ struct ESPTime {
/// @copydoc strftime(const std::string &format)
std::string strftime(const char *format);
/// Check if this ESPTime is valid (all fields in range and year is greater than 2018)
/// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019)
bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); }
/// Check if all time fields of this ESPTime are in range.
bool fields_in_range() const {
return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 &&
this->day_of_week < 8 && this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 &&
this->day_of_year < 367 && this->month > 0 && this->month < 13;
this->day_of_week < 8 && this->day_of_year > 0 && this->day_of_year < 367 && this->month > 0 &&
this->month < 13 && this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year);
}
/** Convert a string to ESPTime struct as specified by the format argument.
+59 -6
View File
@@ -1913,6 +1913,37 @@ def build_type_usage_map(
)
def get_varint64_ifdef(
file_desc: descriptor.FileDescriptorProto,
message_ifdef_map: dict[str, str | None],
) -> tuple[bool, str | None]:
"""Check if 64-bit varint fields exist and get their common ifdef guard.
Returns:
(has_varint64, ifdef_guard) - has_varint64 is True if any fields exist,
ifdef_guard is the common guard or None if unconditional.
"""
varint64_types = {
FieldDescriptorProto.TYPE_INT64,
FieldDescriptorProto.TYPE_UINT64,
FieldDescriptorProto.TYPE_SINT64,
}
ifdefs: set[str | None] = {
message_ifdef_map.get(msg.name)
for msg in file_desc.message_type
if not msg.options.deprecated
for field in msg.field
if not field.options.deprecated and field.type in varint64_types
}
if not ifdefs:
return False, None
if None in ifdefs:
# At least one 64-bit varint field is unconditional, so the guard must be unconditional.
return True, None
ifdefs.discard(None)
return True, ifdefs.pop() if len(ifdefs) == 1 else None
def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]:
"""Builds the enum type.
@@ -2567,11 +2598,38 @@ def main() -> None:
file = d.file[0]
# Build dynamic ifdef mappings early so we can emit USE_API_VARINT64 before includes
enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = (
build_type_usage_map(file)
)
# Find the ifdef guard for 64-bit varint fields (int64/uint64/sint64).
# Generated into api_pb2_defines.h so proto.h can include it, ensuring
# consistent ProtoVarInt layout across all translation units.
has_varint64, varint64_guard = get_varint64_ifdef(file, message_ifdef_map)
# Generate api_pb2_defines.h — included by proto.h to ensure all translation
# units see USE_API_VARINT64 consistently (avoids ODR violations in ProtoVarInt).
defines_content = FILE_HEADER
defines_content += "#pragma once\n\n"
defines_content += '#include "esphome/core/defines.h"\n'
if has_varint64:
lines = [
"#ifndef USE_API_VARINT64",
"#define USE_API_VARINT64",
"#endif",
]
defines_content += "\n".join(wrap_with_ifdef(lines, varint64_guard))
defines_content += "\n"
defines_content += "\nnamespace esphome::api {} // namespace esphome::api\n"
with open(root / "api_pb2_defines.h", "w", encoding="utf-8") as f:
f.write(defines_content)
content = FILE_HEADER
content += """\
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/string_ref.h"
#include "proto.h"
@@ -2702,11 +2760,6 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
content += "namespace enums {\n\n"
# Build dynamic ifdef mappings for both enums and messages
enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = (
build_type_usage_map(file)
)
# Simple grouping of enums by ifdef
current_ifdef = None
+26
View File
@@ -71,6 +71,32 @@ esphome:
- light.control:
id: test_monochromatic_light
state: on
# Test static effect name resolution at codegen time
- light.turn_on:
id: test_monochromatic_light
effect: Strobe
- light.turn_on:
id: test_monochromatic_light
effect: none
# Test resolving a different effect on the same light
- light.control:
id: test_monochromatic_light
effect: My Flicker
# Test effect: None (capitalized)
- light.control:
id: test_monochromatic_light
effect: None
# Test effect lambda with no args (on_boot has empty Ts...)
- light.turn_on:
id: test_monochromatic_light
effect: !lambda 'return "Strobe";'
# Test effect lambda with non-empty args (repeat passes uint32_t iteration)
- repeat:
count: 3
then:
- light.turn_on:
id: test_monochromatic_light
effect: !lambda 'return iteration > 1 ? "Strobe" : "none";'
- light.dim_relative:
id: test_monochromatic_light
relative_brightness: 5%
@@ -1,9 +1,21 @@
from esphome.components import socket
from esphome.const import (
KEY_CORE,
KEY_TARGET_PLATFORM,
PLATFORM_ESP32,
PLATFORM_ESP8266,
)
from esphome.core import CORE
def _setup_platform(platform=PLATFORM_ESP8266) -> None:
"""Set up CORE.data with a platform for testing."""
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
def test_require_wake_loop_threadsafe__first_call() -> None:
"""Test that first call sets up define and consumes socket."""
_setup_platform()
CORE.config = {"wifi": True}
socket.require_wake_loop_threadsafe()
@@ -32,6 +44,7 @@ def test_require_wake_loop_threadsafe__idempotent() -> None:
def test_require_wake_loop_threadsafe__multiple_calls() -> None:
"""Test that multiple calls only set up once."""
_setup_platform()
# Call three times
CORE.config = {"openthread": True}
socket.require_wake_loop_threadsafe()
@@ -75,3 +88,29 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
assert "socket.wake_loop_threadsafe" not in udp_consumers
assert udp_consumers == initial_udp
def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None:
"""Test that ESP32 uses task notifications instead of UDP socket."""
_setup_platform(PLATFORM_ESP32)
CORE.config = {"wifi": True}
socket.require_wake_loop_threadsafe()
# Verify the define was added
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
# Verify no UDP socket was consumed (ESP32 uses FreeRTOS task notifications)
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
assert "socket.wake_loop_threadsafe" not in udp_consumers
def test_require_wake_loop_threadsafe__non_esp32_consumes_udp_socket() -> None:
"""Test that non-ESP32 platforms consume a UDP socket for wake notifications."""
_setup_platform(PLATFORM_ESP8266)
CORE.config = {"wifi": True}
socket.require_wake_loop_threadsafe()
# Verify UDP socket was consumed
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
assert udp_consumers.get("socket.wake_loop_threadsafe") == 1
@@ -28,6 +28,11 @@ sensor:
id: source_sensor_4
accuracy_decimals: 1
- platform: template
name: "Source Sensor 5"
id: source_sensor_5
accuracy_decimals: 1
- platform: copy
source_id: source_sensor_1
name: "Filter Min"
@@ -69,6 +74,13 @@ sensor:
filters:
- delta: 0
- platform: copy
source_id: source_sensor_5
name: "Filter Percentage"
id: filter_percentage
filters:
- delta: 50%
script:
- id: test_filter_min
then:
@@ -154,6 +166,28 @@ script:
id: source_sensor_4
state: 2.0
- id: test_filter_percentage
then:
- sensor.template.publish:
id: source_sensor_5
state: 100.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_5
state: 120.0 # Filtered out (delta=20, need >50)
- delay: 20ms
- sensor.template.publish:
id: source_sensor_5
state: 160.0 # Passes (delta=60 > 50% of 100=50)
- delay: 20ms
- sensor.template.publish:
id: source_sensor_5
state: 200.0 # Filtered out (delta=40, need >50% of 160=80)
- delay: 20ms
- sensor.template.publish:
id: source_sensor_5
state: 250.0 # Passes (delta=90 > 80)
button:
- platform: template
name: "Test Filter Min"
@@ -178,3 +212,9 @@ button:
id: btn_filter_zero_delta
on_press:
- script.execute: test_filter_zero_delta
- platform: template
name: "Test Filter Percentage"
id: btn_filter_percentage
on_press:
- script.execute: test_filter_percentage
@@ -0,0 +1,47 @@
esphome:
name: varint-5byte-test
# Define areas and devices - device_ids will be FNV hashes > 2^28,
# requiring 5-byte varint encoding that exercises the 32-bit parse boundary.
areas:
- id: test_area
name: Test Area
devices:
- id: sub_device_one
name: Sub Device One
area_id: test_area
- id: sub_device_two
name: Sub Device Two
area_id: test_area
host:
api:
logger:
# Switches on sub-devices so we can send commands with large device_id varints
switch:
- platform: template
name: Device Switch
device_id: sub_device_one
id: device_switch_one
optimistic: true
turn_on_action:
- logger.log: "Switch one on"
turn_off_action:
- logger.log: "Switch one off"
- platform: template
name: Device Switch
device_id: sub_device_two
id: device_switch_two
optimistic: true
turn_on_action:
- logger.log: "Switch two on"
turn_off_action:
- logger.log: "Switch two off"
sensor:
- platform: template
name: Device Sensor
device_id: sub_device_one
lambda: return 42.0;
update_interval: 0.1s
+26 -1
View File
@@ -24,12 +24,14 @@ async def test_sensor_filters_delta(
"filter_max": [],
"filter_baseline_max": [],
"filter_zero_delta": [],
"filter_percentage": [],
}
filter_min_done = loop.create_future()
filter_max_done = loop.create_future()
filter_baseline_max_done = loop.create_future()
filter_zero_delta_done = loop.create_future()
filter_percentage_done = loop.create_future()
def on_state(state: EntityState) -> None:
if not isinstance(state, SensorState) or state.missing_state:
@@ -66,6 +68,12 @@ async def test_sensor_filters_delta(
and not filter_zero_delta_done.done()
):
filter_zero_delta_done.set_result(True)
elif (
sensor_name == "filter_percentage"
and len(sensor_values[sensor_name]) == 3
and not filter_percentage_done.done()
):
filter_percentage_done.set_result(True)
async with (
run_compiled(yaml_config),
@@ -80,6 +88,7 @@ async def test_sensor_filters_delta(
"filter_max": "Filter Max",
"filter_baseline_max": "Filter Baseline Max",
"filter_zero_delta": "Filter Zero Delta",
"filter_percentage": "Filter Percentage",
},
)
@@ -98,13 +107,14 @@ async def test_sensor_filters_delta(
"Test Filter Max": "filter_max",
"Test Filter Baseline Max": "filter_baseline_max",
"Test Filter Zero Delta": "filter_zero_delta",
"Test Filter Percentage": "filter_percentage",
}
buttons = {}
for entity in entities:
if isinstance(entity, ButtonInfo) and entity.name in button_name_map:
buttons[button_name_map[entity.name]] = entity.key
assert len(buttons) == 4, f"Expected 3 buttons, found {len(buttons)}"
assert len(buttons) == 5, f"Expected 5 buttons, found {len(buttons)}"
# Test 1: Min
sensor_values["filter_min"].clear()
@@ -161,3 +171,18 @@ async def test_sensor_filters_delta(
assert sensor_values["filter_zero_delta"] == pytest.approx(expected), (
f"Test 4 failed: expected {expected}, got {sensor_values['filter_zero_delta']}"
)
# Test 5: Percentage (delta: 50%)
sensor_values["filter_percentage"].clear()
client.button_command(buttons["filter_percentage"])
try:
await asyncio.wait_for(filter_percentage_done, timeout=2.0)
except TimeoutError:
pytest.fail(
f"Test 5 timed out. Values: {sensor_values['filter_percentage']}"
)
expected = [100.0, 160.0, 250.0]
assert sensor_values["filter_percentage"] == pytest.approx(expected), (
f"Test 5 failed: expected {expected}, got {sensor_values['filter_percentage']}"
)
@@ -0,0 +1,120 @@
"""Integration test for 5-byte varint parsing of device_id fields.
Device IDs are FNV hashes (uint32) that frequently exceed 2^28 (268435456),
requiring 5 varint bytes. This test verifies that:
1. The firmware correctly decodes 5-byte varint device_id in incoming commands
2. The firmware correctly encodes large device_id values in state responses
3. Switch commands with large device_id reach the correct entity
"""
from __future__ import annotations
import asyncio
from aioesphomeapi import EntityState, SwitchInfo, SwitchState
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_varint_five_byte_device_id(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that device_id values requiring 5-byte varints parse correctly."""
async with run_compiled(yaml_config), api_client_connected() as client:
device_info = await client.device_info()
devices = device_info.devices
assert len(devices) >= 2, f"Expected at least 2 devices, got {len(devices)}"
# Verify at least one device_id exceeds the 4-byte varint boundary (2^28)
large_ids = [d for d in devices if d.device_id >= (1 << 28)]
assert len(large_ids) > 0, (
"Expected at least one device_id >= 2^28 to exercise 5-byte varint path. "
f"Got device_ids: {[d.device_id for d in devices]}"
)
# Get entities
all_entities, _ = await client.list_entities_services()
switch_entities = [e for e in all_entities if isinstance(e, SwitchInfo)]
# Find switches named "Device Switch" — one per sub-device
device_switches = [e for e in switch_entities if e.name == "Device Switch"]
assert len(device_switches) == 2, (
f"Expected 2 'Device Switch' entities, got {len(device_switches)}"
)
# Verify switches have different device_ids matching the sub-devices
switch_device_ids = {s.device_id for s in device_switches}
assert len(switch_device_ids) == 2, "Switches should have different device_ids"
# Subscribe to states and wait for initial states
loop = asyncio.get_running_loop()
states: dict[tuple[int, int], EntityState] = {}
switch_futures: dict[tuple[int, int], asyncio.Future[EntityState]] = {}
initial_done: asyncio.Future[bool] = loop.create_future()
def on_state(state: EntityState) -> None:
key = (state.device_id, state.key)
states[key] = state
if len(states) >= 3 and not initial_done.done():
initial_done.set_result(True)
if initial_done.done() and key in switch_futures:
fut = switch_futures[key]
if not fut.done() and isinstance(state, SwitchState):
fut.set_result(state)
client.subscribe_states(on_state)
try:
await asyncio.wait_for(initial_done, timeout=10.0)
except TimeoutError:
pytest.fail(
f"Timed out waiting for initial states. Got {len(states)} states"
)
# Verify state responses contain correct large device_id values
for device in devices:
device_states = [
s for (did, _), s in states.items() if did == device.device_id
]
assert len(device_states) > 0, (
f"No states received for device '{device.name}' "
f"(device_id={device.device_id})"
)
# Test switch commands with large device_id varints —
# this is the critical path: the client encodes device_id as a varint
# in the SwitchCommandRequest, and the firmware must decode it correctly.
for switch in device_switches:
state_key = (switch.device_id, switch.key)
# Turn on
switch_futures[state_key] = loop.create_future()
client.switch_command(switch.key, True, device_id=switch.device_id)
try:
await asyncio.wait_for(switch_futures[state_key], timeout=2.0)
except TimeoutError:
pytest.fail(
f"Timed out waiting for switch ON state "
f"(device_id={switch.device_id}, key={switch.key}). "
f"This likely means the firmware failed to decode the "
f"5-byte varint device_id in SwitchCommandRequest."
)
assert states[state_key].state is True
# Turn off
switch_futures[state_key] = loop.create_future()
client.switch_command(switch.key, False, device_id=switch.device_id)
try:
await asyncio.wait_for(switch_futures[state_key], timeout=2.0)
except TimeoutError:
pytest.fail(
f"Timed out waiting for switch OFF state "
f"(device_id={switch.device_id}, key={switch.key})"
)
assert states[state_key].state is False