1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-02 16:11:53 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Jesse Hills
bc3212d981 [quality] Use `uint8_t instead of unsigned char` 2025-08-13 08:53:23 +12:00
81 changed files with 368 additions and 1646 deletions

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2025.8.1
PROJECT_NUMBER = 2025.8.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@@ -476,7 +476,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
from esphome.components.api.client import run_logs
return run_logs(config, addresses_to_use)
if get_port_type(port) in ("NETWORK", "MQTT") and "mqtt" in config:
if get_port_type(port) == "MQTT" and "mqtt" in config:
from esphome import mqtt
return mqtt.show_logs(

View File

@@ -321,7 +321,6 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
)
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service("esphome.tag_scanned"))

View File

@@ -455,7 +455,9 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
resp.cold_white = values.get_cold_white();
resp.warm_white = values.get_warm_white();
if (light->supports_effects()) {
resp.set_effect(light->get_effect_name_ref());
// get_effect_name() returns temporary std::string - must store it
std::string effect_name = light->get_effect_name();
resp.set_effect(StringRef(effect_name));
}
return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
@@ -1413,7 +1415,9 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
resp.set_esphome_version(ESPHOME_VERSION_REF);
resp.set_compilation_time(App.get_compilation_time_ref());
// get_compilation_time() returns temporary std::string - must store it
std::string compilation_time = App.get_compilation_time();
resp.set_compilation_time(StringRef(compilation_time));
// Compile-time StringRef constants for manufacturers
#if defined(USE_ESP8266) || defined(USE_ESP32)

View File

