mirror of
https://github.com/esphome/esphome.git
synced 2025-09-04 12:22:20 +01:00
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# control system is used.
|
||||||
|
|
||||||
PROJECT_NUMBER = 2025.8.0
|
PROJECT_NUMBER = 2025.8.1
|
||||||
|
|
||||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
# 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
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
@@ -321,6 +321,7 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
|
|||||||
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
|
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
|
||||||
)
|
)
|
||||||
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
|
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])
|
serv = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||||
cg.add(var.set_service("esphome.tag_scanned"))
|
cg.add(var.set_service("esphome.tag_scanned"))
|
||||||
|
@@ -455,9 +455,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
|
|||||||
resp.cold_white = values.get_cold_white();
|
resp.cold_white = values.get_cold_white();
|
||||||
resp.warm_white = values.get_warm_white();
|
resp.warm_white = values.get_warm_white();
|
||||||
if (light->supports_effects()) {
|
if (light->supports_effects()) {
|
||||||
// get_effect_name() returns temporary std::string - must store it
|
resp.set_effect(light->get_effect_name_ref());
|
||||||
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);
|
return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||||
}
|
}
|
||||||
@@ -1415,9 +1413,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
|||||||
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
|
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
|
||||||
resp.set_esphome_version(ESPHOME_VERSION_REF);
|
resp.set_esphome_version(ESPHOME_VERSION_REF);
|
||||||
|
|
||||||
// get_compilation_time() returns temporary std::string - must store it
|
resp.set_compilation_time(App.get_compilation_time_ref());
|
||||||
std::string compilation_time = App.get_compilation_time();
|
|
||||||
resp.set_compilation_time(StringRef(compilation_time));
|
|
||||||
|
|
||||||
// Compile-time StringRef constants for manufacturers
|
// Compile-time StringRef constants for manufacturers
|
||||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
#include "soc/soc_caps.h"
|
||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
#include "deep_sleep_component.h"
|
#include "deep_sleep_component.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
@@ -83,7 +84,11 @@ void DeepSleepComponent::deep_sleep_() {
|
|||||||
}
|
}
|
||||||
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
|
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
|
||||||
gpio_hold_en(gpio_pin);
|
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();
|
gpio_deep_sleep_hold_en();
|
||||||
|
#endif
|
||||||
bool level = !this->wakeup_pin_->is_inverted();
|
bool level = !this->wakeup_pin_->is_inverted();
|
||||||
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
|
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
|
||||||
level = !level;
|
level = !level;
|
||||||
@@ -120,7 +125,11 @@ void DeepSleepComponent::deep_sleep_() {
|
|||||||
}
|
}
|
||||||
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
|
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
|
||||||
gpio_hold_en(gpio_pin);
|
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();
|
gpio_deep_sleep_hold_en();
|
||||||
|
#endif
|
||||||
bool level = !this->wakeup_pin_->is_inverted();
|
bool level = !this->wakeup_pin_->is_inverted();
|
||||||
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
|
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
|
||||||
level = !level;
|
level = !level;
|
||||||
|
@@ -280,6 +280,10 @@ async def to_code(config):
|
|||||||
add_idf_sdkconfig_option(
|
add_idf_sdkconfig_option(
|
||||||
"CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds
|
"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
|
# Set the maximum number of notification registrations
|
||||||
# This controls how many BLE characteristics can have notifications enabled
|
# This controls how many BLE characteristics can have notifications enabled
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <esp_gap_ble_api.h>
|
#include <esp_gap_ble_api.h>
|
||||||
#include <esp_gatt_defs.h>
|
#include <esp_gatt_defs.h>
|
||||||
|
#include <esp_gattc_api.h>
|
||||||
|
|
||||||
namespace esphome::esp32_ble_client {
|
namespace esphome::esp32_ble_client {
|
||||||
|
|
||||||
@@ -111,43 +112,19 @@ void BLEClientBase::connect() {
|
|||||||
this->remote_addr_type_);
|
this->remote_addr_type_);
|
||||||
this->paired_ = false;
|
this->paired_ = false;
|
||||||
|
|
||||||
// Set preferred connection parameters before connecting
|
// Determine connection parameters based on connection type
|
||||||
// Use FAST for all V3 connections (better latency and reliability)
|
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||||
// Use MEDIUM for V1/legacy connections (balanced performance)
|
// V3 without cache needs fast params for service discovery
|
||||||
uint16_t min_interval, max_interval, timeout;
|
this->set_conn_params_(FAST_MIN_CONN_INTERVAL, FAST_MAX_CONN_INTERVAL, 0, FAST_CONN_TIMEOUT, "fast");
|
||||||
const char *param_type;
|
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||||
|
// V3 with cache can use medium params
|
||||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
this->set_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
|
// For V1/Legacy, don't set params - use ESP-IDF defaults
|
||||||
|
|
||||||
auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
|
// Open the connection
|
||||||
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);
|
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
|
||||||
if (ret) {
|
this->handle_connection_result_(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); }
|
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
|
||||||
@@ -159,7 +136,7 @@ void BLEClientBase::disconnect() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
|
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
|
||||||
ESP_LOGW(TAG, "[%d] [%s] Disconnecting before connected, disconnect scheduled.", this->connection_index_,
|
ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
|
||||||
this->address_str_.c_str());
|
this->address_str_.c_str());
|
||||||
this->want_disconnect_ = true;
|
this->want_disconnect_ = true;
|
||||||
return;
|
return;
|
||||||
@@ -172,13 +149,11 @@ void BLEClientBase::unconditional_disconnect() {
|
|||||||
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(),
|
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(),
|
||||||
this->conn_id_);
|
this->conn_id_);
|
||||||
if (this->state_ == espbt::ClientState::DISCONNECTING) {
|
if (this->state_ == espbt::ClientState::DISCONNECTING) {
|
||||||
ESP_LOGE(TAG, "[%d] [%s] Tried to disconnect while already disconnecting.", this->connection_index_,
|
this->log_error_("Already disconnecting");
|
||||||
this->address_str_.c_str());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this->conn_id_ == UNSET_CONN_ID) {
|
if (this->conn_id_ == UNSET_CONN_ID) {
|
||||||
ESP_LOGE(TAG, "[%d] [%s] No connection ID set, cannot disconnect.", this->connection_index_,
|
this->log_error_("conn id unset, cannot disconnect");
|
||||||
this->address_str_.c_str());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
|
auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
|
||||||
@@ -234,17 +209,51 @@ 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);
|
ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BLEClientBase::restore_medium_conn_params_() {
|
void BLEClientBase::handle_connection_result_(esp_err_t ret) {
|
||||||
// Restore to medium connection parameters after initial connection phase
|
if (ret) {
|
||||||
// This balances performance with bandwidth usage for normal operation
|
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) {
|
||||||
esp_ble_conn_update_params_t conn_params = {{0}};
|
esp_ble_conn_update_params_t conn_params = {{0}};
|
||||||
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
|
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
|
||||||
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
|
conn_params.min_int = min_interval;
|
||||||
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
|
conn_params.max_int = max_interval;
|
||||||
conn_params.latency = 0;
|
conn_params.latency = latency;
|
||||||
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
|
conn_params.timeout = timeout;
|
||||||
this->log_connection_params_("medium");
|
this->log_connection_params_(param_type);
|
||||||
esp_ble_gap_update_conn_params(&conn_params);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
|
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
|
||||||
@@ -264,8 +273,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
this->app_id);
|
this->app_id);
|
||||||
this->gattc_if_ = esp_gattc_if;
|
this->gattc_if_ = esp_gattc_if;
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_,
|
this->log_error_("gattc app registration failed status", param->reg.status);
|
||||||
this->address_str_.c_str(), param->reg.app_id, param->reg.status);
|
|
||||||
this->status_ = param->reg.status;
|
this->status_ = param->reg.status;
|
||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
}
|
}
|
||||||
@@ -277,11 +285,21 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
this->log_gattc_event_("OPEN");
|
this->log_gattc_event_("OPEN");
|
||||||
// conn_id was already set in ESP_GATTC_CONNECT_EVT
|
// conn_id was already set in ESP_GATTC_CONNECT_EVT
|
||||||
this->service_count_ = 0;
|
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) {
|
if (this->state_ != espbt::ClientState::CONNECTING) {
|
||||||
// This should not happen but lets log it in case it does
|
// This should not happen but lets log it in case it does
|
||||||
// because it means we have a bad assumption about how the
|
// because it means we have a bad assumption about how the
|
||||||
// ESP BT stack works.
|
// ESP BT stack works.
|
||||||
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while in %s state, status=%d", this->connection_index_,
|
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
|
||||||
this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status);
|
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) {
|
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||||
@@ -301,13 +319,14 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
this->set_state(espbt::ClientState::CONNECTED);
|
this->set_state(espbt::ClientState::CONNECTED);
|
||||||
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
|
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
|
||||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||||
// Restore to medium connection parameters for cached connections too
|
// Cached connections already connected with medium parameters, no update needed
|
||||||
this->restore_medium_conn_params_();
|
|
||||||
// only set our state, subclients might have more stuff to do yet.
|
// only set our state, subclients might have more stuff to do yet.
|
||||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ESP_LOGD(TAG, "[%d] [%s] Searching for services", this->connection_index_, this->address_str_.c_str());
|
// 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_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
|
esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -332,8 +351,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
// Check if we were disconnected while waiting for service discovery
|
// Check if we were disconnected while waiting for service discovery
|
||||||
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
|
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
|
||||||
this->state_ == espbt::ClientState::CONNECTED) {
|
this->state_ == espbt::ClientState::CONNECTED) {
|
||||||
ESP_LOGW(TAG, "[%d] [%s] Disconnected by remote during service discovery", this->connection_index_,
|
this->log_warning_("Remote closed during discovery");
|
||||||
this->address_str_.c_str());
|
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_,
|
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_,
|
||||||
this->address_str_.c_str(), param->disconnect.reason);
|
this->address_str_.c_str(), param->disconnect.reason);
|
||||||
@@ -389,12 +407,11 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
if (this->conn_id_ != param->search_cmpl.conn_id)
|
if (this->conn_id_ != param->search_cmpl.conn_id)
|
||||||
return false;
|
return false;
|
||||||
this->log_gattc_event_("SEARCH_CMPL");
|
this->log_gattc_event_("SEARCH_CMPL");
|
||||||
// For V3 connections, restore to medium connection parameters after service discovery
|
// For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery
|
||||||
// This balances performance with bandwidth usage after the critical discovery phase
|
// This balances performance with bandwidth usage after the critical discovery phase
|
||||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
|
||||||
this->restore_medium_conn_params_();
|
} else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) {
|
||||||
} else {
|
|
||||||
#ifdef USE_ESP32_BLE_DEVICE
|
#ifdef USE_ESP32_BLE_DEVICE
|
||||||
for (auto &svc : this->services_) {
|
for (auto &svc : this->services_) {
|
||||||
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
|
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
|
||||||
@@ -506,16 +523,14 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_
|
|||||||
return;
|
return;
|
||||||
esp_bd_addr_t bd_addr;
|
esp_bd_addr_t bd_addr;
|
||||||
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
|
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
|
||||||
ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(),
|
ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_.c_str(),
|
||||||
format_hex(bd_addr, 6).c_str());
|
format_hex(bd_addr, 6).c_str());
|
||||||
if (!param->ble_security.auth_cmpl.success) {
|
if (!param->ble_security.auth_cmpl.success) {
|
||||||
ESP_LOGE(TAG, "[%d] [%s] auth fail reason = 0x%x", this->connection_index_, this->address_str_.c_str(),
|
this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason);
|
||||||
param->ble_security.auth_cmpl.fail_reason);
|
|
||||||
} else {
|
} else {
|
||||||
this->paired_ = true;
|
this->paired_ = true;
|
||||||
ESP_LOGD(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_,
|
ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_.c_str(),
|
||||||
this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type,
|
param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode);
|
||||||
param->ble_security.auth_cmpl.auth_mode);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@@ -133,10 +133,18 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
|||||||
|
|
||||||
void log_event_(const char *name);
|
void log_event_(const char *name);
|
||||||
void log_gattc_event_(const char *name);
|
void log_gattc_event_(const char *name);
|
||||||
void restore_medium_conn_params_();
|
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 log_gattc_warning_(const char *operation, esp_gatt_status_t status);
|
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_gattc_warning_(const char *operation, esp_err_t err);
|
||||||
void log_connection_params_(const char *param_type);
|
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
|
} // namespace esphome::esp32_ble_client
|
||||||
|
@@ -80,14 +80,17 @@ class BLEManufacturerDataAdvertiseTrigger : public Trigger<const adv_data_t &>,
|
|||||||
ESPBTUUID uuid_;
|
ESPBTUUID uuid_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#endif // USE_ESP32_BLE_DEVICE
|
||||||
|
|
||||||
class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener {
|
class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener {
|
||||||
public:
|
public:
|
||||||
explicit BLEEndOfScanTrigger(ESP32BLETracker *parent) { parent->register_listener(this); }
|
explicit BLEEndOfScanTrigger(ESP32BLETracker *parent) { parent->register_listener(this); }
|
||||||
|
|
||||||
|
#ifdef USE_ESP32_BLE_DEVICE
|
||||||
bool parse_device(const ESPBTDevice &device) override { return false; }
|
bool parse_device(const ESPBTDevice &device) override { return false; }
|
||||||
|
#endif
|
||||||
void on_scan_end() override { this->trigger(); }
|
void on_scan_end() override { this->trigger(); }
|
||||||
};
|
};
|
||||||
#endif // USE_ESP32_BLE_DEVICE
|
|
||||||
|
|
||||||
template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> {
|
template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> {
|
||||||
public:
|
public:
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
#include "http_request_host.h"
|
|
||||||
|
|
||||||
#ifdef USE_HOST
|
#ifdef USE_HOST
|
||||||
|
|
||||||
|
#define USE_HTTP_REQUEST_HOST_H
|
||||||
|
#define CPPHTTPLIB_NO_EXCEPTIONS
|
||||||
|
#include "httplib.h"
|
||||||
|
#include "http_request_host.h"
|
||||||
|
|
||||||
#include <regex>
|
#include <regex>
|
||||||
#include "esphome/components/network/util.h"
|
#include "esphome/components/network/util.h"
|
||||||
#include "esphome/components/watchdog/watchdog.h"
|
#include "esphome/components/watchdog/watchdog.h"
|
||||||
|
@@ -1,11 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "http_request.h"
|
|
||||||
|
|
||||||
#ifdef USE_HOST
|
#ifdef USE_HOST
|
||||||
|
#include "http_request.h"
|
||||||
#define CPPHTTPLIB_NO_EXCEPTIONS
|
|
||||||
#include "httplib.h"
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace http_request {
|
namespace http_request {
|
||||||
|
|
||||||
|
@@ -3,12 +3,10 @@
|
|||||||
/**
|
/**
|
||||||
* NOTE: This is a copy of httplib.h from https://github.com/yhirose/cpp-httplib
|
* NOTE: This is a copy of httplib.h from https://github.com/yhirose/cpp-httplib
|
||||||
*
|
*
|
||||||
* It has been modified only to add ifdefs for USE_HOST. While it contains many functions unused in ESPHome,
|
* It has been modified 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.
|
* it was considered preferable to use it with as few changes as possible, to facilitate future updates.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "esphome/core/defines.h"
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// httplib.h
|
// httplib.h
|
||||||
//
|
//
|
||||||
@@ -17,6 +15,11 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#ifdef USE_HOST
|
#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
|
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||||
#define CPPHTTPLIB_HTTPLIB_H
|
#define CPPHTTPLIB_HTTPLIB_H
|
||||||
|
|
||||||
@@ -9687,5 +9690,6 @@ inline SSL_CTX *Client::ssl_context() const {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif // CPPHTTPLIB_HTTPLIB_H
|
#endif // CPPHTTPLIB_HTTPLIB_H
|
||||||
|
#endif // USE_HTTP_REQUEST_HOST_H
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@@ -140,12 +140,22 @@ float LightState::get_setup_priority() const { return setup_priority::HARDWARE -
|
|||||||
void LightState::publish_state() { this->remote_values_callback_.call(); }
|
void LightState::publish_state() { this->remote_values_callback_.call(); }
|
||||||
|
|
||||||
LightOutput *LightState::get_output() const { return this->output_; }
|
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() {
|
std::string LightState::get_effect_name() {
|
||||||
if (this->active_effect_index_ > 0) {
|
if (this->active_effect_index_ > 0) {
|
||||||
return this->effects_[this->active_effect_index_ - 1]->get_name();
|
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) {
|
void LightState::add_new_remote_values_callback(std::function<void()> &&send_callback) {
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
#include "esphome/core/entity_base.h"
|
#include "esphome/core/entity_base.h"
|
||||||
#include "esphome/core/optional.h"
|
#include "esphome/core/optional.h"
|
||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
|
#include "esphome/core/string_ref.h"
|
||||||
#include "light_call.h"
|
#include "light_call.h"
|
||||||
#include "light_color_values.h"
|
#include "light_color_values.h"
|
||||||
#include "light_effect.h"
|
#include "light_effect.h"
|
||||||
@@ -116,6 +117,8 @@ class LightState : public EntityBase, public Component {
|
|||||||
|
|
||||||
/// Return the name of the current effect, or if no effect is active "None".
|
/// Return the name of the current effect, or if no effect is active "None".
|
||||||
std::string get_effect_name();
|
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
|
* This lets front-end components subscribe to light change events. This callback is called once
|
||||||
|
@@ -24,7 +24,7 @@ from ..defines import (
|
|||||||
literal,
|
literal,
|
||||||
)
|
)
|
||||||
from ..lv_validation import (
|
from ..lv_validation import (
|
||||||
lv_angle,
|
lv_angle_degrees,
|
||||||
lv_bool,
|
lv_bool,
|
||||||
lv_color,
|
lv_color,
|
||||||
lv_image,
|
lv_image,
|
||||||
@@ -395,15 +395,15 @@ ARC_PROPS = {
|
|||||||
DRAW_OPA_SCHEMA.extend(
|
DRAW_OPA_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_RADIUS): pixels,
|
cv.Required(CONF_RADIUS): pixels,
|
||||||
cv.Required(CONF_START_ANGLE): lv_angle,
|
cv.Required(CONF_START_ANGLE): lv_angle_degrees,
|
||||||
cv.Required(CONF_END_ANGLE): lv_angle,
|
cv.Required(CONF_END_ANGLE): lv_angle_degrees,
|
||||||
}
|
}
|
||||||
).extend({cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}),
|
).extend({cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}),
|
||||||
)
|
)
|
||||||
async def canvas_draw_arc(config, action_id, template_arg, args):
|
async def canvas_draw_arc(config, action_id, template_arg, args):
|
||||||
radius = await size.process(config[CONF_RADIUS])
|
radius = await size.process(config[CONF_RADIUS])
|
||||||
start_angle = await lv_angle.process(config[CONF_START_ANGLE])
|
start_angle = await lv_angle_degrees.process(config[CONF_START_ANGLE])
|
||||||
end_angle = await lv_angle.process(config[CONF_END_ANGLE])
|
end_angle = await lv_angle_degrees.process(config[CONF_END_ANGLE])
|
||||||
|
|
||||||
async def do_draw_arc(w: Widget, x, y, dsc_addr):
|
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)
|
lv.canvas_draw_arc(w.obj, x, y, radius, start_angle, end_angle, dsc_addr)
|
||||||
|
@@ -14,7 +14,6 @@ from esphome.const import (
|
|||||||
CONF_VALUE,
|
CONF_VALUE,
|
||||||
CONF_WIDTH,
|
CONF_WIDTH,
|
||||||
)
|
)
|
||||||
from esphome.cpp_generator import IntLiteral
|
|
||||||
|
|
||||||
from ..automation import action_to_code
|
from ..automation import action_to_code
|
||||||
from ..defines import (
|
from ..defines import (
|
||||||
@@ -32,7 +31,7 @@ from ..helpers import add_lv_use, lvgl_components_required
|
|||||||
from ..lv_validation import (
|
from ..lv_validation import (
|
||||||
get_end_value,
|
get_end_value,
|
||||||
get_start_value,
|
get_start_value,
|
||||||
lv_angle,
|
lv_angle_degrees,
|
||||||
lv_bool,
|
lv_bool,
|
||||||
lv_color,
|
lv_color,
|
||||||
lv_float,
|
lv_float,
|
||||||
@@ -163,7 +162,7 @@ SCALE_SCHEMA = cv.Schema(
|
|||||||
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
|
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
|
||||||
cv.Optional(CONF_RANGE_TO, default=100.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_ANGLE_RANGE, default=270): cv.int_range(0, 360),
|
||||||
cv.Optional(CONF_ROTATION): lv_angle,
|
cv.Optional(CONF_ROTATION): lv_angle_degrees,
|
||||||
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
|
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -188,9 +187,7 @@ class MeterType(WidgetType):
|
|||||||
for scale_conf in config.get(CONF_SCALES, ()):
|
for scale_conf in config.get(CONF_SCALES, ()):
|
||||||
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
||||||
if CONF_ROTATION in scale_conf:
|
if CONF_ROTATION in scale_conf:
|
||||||
rotation = await lv_angle.process(scale_conf[CONF_ROTATION])
|
rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION])
|
||||||
if isinstance(rotation, IntLiteral):
|
|
||||||
rotation = int(str(rotation)) // 10
|
|
||||||
with LocalVariable(
|
with LocalVariable(
|
||||||
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
||||||
) as meter_var:
|
) as meter_var:
|
||||||
|
@@ -46,10 +46,32 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t
|
|||||||
}
|
}
|
||||||
this->connection_established_ = true;
|
this->connection_established_ = true;
|
||||||
this->char_handle_ = chr->handle;
|
this->char_handle_ = chr->handle;
|
||||||
#ifdef USE_TIME
|
|
||||||
this->sync_time_();
|
// Attempt to write immediately
|
||||||
#endif
|
// For devices without security, this will work
|
||||||
this->display();
|
// 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());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -127,6 +149,13 @@ void PVVXDisplay::delayed_disconnect_() {
|
|||||||
this->set_timeout("disconnect", this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); });
|
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
|
#ifdef USE_TIME
|
||||||
void PVVXDisplay::sync_time_() {
|
void PVVXDisplay::sync_time_() {
|
||||||
if (this->time_ == nullptr)
|
if (this->time_ == nullptr)
|
||||||
|
@@ -43,6 +43,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent {
|
|||||||
|
|
||||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||||
esp_ble_gattc_cb_param_t *param) override;
|
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)
|
/// Set validity period of the display information in seconds (1..65535)
|
||||||
void set_validity_period(uint16_t validity_period) { this->validity_period_ = validity_period; }
|
void set_validity_period(uint16_t validity_period) { this->validity_period_ = validity_period; }
|
||||||
@@ -112,6 +113,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent {
|
|||||||
void setcfgbit_(uint8_t bit, bool value);
|
void setcfgbit_(uint8_t bit, bool value);
|
||||||
void send_to_setup_char_(uint8_t *blk, size_t size);
|
void send_to_setup_char_(uint8_t *blk, size_t size);
|
||||||
void delayed_disconnect_();
|
void delayed_disconnect_();
|
||||||
|
void sync_time_and_display_();
|
||||||
#ifdef USE_TIME
|
#ifdef USE_TIME
|
||||||
void sync_time_();
|
void sync_time_();
|
||||||
time::RealTimeClock *time_{nullptr};
|
time::RealTimeClock *time_{nullptr};
|
||||||
|
@@ -52,9 +52,9 @@ def default_url(config: ConfigType) -> ConfigType:
|
|||||||
config = config.copy()
|
config = config.copy()
|
||||||
if config[CONF_VERSION] == 1:
|
if config[CONF_VERSION] == 1:
|
||||||
if CONF_CSS_URL not in config:
|
if CONF_CSS_URL not in config:
|
||||||
config[CONF_CSS_URL] = "https://esphome.io/_static/webserver-v1.min.css"
|
config[CONF_CSS_URL] = "https://oi.esphome.io/v1/webserver-v1.min.css"
|
||||||
if CONF_JS_URL not in config:
|
if CONF_JS_URL not in config:
|
||||||
config[CONF_JS_URL] = "https://esphome.io/_static/webserver-v1.min.js"
|
config[CONF_JS_URL] = "https://oi.esphome.io/v1/webserver-v1.min.js"
|
||||||
if config[CONF_VERSION] == 2:
|
if config[CONF_VERSION] == 2:
|
||||||
if CONF_CSS_URL not in config:
|
if CONF_CSS_URL not in config:
|
||||||
config[CONF_CSS_URL] = ""
|
config[CONF_CSS_URL] = ""
|
||||||
|
@@ -173,14 +173,14 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
|
|||||||
|
|
||||||
#if USE_WEBSERVER_VERSION == 1
|
#if USE_WEBSERVER_VERSION == 1
|
||||||
/** Set the URL to the CSS <link> that's sent to each client. Defaults to
|
/** Set the URL to the CSS <link> that's sent to each client. Defaults to
|
||||||
* https://esphome.io/_static/webserver-v1.min.css
|
* https://oi.esphome.io/v1/webserver-v1.min.css
|
||||||
*
|
*
|
||||||
* @param css_url The url to the web server stylesheet.
|
* @param css_url The url to the web server stylesheet.
|
||||||
*/
|
*/
|
||||||
void set_css_url(const char *css_url);
|
void set_css_url(const char *css_url);
|
||||||
|
|
||||||
/** Set the URL to the script that's embedded in the index page. Defaults to
|
/** Set the URL to the script that's embedded in the index page. Defaults to
|
||||||
* https://esphome.io/_static/webserver-v1.min.js
|
* https://oi.esphome.io/v1/webserver-v1.min.js
|
||||||
*
|
*
|
||||||
* @param js_url The url to the web server script.
|
* @param js_url The url to the web server script.
|
||||||
*/
|
*/
|
||||||
|
@@ -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,
|
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest.get()), n, &out,
|
||||||
reinterpret_cast<const uint8_t *>(user_info.c_str()), user_info.size());
|
reinterpret_cast<const uint8_t *>(user_info.c_str()), user_info.size());
|
||||||
|
|
||||||
return strncmp(digest.get(), auth_str + auth_prefix_len, auth.value().size() - auth_prefix_len) == 0;
|
return strcmp(digest.get(), auth_str + auth_prefix_len) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
|
void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
|
||||||
|
@@ -4,7 +4,7 @@ from enum import Enum
|
|||||||
|
|
||||||
from esphome.enum import StrEnum
|
from esphome.enum import StrEnum
|
||||||
|
|
||||||
__version__ = "2025.8.0"
|
__version__ = "2025.8.1"
|
||||||
|
|
||||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||||
|
@@ -10,6 +10,7 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
#include "esphome/core/scheduler.h"
|
#include "esphome/core/scheduler.h"
|
||||||
|
#include "esphome/core/string_ref.h"
|
||||||
|
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
#include "esphome/core/device.h"
|
#include "esphome/core/device.h"
|
||||||
@@ -248,6 +249,8 @@ class Application {
|
|||||||
bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; }
|
bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; }
|
||||||
|
|
||||||
std::string get_compilation_time() const { return this->compilation_time_; }
|
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
|
/// 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_; }
|
inline uint32_t IRAM_ATTR HOT get_loop_component_start_time() const { return this->loop_component_start_time_; }
|
||||||
|
@@ -5,6 +5,8 @@
|
|||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
|
#include "esphome/core/scheduler.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -158,7 +160,16 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
|
|||||||
void play_complex(Ts... x) override {
|
void play_complex(Ts... x) override {
|
||||||
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
|
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
|
||||||
this->num_running_++;
|
this->num_running_++;
|
||||||
this->set_timeout("delay", this->delay_.value(x...), f);
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||||
|
|
||||||
|
@@ -236,10 +236,21 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
|
|||||||
if existing_component != "unknown":
|
if existing_component != "unknown":
|
||||||
conflict_msg += f" from component '{existing_component}'"
|
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(
|
raise cv.Invalid(
|
||||||
f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. "
|
f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. "
|
||||||
f"{conflict_msg}. "
|
f"{conflict_msg}. "
|
||||||
f"Each entity on a device must have a unique name within its platform."
|
"Each entity on a device must have a unique name within its platform."
|
||||||
|
f"{sanitized_msg}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store metadata about this entity
|
# Store metadata about this entity
|
||||||
|
@@ -65,14 +65,17 @@ static void validate_static_string(const char *name) {
|
|||||||
|
|
||||||
// Common implementation for both timeout and interval
|
// Common implementation for both timeout and interval
|
||||||
void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string,
|
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) {
|
const void *name_ptr, uint32_t delay, std::function<void()> func, bool is_retry,
|
||||||
|
bool skip_cancel) {
|
||||||
// Get the name as const char*
|
// Get the name as const char*
|
||||||
const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
|
const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
|
||||||
|
|
||||||
if (delay == SCHEDULER_DONT_RUN) {
|
if (delay == SCHEDULER_DONT_RUN) {
|
||||||
// Still need to cancel existing timer if name is not empty
|
// Still need to cancel existing timer if name is not empty
|
||||||
LockGuard guard{this->lock_};
|
if (!skip_cancel) {
|
||||||
this->cancel_item_locked_(component, name_cstr, type);
|
LockGuard guard{this->lock_};
|
||||||
|
this->cancel_item_locked_(component, name_cstr, type);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +100,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
if (delay == 0 && type == SchedulerItem::TIMEOUT) {
|
if (delay == 0 && type == SchedulerItem::TIMEOUT) {
|
||||||
// Put in defer queue for guaranteed FIFO execution
|
// Put in defer queue for guaranteed FIFO execution
|
||||||
LockGuard guard{this->lock_};
|
LockGuard guard{this->lock_};
|
||||||
this->cancel_item_locked_(component, name_cstr, type);
|
if (!skip_cancel) {
|
||||||
|
this->cancel_item_locked_(component, name_cstr, type);
|
||||||
|
}
|
||||||
this->defer_queue_.push_back(std::move(item));
|
this->defer_queue_.push_back(std::move(item));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -150,9 +155,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If name is provided, do atomic cancel-and-add
|
// If name is provided, do atomic cancel-and-add (unless skip_cancel is true)
|
||||||
// Cancel existing items
|
// Cancel existing items
|
||||||
this->cancel_item_locked_(component, name_cstr, type);
|
if (!skip_cancel) {
|
||||||
|
this->cancel_item_locked_(component, name_cstr, type);
|
||||||
|
}
|
||||||
// Add new item directly to to_add_
|
// Add new item directly to to_add_
|
||||||
// since we have the lock held
|
// since we have the lock held
|
||||||
this->to_add_.push_back(std::move(item));
|
this->to_add_.push_back(std::move(item));
|
||||||
|
@@ -21,8 +21,13 @@ struct RetryArgs;
|
|||||||
void retry_handler(const std::shared_ptr<RetryArgs> &args);
|
void retry_handler(const std::shared_ptr<RetryArgs> &args);
|
||||||
|
|
||||||
class Scheduler {
|
class Scheduler {
|
||||||
// Allow retry_handler to access protected members
|
// Allow retry_handler to access protected members for internal retry mechanism
|
||||||
friend void ::esphome::retry_handler(const std::shared_ptr<RetryArgs> &args);
|
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:
|
||||||
// Public API - accepts std::string for backward compatibility
|
// Public API - accepts std::string for backward compatibility
|
||||||
@@ -184,7 +189,7 @@ class Scheduler {
|
|||||||
|
|
||||||
// Common implementation for both timeout and interval
|
// Common implementation for both timeout and interval
|
||||||
void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr,
|
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);
|
uint32_t delay, std::function<void()> func, bool is_retry = false, bool skip_cancel = false);
|
||||||
|
|
||||||
// Common implementation for retry
|
// Common implementation for retry
|
||||||
void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time,
|
void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time,
|
||||||
|
5
tests/components/deep_sleep/test.esp32-c6-idf.yaml
Normal file
5
tests/components/deep_sleep/test.esp32-c6-idf.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
substitutions:
|
||||||
|
wakeup_pin: GPIO4
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
|
<<: !include common-esp32.yaml
|
5
tests/components/deep_sleep/test.esp32-s2-idf.yaml
Normal file
5
tests/components/deep_sleep/test.esp32-s2-idf.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
substitutions:
|
||||||
|
wakeup_pin: GPIO4
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
|
<<: !include common-esp32.yaml
|
5
tests/components/deep_sleep/test.esp32-s3-idf.yaml
Normal file
5
tests/components/deep_sleep/test.esp32-s3-idf.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
substitutions:
|
||||||
|
wakeup_pin: GPIO4
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
|
<<: !include common-esp32.yaml
|
@@ -0,0 +1,3 @@
|
|||||||
|
esp32_ble_tracker:
|
||||||
|
on_scan_end:
|
||||||
|
- logger.log: "Scan ended!"
|
@@ -56,10 +56,29 @@ light:
|
|||||||
warm_white_color_temperature: 2000 K
|
warm_white_color_temperature: 2000 K
|
||||||
constant_brightness: true
|
constant_brightness: true
|
||||||
effects:
|
effects:
|
||||||
|
# Use default parameters:
|
||||||
- random:
|
- random:
|
||||||
name: "Random Effect"
|
# 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"
|
||||||
transition_length: 100ms
|
transition_length: 100ms
|
||||||
update_interval: 200ms
|
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:
|
- strobe:
|
||||||
name: "Strobe Effect"
|
name: "Strobe Effect"
|
||||||
- pulse:
|
- pulse:
|
||||||
@@ -73,6 +92,17 @@ light:
|
|||||||
red: test_red
|
red: test_red
|
||||||
green: test_green
|
green: test_green
|
||||||
blue: test_blue
|
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
|
- platform: binary
|
||||||
name: "Test Binary Light"
|
name: "Test Binary Light"
|
||||||
|
45
tests/integration/fixtures/parallel_script_delays.yaml
Normal file
45
tests/integration/fixtures/parallel_script_delays.yaml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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);
|
@@ -89,3 +89,73 @@ async def test_delay_action_cancellation(
|
|||||||
assert 0.4 < time_from_second_start < 0.6, (
|
assert 0.4 < time_from_second_start < 0.6, (
|
||||||
f"Delay completed {time_from_second_start:.3f}s after second start, expected ~0.5s"
|
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"
|
||||||
|
)
|
||||||
|
@@ -108,14 +108,51 @@ async def test_light_calls(
|
|||||||
# Wait for flash to end
|
# Wait for flash to end
|
||||||
state = await wait_for_state_change(rgbcw_light.key)
|
state = await wait_for_state_change(rgbcw_light.key)
|
||||||
|
|
||||||
# Test 13: effect only
|
# Test 13: effect only - test all random effects
|
||||||
# First ensure light is on
|
# First ensure light is on
|
||||||
client.light_command(key=rgbcw_light.key, state=True)
|
client.light_command(key=rgbcw_light.key, state=True)
|
||||||
state = await wait_for_state_change(rgbcw_light.key)
|
state = await wait_for_state_change(rgbcw_light.key)
|
||||||
# Now set effect
|
|
||||||
client.light_command(key=rgbcw_light.key, effect="Random Effect")
|
# Test 13a: Default random effect (no name, gets default name "Random")
|
||||||
|
client.light_command(key=rgbcw_light.key, effect="Random")
|
||||||
state = await wait_for_state_change(rgbcw_light.key)
|
state = await wait_for_state_change(rgbcw_light.key)
|
||||||
assert state.effect == "Random Effect"
|
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"
|
||||||
|
|
||||||
# Test 14: stop effect
|
# Test 14: stop effect
|
||||||
client.light_command(key=rgbcw_light.key, effect="None")
|
client.light_command(key=rgbcw_light.key, effect="None")
|
||||||
|
@@ -705,3 +705,48 @@ def test_empty_or_null_device_id_on_entity() -> None:
|
|||||||
config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: None}
|
config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: None}
|
||||||
validated2 = validator(config2)
|
validated2 = validator(config2)
|
||||||
assert validated2 == 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)
|
||||||
|
Reference in New Issue
Block a user