Merge branch 'dev' into api-proto-max-length

This commit is contained in:
J. Nick Koston
2026-04-06 16:26:01 -10:00
committed by GitHub
22 changed files with 139 additions and 62 deletions
-1
View File
@@ -132,7 +132,6 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
cv.Optional(CONF_PHASE_ANGLE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HARMONIC_POWER): sensor.sensor_schema(
+1 -1
View File
@@ -390,7 +390,7 @@ def validate_multi_click_timing(value):
new_state = v_.get(CONF_STATE, not state)
if new_state == state:
raise cv.Invalid(
f"Timings must have alternating state. Indices {i} and {i + 1} have the same state {state}"
f"Timings must have alternating state. Indices {i - 1} and {i} have the same state {state}"
)
if max_length is not None and max_length < min_length:
raise cv.Invalid(
+3 -3
View File
@@ -373,14 +373,14 @@ def bt_uuid(value):
value = in_value.upper()
if len(value) == len(bt_uuid16_format):
pattern = re.compile("^[A-F|0-9]{4,}$")
pattern = re.compile("^[A-F0-9]{4,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'"
)
return value
if len(value) == len(bt_uuid32_format):
pattern = re.compile("^[A-F|0-9]{8,}$")
pattern = re.compile("^[A-F0-9]{8,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'"
@@ -388,7 +388,7 @@ def bt_uuid(value):
return value
if len(value) == len(bt_uuid128_format):
pattern = re.compile(
"^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$"
"^[A-F0-9]{8,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{12,}$"
)
if not pattern.match(value):
raise cv.Invalid(
@@ -10,11 +10,7 @@ AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"]
esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon")
ESP32BLEBeacon = esp32_ble_beacon_ns.class_(
"ESP32BLEBeacon",
cg.Component,
cg.Parented.template(esp32_ble.ESP32BLE),
)
ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component)
CONF_MAJOR = "major"
CONF_MINOR = "minor"
CONF_MIN_INTERVAL = "min_interval"
@@ -35,7 +35,7 @@ using esp_ble_ibeacon_t = struct {
using namespace esp32_ble;
class ESP32BLEBeacon : public Component, public Parented<ESP32BLE> {
class ESP32BLEBeacon : public Component {
public:
explicit ESP32BLEBeacon(const std::array<uint8_t, 16> &uuid) : uuid_(uuid) {}
@@ -307,24 +307,30 @@ def final_validate_config(config):
# 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
char_config = [
matches = [
char_conf
for service_conf in config[CONF_SERVICES]
for char_conf in service_conf[CONF_CHARACTERISTICS]
if char_conf[CONF_ID] == char_id
][0]
]
if not matches:
continue
char_config = matches[0]
if not char_config[CONF_NOTIFY]:
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has notify actions and the {CONF_NOTIFY} property is not set"
)
for char_id in CORE.data.get(DOMAIN, {}).get(KEY_SET_VALUE, set()):
# Look for the characteristic in the configuration
char_config = [
matches = [
char_conf
for service_conf in config[CONF_SERVICES]
for char_conf in service_conf[CONF_CHARACTERISTICS]
if char_conf[CONF_ID] == char_id
][0]
]
if not matches:
continue
char_config = matches[0]
if isinstance(char_config.get(CONF_VALUE, {}).get(CONF_DATA), cv.Lambda):
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has both a set_value action and a templated value"
+1 -1
View File
@@ -155,7 +155,7 @@ ESP8266_PIN_SCHEMA = cv.All(
@dataclass
class PinInitialState:
mode = 255
mode: int = 255
level: int = 255
+3 -3
View File
@@ -158,15 +158,15 @@ def validate_peer(value):
def _validate_raw_data(value):
if isinstance(value, str):
if len(value) >= MAX_ESPNOW_PACKET_SIZE:
if len(value) > MAX_ESPNOW_PACKET_SIZE:
raise cv.Invalid(
f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} characters long, got {len(value)}"
f"'{CONF_DATA}' must be at most {MAX_ESPNOW_PACKET_SIZE} characters long, got {len(value)}"
)
return value
if isinstance(value, list):
if len(value) > MAX_ESPNOW_PACKET_SIZE:
raise cv.Invalid(
f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} bytes long, got {len(value)}"
f"'{CONF_DATA}' must be at most {MAX_ESPNOW_PACKET_SIZE} bytes long, got {len(value)}"
)
return cv.Schema([cv.hex_uint8_t])(value)
raise cv.Invalid(
+2 -1
View File
@@ -13,6 +13,7 @@ from esphome.const import (
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLUME,
DEVICE_CLASS_VOLUME_FLOW_RATE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_CELSIUS,
@@ -75,7 +76,7 @@ CONFIG_SCHEMA = (
),
cv.Optional(CONF_FLOW): sensor.sensor_schema(
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLUME,
device_class=DEVICE_CLASS_VOLUME_FLOW_RATE,
state_class=STATE_CLASS_MEASUREMENT,
unit_of_measurement=UNIT_LITRE_PER_HOUR,
),
+1 -1
View File
@@ -36,7 +36,7 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(lc709203f),
cv.Optional(CONF_SIZE, default="500"): cv.int_range(100, 3000),
cv.Optional(CONF_SIZE, default=500): cv.int_range(100, 3000),
cv.Optional(CONF_VOLTAGE, default="3.7"): cv.enum(
BATTERY_VOLTAGE_OPTIONS, upper=True
),
@@ -405,7 +405,7 @@ def _model_config_to_manifest_data(model_config):
file = _compute_local_file_path(model_config) / "manifest.json"
else:
raise ValueError("Unsupported config type: {model_config[CONF_TYPE]}")
raise ValueError(f"Unsupported config type: {model_config[CONF_TYPE]}")
return _load_model_data(file)
+1 -1
View File
@@ -50,7 +50,7 @@ def validate_min_max_value(config):
max_val = config[CONF_MAX_VALUE]
if min_val >= max_val:
raise cv.Invalid(
f"Max value {max_val} must be smaller than min value {min_val}"
f"Max value {max_val} must be greater than min value {min_val}"
)
return config
+1 -1
View File
@@ -272,7 +272,7 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema(
),
cv.Optional(
CONF_UNIT_OF_MEASUREMENT, default=UNIT_SECOND
): cv.one_of(UNIT_MINUTE, UNIT_SECOND, lower="True"),
): cv.one_of(UNIT_MINUTE, UNIT_SECOND, lower=True),
}
)
.extend(cv.COMPONENT_SCHEMA),
+1 -1
View File
@@ -127,7 +127,7 @@ def validate_st7789v(config):
if model_data[REQUIRE_PS] and CONF_POWER_SUPPLY not in config:
raise cv.Invalid(
f'{CONF_POWER_SUPPLY} must be specified when {CONF_MODEL} is {config[CONF_MODEL]}"'
f"{CONF_POWER_SUPPLY} must be specified when {CONF_MODEL} is {config[CONF_MODEL]}"
)
if (
+1 -1
View File
@@ -46,7 +46,7 @@ def validate_acceleration(value):
def validate_speed(value):
value = cv.string(value)
for suffix in ("steps/s", "steps/s"):
for suffix in ("steps/s",):
value = value.removesuffix(suffix)
if value == "inf":
+13
View File
@@ -22,6 +22,19 @@ namespace esphome {
static const char *const TAG = "helpers";
__attribute__((noinline, cold)) void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity,
size_t elem_size) {
ESPHOME_DEBUG_ASSERT(size < UINT16_MAX);
uint16_t new_cap = size + 1;
auto *new_data = ::operator new(new_cap *elem_size);
if (data) {
__builtin_memcpy(new_data, data, size * elem_size);
::operator delete(data);
}
capacity = new_cap;
return new_data;
}
static const uint16_t CRC16_A001_LE_LUT_L[] = {0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440};
static const uint16_t CRC16_A001_LE_LUT_H[] = {0x0000, 0xcc01, 0xd801, 0x1400, 0xf001, 0x3c00, 0x2800, 0xe401,
+56 -12
View File
@@ -1801,33 +1801,77 @@ template<typename... Ts> struct Callback<void(Ts...)> {
}
};
/// Grow a CallbackManager's backing array to exactly size+1. Defined in helpers.cpp.
void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, size_t elem_size);
template<typename... X> class CallbackManager;
/** Helper class to allow having multiple subscribers to a callback.
*
* Uses a trivial-copyable-specialized container instead of std::vector to avoid
* template bloat (_M_realloc_insert, exception-safe copies). Since Callback is
* trivially copyable (just {fn_ptr, ctx_ptr}), reallocation is a plain memcpy.
* Uses uint16_t for size/capacity (8 bytes on 32-bit vs 12 for std::vector).
* Grows to exact size on each add — callbacks are registered during setup()
* and most instances have only 1-2 callbacks, so slack capacity is wasteful.
*
* @tparam Ts The arguments for the callbacks, wrapped in void().
*/
template<typename... Ts> class CallbackManager<void(Ts...)> {
using CbType = Callback<void(Ts...)>;
static_assert(std::is_trivially_copyable_v<CbType>, "Callback must be trivially copyable");
public:
CallbackManager() = default;
~CallbackManager() { ::operator delete(this->data_); }
// Non-copyable (would alias data_), movable (for std::map support)
CallbackManager(const CallbackManager &) = delete;
CallbackManager &operator=(const CallbackManager &) = delete;
CallbackManager(CallbackManager &&other) noexcept
: data_(other.data_), size_(other.size_), capacity_(other.capacity_) {
other.data_ = nullptr;
other.size_ = 0;
other.capacity_ = 0;
}
CallbackManager &operator=(CallbackManager &&other) noexcept {
std::swap(this->data_, other.data_);
std::swap(this->size_, other.size_);
std::swap(this->capacity_, other.capacity_);
return *this;
}
/// Add any callable. Small trivially-copyable callables (like [this] lambdas)
/// are stored inline without heap allocation or std::function.
template<typename F> void add(F &&callback) { this->add_(Callback<void(Ts...)>::create(std::forward<F>(callback))); }
/// Call all callbacks in this manager. No null check on invoke.
void call(Ts... args) {
for (auto &cb : this->callbacks_)
cb.call(args...);
}
size_t size() const { return this->callbacks_.size(); }
template<typename F> void add(F &&callback) { this->add_(CbType::create(std::forward<F>(callback))); }
/// Call all callbacks in this manager.
void operator()(Ts... args) { call(args...); }
inline void ESPHOME_ALWAYS_INLINE call(Ts... args) {
if (this->size_ != 0) {
for (auto *it = this->data_, *end = it + this->size_; it != end; ++it) {
it->call(args...);
}
}
}
uint16_t size() const { return this->size_; }
/// Call all callbacks in this manager.
void operator()(Ts... args) { this->call(args...); }
protected:
template<typename...> friend class LazyCallbackManager;
/// Non-template core to avoid code duplication per lambda type.
void add_(Callback<void(Ts...)> cb) { this->callbacks_.push_back(cb); }
std::vector<Callback<void(Ts...)>> callbacks_;
/// Inline fast path; cold growth path is in helpers.cpp via callback_manager_grow().
void add_(CbType cb) {
if (this->size_ == this->capacity_) {
this->data_ =
static_cast<CbType *>(callback_manager_grow(this->data_, this->size_, this->capacity_, sizeof(CbType)));
}
this->data_[this->size_++] = cb;
}
CbType *data_{nullptr};
uint16_t size_{0};
uint16_t capacity_{0};
};
/** CallbackManager backed by StaticVector for compile-time-known callback counts.
@@ -1871,7 +1915,7 @@ template<typename... X> class LazyCallbackManager;
* from API and web_server components).
*
* Memory overhead comparison (32-bit systems):
* - CallbackManager: 12 bytes (empty std::vector)
* - CallbackManager: 8 bytes (pointer + uint16 size + uint16 capacity)
* - LazyCallbackManager: 4 bytes (nullptr pointer)
*
* Uses plain pointer instead of unique_ptr to avoid template instantiation overhead.
+21 -3
View File
@@ -214,8 +214,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
#endif /* ESPHOME_DEBUG_SCHEDULER */
}
// Common epilogue: atomic cancel-and-add (unless skip_cancel is true)
if (!skip_cancel) {
// Common epilogue: atomic cancel-and-add (unless skip_cancel is true or anonymous)
// Anonymous items (STATIC_STRING with nullptr) can never match anything, so skip the scan.
if (!skip_cancel && (name_type != NameType::STATIC_STRING || static_name != nullptr)) {
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* match_retry= */ false,
/* find_first= */ true);
}
@@ -742,6 +743,23 @@ bool HOT Scheduler::cancel_item_(Component *component, NameType name_type, const
// When find_first=false, cancels ALL matches across all containers (needed for
// public cancel path where DelayAction parallel mode can create duplicates).
// name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id
size_t Scheduler::mark_matching_items_removed_slow_locked_(std::vector<SchedulerItem *> &container,
Component *component, NameType name_type,
const char *static_name, uint32_t hash_or_id,
SchedulerItem::Type type, bool match_retry,
bool find_first) {
size_t count = 0;
for (auto *item : container) {
if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) {
this->set_item_removed_(item, true);
if (find_first)
return 1;
count++;
}
}
return count;
}
bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type, const char *static_name,
uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry,
bool find_first) {
@@ -767,7 +785,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type
// The main loop may be executing an item's callback right now, and recycling
// would destroy the callback while it's running (use-after-free).
// Only the main loop in call() should recycle items after execution completes.
if (!this->items_.empty()) {
{
size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name,
hash_or_id, type, match_retry, find_first);
total_cancelled += heap_cancelled;
+15 -15
View File
@@ -495,23 +495,23 @@ 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
__attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector<SchedulerItem *> &container,
Component *component, NameType name_type,
const char *static_name, uint32_t hash_or_id,
SchedulerItem::Type type, bool match_retry,
bool find_first = false) {
size_t count = 0;
for (auto *item : container) {
if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) {
this->set_item_removed_(item, true);
if (find_first)
return 1;
count++;
}
}
return count;
// Inlined: the fast path (empty container) avoids calling the out-of-line scan.
inline size_t HOT mark_matching_items_removed_locked_(std::vector<SchedulerItem *> &container, Component *component,
NameType name_type, const char *static_name,
uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry,
bool find_first = false) {
if (container.empty())
return 0;
return this->mark_matching_items_removed_slow_locked_(container, component, name_type, static_name, hash_or_id,
type, match_retry, find_first);
}
// Out-of-line slow path for mark_matching_items_removed_locked_ when container is non-empty.
// IMPORTANT: Must be called with scheduler lock held
__attribute__((noinline)) size_t mark_matching_items_removed_slow_locked_(
std::vector<SchedulerItem *> &container, Component *component, NameType name_type, const char *static_name,
uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, bool find_first);
Mutex lock_;
std::vector<SchedulerItem *> items_;
std::vector<SchedulerItem *> to_add_;
+2 -2
View File
@@ -130,7 +130,7 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None
:param expect: Expected response code(s), None to skip validation.
:raises OTAError: If an error code is detected or response doesn't match expected.
"""
if not expect:
if expect is None:
return
if not data:
raise OTAError(
@@ -278,7 +278,7 @@ def perform_ota(
raise OTAError("ESP requests password, but no password given!")
nonce_bytes = receive_exactly(
sock, nonce_size, f"{hash_name} authentication nonce", [], decode=False
sock, nonce_size, f"{hash_name} authentication nonce", None, decode=False
)
assert isinstance(nonce_bytes, bytes)
nonce = nonce_bytes.decode()
+3 -3
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import datetime
from datetime import UTC, datetime
import logging
from pathlib import Path
@@ -27,8 +27,8 @@ def has_remote_file_changed(url: str, local_file_path: Path) -> bool:
_LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path)
try:
local_modification_time = local_file_path.stat().st_mtime
local_modification_time_str = datetime.utcfromtimestamp(
local_modification_time
local_modification_time_str = datetime.fromtimestamp(
local_modification_time, tz=UTC
).strftime("%a, %d %b %Y %H:%M:%S GMT")
headers = {
+1 -1
View File
@@ -25,7 +25,7 @@ _BACKGROUND_TASKS: set[asyncio.Task] = set()
class DashboardStatus:
def __init__(self, on_update: Callable[[dict[str, bool | None], []]]) -> None:
def __init__(self, on_update: Callable[[dict[str, bool | None]], None]) -> None:
"""Initialize the dashboard status."""
self.on_update = on_update