@@ -382,15 +382,20 @@ float ATM90E32Component::get_setup_priority() const { return setup_priority::IO;
// R/C registers can conly be cleared after the LastSPIData register is updated (register 78H)
// Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period
// Default is 143FH (20ms, 63ms)
uint16_t ATM90E32Component::read16_(uint16_t a_register) {
this->enable();
delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1us is plenty
uint16_t ATM90E32Component::read16_transaction_(uint16_t a_register) {
uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03);
uint8_t addrl = (a_register & 0xFF);
uint8_t data[4] = {addrh, addrl, 0x00, 0x00};
this->transfer_array(data, 4);
uint16_t output = encode_uint16(data[2], data[3]);
ESP_LOGVV(TAG, "read16_ 0x%04" PRIX16 " output 0x%04" PRIX16, a_register, output);
return output;
}
uint16_t ATM90E32Component::read16_(uint16_t a_register) {
this->enable();
delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1us is plenty
uint16_t output = this->read16_transaction_(a_register);
delay_microseconds_safe(1); // allow the last clock to propagate before releasing CS
this->disable();
delay_microseconds_safe(1); // meet minimum CS high time before next transaction
@@ -398,8 +403,14 @@ uint16_t ATM90E32Component::read16_(uint16_t a_register) {
}
int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) {
const uint16_t val_h = this->read16_(addr_h);
const uint16_t val_l = this->read16_(addr_l);
this->enable();
delay_microseconds_safe(1);
const uint16_t val_h = this->read16_transaction_(addr_h);
delay_microseconds_safe(1);
const uint16_t val_l = this->read16_transaction_(addr_l);
delay_microseconds_safe(1);
this->disable();
delay_microseconds_safe(1);
const int32_t val = (val_h << 16) | val_l;
ESP_LOGVV(TAG,

View File

@@ -140,6 +140,7 @@ class ATM90E32Component : public PollingComponent,
number::Number *ref_currents_[3]{nullptr, nullptr, nullptr};
#endif
uint16_t read16_(uint16_t a_register);
uint16_t read16_transaction_(uint16_t a_register);
int read32_(uint16_t addr_h, uint16_t addr_l);
void write16_(uint16_t a_register, uint16_t val, bool validate = true);
float get_local_phase_voltage_(uint8_t phase);

View File

@@ -286,7 +286,6 @@ async def remove_bond_to_code(config, action_id, template_arg, args):
async def to_code(config):
# Register the loggers this component needs
esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP)
cg.add_define("USE_ESP32_BLE_UUID")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -12,30 +12,16 @@ namespace esphome::bluetooth_proxy {
static const char *const TAG = "bluetooth_proxy.connection";
// This function is allocation-free and directly packs UUIDs into the output array
// using precalculated constants for the Bluetooth base UUID
static void fill_128bit_uuid_array(std::array<uint64_t, 2> &out, esp_bt_uuid_t uuid_source) {
// Bluetooth base UUID: 00000000-0000-1000-8000-00805F9B34FB
// out[0] = bytes 8-15 (big-endian)
// - For 128-bit UUIDs: use bytes 8-15 as-is
// - For 16/32-bit UUIDs: insert into bytes 12-15, use 0x00001000 for bytes 8-11
out[0] = uuid_source.len == ESP_UUID_LEN_128
? (((uint64_t) uuid_source.uuid.uuid128[15] << 56) | ((uint64_t) uuid_source.uuid.uuid128[14] << 48) |
((uint64_t) uuid_source.uuid.uuid128[13] << 40) | ((uint64_t) uuid_source.uuid.uuid128[12] << 32) |
((uint64_t) uuid_source.uuid.uuid128[11] << 24) | ((uint64_t) uuid_source.uuid.uuid128[10] << 16) |
((uint64_t) uuid_source.uuid.uuid128[9] << 8) | ((uint64_t) uuid_source.uuid.uuid128[8]))
: (((uint64_t) (uuid_source.len == ESP_UUID_LEN_16 ? uuid_source.uuid.uuid16 : uuid_source.uuid.uuid32)
<< 32) |
0x00001000ULL); // Base UUID bytes 8-11
// out[1] = bytes 0-7 (big-endian)
// - For 128-bit UUIDs: use bytes 0-7 as-is
// - For 16/32-bit UUIDs: use precalculated base UUID constant
out[1] = uuid_source.len == ESP_UUID_LEN_128
? ((uint64_t) uuid_source.uuid.uuid128[7] << 56) | ((uint64_t) uuid_source.uuid.uuid128[6] << 48) |
((uint64_t) uuid_source.uuid.uuid128[5] << 40) | ((uint64_t) uuid_source.uuid.uuid128[4] << 32) |
((uint64_t) uuid_source.uuid.uuid128[3] << 24) | ((uint64_t) uuid_source.uuid.uuid128[2] << 16) |
((uint64_t) uuid_source.uuid.uuid128[1] << 8) | ((uint64_t) uuid_source.uuid.uuid128[0])
: 0x800000805F9B34FBULL; // Base UUID bytes 0-7: 80-00-00-80-5F-9B-34-FB
esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid();
out[0] = ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) |
((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) |
((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) |
((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]);
out[1] = ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) |
((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) |
((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) |
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]);
}
// Helper to fill UUID in the appropriate format based on client support and UUID type
@@ -133,7 +119,7 @@ void BluetoothConnection::loop() {
// Check if we should disable the loop
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
// - For other connections: Disable only after service discovery is complete
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->send_service_ == DONE_SENDING_SERVICES)) {
@@ -160,7 +146,10 @@ void BluetoothConnection::send_service_for_discovery_() {
if (this->send_service_ >= this->service_count_) {
this->send_service_ = DONE_SENDING_SERVICES;
this->proxy_->send_gatt_services_done(this->address_);
this->release_services();
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
this->release_services();
}
return;
}
@@ -375,19 +364,10 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
switch (event) {
case ESP_GATTC_DISCONNECT_EVT: {
// Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources
// This prevents race condition where we mark slot as free before controller cleanup is complete
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(),
param->disconnect.reason);
// Send disconnection notification but don't free the slot yet
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
this->reset_connection_(param->disconnect.reason);
break;
}
case ESP_GATTC_CLOSE_EVT: {
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(),
param->close.reason);
// Now the GATT connection is fully closed and controller resources are freed
// Safe to mark the connection slot as available
this->reset_connection_(param->close.reason);
break;
}

View File

@@ -7,8 +7,6 @@
#include <esphome/components/sensor/sensor.h>
#include <esphome/core/component.h>
#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID"
namespace esphome {
namespace bme280_base {
@@ -100,18 +98,18 @@ void BME280Component::setup() {
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed();
return;
}
if (chip_id != 0x60) {
this->error_code_ = WRONG_CHIP_ID;
this->mark_failed(BME280_ERROR_WRONG_CHIP_ID);
this->mark_failed();
return;
}
// Send a soft reset.
if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) {
this->mark_failed("Reset failed");
this->mark_failed();
return;
}
// Wait until the NVM data has finished loading.
@@ -120,12 +118,14 @@ void BME280Component::setup() {
do { // NOLINT
delay(2);
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
this->mark_failed("Error reading status register");
ESP_LOGW(TAG, "Error reading status register.");
this->mark_failed();
return;
}
} while ((status & BME280_STATUS_IM_UPDATE) && (--retry));
if (status & BME280_STATUS_IM_UPDATE) {
this->mark_failed("Timeout loading NVM");
ESP_LOGW(TAG, "Timeout loading NVM.");
this->mark_failed();
return;
}
@@ -153,26 +153,26 @@ void BME280Component::setup() {
uint8_t humid_control_val = 0;
if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) {
this->mark_failed("Read humidity control");
this->mark_failed();
return;
}
humid_control_val &= ~0b00000111;
humid_control_val |= this->humidity_oversampling_ & 0b111;
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) {
this->mark_failed("Write humidity control");
this->mark_failed();
return;
}
uint8_t config_register = 0;
if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) {
this->mark_failed("Read config");
this->mark_failed();
return;
}
config_register &= ~0b11111100;
config_register |= 0b101 << 5; // 1000 ms standby time
config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) {
this->mark_failed("Write config");
this->mark_failed();
return;
}
}
@@ -183,7 +183,7 @@ void BME280Component::dump_config() {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
break;
case WRONG_CHIP_ID:
ESP_LOGE(TAG, BME280_ERROR_WRONG_CHIP_ID);
ESP_LOGE(TAG, "BME280 has wrong chip ID! Is it a BME280?");
break;
case NONE:
default:
@@ -223,21 +223,21 @@ void BME280Component::update() {
this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() {
uint8_t data[8];
if (!this->read_bytes(BME280_REGISTER_MEASUREMENTS, data, 8)) {
ESP_LOGW(TAG, "Error reading registers");
ESP_LOGW(TAG, "Error reading registers.");
this->status_set_warning();
return;
}
int32_t t_fine = 0;
float const temperature = this->read_temperature_(data, &t_fine);
if (std::isnan(temperature)) {
ESP_LOGW(TAG, "Invalid temperature");
ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values.");
this->status_set_warning();
return;
}
float const pressure = this->read_pressure_(data, t_fine);
float const humidity = this->read_humidity_(data, t_fine);
ESP_LOGV(TAG, "Temperature=%.1f°C Pressure=%.1fhPa Humidity=%.1f%%", temperature, pressure, humidity);
ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity);
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature);
if (this->pressure_sensor_ != nullptr)

View File

@@ -2,8 +2,6 @@
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID"
namespace esphome {
namespace bmp280_base {
@@ -65,23 +63,23 @@ void BMP280Component::setup() {
// https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
if (!this->read_byte(0xD0, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed();
return;
}
if (!this->read_byte(0xD0, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed();
return;
}
if (chip_id != 0x58) {
this->error_code_ = WRONG_CHIP_ID;
this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID);
this->mark_failed();
return;
}
// Send a soft reset.
if (!this->write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
this->mark_failed("Reset failed");
this->mark_failed();
return;
}
// Wait until the NVM data has finished loading.
@@ -90,12 +88,14 @@ void BMP280Component::setup() {
do {
delay(2);
if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) {
this->mark_failed("Error reading status register");
ESP_LOGW(TAG, "Error reading status register.");
this->mark_failed();
return;
}
} while ((status & BMP280_STATUS_IM_UPDATE) && (--retry));
if (status & BMP280_STATUS_IM_UPDATE) {
this->mark_failed("Timeout loading NVM");
ESP_LOGW(TAG, "Timeout loading NVM.");
this->mark_failed();
return;
}
@@ -116,14 +116,14 @@ void BMP280Component::setup() {
uint8_t config_register = 0;
if (!this->read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
this->mark_failed("Read config");
this->mark_failed();
return;
}
config_register &= ~0b11111100;
config_register |= 0b000 << 5; // 0.5 ms standby time
config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->write_byte(BMP280_REGISTER_CONFIG, config_register)) {
this->mark_failed("Write config");
this->mark_failed();
return;
}
}
@@ -134,7 +134,7 @@ void BMP280Component::dump_config() {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
break;
case WRONG_CHIP_ID:
ESP_LOGE(TAG, BMP280_ERROR_WRONG_CHIP_ID);
ESP_LOGE(TAG, "BMP280 has wrong chip ID! Is it a BME280?");
break;
case NONE:
default:
@@ -172,13 +172,13 @@ void BMP280Component::update() {
int32_t t_fine = 0;
float temperature = this->read_temperature_(&t_fine);
if (std::isnan(temperature)) {
ESP_LOGW(TAG, "Invalid temperature");
ESP_LOGW(TAG, "Invalid temperature, cannot read pressure values.");
this->status_set_warning();
return;
}
float pressure = this->read_pressure_(t_fine);
ESP_LOGV(TAG, "Temperature=%.1f°C Pressure=%.1fhPa", temperature, pressure);
ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa", temperature, pressure);
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature);
if (this->pressure_sensor_ != nullptr)

View File

@@ -1,5 +1,4 @@
#ifdef USE_ESP32
#include "soc/soc_caps.h"
#include "driver/gpio.h"
#include "deep_sleep_component.h"
#include "esphome/core/log.h"
@@ -84,11 +83,7 @@ void DeepSleepComponent::deep_sleep_() {
}
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
gpio_hold_en(gpio_pin);
#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
// Some ESP32 variants support holding a single GPIO during deep sleep without this function
// For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep
gpio_deep_sleep_hold_en();
#endif
bool level = !this->wakeup_pin_->is_inverted();
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
level = !level;
@@ -125,11 +120,7 @@ void DeepSleepComponent::deep_sleep_() {
}
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
gpio_hold_en(gpio_pin);
#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
// Some ESP32 variants support holding a single GPIO during deep sleep without this function
// For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep
gpio_deep_sleep_hold_en();
#endif
bool level = !this->wakeup_pin_->is_inverted();
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
level = !level;

View File

@@ -23,7 +23,7 @@ void DelonghiClimate::transmit_state() {
data->mark(DELONGHI_HEADER_MARK);
data->space(DELONGHI_HEADER_SPACE);
for (unsigned char b : remote_state) {
for (uint8_t b : remote_state) {
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
data->mark(DELONGHI_BIT_MARK);
bool bit = b & mask;

View File

@@ -824,9 +824,8 @@ async def to_code(config):
cg.set_cpp_standard("gnu++20")
cg.add_build_flag("-DUSE_ESP32")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
variant = config[CONF_VARIANT]
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}")
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
cg.add_define(ThreadModel.MULTI_ATOMICS)
cg.add_platformio_option("lib_ldf_mode", "off")
@@ -860,7 +859,6 @@ async def to_code(config):
cg.add_platformio_option(
"platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"]
)
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
)

View File

@@ -280,10 +280,6 @@ async def to_code(config):
add_idf_sdkconfig_option(
"CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds
)
# Increase GATT client connection retry count for problematic devices
# Default in ESP-IDF is 3, we increase to 10 for better reliability with
# low-power/timing-sensitive devices
add_idf_sdkconfig_option("CONFIG_BT_GATTC_CONNECT_RETRY_COUNT", 10)
# Set the maximum number of notification registrations
# This controls how many BLE characteristics can have notifications enabled
@@ -298,7 +294,6 @@ async def to_code(config):
if config[CONF_ADVERTISING]:
cg.add_define("USE_ESP32_BLE_ADVERTISING")
cg.add_define("USE_ESP32_BLE_UUID")
@automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({}))

View File

@@ -306,7 +306,7 @@ void ESP32BLE::loop() {
case BLEEvent::GATTS: {
esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event;
esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if;
esp_ble_gatts_cb_param_t *param = &ble_event->event_.gatts.gatts_param;
esp_ble_gatts_cb_param_t *param = ble_event->event_.gatts.gatts_param;
ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event);
for (auto *gatts_handler : this->gatts_event_handlers_) {
gatts_handler->gatts_event_handler(event, gatts_if, param);
@@ -316,7 +316,7 @@ void ESP32BLE::loop() {
case BLEEvent::GATTC: {
esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event;
esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if;
esp_ble_gattc_cb_param_t *param = &ble_event->event_.gattc.gattc_param;
esp_ble_gattc_cb_param_t *param = ble_event->event_.gattc.gattc_param;
ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event);
for (auto *gattc_handler : this->gattc_event_handlers_) {
gattc_handler->gattc_event_handler(event, gattc_if, param);

View File

@@ -3,7 +3,8 @@
#ifdef USE_ESP32
#include <cstddef> // for offsetof
#include <cstring> // for memcpy
#include <vector>
#include <esp_gap_ble_api.h>
#include <esp_gattc_api.h>
#include <esp_gatts_api.h>
@@ -61,24 +62,10 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(es
static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t),
"remote_addr must follow rssi in read_rssi_cmpl");
// Param struct sizes on ESP32
static constexpr size_t GATTC_PARAM_SIZE = 28;
static constexpr size_t GATTS_PARAM_SIZE = 32;
// Maximum size for inline storage of data
// GATTC: 80 - 28 (param) - 8 (other fields) = 44 bytes for data
// GATTS: 80 - 32 (param) - 8 (other fields) = 40 bytes for data
static constexpr size_t GATTC_INLINE_DATA_SIZE = 44;
static constexpr size_t GATTS_INLINE_DATA_SIZE = 40;
// Verify param struct sizes
static_assert(sizeof(esp_ble_gattc_cb_param_t) == GATTC_PARAM_SIZE, "GATTC param size unexpected");
static_assert(sizeof(esp_ble_gatts_cb_param_t) == GATTS_PARAM_SIZE, "GATTS param size unexpected");
// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
// This class stores each event with minimal memory usage.
// GAP events (99% of traffic) don't have the heap allocation overhead.
// GATTC/GATTS events use heap allocation for their param and inline storage for small data.
// GAP events (99% of traffic) don't have the vector overhead.
// GATTC/GATTS events use heap allocation for their param and data.
//
// Event flow:
// 1. ESP-IDF BLE stack calls our static handlers in the BLE task context
@@ -125,21 +112,21 @@ class BLEEvent {
this->init_gap_data_(e, p);
}
// Constructor for GATTC events - param stored inline, data may use heap
// IMPORTANT: We MUST copy the param struct because the pointer from ESP-IDF
// is only valid during the callback execution. Since BLE events are processed
// asynchronously in the main loop, we store our own copy inline to ensure
// the data remains valid until the event is processed.
// Constructor for GATTC events - uses heap allocation
// IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
// The param pointer from ESP-IDF is only valid during the callback execution.
// Since BLE events are processed asynchronously in the main loop, we must create
// our own copy to ensure the data remains valid until the event is processed.
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->type_ = GATTC;
this->init_gattc_data_(e, i, p);
}
// Constructor for GATTS events - param stored inline, data may use heap
// IMPORTANT: We MUST copy the param struct because the pointer from ESP-IDF
// is only valid during the callback execution. Since BLE events are processed
// asynchronously in the main loop, we store our own copy inline to ensure
// the data remains valid until the event is processed.
// Constructor for GATTS events - uses heap allocation
// IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
// The param pointer from ESP-IDF is only valid during the callback execution.
// Since BLE events are processed asynchronously in the main loop, we must create
// our own copy to ensure the data remains valid until the event is processed.
BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->type_ = GATTS;
this->init_gatts_data_(e, i, p);
@@ -149,32 +136,25 @@ class BLEEvent {
~BLEEvent() { this->release(); }
// Default constructor for pre-allocation in pool
BLEEvent() : event_{}, type_(GAP) {}
BLEEvent() : type_(GAP) {}
// Invoked on return to EventPool - clean up any heap-allocated data
void release() {
switch (this->type_) {
case GAP:
// GAP events don't have heap allocations
break;
case GATTC:
// Param is now stored inline, only delete heap data if it was heap-allocated
if (!this->event_.gattc.is_inline && this->event_.gattc.data.heap_data != nullptr) {
delete[] this->event_.gattc.data.heap_data;
}
// Clear critical fields to prevent issues if type changes
this->event_.gattc.is_inline = false;
this->event_.gattc.data.heap_data = nullptr;
break;
case GATTS:
// Param is now stored inline, only delete heap data if it was heap-allocated
if (!this->event_.gatts.is_inline && this->event_.gatts.data.heap_data != nullptr) {
delete[] this->event_.gatts.data.heap_data;
}
// Clear critical fields to prevent issues if type changes
this->event_.gatts.is_inline = false;
this->event_.gatts.data.heap_data = nullptr;
break;
if (this->type_ == GAP) {
return;
}
if (this->type_ == GATTC) {
delete this->event_.gattc.gattc_param;
delete this->event_.gattc.data;
this->event_.gattc.gattc_param = nullptr;
this->event_.gattc.data = nullptr;
return;
}
if (this->type_ == GATTS) {
delete this->event_.gatts.gatts_param;
delete this->event_.gatts.data;
this->event_.gatts.gatts_param = nullptr;
this->event_.gatts.data = nullptr;
}
}
@@ -226,30 +206,20 @@ class BLEEvent {
// NOLINTNEXTLINE(readability-identifier-naming)
struct gattc_event {
esp_ble_gattc_cb_param_t gattc_param; // Stored inline (28 bytes)
esp_gattc_cb_event_t gattc_event; // 4 bytes
union {
uint8_t *heap_data; // 4 bytes when heap-allocated
uint8_t inline_data[GATTC_INLINE_DATA_SIZE]; // 44 bytes when stored inline
} data; // 44 bytes total
uint16_t data_len; // 2 bytes
esp_gatt_if_t gattc_if; // 1 byte
bool is_inline; // 1 byte - true when data is stored inline
} gattc; // Total: 80 bytes
esp_gattc_cb_event_t gattc_event;
esp_gatt_if_t gattc_if;
esp_ble_gattc_cb_param_t *gattc_param; // Heap-allocated
std::vector<uint8_t> *data; // Heap-allocated
} gattc; // 16 bytes (pointers only)
// NOLINTNEXTLINE(readability-identifier-naming)
struct gatts_event {
esp_ble_gatts_cb_param_t gatts_param; // Stored inline (32 bytes)
esp_gatts_cb_event_t gatts_event; // 4 bytes
union {
uint8_t *heap_data; // 4 bytes when heap-allocated
uint8_t inline_data[GATTS_INLINE_DATA_SIZE]; // 40 bytes when stored inline
} data; // 40 bytes total
uint16_t data_len; // 2 bytes
esp_gatt_if_t gatts_if; // 1 byte
bool is_inline; // 1 byte - true when data is stored inline
} gatts; // Total: 80 bytes
} event_; // 80 bytes
esp_gatts_cb_event_t gatts_event;
esp_gatt_if_t gatts_if;
esp_ble_gatts_cb_param_t *gatts_param; // Heap-allocated
std::vector<uint8_t> *data; // Heap-allocated
} gatts; // 16 bytes (pointers only)
} event_; // 80 bytes
ble_event_t type_;
@@ -263,29 +233,6 @@ class BLEEvent {
const esp_ble_sec_t &security() const { return event_.gap.security; }
private:
// Helper to copy data with inline storage optimization
template<typename EventStruct, size_t InlineSize>
void copy_data_with_inline_storage_(EventStruct &event, const uint8_t *src_data, uint16_t len,
uint8_t **param_value_ptr) {
event.data_len = len;
if (len > 0) {
if (len <= InlineSize) {
event.is_inline = true;
memcpy(event.data.inline_data, src_data, len);
*param_value_ptr = event.data.inline_data;
} else {
event.is_inline = false;
event.data.heap_data = new uint8_t[len];
memcpy(event.data.heap_data, src_data, len);
*param_value_ptr = event.data.heap_data;
}
} else {
event.is_inline = false;
event.data.heap_data = nullptr;
*param_value_ptr = nullptr;
}
}
// Initialize GAP event data
void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->event_.gap.gap_event = e;
@@ -370,38 +317,35 @@ class BLEEvent {
this->event_.gattc.gattc_if = i;
if (p == nullptr) {
// Zero out the param struct when null
memset(&this->event_.gattc.gattc_param, 0, sizeof(this->event_.gattc.gattc_param));
this->event_.gattc.is_inline = false;
this->event_.gattc.data.heap_data = nullptr;
this->event_.gattc.data_len = 0;
this->event_.gattc.gattc_param = nullptr;
this->event_.gattc.data = nullptr;
return; // Invalid event, but we can't log in header file
}
// Copy param struct inline (no heap allocation!)
// GATTC/GATTS events are rare (<1% of events) but we can still store them inline
// along with small data payloads, eliminating all heap allocations for typical BLE operations
// CRITICAL: This copy is REQUIRED for memory safety - the ESP-IDF param pointer
// is only valid during the callback and will be reused/freed after we return
this->event_.gattc.gattc_param = *p;
// Heap-allocate param and data
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
// while GAP events (99%) are stored inline to minimize memory usage
// IMPORTANT: This heap allocation provides clear ownership semantics:
// - The BLEEvent owns the allocated memory for its lifetime
// - The data remains valid from the BLE callback context until processed in the main loop
// - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
// Copy data for events that need it
// The param struct contains pointers (e.g., notify.value) that point to temporary buffers.
// We must copy this data to ensure it remains valid when the event is processed later.
switch (e) {
case ESP_GATTC_NOTIFY_EVT:
copy_data_with_inline_storage_<decltype(this->event_.gattc), GATTC_INLINE_DATA_SIZE>(
this->event_.gattc, p->notify.value, p->notify.value_len, &this->event_.gattc.gattc_param.notify.value);
this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
break;
case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_READ_DESCR_EVT:
copy_data_with_inline_storage_<decltype(this->event_.gattc), GATTC_INLINE_DATA_SIZE>(
this->event_.gattc, p->read.value, p->read.value_len, &this->event_.gattc.gattc_param.read.value);
this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
break;
default:
this->event_.gattc.is_inline = false;
this->event_.gattc.data.heap_data = nullptr;
this->event_.gattc.data_len = 0;
this->event_.gattc.data = nullptr;
break;
}
}
@@ -412,33 +356,30 @@ class BLEEvent {
this->event_.gatts.gatts_if = i;
if (p == nullptr) {
// Zero out the param struct when null
memset(&this->event_.gatts.gatts_param, 0, sizeof(this->event_.gatts.gatts_param));
this->event_.gatts.is_inline = false;
this->event_.gatts.data.heap_data = nullptr;
this->event_.gatts.data_len = 0;
this->event_.gatts.gatts_param = nullptr;
this->event_.gatts.data = nullptr;
return; // Invalid event, but we can't log in header file
}
// Copy param struct inline (no heap allocation!)
// GATTC/GATTS events are rare (<1% of events) but we can still store them inline
// along with small data payloads, eliminating all heap allocations for typical BLE operations
// CRITICAL: This copy is REQUIRED for memory safety - the ESP-IDF param pointer
// is only valid during the callback and will be reused/freed after we return
this->event_.gatts.gatts_param = *p;
// Heap-allocate param and data
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
// while GAP events (99%) are stored inline to minimize memory usage
// IMPORTANT: This heap allocation provides clear ownership semantics:
// - The BLEEvent owns the allocated memory for its lifetime
// - The data remains valid from the BLE callback context until processed in the main loop
// - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
// Copy data for events that need it
// The param struct contains pointers (e.g., write.value) that point to temporary buffers.
// We must copy this data to ensure it remains valid when the event is processed later.
switch (e) {
case ESP_GATTS_WRITE_EVT:
copy_data_with_inline_storage_<decltype(this->event_.gatts), GATTS_INLINE_DATA_SIZE>(
this->event_.gatts, p->write.value, p->write.len, &this->event_.gatts.gatts_param.write.value);
this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
break;
default:
this->event_.gatts.is_inline = false;
this->event_.gatts.data.heap_data = nullptr;
this->event_.gatts.data_len = 0;
this->event_.gatts.data = nullptr;
break;
}
}
@@ -448,15 +389,6 @@ class BLEEvent {
// The gap member in the union should be 80 bytes (including the gap_event enum)
static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes");
// Verify GATTC and GATTS structs don't exceed GAP struct size
// This ensures the union size is determined by GAP (the most common event type)
static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gattc)) <=
sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)),
"gattc_event struct exceeds gap_event size - union size would increase");
static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gatts)) <=
sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)),
"gatts_event struct exceeds gap_event size - union size would increase");
// Verify esp_ble_sec_t fits within our union
static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult");

