Merge remote-tracking branch 'upstream/dev' into extend_ultra_low_latency_select_libretiny

# Conflicts:
#	esphome/components/socket/__init__.py
#	esphome/core/application.cpp
#	esphome/core/application.h
#	esphome/core/lwip_fast_select.c
#	esphome/core/lwip_fast_select.h
#	tests/components/socket/test_wake_loop_threadsafe.py
This commit is contained in:
J. Nick Koston
2026-02-26 06:27:15 -10:00
88 changed files with 6016 additions and 5479 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);
}
}
+9 -2
View File
@@ -790,8 +790,15 @@ def _detect_variant(value):
engineering_sample = value.get(CONF_ENGINEERING_SAMPLE)
if engineering_sample is None:
_LOGGER.warning(
"No board specified for ESP32-P4. Defaulting to production silicon (rev3). "
"If you have an early engineering sample (pre-rev3), set 'engineering_sample: true'."
"No board specified for ESP32-P4. Defaulting to production silicon (rev3).\n"
"If you have an early engineering sample (pre-rev3), add this to your config:\n"
"\n"
" esp32:\n"
" engineering_sample: true\n"
"\n"
"To check your chip revision, look for 'chip revision: vX.Y' in the boot log.\n"
"Engineering samples will show a revision below v3.0.\n"
"The 'debug:' component also reports the revision (e.g. Revision: 100 = v1.0, 300 = v3.0)."
)
elif engineering_sample:
value[CONF_BOARD] = "esp32-p4-evboard"
@@ -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;
+14 -10
View File
@@ -39,6 +39,7 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_COLOR_ORDER,
CONF_DIMENSIONS,
CONF_DISABLED,
CONF_ENABLE_PIN,
CONF_ID,
CONF_INIT_SEQUENCE,
@@ -88,14 +89,17 @@ COLOR_DEPTHS = {
def model_schema(config):
model = MODELS[config[CONF_MODEL].upper()]
model.defaults[CONF_SWAP_XY] = cv.UNDEFINED
transform = cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
cv.Optional(CONF_SWAP_XY): cv.invalid(
"Axis swapping not supported by DSI displays"
),
}
transform = cv.Any(
cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
cv.Optional(CONF_SWAP_XY): cv.invalid(
"Axis swapping not supported by DSI displays"
),
}
),
cv.one_of(CONF_DISABLED, lower=True),
)
# CUSTOM model will need to provide a custom init sequence
iseqconf = (
@@ -199,9 +203,9 @@ async def to_code(config):
cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH]))
cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH]))
cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH]))
cg.add(var.set_pclk_frequency(int(config[CONF_PCLK_FREQUENCY] / 1e6)))
cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY] / 1.0e6))
cg.add(var.set_lanes(int(config[CONF_LANES])))
cg.add(var.set_lane_bit_rate(int(config[CONF_LANE_BIT_RATE] / 1e6)))
cg.add(var.set_lane_bit_rate(config[CONF_LANE_BIT_RATE] / 1.0e6))
if reset_pin := config.get(CONF_RESET_PIN):
reset = await cg.gpio_pin_expression(reset_pin)
cg.add(var.set_reset_pin(reset))
+2 -2
View File
@@ -374,7 +374,7 @@ void MIPI_DSI::dump_config() {
"\n Swap X/Y: %s"
"\n Rotation: %d degrees"
"\n DSI Lanes: %u"
"\n Lane Bit Rate: %uMbps"
"\n Lane Bit Rate: %.0fMbps"
"\n HSync Pulse Width: %u"
"\n HSync Back Porch: %u"
"\n HSync Front Porch: %u"
@@ -385,7 +385,7 @@ void MIPI_DSI::dump_config() {
"\n Display Pixel Mode: %d bit"
"\n Color Order: %s"
"\n Invert Colors: %s"
"\n Pixel Clock: %dMHz",
"\n Pixel Clock: %.1fMHz",
this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)),
YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_,
this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_,
+5 -5
View File
@@ -47,7 +47,7 @@ class MIPI_DSI : public display::Display {
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
void set_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; }
void set_pclk_frequency(float pclk_frequency) { this->pclk_frequency_ = pclk_frequency; }
int get_width_internal() override { return this->width_; }
int get_height_internal() override { return this->height_; }
void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; }
@@ -58,7 +58,7 @@ class MIPI_DSI : public display::Display {
void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; }
void set_init_sequence(const std::vector<uint8_t> &init_sequence) { this->init_sequence_ = init_sequence; }
void set_model(const char *model) { this->model_ = model; }
void set_lane_bit_rate(uint16_t lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; }
void set_lane_bit_rate(float lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; }
void set_lanes(uint8_t lanes) { this->lanes_ = lanes; }
void set_madctl(uint8_t madctl) { this->madctl_ = madctl; }
@@ -95,9 +95,9 @@ class MIPI_DSI : public display::Display {
uint16_t vsync_front_porch_ = 10;
const char *model_{"Unknown"};
std::vector<uint8_t> init_sequence_{};
uint16_t pclk_frequency_ = 16; // in MHz
uint16_t lane_bit_rate_{1500}; // in Mbps
uint8_t lanes_{2}; // 1, 2, 3 or 4 lanes
float pclk_frequency_ = 16; // in MHz
float lane_bit_rate_{1500}; // in Mbps
uint8_t lanes_{2}; // 1, 2, 3 or 4 lanes
bool invert_colors_{};
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
@@ -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)) {
+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};
+10 -1
View File
@@ -1,7 +1,12 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import CONF_HIDE_TIMESTAMP, ENTITY_CATEGORY_DIAGNOSTIC, ICON_NEW_BOX
from esphome.const import (
CONF_HIDE_HASH,
CONF_HIDE_TIMESTAMP,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_NEW_BOX,
)
version_ns = cg.esphome_ns.namespace("version")
VersionTextSensor = version_ns.class_(
@@ -16,6 +21,9 @@ CONFIG_SCHEMA = (
.extend(
{
cv.GenerateID(): cv.declare_id(VersionTextSensor),
# Hide the config hash suffix and restore the pre-2026.1
# version text format when set to true.
cv.Optional(CONF_HIDE_HASH, default=False): cv.boolean,
cv.Optional(CONF_HIDE_TIMESTAMP, default=False): cv.boolean,
}
)
@@ -26,4 +34,5 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = await text_sensor.new_text_sensor(config)
await cg.register_component(var, config)
cg.add(var.set_hide_hash(config[CONF_HIDE_HASH]))
cg.add(var.set_hide_timestamp(config[CONF_HIDE_TIMESTAMP]))
@@ -1,37 +1,53 @@
#include "version_text_sensor.h"
#include "esphome/core/application.h"
#include "esphome/core/build_info_data.h"
#include "esphome/core/log.h"
#include "esphome/core/version.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/version.h"
namespace esphome::version {
static const char *const TAG = "version.text_sensor";
void VersionTextSensor::setup() {
static const char PREFIX[] PROGMEM = ESPHOME_VERSION " (config hash 0x";
static const char HASH_PREFIX[] PROGMEM = ESPHOME_VERSION " (config hash 0x";
static const char VERSION_PREFIX[] PROGMEM = ESPHOME_VERSION;
static const char BUILT_STR[] PROGMEM = ", built ";
// Buffer size: PREFIX + 8 hex chars + BUILT_STR + BUILD_TIME_STR_SIZE + ")" + null
constexpr size_t buf_size = sizeof(PREFIX) + 8 + sizeof(BUILT_STR) + esphome::Application::BUILD_TIME_STR_SIZE + 2;
// Buffer size: HASH_PREFIX + 8 hex chars + BUILT_STR + BUILD_TIME_STR_SIZE + ")" + null
constexpr size_t buf_size =
sizeof(HASH_PREFIX) + 8 + sizeof(BUILT_STR) + esphome::Application::BUILD_TIME_STR_SIZE + 2;
char version_str[buf_size];
ESPHOME_strncpy_P(version_str, PREFIX, sizeof(version_str));
// hide_hash restores the pre-2026.1 base format by omitting
// the " (config hash 0x...)" suffix entirely.
if (this->hide_hash_) {
ESPHOME_strncpy_P(version_str, VERSION_PREFIX, sizeof(version_str));
} else {
ESPHOME_strncpy_P(version_str, HASH_PREFIX, sizeof(version_str));
size_t len = strlen(version_str);
snprintf(version_str + len, sizeof(version_str) - len, "%08" PRIx32, App.get_config_hash());
size_t len = strlen(version_str);
snprintf(version_str + len, sizeof(version_str) - len, "%08" PRIx32, App.get_config_hash());
}
// Keep hide_timestamp behavior independent from hide_hash so all
// combinations remain available to users.
if (!this->hide_timestamp_) {
size_t len = strlen(version_str);
ESPHOME_strncat_P(version_str, BUILT_STR, sizeof(version_str) - len - 1);
ESPHOME_strncat_P(version_str, ESPHOME_BUILD_TIME_STR, sizeof(version_str) - strlen(version_str) - 1);
}
strncat(version_str, ")", sizeof(version_str) - strlen(version_str) - 1);
// The closing parenthesis is part of the config-hash suffix and must
// only be appended when that suffix is present.
if (!this->hide_hash_) {
strncat(version_str, ")", sizeof(version_str) - strlen(version_str) - 1);
}
version_str[sizeof(version_str) - 1] = '\0';
this->publish_state(version_str);
}
void VersionTextSensor::set_hide_hash(bool hide_hash) { this->hide_hash_ = hide_hash; }
void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; }
void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); }
@@ -7,11 +7,13 @@ namespace esphome::version {
class VersionTextSensor : public text_sensor::TextSensor, public Component {
public:
void set_hide_hash(bool hide_hash);
void set_hide_timestamp(bool hide_timestamp);
void setup() override;
void dump_config() override;
protected:
bool hide_hash_{false};
bool hide_timestamp_{false};
};
@@ -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(
+1
View File
@@ -463,6 +463,7 @@ CONF_HEAT_OVERRUN = "heat_overrun"
CONF_HEATER = "heater"
CONF_HEIGHT = "height"
CONF_HIDDEN = "hidden"
CONF_HIDE_HASH = "hide_hash"
CONF_HIDE_TIMESTAMP = "hide_timestamp"
CONF_HIGH = "high"
CONF_HIGH_VOLTAGE_REFERENCE = "high_voltage_reference"
+2 -2
View File
@@ -144,7 +144,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!");
@@ -539,7 +539,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) && !defined(USE_ESP32)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
// Drain wake notifications first to clear socket for next wake
this->drain_wake_notifications_();
#endif
+2 -2
View File
@@ -699,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) && !defined(USE_ESP32)
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
// Inline implementations for hot-path functions
// drain_wake_notifications_() is called on every loop iteration
@@ -721,6 +721,6 @@ inline void Application::drain_wake_notifications_() {
}
}
}
#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
} // 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
+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
@@ -22,6 +22,23 @@ display:
id: p4_86
model: "WAVESHARE-P4-86-PANEL"
rotation: 180
- platform: mipi_dsi
model: custom
id: custom_id
dimensions:
width: 400
height: 1280
hsync_back_porch: 40
hsync_pulse_width: 30
hsync_front_porch: 40
vsync_back_porch: 20
vsync_pulse_width: 10
vsync_front_porch: 20
pclk_frequency: 48Mhz
lane_bit_rate: 1.2Gbps
rotation: 180
transform: disabled
init_sequence:
i2c:
sda: GPIO7
scl: GPIO8
@@ -123,7 +123,8 @@ def test_code_generation(
in main_cpp
)
assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp
assert "p4_nano->set_lane_bit_rate(1500);" in main_cpp
assert "p4_nano->set_lane_bit_rate(1500.0f);" in main_cpp
assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp
assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp
assert "custom_id->set_rotation(display::DISPLAY_ROTATION_180_DEGREES);" in main_cpp
# assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp
+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%
+14 -1
View File
@@ -1,3 +1,16 @@
text_sensor:
- platform: version
name: "ESPHome Version"
name: "ESPHome Version Full"
- platform: version
name: "ESPHome Version No Timestamp"
hide_timestamp: true
- platform: version
name: "ESPHome Version No Hash"
hide_hash: true
- platform: version
name: "ESPHome Version Shortest"
hide_timestamp: true
hide_hash: true
@@ -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