View File

@@ -1,7 +1,6 @@
#include "ble_uuid.h"
#ifdef USE_ESP32
#ifdef USE_ESP32_BLE_UUID
#include <cstring>
#include <cstdio>
@@ -191,5 +190,4 @@ std::string ESPBTUUID::to_string() const {
} // namespace esphome::esp32_ble
#endif // USE_ESP32_BLE_UUID
#endif // USE_ESP32
#endif

View File

@@ -1,11 +1,9 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32
#ifdef USE_ESP32_BLE_UUID
#include <string>
#include <esp_bt_defs.h>
@@ -44,5 +42,4 @@ class ESPBTUUID {
} // namespace esphome::esp32_ble
#endif // USE_ESP32_BLE_UUID
#endif // USE_ESP32
#endif

View File

@@ -65,8 +65,6 @@ FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
async def to_code(config):
cg.add_define("USE_ESP32_BLE_UUID")
uuid = config[CONF_UUID].hex
uuid_arr = [
cg.RawExpression(f"0x{uuid[i : i + 2]}") for i in range(0, len(uuid), 2)

View File

@@ -7,7 +7,6 @@
#include <esp_gap_ble_api.h>
#include <esp_gatt_defs.h>
#include <esp_gattc_api.h>
namespace esphome::esp32_ble_client {
@@ -112,19 +111,43 @@ void BLEClientBase::connect() {
this->remote_addr_type_);
this->paired_ = false;
// Determine connection parameters based on connection type
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
// V3 without cache needs fast params for service discovery
this->set_conn_params_(FAST_MIN_CONN_INTERVAL, FAST_MAX_CONN_INTERVAL, 0, FAST_CONN_TIMEOUT, "fast");
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
// V3 with cache can use medium params
this->set_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
}
// For V1/Legacy, don't set params - use ESP-IDF defaults
// Set preferred connection parameters before connecting
// Use FAST for all V3 connections (better latency and reliability)
// Use MEDIUM for V1/legacy connections (balanced performance)
uint16_t min_interval, max_interval, timeout;
const char *param_type;
// Open the connection
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
min_interval = FAST_MIN_CONN_INTERVAL;
max_interval = FAST_MAX_CONN_INTERVAL;
timeout = FAST_CONN_TIMEOUT;
param_type = "fast";
} else {
min_interval = MEDIUM_MIN_CONN_INTERVAL;
max_interval = MEDIUM_MAX_CONN_INTERVAL;
timeout = MEDIUM_CONN_TIMEOUT;
param_type = "medium";
}
auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
0, // latency: 0
timeout);
if (param_ret != ESP_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
this->address_str_.c_str(), param_ret);
} else {
this->log_connection_params_(param_type);
}
// Now open the connection
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
this->handle_connection_result_(ret);
if (ret) {
this->log_gattc_warning_("esp_ble_gattc_open", ret);
this->set_state(espbt::ClientState::IDLE);
} else {
this->set_state(espbt::ClientState::CONNECTING);
}
}
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
@@ -136,7 +159,7 @@ void BLEClientBase::disconnect() {
return;
}
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
ESP_LOGW(TAG, "[%d] [%s] Disconnecting before connected, disconnect scheduled.", this->connection_index_,
this->address_str_.c_str());
this->want_disconnect_ = true;
return;
@@ -149,11 +172,13 @@ void BLEClientBase::unconditional_disconnect() {
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(),
this->conn_id_);
if (this->state_ == espbt::ClientState::DISCONNECTING) {
this->log_error_("Already disconnecting");
ESP_LOGE(TAG, "[%d] [%s] Tried to disconnect while already disconnecting.", this->connection_index_,
this->address_str_.c_str());
return;
}
if (this->conn_id_ == UNSET_CONN_ID) {
this->log_error_("conn id unset, cannot disconnect");
ESP_LOGE(TAG, "[%d] [%s] No connection ID set, cannot disconnect.", this->connection_index_,
this->address_str_.c_str());
return;
}
auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
@@ -209,51 +234,17 @@ void BLEClientBase::log_connection_params_(const char *param_type) {
ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
}
void BLEClientBase::handle_connection_result_(esp_err_t ret) {
if (ret) {
this->log_gattc_warning_("esp_ble_gattc_open", ret);
this->set_state(espbt::ClientState::IDLE);
} else {
this->set_state(espbt::ClientState::CONNECTING);
}
}
void BLEClientBase::log_error_(const char *message) {
ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message);
}
void BLEClientBase::log_error_(const char *message, int code) {
ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_.c_str(), message, code);
}
void BLEClientBase::log_warning_(const char *message) {
ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message);
}
void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) {
void BLEClientBase::restore_medium_conn_params_() {
// Restore to medium connection parameters after initial connection phase
// This balances performance with bandwidth usage for normal operation
esp_ble_conn_update_params_t conn_params = {{0}};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = min_interval;
conn_params.max_int = max_interval;
conn_params.latency = latency;
conn_params.timeout = timeout;
this->log_connection_params_(param_type);
esp_err_t err = esp_ble_gap_update_conn_params(&conn_params);
if (err != ESP_OK) {
this->log_gattc_warning_("esp_ble_gap_update_conn_params", err);
}
}
void BLEClientBase::set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type) {
// Set preferred connection parameters before connecting
// These will be used when establishing the connection
this->log_connection_params_(param_type);
esp_err_t err = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval, latency, timeout);
if (err != ESP_OK) {
this->log_gattc_warning_("esp_ble_gap_set_prefer_conn_params", err);
}
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
conn_params.latency = 0;
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
this->log_connection_params_("medium");
esp_ble_gap_update_conn_params(&conn_params);
}
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
@@ -273,7 +264,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->app_id);
this->gattc_if_ = esp_gattc_if;
} else {
this->log_error_("gattc app registration failed status", param->reg.status);
ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_,
this->address_str_.c_str(), param->reg.app_id, param->reg.status);
this->status_ = param->reg.status;
this->mark_failed();
}
@@ -285,21 +277,11 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->log_gattc_event_("OPEN");
// conn_id was already set in ESP_GATTC_CONNECT_EVT
this->service_count_ = 0;
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
// error, if the error occurred at the BTA/GATT layer. This can result in the event
// arriving after we've already transitioned to IDLE state.
if (this->state_ == espbt::ClientState::IDLE) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
this->address_str_.c_str(), param->open.status);
break;
}
if (this->state_ != espbt::ClientState::CONNECTING) {
// This should not happen but lets log it in case it does
// because it means we have a bad assumption about how the
// ESP BT stack works.
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while in %s state, status=%d", this->connection_index_,
this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status);
}
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
@@ -319,14 +301,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->set_state(espbt::ClientState::CONNECTED);
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
// Cached connections already connected with medium parameters, no update needed
// Restore to medium connection parameters for cached connections too
this->restore_medium_conn_params_();
// only set our state, subclients might have more stuff to do yet.
this->state_ = espbt::ClientState::ESTABLISHED;
break;
}
// For V3_WITHOUT_CACHE, we already set fast params before connecting
// No need to update them again here
this->log_event_("Searching for services");
ESP_LOGD(TAG, "[%d] [%s] Searching for services", this->connection_index_, this->address_str_.c_str());
esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
break;
}
@@ -351,7 +332,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
// Check if we were disconnected while waiting for service discovery
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
this->state_ == espbt::ClientState::CONNECTED) {
this->log_warning_("Remote closed during discovery");
ESP_LOGW(TAG, "[%d] [%s] Disconnected by remote during service discovery", this->connection_index_,
this->address_str_.c_str());
} else {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_,
this->address_str_.c_str(), param->disconnect.reason);
@@ -407,11 +389,12 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (this->conn_id_ != param->search_cmpl.conn_id)
return false;
this->log_gattc_event_("SEARCH_CMPL");
// For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery
// For V3 connections, restore to medium connection parameters after service discovery
// This balances performance with bandwidth usage after the critical discovery phase
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
} else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) {
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
this->restore_medium_conn_params_();
} else {
#ifdef USE_ESP32_BLE_DEVICE
for (auto &svc : this->services_) {
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
@@ -523,14 +506,16 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_
return;
esp_bd_addr_t bd_addr;
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_.c_str(),
ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(),
format_hex(bd_addr, 6).c_str());
if (!param->ble_security.auth_cmpl.success) {
this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason);
ESP_LOGE(TAG, "[%d] [%s] auth fail reason = 0x%x", this->connection_index_, this->address_str_.c_str(),
param->ble_security.auth_cmpl.fail_reason);
} else {
this->paired_ = true;
ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_.c_str(),
param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode);
ESP_LOGD(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_,
this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type,
param->ble_security.auth_cmpl.auth_mode);
}
break;

View File

@@ -133,18 +133,10 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void log_event_(const char *name);
void log_gattc_event_(const char *name);
void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type);
void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type);
void restore_medium_conn_params_();
void log_gattc_warning_(const char *operation, esp_gatt_status_t status);
void log_gattc_warning_(const char *operation, esp_err_t err);
void log_connection_params_(const char *param_type);
void handle_connection_result_(esp_err_t ret);
// Compact error logging helpers to reduce flash usage
void log_error_(const char *message);
void log_error_(const char *message, int code);
void log_warning_(const char *message);
};
} // namespace esphome::esp32_ble_client

View File

@@ -529,7 +529,6 @@ async def to_code_characteristic(service_var, char_conf):
async def to_code(config):
# Register the loggers this component needs
esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP)
cg.add_define("USE_ESP32_BLE_UUID")
var = cg.new_Pvariable(config[CONF_ID])

View File

@@ -373,7 +373,6 @@ async def _add_ble_features():
# Add feature-specific defines based on what's needed
if BLEFeatures.ESP_BT_DEVICE in _required_features:
cg.add_define("USE_ESP32_BLE_DEVICE")
cg.add_define("USE_ESP32_BLE_UUID")
ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(

View File

@@ -80,17 +80,14 @@ class BLEManufacturerDataAdvertiseTrigger : public Trigger<const adv_data_t &>,
ESPBTUUID uuid_;
};
#endif // USE_ESP32_BLE_DEVICE
class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener {
public:
explicit BLEEndOfScanTrigger(ESP32BLETracker *parent) { parent->register_listener(this); }
#ifdef USE_ESP32_BLE_DEVICE
bool parse_device(const ESPBTDevice &device) override { return false; }
#endif
void on_scan_end() override { this->trigger(); }
};
#endif // USE_ESP32_BLE_DEVICE
template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> {
public:

View File

@@ -33,12 +33,10 @@ enum AdvertisementParserType {
RAW_ADVERTISEMENTS,
};
#ifdef USE_ESP32_BLE_UUID
struct ServiceData {
ESPBTUUID uuid;
adv_data_t data;
};
#endif
#ifdef USE_ESP32_BLE_DEVICE
class ESPBLEiBeacon {

View File

@@ -208,11 +208,11 @@ void ESPNowComponent::enable_() {
esp_wifi_connectionless_module_set_wake_interval(CONFIG_ESPNOW_WAKE_INTERVAL);
#endif
this->state_ = ESPNOW_STATE_ENABLED;
for (auto peer : this->peers_) {
this->add_peer(peer.address);
}
this->state_ = ESPNOW_STATE_ENABLED;
}
void ESPNowComponent::disable() {
@@ -407,7 +407,7 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) {
}
if (memcmp(peer, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
this->status_momentary_warning("peer-add-failed");
this->mark_failed();
return ESP_ERR_INVALID_MAC;
}

View File

@@ -1,10 +1,7 @@
#ifdef USE_HOST
#define USE_HTTP_REQUEST_HOST_H
#define CPPHTTPLIB_NO_EXCEPTIONS
#include "httplib.h"
#include "http_request_host.h"
#ifdef USE_HOST
#include <regex>
#include "esphome/components/network/util.h"
#include "esphome/components/watchdog/watchdog.h"

View File

@@ -1,7 +1,11 @@
#pragma once
#ifdef USE_HOST
#include "http_request.h"
#ifdef USE_HOST
#define CPPHTTPLIB_NO_EXCEPTIONS
#include "httplib.h"
namespace esphome {
namespace http_request {

View File

@@ -3,10 +3,12 @@
/**
* NOTE: This is a copy of httplib.h from https://github.com/yhirose/cpp-httplib
*
* It has been modified to add ifdefs for USE_HOST. While it contains many functions unused in ESPHome,
* It has been modified only to add ifdefs for USE_HOST. While it contains many functions unused in ESPHome,
* it was considered preferable to use it with as few changes as possible, to facilitate future updates.
*/
#include "esphome/core/defines.h"
//
// httplib.h
//
@@ -15,11 +17,6 @@
//
#ifdef USE_HOST
// Prevent this code being included in main.cpp
#ifdef USE_HTTP_REQUEST_HOST_H
#include "esphome/core/defines.h"
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
@@ -9690,6 +9687,5 @@ inline SSL_CTX *Client::ssl_context() const {
#endif
#endif // CPPHTTPLIB_HTTPLIB_H
#endif // USE_HTTP_REQUEST_HOST_H
#endif

View File

@@ -17,8 +17,9 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(LD2450Component),
cv.Optional(CONF_THROTTLE): cv.invalid(
f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead"
cv.Optional(CONF_THROTTLE, default="1000ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(min=cv.TimePeriod(milliseconds=1)),
),
}
)
@@ -45,3 +46,4 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
cg.add(var.set_throttle(config[CONF_THROTTLE]))

View File

@@ -21,17 +21,14 @@ CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_SHIELD_ACCOUNT,
),
cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_MOTION,
filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_TARGET_ACCOUNT,
),
cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_MEDITATION,
),
}

View File

@@ -199,8 +199,9 @@ void LD2450Component::dump_config() {
ESP_LOGCONFIG(TAG,
"LD2450:\n"
" Firmware version: %s\n"
" MAC address: %s",
version.c_str(), mac_str.c_str());
" MAC address: %s\n"
" Throttle: %u ms",
version.c_str(), mac_str.c_str(), this->throttle_);
#ifdef USE_BINARY_SENSOR
ESP_LOGCONFIG(TAG, "Binary Sensors:");
LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_);
@@ -430,6 +431,11 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu
// [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC]
// Header Target 1 Target 2 Target 3 End
void LD2450Component::handle_periodic_data_() {
// Early throttle check - moved before any processing to save CPU cycles
if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) {
return;
}
if (this->buffer_pos_ < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
ESP_LOGE(TAG, "Invalid length");
return;
@@ -440,6 +446,8 @@ void LD2450Component::handle_periodic_data_() {
ESP_LOGE(TAG, "Invalid header/footer");
return;
}
// Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately
this->last_periodic_millis_ = App.get_loop_component_start_time();
int16_t target_count = 0;
int16_t still_target_count = 0;

View File

@@ -110,6 +110,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
void dump_config() override;
void loop() override;
void set_presence_timeout();
void set_throttle(uint16_t value) { this->throttle_ = value; }
void read_all_info();
void query_zone_info();
void restart_and_read_all_info();
@@ -160,9 +161,11 @@ class LD2450Component : public Component, public uart::UARTDevice {
bool get_timeout_status_(uint32_t check_millis);
uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving);
uint32_t last_periodic_millis_ = 0;
uint32_t presence_millis_ = 0;
uint32_t still_presence_millis_ = 0;
uint32_t moving_presence_millis_ = 0;
uint16_t throttle_ = 0;
uint16_t timeout_ = 5;
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0};

View File

@@ -42,43 +42,16 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
icon=ICON_ACCOUNT_GROUP,
accuracy_decimals=0,
),
cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
icon=ICON_HUMAN_GREETING_PROXIMITY,
accuracy_decimals=0,
),
cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
icon=ICON_ACCOUNT_SWITCH,
accuracy_decimals=0,
),
}
)
@@ -89,86 +62,32 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
cv.Optional(CONF_X): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
icon=ICON_ALPHA_X_BOX_OUTLINE,
unit_of_measurement=UNIT_MILLIMETER,
icon=ICON_ALPHA_X_BOX_OUTLINE,
),
cv.Optional(CONF_Y): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
icon=ICON_ALPHA_Y_BOX_OUTLINE,
unit_of_measurement=UNIT_MILLIMETER,
icon=ICON_ALPHA_Y_BOX_OUTLINE,
),
cv.Optional(CONF_SPEED): sensor.sensor_schema(
device_class=DEVICE_CLASS_SPEED,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
icon=ICON_SPEEDOMETER_SLOW,
unit_of_measurement=UNIT_MILLIMETER_PER_SECOND,
icon=ICON_SPEEDOMETER_SLOW,
),
cv.Optional(CONF_ANGLE): sensor.sensor_schema(
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP,
unit_of_measurement=UNIT_DEGREES,
icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP,
),
cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
icon=ICON_MAP_MARKER_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
icon=ICON_MAP_MARKER_DISTANCE,
),
cv.Optional(CONF_RESOLUTION): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
icon=ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE,
unit_of_measurement=UNIT_MILLIMETER,
icon=ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE,
),
}
)
@@ -178,43 +97,16 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
cv.Optional(f"zone_{n + 1}"): cv.Schema(
{
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
icon=ICON_MAP_MARKER_ACCOUNT,
accuracy_decimals=0,
),
cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
icon=ICON_MAP_MARKER_ACCOUNT,
accuracy_decimals=0,
),
cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
icon=ICON_MAP_MARKER_ACCOUNT,
accuracy_decimals=0,
),
}
)

View File

@@ -140,22 +140,12 @@ float LightState::get_setup_priority() const { return setup_priority::HARDWARE -
void LightState::publish_state() { this->remote_values_callback_.call(); }
LightOutput *LightState::get_output() const { return this->output_; }
static constexpr const char *EFFECT_NONE = "None";
static constexpr auto EFFECT_NONE_REF = StringRef::from_lit("None");
std::string LightState::get_effect_name() {
if (this->active_effect_index_ > 0) {
return this->effects_[this->active_effect_index_ - 1]->get_name();
} else {
return "None";
}
return EFFECT_NONE;
}
StringRef LightState::get_effect_name_ref() {
if (this->active_effect_index_ > 0) {
return StringRef(this->effects_[this->active_effect_index_ - 1]->get_name());
}
return EFFECT_NONE_REF;
}
void LightState::add_new_remote_values_callback(std::function<void()> &&send_callback) {

View File

@@ -4,7 +4,6 @@
#include "esphome/core/entity_base.h"
#include "esphome/core/optional.h"
#include "esphome/core/preferences.h"
#include "esphome/core/string_ref.h"
#include "light_call.h"
#include "light_color_values.h"
#include "light_effect.h"
@@ -117,8 +116,6 @@ class LightState : public EntityBase, public Component {
/// Return the name of the current effect, or if no effect is active "None".
std::string get_effect_name();
/// Return the name of the current effect as StringRef (for API usage)
StringRef get_effect_name_ref();
/**
* This lets front-end components subscribe to light change events. This callback is called once

View File

@@ -24,7 +24,7 @@ from ..defines import (
literal,
)
from ..lv_validation import (
lv_angle_degrees,
lv_angle,
lv_bool,
lv_color,
lv_image,
@@ -395,15 +395,15 @@ ARC_PROPS = {
DRAW_OPA_SCHEMA.extend(
{
cv.Required(CONF_RADIUS): pixels,
cv.Required(CONF_START_ANGLE): lv_angle_degrees,
cv.Required(CONF_END_ANGLE): lv_angle_degrees,
cv.Required(CONF_START_ANGLE): lv_angle,
cv.Required(CONF_END_ANGLE): lv_angle,
}
).extend({cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}),
)
async def canvas_draw_arc(config, action_id, template_arg, args):
radius = await size.process(config[CONF_RADIUS])
start_angle = await lv_angle_degrees.process(config[CONF_START_ANGLE])
end_angle = await lv_angle_degrees.process(config[CONF_END_ANGLE])
start_angle = await lv_angle.process(config[CONF_START_ANGLE])
end_angle = await lv_angle.process(config[CONF_END_ANGLE])
async def do_draw_arc(w: Widget, x, y, dsc_addr):
lv.canvas_draw_arc(w.obj, x, y, radius, start_angle, end_angle, dsc_addr)

View File

@@ -14,6 +14,7 @@ from esphome.const import (
CONF_VALUE,
CONF_WIDTH,
)
from esphome.cpp_generator import IntLiteral
from ..automation import action_to_code
from ..defines import (
@@ -31,7 +32,7 @@ from ..helpers import add_lv_use, lvgl_components_required
from ..lv_validation import (
get_end_value,
get_start_value,
lv_angle_degrees,
lv_angle,
lv_bool,
lv_color,
lv_float,
@@ -162,7 +163,7 @@ SCALE_SCHEMA = cv.Schema(
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_,
cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360),
cv.Optional(CONF_ROTATION): lv_angle_degrees,
cv.Optional(CONF_ROTATION): lv_angle,
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
}
)
@@ -187,7 +188,9 @@ class MeterType(WidgetType):
for scale_conf in config.get(CONF_SCALES, ()):
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
if CONF_ROTATION in scale_conf:
rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION])
rotation = await lv_angle.process(scale_conf[CONF_ROTATION])
if isinstance(rotation, IntLiteral):
rotation = int(str(rotation)) // 10
with LocalVariable(
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
) as meter_var:

View File

@@ -764,8 +764,7 @@ void Nextion::process_nextion_commands_() {
variable_name = to_process.substr(0, index);
++index;
// Get variable value without terminating NUL byte. Length check above ensures substr len >= 0.
text_value = to_process.substr(index, to_process_length - index - 1);
text_value = to_process.substr(index);
ESP_LOGN(TAG, "Text sensor: %s='%s'", variable_name.c_str(), text_value.c_str());

View File

@@ -23,18 +23,20 @@ void Pipsolar::loop() {
// Read message
if (this->state_ == STATE_IDLE) {
this->empty_uart_buffer_();
if (this->send_next_command_()) {
// command sent
return;
switch (this->send_next_command_()) {
case 0:
// no command send (empty queue) time to poll
if (millis() - this->last_poll_ > this->update_interval_) {
this->send_next_poll_();
this->last_poll_ = millis();
}
return;
break;
case 1:
// command send
return;
break;
}
if (this->send_next_poll_()) {
// poll sent
return;
}
return;
}
if (this->state_ == STATE_COMMAND_COMPLETE) {
if (this->check_incoming_length_(4)) {
@@ -528,7 +530,7 @@ void Pipsolar::loop() {
// '(00000000000000000000000000000000'
// iterate over all available flag (as not all models have all flags, but at least in the same order)
this->value_warnings_present_ = false;
this->value_faults_present_ = false;
this->value_faults_present_ = true;
for (size_t i = 1; i < strlen(tmp); i++) {
enabled = tmp[i] == '1';
@@ -706,7 +708,6 @@ void Pipsolar::loop() {
return;
}
// crc ok
this->used_polling_commands_[this->last_polling_command_].needs_update = false;
this->state_ = STATE_POLL_CHECKED;
return;
} else {
@@ -787,7 +788,7 @@ uint8_t Pipsolar::check_incoming_crc_() {
}
// send next command used
bool Pipsolar::send_next_command_() {
uint8_t Pipsolar::send_next_command_() {
uint16_t crc16;
if (!this->command_queue_[this->command_queue_position_].empty()) {
const char *command = this->command_queue_[this->command_queue_position_].c_str();
@@ -808,43 +809,37 @@ bool Pipsolar::send_next_command_() {
// end Byte
this->write(0x0D);
ESP_LOGD(TAG, "Sending command from queue: %s with length %d", command, length);
return true;
return 1;
}
return false;
return 0;
}
bool Pipsolar::send_next_poll_() {
void Pipsolar::send_next_poll_() {
uint16_t crc16;
for (uint8_t i = 0; i < POLLING_COMMANDS_MAX; i++) {
this->last_polling_command_ = (this->last_polling_command_ + 1) % POLLING_COMMANDS_MAX;
if (this->used_polling_commands_[this->last_polling_command_].length == 0) {
// not enabled
continue;
}
if (!this->used_polling_commands_[this->last_polling_command_].needs_update) {
// no update requested
continue;
}
this->state_ = STATE_POLL;
this->command_start_millis_ = millis();
this->empty_uart_buffer_();
this->read_pos_ = 0;
crc16 = this->pipsolar_crc_(this->used_polling_commands_[this->last_polling_command_].command,
this->used_polling_commands_[this->last_polling_command_].length);
this->write_array(this->used_polling_commands_[this->last_polling_command_].command,
this->used_polling_commands_[this->last_polling_command_].length);
// checksum
this->write(((uint8_t) ((crc16) >> 8))); // highbyte
this->write(((uint8_t) ((crc16) &0xff))); // lowbyte
// end Byte
this->write(0x0D);
ESP_LOGD(TAG, "Sending polling command : %s with length %d",
this->used_polling_commands_[this->last_polling_command_].command,
this->used_polling_commands_[this->last_polling_command_].length);
return true;
this->last_polling_command_ = (this->last_polling_command_ + 1) % 15;
if (this->used_polling_commands_[this->last_polling_command_].length == 0) {
this->last_polling_command_ = 0;
}
return false;
if (this->used_polling_commands_[this->last_polling_command_].length == 0) {
// no command specified
return;
}
this->state_ = STATE_POLL;
this->command_start_millis_ = millis();
this->empty_uart_buffer_();
this->read_pos_ = 0;
crc16 = this->pipsolar_crc_(this->used_polling_commands_[this->last_polling_command_].command,
this->used_polling_commands_[this->last_polling_command_].length);
this->write_array(this->used_polling_commands_[this->last_polling_command_].command,
this->used_polling_commands_[this->last_polling_command_].length);
// checksum
this->write(((uint8_t) ((crc16) >> 8))); // highbyte
this->write(((uint8_t) ((crc16) &0xff))); // lowbyte
// end Byte
this->write(0x0D);
ESP_LOGD(TAG, "Sending polling command : %s with length %d",
this->used_polling_commands_[this->last_polling_command_].command,
this->used_polling_commands_[this->last_polling_command_].length);
}
void Pipsolar::queue_command_(const char *command, uint8_t length) {
@@ -874,13 +869,7 @@ void Pipsolar::dump_config() {
}
}
}
void Pipsolar::update() {
for (auto &used_polling_command : this->used_polling_commands_) {
if (used_polling_command.length != 0) {
used_polling_command.needs_update = true;
}
}
}
void Pipsolar::update() {}
void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand polling_command) {
for (auto &used_polling_command : this->used_polling_commands_) {
@@ -902,7 +891,6 @@ void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand poll
used_polling_command.errors = 0;
used_polling_command.identifier = polling_command;
used_polling_command.length = length - 1;
used_polling_command.needs_update = true;
return;
}
}

View File

@@ -25,7 +25,6 @@ struct PollingCommand {
uint8_t length = 0;
uint8_t errors;
ENUMPollingCommand identifier;
bool needs_update;
};
#define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \
@@ -190,14 +189,14 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 110; // maximum supported answer length
static const size_t COMMAND_QUEUE_LENGTH = 10;
static const size_t COMMAND_TIMEOUT = 5000;
static const size_t POLLING_COMMANDS_MAX = 15;
uint32_t last_poll_ = 0;
void add_polling_command_(const char *command, ENUMPollingCommand polling_command);
void empty_uart_buffer_();
uint8_t check_incoming_crc_();
uint8_t check_incoming_length_(uint8_t length);
uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len);
bool send_next_command_();
bool send_next_poll_();
uint8_t send_next_command_();
void send_next_poll_();
void queue_command_(const char *command, uint8_t length);
std::string command_queue_[COMMAND_QUEUE_LENGTH];
uint8_t command_queue_position_ = 0;
@@ -217,7 +216,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
};
uint8_t last_polling_command_ = 0;
PollingCommand used_polling_commands_[POLLING_COMMANDS_MAX];
PollingCommand used_polling_commands_[15];
};
} // namespace pipsolar

View File

@@ -16,7 +16,6 @@ from esphome.components.esp32.const import (
import esphome.config_validation as cv
from esphome.const import (
CONF_ADVANCED,
CONF_DISABLED,
CONF_FRAMEWORK,
CONF_ID,
CONF_MODE,
@@ -103,7 +102,6 @@ def get_config_schema(config):
cv.Optional(CONF_MODE, default=modes[0]): cv.one_of(*modes, lower=True),
cv.Optional(CONF_ENABLE_ECC, default=False): cv.boolean,
cv.Optional(CONF_SPEED, default=speeds[0]): cv.one_of(*speeds, upper=True),
cv.Optional(CONF_DISABLED, default=False): cv.boolean,
}
)(config)
@@ -114,8 +112,6 @@ FINAL_VALIDATE_SCHEMA = validate_psram_mode
async def to_code(config):
if config[CONF_DISABLED]:
return
if CORE.using_arduino:
cg.add_build_flag("-DBOARD_HAS_PSRAM")
if config[CONF_MODE] == TYPE_OCTAL:

View File

@@ -46,32 +46,10 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t
}
this->connection_established_ = true;
this->char_handle_ = chr->handle;
// Attempt to write immediately
// For devices without security, this will work
// For devices with security that are already paired, this will work
// For devices that need pairing, the write will be retried after auth completes
this->sync_time_and_display_();
break;
}
default:
break;
}
}
void PVVXDisplay::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) {
case ESP_GAP_BLE_AUTH_CMPL_EVT: {
if (!this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr))
return;
if (param->ble_security.auth_cmpl.success) {
ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str().c_str());
// Now that pairing is complete, perform the pending writes
this->sync_time_and_display_();
} else {
ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str().c_str());
}
#ifdef USE_TIME
this->sync_time_();
#endif
this->display();
break;
}
default:
@@ -149,13 +127,6 @@ void PVVXDisplay::delayed_disconnect_() {
this->set_timeout("disconnect", this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); });
}
void PVVXDisplay::sync_time_and_display_() {
#ifdef USE_TIME
this->sync_time_();
#endif
this->display();
}
#ifdef USE_TIME
void PVVXDisplay::sync_time_() {
if (this->time_ == nullptr)

View File

@@ -43,7 +43,6 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent {
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
/// Set validity period of the display information in seconds (1..65535)
void set_validity_period(uint16_t validity_period) { this->validity_period_ = validity_period; }
@@ -113,7 +112,6 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent {
void setcfgbit_(uint8_t bit, bool value);
void send_to_setup_char_(uint8_t *blk, size_t size);
void delayed_disconnect_();
void sync_time_and_display_();
#ifdef USE_TIME
void sync_time_();
time::RealTimeClock *time_{nullptr};

View File

@@ -110,10 +110,10 @@ static uint8_t find_nearest_index(float value, const float *arr, int size) {
* @param value The float value to convert.
* @param bytes The byte array to store the converted value.
*/
static void float_to_bytes(float value, unsigned char *bytes) {
static void float_to_bytes(float value, uint8_t *bytes) {
union {
float float_value;
unsigned char byte_array[4];
uint8_t byte_array[4];
} u;
u.float_value = value;
@@ -128,7 +128,7 @@ static void float_to_bytes(float value, unsigned char *bytes) {
* @param value The 32-bit unsigned integer to convert.
* @param bytes The byte array to store the converted value.
*/
static void int_to_bytes(uint32_t value, unsigned char *bytes) {
static void int_to_bytes(uint32_t value, uint8_t *bytes) {
bytes[0] = value & 0xFF;
bytes[1] = (value >> 8) & 0xFF;
bytes[2] = (value >> 16) & 0xFF;

View File

@@ -53,14 +53,10 @@ void SenseAirComponent::update() {
this->status_clear_warning();
const uint8_t length = response[2];
const uint16_t status = encode_uint16(response[3], response[4]);
const uint16_t ppm = encode_uint16(response[length + 1], response[length + 2]);
const uint16_t status = (uint16_t(response[3]) << 8) | response[4];
const int16_t ppm = int16_t((response[length + 1] << 8) | response[length + 2]);
ESP_LOGD(TAG, "SenseAir Received CO₂=%uppm Status=0x%02X", ppm, status);
if (ppm == 0 && (status & SenseAirStatus::OUT_OF_RANGE_ERROR) != 0) {
ESP_LOGD(TAG, "Discarding 0 ppm reading with out-of-range status.");
return;
}
ESP_LOGD(TAG, "SenseAir Received CO₂=%dppm Status=0x%02X", ppm, status);
if (this->co2_sensor_ != nullptr)
this->co2_sensor_->publish_state(ppm);
}

View File

@@ -8,17 +8,6 @@
namespace esphome {
namespace senseair {
enum SenseAirStatus : uint8_t {
FATAL_ERROR = 1 << 0,
OFFSET_ERROR = 1 << 1,
ALGORITHM_ERROR = 1 << 2,
OUTPUT_ERROR = 1 << 3,
SELF_DIAGNOSTIC_ERROR = 1 << 4,
OUT_OF_RANGE_ERROR = 1 << 5,
MEMORY_ERROR = 1 << 6,
RESERVED = 1 << 7
};
class SenseAirComponent : public PollingComponent, public uart::UARTDevice {
public:
void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; }

View File

@@ -4,7 +4,7 @@ namespace esphome {
namespace tm1638 {
namespace TM1638Translation {
const unsigned char SEVEN_SEG[] PROGMEM = {
const uint8_t SEVEN_SEG[] PROGMEM = {
0x00, /* (space) */
0x86, /* ! */
0x22, /* " */

View File

@@ -52,9 +52,9 @@ def default_url(config: ConfigType) -> ConfigType:
config = config.copy()
if config[CONF_VERSION] == 1:
if CONF_CSS_URL not in config:
config[CONF_CSS_URL] = "https://oi.esphome.io/v1/webserver-v1.min.css"
config[CONF_CSS_URL] = "https://esphome.io/_static/webserver-v1.min.css"
if CONF_JS_URL not in config:
config[CONF_JS_URL] = "https://oi.esphome.io/v1/webserver-v1.min.js"
config[CONF_JS_URL] = "https://esphome.io/_static/webserver-v1.min.js"
if config[CONF_VERSION] == 2:
if CONF_CSS_URL not in config:
config[CONF_CSS_URL] = ""

View File

@@ -813,7 +813,7 @@ std::string WebServer::cover_state_json_generator(WebServer *web_server, void *s
return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE);
}
std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) {
return web_server->cover_json((cover::Cover *) (source), DETAIL_ALL);
return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE);
}
std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
return json::build_json([this, obj, start_config](JsonObject root) {

View File

@@ -173,14 +173,14 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
#if USE_WEBSERVER_VERSION == 1
/** Set the URL to the CSS <link> that's sent to each client. Defaults to
* https://oi.esphome.io/v1/webserver-v1.min.css
* https://esphome.io/_static/webserver-v1.min.css
*
* @param css_url The url to the web server stylesheet.
*/
void set_css_url(const char *css_url);
/** Set the URL to the script that's embedded in the index page. Defaults to
* https://oi.esphome.io/v1/webserver-v1.min.js
* https://esphome.io/_static/webserver-v1.min.js
*
* @param js_url The url to the web server script.
*/

View File

@@ -253,7 +253,7 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest.get()), n, &out,
reinterpret_cast<const uint8_t *>(user_info.c_str()), user_info.size());
return strcmp(digest.get(), auth_str + auth_prefix_len) == 0;
return strncmp(digest.get(), auth_str + auth_prefix_len, auth.value().size() - auth_prefix_len) == 0;
}
void AsyncWebServerRequest::requestAuthentication(const char *realm) const {

View File

@@ -375,16 +375,11 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
# Track if any network uses Enterprise authentication
has_eap = False
def add_sta(ap, network):
ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
cg.add(var.add_sta(wifi_network(network, ap, ip_config)))
for network in config.get(CONF_NETWORKS, []):
if CONF_EAP in network:
has_eap = True
cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
if CONF_AP in config:
@@ -401,10 +396,6 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False)
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
# Disable Enterprise WiFi support if no EAP is configured
if CORE.is_esp32 and CORE.using_esp_idf and not has_eap:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", False)
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
cg.add(var.set_fast_connect(config[CONF_FAST_CONNECT]))

View File

@@ -393,13 +393,10 @@ def icon(value):
)
def sub_device_id(value: str | None) -> core.ID | None:
def sub_device_id(value: str | None) -> core.ID:
# Lazy import to avoid circular imports
from esphome.core.config import Device
if not value:
return None
return use_id(Device)(value)

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2025.8.1"
__version__ = "2025.8.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -803,10 +803,6 @@ class EsphomeCore:
raise TypeError(
f"Library {library} must be instance of Library, not {type(library)}"
)
if not library.name:
raise ValueError(f"The library for {library.repository} must have a name")
short_name = (
library.name if "/" not in library.name else library.name.split("/")[-1]
)

View File

@@ -10,7 +10,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/scheduler.h"
#include "esphome/core/string_ref.h"
#ifdef USE_DEVICES
#include "esphome/core/device.h"
@@ -249,8 +248,6 @@ class Application {
bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; }
std::string get_compilation_time() const { return this->compilation_time_; }
/// Get the compilation time as StringRef (for API usage)
StringRef get_compilation_time_ref() const { return StringRef(this->compilation_time_); }
/// Get the cached time in milliseconds from when the current component started its loop execution
inline uint32_t IRAM_ATTR HOT get_loop_component_start_time() const { return this->loop_component_start_time_; }

View File

@@ -5,8 +5,6 @@
#include "esphome/core/hal.h"
#include "esphome/core/defines.h"
#include "esphome/core/preferences.h"
#include "esphome/core/scheduler.h"
#include "esphome/core/application.h"
#include <vector>
@@ -160,16 +158,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
void play_complex(Ts... x) override {
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
this->num_running_++;
// If num_running_ > 1, we have multiple instances running in parallel
// In single/restart/queued modes, only one instance runs at a time
// Parallel mode uses skip_cancel=true to allow multiple delays to coexist
// WARNING: This can accumulate delays if scripts are triggered faster than they complete!
// Users should set max_runs on parallel scripts to limit concurrent executions.
// Issue #10264: This is a workaround for parallel script delays interfering with each other.
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT,
/* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f),
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
this->set_timeout("delay", this->delay_.value(x...), f);
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }

View File

@@ -154,7 +154,6 @@
#define USE_ESP32_BLE_CLIENT
#define USE_ESP32_BLE_DEVICE
#define USE_ESP32_BLE_SERVER
#define USE_ESP32_BLE_UUID
#define USE_ESP32_BLE_ADVERTISING
#define USE_I2C
#define USE_IMPROV

View File

@@ -77,8 +77,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
"""
# Get device info
device_name: str | None = None
device_id_obj: ID | None
if device_id_obj := config.get(CONF_DEVICE_ID):
if CONF_DEVICE_ID in config:
device_id_obj: ID = config[CONF_DEVICE_ID]
device: MockObj = await get_variable(device_id_obj)
add(var.set_device(device))
# Get device name for object ID calculation
@@ -199,8 +199,8 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
# Get device name if entity is on a sub-device
device_name = None
device_id = "" # Empty string for main device
device_id_obj: ID | None
if device_id_obj := config.get(CONF_DEVICE_ID):
if CONF_DEVICE_ID in config:
device_id_obj = config[CONF_DEVICE_ID]
device_name = device_id_obj.id
# Use the device ID string directly for uniqueness
device_id = device_id_obj.id
@@ -236,21 +236,10 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
if existing_component != "unknown":
conflict_msg += f" from component '{existing_component}'"
# Show both original names and their ASCII-only versions if they differ
sanitized_msg = ""
if entity_name != existing_name:
sanitized_msg = (
f"\n Original names: '{entity_name}' and '{existing_name}'"
f"\n Both convert to ASCII ID: '{name_key}'"
"\n To fix: Add unique ASCII characters (e.g., '1', '2', or 'A', 'B')"
"\n to distinguish them"
)
raise cv.Invalid(
f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. "
f"{conflict_msg}. "
"Each entity on a device must have a unique name within its platform."
f"{sanitized_msg}"
f"Each entity on a device must have a unique name within its platform."
)
# Store metadata about this entity

View File

@@ -65,17 +65,14 @@ static void validate_static_string(const char *name) {
// Common implementation for both timeout and interval
void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string,
const void *name_ptr, uint32_t delay, std::function<void()> func, bool is_retry,
bool skip_cancel) {
const void *name_ptr, uint32_t delay, std::function<void()> func, bool is_retry) {
// Get the name as const char*
const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
if (delay == SCHEDULER_DONT_RUN) {
// Still need to cancel existing timer if name is not empty
if (!skip_cancel) {
LockGuard guard{this->lock_};
this->cancel_item_locked_(component, name_cstr, type);
}
LockGuard guard{this->lock_};
this->cancel_item_locked_(component, name_cstr, type);
return;
}
@@ -85,13 +82,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
item->set_name(name_cstr, !is_static_string);
item->type = type;
item->callback = std::move(func);
// Initialize remove to false (though it should already be from constructor)
// Not using mark_item_removed_ helper since we're setting to false, not true
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
item->remove.store(false, std::memory_order_relaxed);
#else
item->remove = false;
#endif
item->is_retry = is_retry;
#ifndef ESPHOME_THREAD_SINGLE
@@ -100,9 +91,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
if (delay == 0 && type == SchedulerItem::TIMEOUT) {
// Put in defer queue for guaranteed FIFO execution
LockGuard guard{this->lock_};
if (!skip_cancel) {
this->cancel_item_locked_(component, name_cstr, type);
}
this->cancel_item_locked_(component, name_cstr, type);
this->defer_queue_.push_back(std::move(item));
return;
}
@@ -155,11 +144,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
return;
}
// If name is provided, do atomic cancel-and-add (unless skip_cancel is true)
// If name is provided, do atomic cancel-and-add
// Cancel existing items
if (!skip_cancel) {
this->cancel_item_locked_(component, name_cstr, type);
}
this->cancel_item_locked_(component, name_cstr, type);
// Add new item directly to to_add_
// since we have the lock held
this->to_add_.push_back(std::move(item));
@@ -411,31 +398,6 @@ void HOT Scheduler::call(uint32_t now) {
this->pop_raw_();
continue;
}
// Check if item is marked for removal
// This handles two cases:
// 1. Item was marked for removal after cleanup_() but before we got here
// 2. Item is marked for removal but wasn't at the front of the heap during cleanup_()
#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS
// Multi-threaded platforms without atomics: must take lock to safely read remove flag
{
LockGuard guard{this->lock_};
if (is_item_removed_(item.get())) {
this->pop_raw_();
this->to_remove_--;
continue;
}
}
#else
// Single-threaded or multi-threaded with atomics: can check without lock
if (is_item_removed_(item.get())) {
LockGuard guard{this->lock_};
this->pop_raw_();
this->to_remove_--;
continue;
}
#endif
#ifdef ESPHOME_DEBUG_SCHEDULER
const char *item_name = item->get_name();
ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
@@ -556,7 +518,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
if (type == SchedulerItem::TIMEOUT) {
for (auto &item : this->defer_queue_) {
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
this->mark_item_removed_(item.get());
item->remove = true;
total_cancelled++;
}
}
@@ -566,7 +528,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
// Cancel items in the main heap
for (auto &item : this->items_) {
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
this->mark_item_removed_(item.get());
item->remove = true;
total_cancelled++;
this->to_remove_++; // Track removals for heap items
}
@@ -575,7 +537,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
// Cancel items in to_add_
for (auto &item : this->to_add_) {
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
this->mark_item_removed_(item.get());
item->remove = true;
total_cancelled++;
// Don't track removals for to_add_ items
}

View File

@@ -21,13 +21,8 @@ struct RetryArgs;
void retry_handler(const std::shared_ptr<RetryArgs> &args);
class Scheduler {
// Allow retry_handler to access protected members for internal retry mechanism
// Allow retry_handler to access protected members
friend void ::esphome::retry_handler(const std::shared_ptr<RetryArgs> &args);
// Allow DelayAction to call set_timer_common_ with skip_cancel=true for parallel script delays.
// This is needed to fix issue #10264 where parallel scripts with delays interfere with each other.
// We use friend instead of a public API because skip_cancel is dangerous - it can cause delays
// to accumulate and overload the scheduler if misused.
template<typename... Ts> friend class DelayAction;
public:
// Public API - accepts std::string for backward compatibility
@@ -102,42 +97,22 @@ class Scheduler {
std::function<void()> callback;
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Multi-threaded with atomics: use atomic for lock-free access
// Place atomic<bool> separately since it can't be packed with bit fields
std::atomic<bool> remove{false};
// Bit-packed fields (3 bits used, 5 bits padding in 1 byte)
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[])
bool is_retry : 1; // True if this is a retry timeout
// 5 bits padding
#else
// Single-threaded or multi-threaded without atomics: can pack all fields together
// Bit-packed fields (4 bits used, 4 bits padding in 1 byte)
// Bit-packed fields to minimize padding
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
bool remove : 1;
bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[])
bool is_retry : 1; // True if this is a retry timeout
// 4 bits padding
#endif
// 4 bits padding
// Constructor
SchedulerItem()
: component(nullptr),
interval(0),
next_execution_(0),
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// remove is initialized in the member declaration as std::atomic<bool>{false}
type(TIMEOUT),
name_is_dynamic(false),
is_retry(false) {
#else
type(TIMEOUT),
remove(false),
name_is_dynamic(false),
is_retry(false) {
#endif
name_.static_name = nullptr;
}
@@ -189,7 +164,7 @@ class Scheduler {
// Common implementation for both timeout and interval
void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr,
uint32_t delay, std::function<void()> func, bool is_retry = false, bool skip_cancel = false);
uint32_t delay, std::function<void()> func, bool is_retry = false);
// Common implementation for retry
void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time,
@@ -244,37 +219,6 @@ class Scheduler {
return item->remove || (item->component != nullptr && item->component->is_failed());
}
// Helper to check if item is marked for removal (platform-specific)
// Returns true if item should be skipped, handles platform-specific synchronization
// For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
// function.
bool is_item_removed_(SchedulerItem *item) const {
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Multi-threaded with atomics: use atomic load for lock-free access
return item->remove.load(std::memory_order_acquire);
#else
// Single-threaded (ESPHOME_THREAD_SINGLE) or
// multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct read
// For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
return item->remove;
#endif
}
// Helper to mark item for removal (platform-specific)
// For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
// function.
void mark_item_removed_(SchedulerItem *item) {
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Multi-threaded with atomics: use atomic store
item->remove.store(true, std::memory_order_release);
#else
// Single-threaded (ESPHOME_THREAD_SINGLE) or
// multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write
// For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
item->remove = true;
#endif
}
// Template helper to check if any item in a container matches our criteria
template<typename Container>
bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr,

View File

@@ -80,16 +80,13 @@ def replace_file_content(text, pattern, repl):
return content_new, count
def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool:
def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool:
if old is None:
return True
if old.src_version != new.src_version:
return True
if old.build_path != new.build_path:
return True
# Check if any components have been removed
return bool(old.loaded_integrations - new.loaded_integrations)
return old.build_path != new.build_path
def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
@@ -103,7 +100,7 @@ def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> boo
return False
def update_storage_json() -> None:
def update_storage_json():
path = storage_path()
old = StorageJSON.load(path)
new = StorageJSON.from_esphome_core(CORE, old)
@@ -111,14 +108,7 @@ def update_storage_json() -> None:
return
if storage_should_clean(old, new):
if old is not None and old.loaded_integrations - new.loaded_integrations:
removed = old.loaded_integrations - new.loaded_integrations
_LOGGER.info(
"Components removed (%s), cleaning build files...",
", ".join(sorted(removed)),
)
else:
_LOGGER.info("Core config or version changed, cleaning build files...")
_LOGGER.info("Core config, version changed, cleaning build files...")
clean_build()
elif storage_should_update_cmake_cache(old, new):
_LOGGER.info("Integrations changed, cleaning cmake cache...")

View File

@@ -11,8 +11,8 @@ pyserial==3.5
platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.0.2
click==8.1.7
esphome-dashboard==20250814.0
aioesphomeapi==39.0.0
esphome-dashboard==20250514.0
aioesphomeapi==38.2.1
zeroconf==0.147.0
puremagic==1.30
ruamel.yaml==0.18.14 # dashboard_import

View File

@@ -139,24 +139,9 @@ def _get_changed_files_github_actions() -> list[str] | None:
if event_name == "pull_request":
pr_number = _get_pr_number_from_github_env()
if pr_number:
# Try gh pr diff first (faster for small PRs)
# Use GitHub CLI to get changed files directly
cmd = ["gh", "pr", "diff", pr_number, "--name-only"]
try:
return _get_changed_files_from_command(cmd)
except Exception as e:
# If it fails due to the 300 file limit, use the API method
if "maximum" in str(e) and "files" in str(e):
cmd = [
"gh",
"api",
f"repos/esphome/esphome/pulls/{pr_number}/files",
"--paginate",
"--jq",
".[].filename",
]
return _get_changed_files_from_command(cmd)
# Re-raise for other errors
raise
return _get_changed_files_from_command(cmd)
# For pushes (including squash-and-merge)
elif event_name == "push":

View File

@@ -1,5 +0,0 @@
substitutions:
wakeup_pin: GPIO4
<<: !include common.yaml
<<: !include common-esp32.yaml

View File

@@ -1,5 +0,0 @@
substitutions:
wakeup_pin: GPIO4
<<: !include common.yaml
<<: !include common-esp32.yaml

View File

@@ -1,5 +0,0 @@
substitutions:
wakeup_pin: GPIO4
<<: !include common.yaml
<<: !include common-esp32.yaml

View File

@@ -1,3 +0,0 @@
esp32_ble_tracker:
on_scan_end:
- logger.log: "Scan ended!"

View File

@@ -9,6 +9,7 @@ uart:
ld2450:
- id: ld2450_radar
uart_id: ld2450_uart
throttle: 1000ms
button:
- platform: ld2450

View File

@@ -55,12 +55,6 @@ sensor:
lambda: return 4.0;
update_interval: 0.1s
- platform: template
name: Living Room Sensor
device_id: ""
lambda: return 5.0;
update_interval: 0.1s
# Switches with the same name on different devices to test device_id lookup
switch:
# Switch with no device_id (defaults to 0)
@@ -102,23 +96,3 @@ switch:
- logger.log: "Turning on Test Switch on Motion Detector"
turn_off_action:
- logger.log: "Turning off Test Switch on Motion Detector"
- platform: template
name: Living Room Blank Switch
device_id: ""
id: test_switch_blank_living_room
optimistic: true
turn_on_action:
- logger.log: "Turning on Living Room Blank Switch"
turn_off_action:
- logger.log: "Turning off Living Room Blank Switch"
- platform: template
name: Living Room None Switch
device_id:
id: test_switch_none_living_room
optimistic: true
turn_on_action:
- logger.log: "Turning on Living Room None Switch"
turn_off_action:
- logger.log: "Turning off Living Room None Switch"

View File

@@ -56,29 +56,10 @@ light:
warm_white_color_temperature: 2000 K
constant_brightness: true
effects:
# Use default parameters:
- random:
# Customize parameters - use longer names to potentially trigger buffer issues
- random:
name: "My Very Slow Random Effect With Long Name"
transition_length: 30ms
update_interval: 30ms
- random:
name: "My Fast Random Effect That Changes Quickly"
transition_length: 4ms
update_interval: 5ms
- random:
name: "Random Effect With Medium Length Name Here"
name: "Random Effect"
transition_length: 100ms
update_interval: 200ms
- random:
name: "Another Random Effect With Different Parameters"
transition_length: 2ms
update_interval: 3ms
- random:
name: "Yet Another Random Effect To Test Memory"
transition_length: 15ms
update_interval: 20ms
- strobe:
name: "Strobe Effect"
- pulse:
@@ -92,17 +73,6 @@ light:
red: test_red
green: test_green
blue: test_blue
effects:
# Same random effects to test for cross-contamination
- random:
- random:
name: "RGB Slow Random"
transition_length: 20ms
update_interval: 25ms
- random:
name: "RGB Fast Random"
transition_length: 2ms
update_interval: 3ms
- platform: binary
name: "Test Binary Light"

View File

@@ -1,45 +0,0 @@
esphome:
name: test-parallel-delays
host:
logger:
level: DEBUG
api:
actions:
- action: test_parallel_delays
then:
# Start three parallel script instances with small delays between starts
- globals.set:
id: instance_counter
value: '1'
- script.execute: parallel_delay_script
- delay: 10ms
- globals.set:
id: instance_counter
value: '2'
- script.execute: parallel_delay_script
- delay: 10ms
- globals.set:
id: instance_counter
value: '3'
- script.execute: parallel_delay_script
globals:
- id: instance_counter
type: int
initial_value: '0'
script:
- id: parallel_delay_script
mode: parallel
then:
- lambda: !lambda |-
int instance = id(instance_counter);
ESP_LOGI("TEST", "Parallel script instance %d started", instance);
- delay: 1s
- lambda: !lambda |-
static int completed_counter = 0;
completed_counter++;
ESP_LOGI("TEST", "Parallel script instance %d completed after delay", completed_counter);

View File

@@ -1,139 +0,0 @@
esphome:
name: scheduler-removed-item-race
host:
api:
services:
- service: run_test
then:
- script.execute: run_test_script
logger:
level: DEBUG
globals:
- id: test_passed
type: bool
initial_value: 'true'
- id: removed_item_executed
type: int
initial_value: '0'
- id: normal_item_executed
type: int
initial_value: '0'
sensor:
- platform: template
id: test_sensor
name: "Test Sensor"
update_interval: never
lambda: return 0.0;
script:
- id: run_test_script
then:
- logger.log: "=== Starting Removed Item Race Test ==="
# This test creates a scenario where:
# 1. First item in heap is NOT cancelled (cleanup stops immediately)
# 2. Items behind it ARE cancelled (remain in heap after cleanup)
# 3. All items execute at the same time, including cancelled ones
- lambda: |-
// The key to hitting the race:
// 1. Add items in a specific order to control heap structure
// 2. Cancel ONLY items that won't be at the front
// 3. Ensure the first item stays non-cancelled so cleanup_() stops immediately
// Schedule all items to execute at the SAME time (1ms from now)
// Using 1ms instead of 0 to avoid defer queue on multi-core platforms
// This ensures they'll all be ready together and go through the heap
const uint32_t exec_time = 1;
// CRITICAL: Add a non-cancellable item FIRST
// This will be at the front of the heap and block cleanup_()
App.scheduler.set_timeout(id(test_sensor), "blocker", exec_time, []() {
ESP_LOGD("test", "Blocker timeout executed (expected) - was at front of heap");
id(normal_item_executed)++;
});
// Now add items that we WILL cancel
// These will be behind the blocker in the heap
App.scheduler.set_timeout(id(test_sensor), "cancel_1", exec_time, []() {
ESP_LOGE("test", "RACE: Cancelled timeout 1 executed after being cancelled!");
id(removed_item_executed)++;
id(test_passed) = false;
});
App.scheduler.set_timeout(id(test_sensor), "cancel_2", exec_time, []() {
ESP_LOGE("test", "RACE: Cancelled timeout 2 executed after being cancelled!");
id(removed_item_executed)++;
id(test_passed) = false;
});
App.scheduler.set_timeout(id(test_sensor), "cancel_3", exec_time, []() {
ESP_LOGE("test", "RACE: Cancelled timeout 3 executed after being cancelled!");
id(removed_item_executed)++;
id(test_passed) = false;
});
// Add some more normal items
App.scheduler.set_timeout(id(test_sensor), "normal_1", exec_time, []() {
ESP_LOGD("test", "Normal timeout 1 executed (expected)");
id(normal_item_executed)++;
});
App.scheduler.set_timeout(id(test_sensor), "normal_2", exec_time, []() {
ESP_LOGD("test", "Normal timeout 2 executed (expected)");
id(normal_item_executed)++;
});
App.scheduler.set_timeout(id(test_sensor), "normal_3", exec_time, []() {
ESP_LOGD("test", "Normal timeout 3 executed (expected)");
id(normal_item_executed)++;
});
// Force items into the heap before cancelling
App.scheduler.process_to_add();
// NOW cancel the items - they're behind "blocker" in the heap
// When cleanup_() runs, it will see "blocker" (not removed) at the front
// and stop immediately, leaving cancel_1, cancel_2, cancel_3 in the heap
bool c1 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_1");
bool c2 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_2");
bool c3 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_3");
ESP_LOGD("test", "Cancelled items (behind blocker): %s, %s, %s",
c1 ? "true" : "false",
c2 ? "true" : "false",
c3 ? "true" : "false");
// The heap now has:
// - "blocker" at front (not cancelled)
// - cancelled items behind it (marked remove=true but still in heap)
// - When all execute at once, cleanup_() stops at "blocker"
// - The loop then executes ALL ready items including cancelled ones
ESP_LOGD("test", "Setup complete. Blocker at front prevents cleanup of cancelled items behind it");
# Wait for all timeouts to execute (or not)
- delay: 20ms
# Check results
- lambda: |-
ESP_LOGI("test", "=== Test Results ===");
ESP_LOGI("test", "Normal items executed: %d (expected 4)", id(normal_item_executed));
ESP_LOGI("test", "Removed items executed: %d (expected 0)", id(removed_item_executed));
if (id(removed_item_executed) > 0) {
ESP_LOGE("test", "TEST FAILED: %d cancelled items were executed!", id(removed_item_executed));
id(test_passed) = false;
} else if (id(normal_item_executed) != 4) {
ESP_LOGE("test", "TEST FAILED: Expected 4 normal items, got %d", id(normal_item_executed));
id(test_passed) = false;
} else {
ESP_LOGI("test", "TEST PASSED: No cancelled items were executed");
}
ESP_LOGI("test", "=== Test Complete ===");

View File

@@ -132,7 +132,6 @@ async def test_areas_and_devices(
"Temperature Sensor Reading": temp_sensor.device_id,
"Motion Detector Status": motion_detector.device_id,
"Smart Switch Power": smart_switch.device_id,
"Living Room Sensor": 0, # Main device
}
for entity in sensor_entities:
@@ -161,18 +160,6 @@ async def test_areas_and_devices(
"Should have a switch with device_id 0 (main device)"
)
# Verify extra switches with blank and none device_id are correctly available
extra_switches = [
e for e in switch_entities if e.name.startswith("Living Room")
]
assert len(extra_switches) == 2, (
f"Expected 2 extra switches for Living Room, got {len(extra_switches)}"
)
extra_switch_device_ids = [e.device_id for e in extra_switches]
assert all(d == 0 for d in extra_switch_device_ids), (
"All extra switches should have device_id 0 (main device)"
)
# Wait for initial states to be received for all switches
await asyncio.wait_for(initial_states_future, timeout=2.0)

View File

@@ -89,73 +89,3 @@ async def test_delay_action_cancellation(
assert 0.4 < time_from_second_start < 0.6, (
f"Delay completed {time_from_second_start:.3f}s after second start, expected ~0.5s"
)
@pytest.mark.asyncio
async def test_parallel_script_delays(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that parallel scripts with delays don't interfere with each other."""
loop = asyncio.get_running_loop()
# Track script executions
script_starts: list[float] = []
script_ends: list[float] = []
# Patterns to match
start_pattern = re.compile(r"Parallel script instance \d+ started")
end_pattern = re.compile(r"Parallel script instance \d+ completed after delay")
# Future to track when all scripts have completed
all_scripts_completed = loop.create_future()
def check_output(line: str) -> None:
"""Check log output for parallel script messages."""
current_time = loop.time()
if start_pattern.search(line):
script_starts.append(current_time)
if end_pattern.search(line):
script_ends.append(current_time)
# Check if we have all 3 completions
if len(script_ends) == 3 and not all_scripts_completed.done():
all_scripts_completed.set_result(True)
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Get services
entities, services = await client.list_entities_services()
# Find our test service
test_service = next(
(s for s in services if s.name == "test_parallel_delays"), None
)
assert test_service is not None, "test_parallel_delays service not found"
# Execute the test - this will start 3 parallel scripts with 1 second delays
client.execute_service(test_service, {})
# Wait for all scripts to complete (should take ~1 second, not 3)
await asyncio.wait_for(all_scripts_completed, timeout=2.0)
# Verify we had 3 starts and 3 ends
assert len(script_starts) == 3, (
f"Expected 3 script starts, got {len(script_starts)}"
)
assert len(script_ends) == 3, f"Expected 3 script ends, got {len(script_ends)}"
# Verify they ran in parallel - all should complete within ~1.5 seconds
first_start = min(script_starts)
last_end = max(script_ends)
total_time = last_end - first_start
# If running in parallel, total time should be close to 1 second
# If they were interfering (running sequentially), it would take 3+ seconds
assert total_time < 1.5, (
f"Parallel scripts took {total_time:.2f}s total, should be ~1s if running in parallel"
)

View File

@@ -108,51 +108,14 @@ async def test_light_calls(
# Wait for flash to end
state = await wait_for_state_change(rgbcw_light.key)
# Test 13: effect only - test all random effects
# Test 13: effect only
# First ensure light is on
client.light_command(key=rgbcw_light.key, state=True)
state = await wait_for_state_change(rgbcw_light.key)
# Test 13a: Default random effect (no name, gets default name "Random")
client.light_command(key=rgbcw_light.key, effect="Random")
# Now set effect
client.light_command(key=rgbcw_light.key, effect="Random Effect")
state = await wait_for_state_change(rgbcw_light.key)
assert state.effect == "Random"
# Test 13b: Slow random effect with long name
client.light_command(
key=rgbcw_light.key, effect="My Very Slow Random Effect With Long Name"
)
state = await wait_for_state_change(rgbcw_light.key)
assert state.effect == "My Very Slow Random Effect With Long Name"
# Test 13c: Fast random effect with long name
client.light_command(
key=rgbcw_light.key, effect="My Fast Random Effect That Changes Quickly"
)
state = await wait_for_state_change(rgbcw_light.key)
assert state.effect == "My Fast Random Effect That Changes Quickly"
# Test 13d: Random effect with medium length name
client.light_command(
key=rgbcw_light.key, effect="Random Effect With Medium Length Name Here"
)
state = await wait_for_state_change(rgbcw_light.key)
assert state.effect == "Random Effect With Medium Length Name Here"
# Test 13e: Another random effect
client.light_command(
key=rgbcw_light.key,
effect="Another Random Effect With Different Parameters",
)
state = await wait_for_state_change(rgbcw_light.key)
assert state.effect == "Another Random Effect With Different Parameters"
# Test 13f: Yet another random effect
client.light_command(
key=rgbcw_light.key, effect="Yet Another Random Effect To Test Memory"
)
state = await wait_for_state_change(rgbcw_light.key)
assert state.effect == "Yet Another Random Effect To Test Memory"
assert state.effect == "Random Effect"
# Test 14: stop effect
client.light_command(key=rgbcw_light.key, effect="None")

View File

@@ -1,102 +0,0 @@
"""Test for scheduler race condition where removed items still execute."""
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_scheduler_removed_item_race(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that items marked for removal don't execute.
This test verifies the fix for a race condition where:
1. cleanup_() only removes items from the front of the heap
2. Items in the middle of the heap marked for removal still execute
3. This causes cancelled timeouts to run when they shouldn't
"""
loop = asyncio.get_running_loop()
test_complete_future: asyncio.Future[bool] = loop.create_future()
# Track test results
test_passed = False
removed_executed = 0
normal_executed = 0
# Patterns to match
race_pattern = re.compile(r"RACE: .* executed after being cancelled!")
passed_pattern = re.compile(r"TEST PASSED")
failed_pattern = re.compile(r"TEST FAILED")
complete_pattern = re.compile(r"=== Test Complete ===")
normal_count_pattern = re.compile(r"Normal items executed: (\d+)")
removed_count_pattern = re.compile(r"Removed items executed: (\d+)")
def check_output(line: str) -> None:
"""Check log output for test results."""
nonlocal test_passed, removed_executed, normal_executed
if race_pattern.search(line):
# Race condition detected - a cancelled item executed
test_passed = False
if passed_pattern.search(line):
test_passed = True
elif failed_pattern.search(line):
test_passed = False
normal_match = normal_count_pattern.search(line)
if normal_match:
normal_executed = int(normal_match.group(1))
removed_match = removed_count_pattern.search(line)
if removed_match:
removed_executed = int(removed_match.group(1))
if not test_complete_future.done() and complete_pattern.search(line):
test_complete_future.set_result(True)
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Verify we can connect
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "scheduler-removed-item-race"
# List services
_, services = await asyncio.wait_for(
client.list_entities_services(), timeout=5.0
)
# Find run_test service
run_test_service = next((s for s in services if s.name == "run_test"), None)
assert run_test_service is not None, "run_test service not found"
# Execute the test
client.execute_service(run_test_service, {})
# Wait for test completion
try:
await asyncio.wait_for(test_complete_future, timeout=5.0)
except TimeoutError:
pytest.fail("Test did not complete within timeout")
# Verify results
assert test_passed, (
f"Test failed! Removed items executed: {removed_executed}, "
f"Normal items executed: {normal_executed}"
)
assert removed_executed == 0, (
f"Cancelled items should not execute, but {removed_executed} did"
)
assert normal_executed == 4, (
f"Expected 4 normal items to execute, got {normal_executed}"
)

View File

@@ -183,61 +183,6 @@ def test_get_changed_files_github_actions_pull_request(
assert result == expected_files
def test_get_changed_files_github_actions_pull_request_large_pr(
monkeypatch: MonkeyPatch,
) -> None:
"""Test _get_changed_files_github_actions fallback for PRs with >300 files."""
monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request")
expected_files = ["file1.py", "file2.cpp"]
with (
patch("helpers._get_pr_number_from_github_env", return_value="10214"),
patch("helpers._get_changed_files_from_command") as mock_get,
):
# First call fails with too many files error, second succeeds with API method
mock_get.side_effect = [
Exception("Sorry, the diff exceeded the maximum number of files (300)"),
expected_files,
]
result = _get_changed_files_github_actions()
assert mock_get.call_count == 2
mock_get.assert_any_call(["gh", "pr", "diff", "10214", "--name-only"])
mock_get.assert_any_call(
[
"gh",
"api",
"repos/esphome/esphome/pulls/10214/files",
"--paginate",
"--jq",
".[].filename",
]
)
assert result == expected_files
def test_get_changed_files_github_actions_pull_request_other_error(
monkeypatch: MonkeyPatch,
) -> None:
"""Test _get_changed_files_github_actions re-raises non-file-limit errors."""
monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request")
with (
patch("helpers._get_pr_number_from_github_env", return_value="1234"),
patch("helpers._get_changed_files_from_command") as mock_get,
):
# Error that is not about file limit
mock_get.side_effect = Exception("Command failed: authentication required")
with pytest.raises(Exception, match="authentication required"):
_get_changed_files_github_actions()
# Should only be called once (no retry with API)
mock_get.assert_called_once_with(["gh", "pr", "diff", "1234", "--name-only"])
def test_get_changed_files_github_actions_pull_request_no_pr_number(
monkeypatch: MonkeyPatch,
) -> None:

View File

@@ -689,64 +689,3 @@ def test_entity_duplicate_validator_internal_entities() -> None:
Invalid, match=r"Duplicate sensor entity with name 'Temperature' found"
):
validator(config4)
def test_empty_or_null_device_id_on_entity() -> None:
"""Test that empty or null device IDs are handled correctly."""
# Create validator for sensor platform
validator = entity_duplicate_validator("sensor")
# Entity with empty device_id should pass
config1 = {CONF_NAME: "Battery", CONF_DEVICE_ID: ""}
validated1 = validator(config1)
assert validated1 == config1
# Entity with None device_id should pass
config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: None}
validated2 = validator(config2)
assert validated2 == config2
def test_entity_duplicate_validator_non_ascii_names() -> None:
"""Test that non-ASCII names show helpful error messages."""
# Create validator for binary_sensor platform
validator = entity_duplicate_validator("binary_sensor")
# First Russian sensor should pass
config1 = {CONF_NAME: "Датчик открытия основного крана"}
validated1 = validator(config1)
assert validated1 == config1
# Second Russian sensor with different text but same ASCII conversion should fail
config2 = {CONF_NAME: "Датчик закрытия основного крана"}
with pytest.raises(
Invalid,
match=re.compile(
r"Duplicate binary_sensor entity with name 'Датчик закрытия основного крана' found.*"
r"Original names: 'Датчик закрытия основного крана' and 'Датчик открытия основного крана'.*"
r"Both convert to ASCII ID: '_______________________________'.*"
r"To fix: Add unique ASCII characters \(e\.g\., '1', '2', or 'A', 'B'\)",
re.DOTALL,
),
):
validator(config2)
def test_entity_duplicate_validator_same_name_no_enhanced_message() -> None:
"""Test that identical names don't show the enhanced message."""
# Create validator for sensor platform
validator = entity_duplicate_validator("sensor")
# First entity should pass
config1 = {CONF_NAME: "Temperature"}
validated1 = validator(config1)
assert validated1 == config1
# Second entity with exact same name should fail without enhanced message
config2 = {CONF_NAME: "Temperature"}
with pytest.raises(
Invalid,
match=r"Duplicate sensor entity with name 'Temperature' found.*"
r"Each entity on a device must have a unique name within its platform\.$",
):
validator(config2)

View File

@@ -1,220 +0,0 @@
"""Test writer module functionality."""
from collections.abc import Callable
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from esphome.storage_json import StorageJSON
from esphome.writer import storage_should_clean, update_storage_json
@pytest.fixture
def create_storage() -> Callable[..., StorageJSON]:
"""Factory fixture to create StorageJSON instances."""
def _create(
loaded_integrations: list[str] | None = None, **kwargs: Any
) -> StorageJSON:
return StorageJSON(
storage_version=kwargs.get("storage_version", 1),
name=kwargs.get("name", "test"),
friendly_name=kwargs.get("friendly_name", "Test Device"),
comment=kwargs.get("comment"),
esphome_version=kwargs.get("esphome_version", "2025.1.0"),
src_version=kwargs.get("src_version", 1),
address=kwargs.get("address", "test.local"),
web_port=kwargs.get("web_port", 80),
target_platform=kwargs.get("target_platform", "ESP32"),
build_path=kwargs.get("build_path", "/build"),
firmware_bin_path=kwargs.get("firmware_bin_path", "/firmware.bin"),
loaded_integrations=set(loaded_integrations or []),
loaded_platforms=kwargs.get("loaded_platforms", set()),
no_mdns=kwargs.get("no_mdns", False),
framework=kwargs.get("framework", "arduino"),
core_platform=kwargs.get("core_platform", "esp32"),
)
return _create
def test_storage_should_clean_when_old_is_none(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test that clean is triggered when old storage is None."""
new = create_storage(loaded_integrations=["api", "wifi"])
assert storage_should_clean(None, new) is True
def test_storage_should_clean_when_src_version_changes(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test that clean is triggered when src_version changes."""
old = create_storage(loaded_integrations=["api", "wifi"], src_version=1)
new = create_storage(loaded_integrations=["api", "wifi"], src_version=2)
assert storage_should_clean(old, new) is True
def test_storage_should_clean_when_build_path_changes(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test that clean is triggered when build_path changes."""
old = create_storage(loaded_integrations=["api", "wifi"], build_path="/build1")
new = create_storage(loaded_integrations=["api", "wifi"], build_path="/build2")
assert storage_should_clean(old, new) is True
def test_storage_should_clean_when_component_removed(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test that clean is triggered when a component is removed."""
old = create_storage(
loaded_integrations=["api", "wifi", "bluetooth_proxy", "esp32_ble_tracker"]
)
new = create_storage(loaded_integrations=["api", "wifi", "esp32_ble_tracker"])
assert storage_should_clean(old, new) is True
def test_storage_should_clean_when_multiple_components_removed(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test that clean is triggered when multiple components are removed."""
old = create_storage(
loaded_integrations=["api", "wifi", "ota", "web_server", "logger"]
)
new = create_storage(loaded_integrations=["api", "wifi", "logger"])
assert storage_should_clean(old, new) is True
def test_storage_should_not_clean_when_nothing_changes(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test that clean is not triggered when nothing changes."""
old = create_storage(loaded_integrations=["api", "wifi", "logger"])
new = create_storage(loaded_integrations=["api", "wifi", "logger"])
assert storage_should_clean(old, new) is False
def test_storage_should_not_clean_when_component_added(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test that clean is not triggered when a component is only added."""
old = create_storage(loaded_integrations=["api", "wifi"])
new = create_storage(loaded_integrations=["api", "wifi", "ota"])
assert storage_should_clean(old, new) is False
def test_storage_should_not_clean_when_other_fields_change(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test that clean is not triggered when non-relevant fields change."""
old = create_storage(
loaded_integrations=["api", "wifi"],
friendly_name="Old Name",
esphome_version="2024.12.0",
)
new = create_storage(
loaded_integrations=["api", "wifi"],
friendly_name="New Name",
esphome_version="2025.1.0",
)
assert storage_should_clean(old, new) is False
def test_storage_edge_case_empty_integrations(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test edge case when old has integrations but new has none."""
old = create_storage(loaded_integrations=["api", "wifi"])
new = create_storage(loaded_integrations=[])
assert storage_should_clean(old, new) is True
def test_storage_edge_case_from_empty_integrations(
create_storage: Callable[..., StorageJSON],
) -> None:
"""Test edge case when old has no integrations but new has some."""
old = create_storage(loaded_integrations=[])
new = create_storage(loaded_integrations=["api", "wifi"])
assert storage_should_clean(old, new) is False
@patch("esphome.writer.clean_build")
@patch("esphome.writer.StorageJSON")
@patch("esphome.writer.storage_path")
@patch("esphome.writer.CORE")
def test_update_storage_json_logging_when_old_is_none(
mock_core: MagicMock,
mock_storage_path: MagicMock,
mock_storage_json_class: MagicMock,
mock_clean_build: MagicMock,
create_storage: Callable[..., StorageJSON],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that update_storage_json doesn't crash when old storage is None.
This is a regression test for the AttributeError that occurred when
old was None and we tried to access old.loaded_integrations.
"""
# Setup mocks
mock_storage_path.return_value = "/test/path"
mock_storage_json_class.load.return_value = None # Old storage is None
new_storage = create_storage(loaded_integrations=["api", "wifi"])
new_storage.save = MagicMock() # Mock the save method
mock_storage_json_class.from_esphome_core.return_value = new_storage
# Call the function - should not raise AttributeError
with caplog.at_level("INFO"):
update_storage_json()
# Verify clean_build was called
mock_clean_build.assert_called_once()
# Verify the correct log message was used (not the component removal message)
assert "Core config or version changed, cleaning build files..." in caplog.text
assert "Components removed" not in caplog.text
# Verify save was called
new_storage.save.assert_called_once_with("/test/path")
@patch("esphome.writer.clean_build")
@patch("esphome.writer.StorageJSON")
@patch("esphome.writer.storage_path")
@patch("esphome.writer.CORE")
def test_update_storage_json_logging_components_removed(
mock_core: MagicMock,
mock_storage_path: MagicMock,
mock_storage_json_class: MagicMock,
mock_clean_build: MagicMock,
create_storage: Callable[..., StorageJSON],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that update_storage_json logs removed components correctly."""
# Setup mocks
mock_storage_path.return_value = "/test/path"
old_storage = create_storage(loaded_integrations=["api", "wifi", "bluetooth_proxy"])
new_storage = create_storage(loaded_integrations=["api", "wifi"])
new_storage.save = MagicMock() # Mock the save method
mock_storage_json_class.load.return_value = old_storage
mock_storage_json_class.from_esphome_core.return_value = new_storage
# Call the function
with caplog.at_level("INFO"):
update_storage_json()
# Verify clean_build was called
mock_clean_build.assert_called_once()
# Verify the correct log message was used with component names
assert (
"Components removed (bluetooth_proxy), cleaning build files..." in caplog.text
)
assert "Core config or version changed" not in caplog.text
# Verify save was called
new_storage.save.assert_called_once_with("/test/path")