mirror of
https://github.com/esphome/esphome.git
synced 2025-08-28 04:59:25 +00:00
Compare commits
40 Commits
optimize_e
...
beta
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4dc11f05a7 | ||
![]() |
2aceb56606 | ||
![]() |
d071a074ef | ||
![]() |
7a459c8c20 | ||
![]() |
aebd21958a | ||
![]() |
c542db8bfe | ||
![]() |
d9dcfe66ec | ||
![]() |
8517c2e903 | ||
![]() |
684384892a | ||
![]() |
d560831d79 | ||
![]() |
fcc3c8e1b6 | ||
![]() |
959ffde60e | ||
![]() |
07715dd50f | ||
![]() |
03836ee2d2 | ||
![]() |
50408d9abb | ||
![]() |
0de7259428 | ||
![]() |
d054709c2d | ||
![]() |
da16887915 | ||
![]() |
2adb993242 | ||
![]() |
8e67df8059 | ||
![]() |
c5b2c8d971 | ||
![]() |
104906ca11 | ||
![]() |
ad5f6f0cfe | ||
![]() |
8356f7fcd3 | ||
![]() |
225de226b0 | ||
![]() |
fd07e1d979 | ||
![]() |
23554cda06 | ||
![]() |
064385eac6 | ||
![]() |
6502ed70de | ||
![]() |
bb894c3e32 | ||
![]() |
c5858b7032 | ||
![]() |
99f57ecb73 | ||
![]() |
cc6c892678 | ||
![]() |
07a98d2525 | ||
![]() |
e80f616366 | ||
![]() |
46be877594 | ||
![]() |
ac8b48a53c | ||
![]() |
7fdbd8528a | ||
![]() |
80970f972b | ||
![]() |
3c7865cd6f |
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.9
|
||||
rev: v0.12.8
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
2
Doxyfile
2
Doxyfile
@@ -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.9.0-dev
|
||||
PROJECT_NUMBER = 2025.8.1
|
||||
|
||||
# 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
|
||||
|
@@ -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) == "MQTT" and "mqtt" in config:
|
||||
if get_port_type(port) in ("NETWORK", "MQTT") and "mqtt" in config:
|
||||
from esphome import mqtt
|
||||
|
||||
return mqtt.show_logs(
|
||||
|
@@ -321,6 +321,7 @@ 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"))
|
||||
|
@@ -289,26 +289,16 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess
|
||||
return 0; // Doesn't fit
|
||||
}
|
||||
|
||||
// Allocate buffer space - pass payload size, allocation functions add header/footer space
|
||||
ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_size)
|
||||
: conn->allocate_batch_message_buffer(calculated_size);
|
||||
|
||||
// Get buffer size after allocation (which includes header padding)
|
||||
std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref();
|
||||
|
||||
if (is_single || conn->flags_.batch_first_message) {
|
||||
// Single message or first batch message
|
||||
conn->prepare_first_message_buffer(shared_buf, header_padding, total_calculated_size);
|
||||
if (conn->flags_.batch_first_message) {
|
||||
conn->flags_.batch_first_message = false;
|
||||
}
|
||||
} else {
|
||||
// Batch message second or later
|
||||
// Add padding for previous message footer + this message header
|
||||
size_t current_size = shared_buf.size();
|
||||
shared_buf.reserve(current_size + total_calculated_size);
|
||||
shared_buf.resize(current_size + footer_size + header_padding);
|
||||
}
|
||||
size_t size_before_encode = shared_buf.size();
|
||||
|
||||
// Encode directly into buffer
|
||||
size_t size_before_encode = shared_buf.size();
|
||||
msg.encode({&shared_buf});
|
||||
msg.encode(buffer);
|
||||
|
||||
// Calculate actual encoded size (not including header that was already added)
|
||||
size_t actual_payload_size = shared_buf.size() - size_before_encode;
|
||||
@@ -465,9 +455,7 @@ 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()) {
|
||||
// get_effect_name() returns temporary std::string - must store it
|
||||
std::string effect_name = light->get_effect_name();
|
||||
resp.set_effect(StringRef(effect_name));
|
||||
resp.set_effect(light->get_effect_name_ref());
|
||||
}
|
||||
return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@@ -1425,9 +1413,7 @@ 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);
|
||||
|
||||
// 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));
|
||||
resp.set_compilation_time(App.get_compilation_time_ref());
|
||||
|
||||
// Compile-time StringRef constants for manufacturers
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
@@ -1630,6 +1616,14 @@ bool APIConnection::schedule_batch_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
ProtoWriteBuffer APIConnection::allocate_single_message_buffer(uint16_t size) { return this->create_buffer(size); }
|
||||
|
||||
ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) {
|
||||
ProtoWriteBuffer result = this->prepare_message_buffer(size, this->flags_.batch_first_message);
|
||||
this->flags_.batch_first_message = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
void APIConnection::process_batch_() {
|
||||
// Ensure PacketInfo remains trivially destructible for our placement new approach
|
||||
static_assert(std::is_trivially_destructible<PacketInfo>::value,
|
||||
@@ -1737,7 +1731,7 @@ void APIConnection::process_batch_() {
|
||||
}
|
||||
remaining_size -= payload_size;
|
||||
// Calculate where the next message's header padding will start
|
||||
// Current buffer size + footer space for this message
|
||||
// Current buffer size + footer space (that prepare_message_buffer will add for this message)
|
||||
current_offset = shared_buf.size() + footer_size;
|
||||
}
|
||||
|
||||
|
@@ -252,21 +252,44 @@ class APIConnection : public APIServerConnection {
|
||||
|
||||
// Get header padding size - used for both reserve and insert
|
||||
uint8_t header_padding = this->helper_->frame_header_padding();
|
||||
|
||||
// Get shared buffer from parent server
|
||||
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
this->prepare_first_message_buffer(shared_buf, header_padding,
|
||||
reserve_size + header_padding + this->helper_->frame_footer_size());
|
||||
return {&shared_buf};
|
||||
}
|
||||
|
||||
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) {
|
||||
shared_buf.clear();
|
||||
// Reserve space for header padding + message + footer
|
||||
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
|
||||
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
|
||||
shared_buf.reserve(total_size);
|
||||
shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
|
||||
// Resize to add header padding so message encoding starts at the correct position
|
||||
shared_buf.resize(header_padding);
|
||||
return {&shared_buf};
|
||||
}
|
||||
|
||||
// Prepare buffer for next message in batch
|
||||
ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) {
|
||||
// Get reference to shared buffer (it maintains state between batch messages)
|
||||
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
|
||||
if (is_first_message) {
|
||||
shared_buf.clear();
|
||||
}
|
||||
|
||||
size_t current_size = shared_buf.size();
|
||||
|
||||
// Calculate padding to add:
|
||||
// - First message: just header padding
|
||||
// - Subsequent messages: footer for previous message + header padding for this message
|
||||
size_t padding_to_add = is_first_message
|
||||
? this->helper_->frame_header_padding()
|
||||
: this->helper_->frame_header_padding() + this->helper_->frame_footer_size();
|
||||
|
||||
// Reserve space for padding + message
|
||||
shared_buf.reserve(current_size + padding_to_add + message_size);
|
||||
|
||||
// Resize to add the padding bytes
|
||||
shared_buf.resize(current_size + padding_to_add);
|
||||
|
||||
return {&shared_buf};
|
||||
}
|
||||
|
||||
bool try_to_clear_buffer(bool log_out_of_space);
|
||||
@@ -274,6 +297,10 @@ class APIConnection : public APIServerConnection {
|
||||
|
||||
std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); }
|
||||
|
||||
// Buffer allocator methods for batch processing
|
||||
ProtoWriteBuffer allocate_single_message_buffer(uint16_t size);
|
||||
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
|
||||
|
||||
protected:
|
||||
// Helper function to handle authentication completion
|
||||
void complete_authentication_();
|
||||
|
@@ -235,8 +235,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
|
||||
|
||||
for (const auto &packet : packets) {
|
||||
// Calculate varint sizes for header layout
|
||||
uint8_t size_varint_len = api::ProtoSize::varint(packet.payload_size);
|
||||
uint8_t type_varint_len = api::ProtoSize::varint(packet.message_type);
|
||||
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.payload_size));
|
||||
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.message_type));
|
||||
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||
|
||||
// Calculate where to start writing the header
|
||||
@@ -271,8 +271,9 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
|
||||
buf_start[header_offset] = 0x00; // indicator
|
||||
|
||||
// Encode varints directly into buffer
|
||||
encode_varint_unchecked(buf_start + header_offset + 1, packet.payload_size);
|
||||
encode_varint_unchecked(buf_start + header_offset + 1 + size_varint_len, packet.message_type);
|
||||
ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
|
||||
ProtoVarInt(packet.message_type)
|
||||
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
|
||||
|
||||
// Add iovec for this packet (header + payload)
|
||||
size_t packet_len = static_cast<size_t>(total_header_len + packet.payload_size);
|
||||
|
@@ -124,6 +124,34 @@ class ProtoVarInt {
|
||||
// with ZigZag encoding
|
||||
return decode_zigzag64(this->value_);
|
||||
}
|
||||
/**
|
||||
* Encode the varint value to a pre-allocated buffer without bounds checking.
|
||||
*
|
||||
* @param buffer The pre-allocated buffer to write the encoded varint to
|
||||
* @param len The size of the buffer in bytes
|
||||
*
|
||||
* @note The caller is responsible for ensuring the buffer is large enough
|
||||
* to hold the encoded value. Use ProtoSize::varint() to calculate
|
||||
* the exact size needed before calling this method.
|
||||
* @note No bounds checking is performed for performance reasons.
|
||||
*/
|
||||
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
|
||||
uint64_t val = this->value_;
|
||||
if (val <= 0x7F) {
|
||||
buffer[0] = val;
|
||||
return;
|
||||
}
|
||||
size_t i = 0;
|
||||
while (val && i < len) {
|
||||
uint8_t temp = val & 0x7F;
|
||||
val >>= 7;
|
||||
if (val) {
|
||||
buffer[i++] = temp | 0x80;
|
||||
} else {
|
||||
buffer[i++] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
void encode(std::vector<uint8_t> &out) {
|
||||
uint64_t val = this->value_;
|
||||
if (val <= 0x7F) {
|
||||
@@ -302,28 +330,6 @@ class ProtoWriteBuffer {
|
||||
std::vector<uint8_t> *buffer_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Encode a uint16_t value as a varint directly to a buffer without bounds checking
|
||||
*
|
||||
* @param buffer The pre-allocated buffer to write the encoded varint to
|
||||
* @param value The uint16_t value to encode (0-65535)
|
||||
*
|
||||
* @note The caller is responsible for ensuring the buffer is large enough (max 3 bytes for uint16_t)
|
||||
* @note No bounds checking is performed for performance reasons
|
||||
*/
|
||||
inline void encode_varint_unchecked(uint8_t *buffer, uint16_t value) {
|
||||
if (value < 128) {
|
||||
buffer[0] = value;
|
||||
} else if (value < 16384) {
|
||||
buffer[0] = (value & 0x7F) | 0x80;
|
||||
buffer[1] = value >> 7;
|
||||
} else {
|
||||
buffer[0] = (value & 0x7F) | 0x80;
|
||||
buffer[1] = ((value >> 7) & 0x7F) | 0x80;
|
||||
buffer[2] = value >> 14;
|
||||
}
|
||||
}
|
||||
|
||||
// Forward declaration
|
||||
class ProtoSize;
|
||||
|
||||
@@ -380,33 +386,6 @@ class ProtoSize {
|
||||
|
||||
uint32_t get_size() const { return total_size_; }
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint8_t value as a varint
|
||||
*
|
||||
* @param value The uint8_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value (1 or 2)
|
||||
*/
|
||||
static constexpr uint8_t varint(uint8_t value) {
|
||||
// For uint8_t (0-255), we need at most 2 bytes
|
||||
return (value < 128) ? 1 : 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint16_t value as a varint
|
||||
*
|
||||
* @param value The uint16_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value (1-3)
|
||||
*/
|
||||
static constexpr uint8_t varint(uint16_t value) {
|
||||
// For uint16_t (0-65535), we need at most 3 bytes
|
||||
if (value < 128)
|
||||
return 1; // 7 bits
|
||||
else if (value < 16384)
|
||||
return 2; // 14 bits
|
||||
else
|
||||
return 3; // 15-16 bits
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
||||
*
|
||||
@@ -416,9 +395,11 @@ class ProtoSize {
|
||||
static constexpr uint32_t varint(uint32_t value) {
|
||||
// Optimized varint size calculation using leading zeros
|
||||
// Each 7 bits requires one byte in the varint encoding
|
||||
if (value < 128) {
|
||||
if (value < 128)
|
||||
return 1; // 7 bits, common case for small values
|
||||
} else if (value < 16384) {
|
||||
|
||||
// For larger values, count bytes needed based on the position of the highest bit set
|
||||
if (value < 16384) {
|
||||
return 2; // 14 bits
|
||||
} else if (value < 2097152) {
|
||||
return 3; // 21 bits
|
||||
@@ -792,7 +773,7 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
|
||||
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
|
||||
|
||||
// Write the length varint directly
|
||||
encode_varint_unchecked(this->buffer_->data() + begin, static_cast<uint16_t>(msg_length_bytes));
|
||||
ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes);
|
||||
|
||||
// Now encode the message content - it will append to the buffer
|
||||
value.encode(*this);
|
||||
|
@@ -382,20 +382,15 @@ 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_transaction_(uint16_t a_register) {
|
||||
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
|
||||
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
|
||||
@@ -403,14 +398,8 @@ uint16_t ATM90E32Component::read16_(uint16_t a_register) {
|
||||
}
|
||||
|
||||
int ATM90E32Component::read32_(uint16_t addr_h, uint16_t 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 uint16_t val_h = this->read16_(addr_h);
|
||||
const uint16_t val_l = this->read16_(addr_l);
|
||||
const int32_t val = (val_h << 16) | val_l;
|
||||
|
||||
ESP_LOGVV(TAG,
|
||||
|
@@ -140,7 +140,6 @@ 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);
|
||||
|
@@ -375,10 +375,19 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
|
||||
switch (event) {
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
this->reset_connection_(param->disconnect.reason);
|
||||
// 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);
|
||||
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;
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
#ifdef USE_ESP32
|
||||
#include "soc/soc_caps.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "deep_sleep_component.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -83,7 +84,11 @@ 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;
|
||||
@@ -120,7 +125,11 @@ 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;
|
||||
|
@@ -824,8 +824,9 @@ 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])
|
||||
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
|
||||
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
|
||||
variant = config[CONF_VARIANT]
|
||||
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}")
|
||||
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
|
||||
cg.add_define(ThreadModel.MULTI_ATOMICS)
|
||||
|
||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||
@@ -859,6 +860,7 @@ 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
|
||||
)
|
||||
|
@@ -8,7 +8,6 @@
|
||||
#include <cinttypes>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32 {
|
||||
@@ -157,23 +156,20 @@ class ESP32Preferences : public ESPPreferences {
|
||||
return failed == 0;
|
||||
}
|
||||
bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) {
|
||||
NVSData stored_data{};
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
// Check size first before allocating memory
|
||||
if (actual_len != to_save.data.size()) {
|
||||
return true;
|
||||
}
|
||||
auto stored_data = std::make_unique<uint8_t[]>(actual_len);
|
||||
err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.get(), &actual_len);
|
||||
stored_data.data.resize(actual_len);
|
||||
err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
|
||||
return to_save.data != stored_data.data;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
|
@@ -280,6 +280,10 @@ 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
|
||||
|
@@ -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);
|
||||
|
@@ -61,10 +61,24 @@ 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 data.
|
||||
// GATTC/GATTS events use heap allocation for their param and inline storage for small data.
|
||||
//
|
||||
// Event flow:
|
||||
// 1. ESP-IDF BLE stack calls our static handlers in the BLE task context
|
||||
@@ -111,21 +125,21 @@ class BLEEvent {
|
||||
this->init_gap_data_(e, p);
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
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 - 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.
|
||||
// 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.
|
||||
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);
|
||||
@@ -135,27 +149,32 @@ class BLEEvent {
|
||||
~BLEEvent() { this->release(); }
|
||||
|
||||
// Default constructor for pre-allocation in pool
|
||||
BLEEvent() : type_(GAP) {}
|
||||
BLEEvent() : event_{}, type_(GAP) {}
|
||||
|
||||
// Invoked on return to EventPool - clean up any heap-allocated data
|
||||
void release() {
|
||||
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;
|
||||
this->event_.gattc.data_len = 0;
|
||||
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;
|
||||
this->event_.gatts.data_len = 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,22 +226,30 @@ class BLEEvent {
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
struct gattc_event {
|
||||
esp_gattc_cb_event_t gattc_event;
|
||||
esp_gatt_if_t gattc_if;
|
||||
esp_ble_gattc_cb_param_t *gattc_param; // Heap-allocated
|
||||
uint8_t *data; // Heap-allocated raw buffer (manually managed)
|
||||
uint16_t data_len; // Track size separately
|
||||
} gattc;
|
||||
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
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
struct gatts_event {
|
||||
esp_gatts_cb_event_t gatts_event;
|
||||
esp_gatt_if_t gatts_if;
|
||||
esp_ble_gatts_cb_param_t *gatts_param; // Heap-allocated
|
||||
uint8_t *data; // Heap-allocated raw buffer (manually managed)
|
||||
uint16_t data_len; // Track size separately
|
||||
} gatts;
|
||||
} event_; // 80 bytes
|
||||
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
|
||||
|
||||
ble_event_t type_;
|
||||
|
||||
@@ -236,6 +263,29 @@ 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;
|
||||
@@ -320,48 +370,37 @@ class BLEEvent {
|
||||
this->event_.gattc.gattc_if = i;
|
||||
|
||||
if (p == nullptr) {
|
||||
this->event_.gattc.gattc_param = nullptr;
|
||||
this->event_.gattc.data = 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;
|
||||
return; // Invalid event, but we can't log in header file
|
||||
}
|
||||
|
||||
// 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 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;
|
||||
|
||||
// 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:
|
||||
this->event_.gattc.data_len = p->notify.value_len;
|
||||
if (p->notify.value_len > 0) {
|
||||
this->event_.gattc.data = new uint8_t[p->notify.value_len];
|
||||
memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len);
|
||||
} else {
|
||||
this->event_.gattc.data = nullptr;
|
||||
}
|
||||
this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data;
|
||||
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);
|
||||
break;
|
||||
case ESP_GATTC_READ_CHAR_EVT:
|
||||
case ESP_GATTC_READ_DESCR_EVT:
|
||||
this->event_.gattc.data_len = p->read.value_len;
|
||||
if (p->read.value_len > 0) {
|
||||
this->event_.gattc.data = new uint8_t[p->read.value_len];
|
||||
memcpy(this->event_.gattc.data, p->read.value, p->read.value_len);
|
||||
} else {
|
||||
this->event_.gattc.data = nullptr;
|
||||
}
|
||||
this->event_.gattc.gattc_param->read.value = this->event_.gattc.data;
|
||||
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);
|
||||
break;
|
||||
default:
|
||||
this->event_.gattc.data = nullptr;
|
||||
this->event_.gattc.is_inline = false;
|
||||
this->event_.gattc.data.heap_data = nullptr;
|
||||
this->event_.gattc.data_len = 0;
|
||||
break;
|
||||
}
|
||||
@@ -373,37 +412,32 @@ class BLEEvent {
|
||||
this->event_.gatts.gatts_if = i;
|
||||
|
||||
if (p == nullptr) {
|
||||
this->event_.gatts.gatts_param = nullptr;
|
||||
this->event_.gatts.data = 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;
|
||||
return; // Invalid event, but we can't log in header file
|
||||
}
|
||||
|
||||
// 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 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;
|
||||
|
||||
// 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:
|
||||
this->event_.gatts.data_len = p->write.len;
|
||||
if (p->write.len > 0) {
|
||||
this->event_.gatts.data = new uint8_t[p->write.len];
|
||||
memcpy(this->event_.gatts.data, p->write.value, p->write.len);
|
||||
} else {
|
||||
this->event_.gatts.data = nullptr;
|
||||
}
|
||||
this->event_.gatts.gatts_param->write.value = this->event_.gatts.data;
|
||||
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);
|
||||
break;
|
||||
default:
|
||||
this->event_.gatts.data = nullptr;
|
||||
this->event_.gatts.is_inline = false;
|
||||
this->event_.gatts.data.heap_data = nullptr;
|
||||
this->event_.gatts.data_len = 0;
|
||||
break;
|
||||
}
|
||||
@@ -414,6 +448,15 @@ 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");
|
||||
|
||||
|
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gatt_defs.h>
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome::esp32_ble_client {
|
||||
|
||||
@@ -111,43 +112,19 @@ void BLEClientBase::connect() {
|
||||
this->remote_addr_type_);
|
||||
this->paired_ = false;
|
||||
|
||||
// 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;
|
||||
|
||||
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";
|
||||
// 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
|
||||
|
||||
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
|
||||
// Open the connection
|
||||
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
|
||||
if (ret) {
|
||||
this->log_gattc_warning_("esp_ble_gattc_open", ret);
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
} else {
|
||||
this->set_state(espbt::ClientState::CONNECTING);
|
||||
}
|
||||
this->handle_connection_result_(ret);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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->want_disconnect_ = true;
|
||||
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(),
|
||||
this->conn_id_);
|
||||
if (this->state_ == espbt::ClientState::DISCONNECTING) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] Tried to disconnect while already disconnecting.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
this->log_error_("Already disconnecting");
|
||||
return;
|
||||
}
|
||||
if (this->conn_id_ == UNSET_CONN_ID) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] No connection ID set, cannot disconnect.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
this->log_error_("conn id unset, cannot disconnect");
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
void BLEClientBase::restore_medium_conn_params_() {
|
||||
// Restore to medium connection parameters after initial connection phase
|
||||
// This balances performance with bandwidth usage for normal operation
|
||||
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) {
|
||||
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 = 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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->gattc_if_ = esp_gattc_if;
|
||||
} else {
|
||||
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->log_error_("gattc app registration failed status", param->reg.status);
|
||||
this->status_ = param->reg.status;
|
||||
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");
|
||||
// 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] 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);
|
||||
}
|
||||
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);
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
// Restore to medium connection parameters for cached connections too
|
||||
this->restore_medium_conn_params_();
|
||||
// Cached connections already connected with medium parameters, no update needed
|
||||
// only set our state, subclients might have more stuff to do yet.
|
||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||
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);
|
||||
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
|
||||
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
|
||||
this->state_ == espbt::ClientState::CONNECTED) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Disconnected by remote during service discovery", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
this->log_warning_("Remote closed during discovery");
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_,
|
||||
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)
|
||||
return false;
|
||||
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
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
this->restore_medium_conn_params_();
|
||||
} else {
|
||||
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) {
|
||||
#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(),
|
||||
@@ -506,16 +523,14 @@ 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. 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());
|
||||
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(),
|
||||
param->ble_security.auth_cmpl.fail_reason);
|
||||
this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason);
|
||||
} else {
|
||||
this->paired_ = true;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
break;
|
||||
|
||||
|
@@ -133,10 +133,18 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
|
||||
void log_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_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
|
||||
|
@@ -80,14 +80,17 @@ 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:
|
||||
|
@@ -1,7 +1,10 @@
|
||||
#include "http_request_host.h"
|
||||
|
||||
#ifdef USE_HOST
|
||||
|
||||
#define USE_HTTP_REQUEST_HOST_H
|
||||
#define CPPHTTPLIB_NO_EXCEPTIONS
|
||||
#include "httplib.h"
|
||||
#include "http_request_host.h"
|
||||
|
||||
#include <regex>
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/components/watchdog/watchdog.h"
|
||||
|
@@ -1,11 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "http_request.h"
|
||||
|
||||
#ifdef USE_HOST
|
||||
|
||||
#define CPPHTTPLIB_NO_EXCEPTIONS
|
||||
#include "httplib.h"
|
||||
#include "http_request.h"
|
||||
namespace esphome {
|
||||
namespace http_request {
|
||||
|
||||
|
@@ -3,12 +3,10 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
//
|
||||
// httplib.h
|
||||
//
|
||||
@@ -17,6 +15,11 @@
|
||||
//
|
||||
|
||||
#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
|
||||
|
||||
@@ -9687,5 +9690,6 @@ inline SSL_CTX *Client::ssl_context() const {
|
||||
#endif
|
||||
|
||||
#endif // CPPHTTPLIB_HTTPLIB_H
|
||||
#endif // USE_HTTP_REQUEST_HOST_H
|
||||
|
||||
#endif
|
||||
|
@@ -212,7 +212,7 @@ def validate_use_legacy(value):
|
||||
f"All i2s_audio components must set {CONF_USE_LEGACY} to the same value."
|
||||
)
|
||||
if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino):
|
||||
raise cv.Invalid("Arduino supports only the legacy i2s driver")
|
||||
raise cv.Invalid("Arduino supports only the legacy i2s driver.")
|
||||
_use_legacy_driver = value[CONF_USE_LEGACY]
|
||||
return value
|
||||
|
||||
|
@@ -92,7 +92,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
def _final_validate(_):
|
||||
if not use_legacy():
|
||||
raise cv.Invalid("I2S media player is only compatible with legacy i2s driver")
|
||||
raise cv.Invalid("I2S media player is only compatible with legacy i2s driver.")
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
@@ -122,7 +122,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
def _final_validate(config):
|
||||
if not use_legacy() and config[CONF_ADC_TYPE] == "internal":
|
||||
raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver")
|
||||
raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.")
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
@@ -163,7 +163,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
def _final_validate(config):
|
||||
if not use_legacy():
|
||||
if config[CONF_DAC_TYPE] == "internal":
|
||||
raise cv.Invalid("Internal DAC is only compatible with legacy i2s driver")
|
||||
raise cv.Invalid("Internal DAC is only compatible with legacy i2s driver.")
|
||||
if config[CONF_I2S_COMM_FMT] == "stand_max":
|
||||
raise cv.Invalid(
|
||||
"I2S standard max format only implemented with legacy i2s driver."
|
||||
|
@@ -140,12 +140,22 @@ 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) {
|
||||
|
@@ -4,6 +4,7 @@
|
||||
#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"
|
||||
@@ -116,6 +117,8 @@ 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
|
||||
|
@@ -24,7 +24,7 @@ from ..defines import (
|
||||
literal,
|
||||
)
|
||||
from ..lv_validation import (
|
||||
lv_angle,
|
||||
lv_angle_degrees,
|
||||
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,
|
||||
cv.Required(CONF_END_ANGLE): lv_angle,
|
||||
cv.Required(CONF_START_ANGLE): lv_angle_degrees,
|
||||
cv.Required(CONF_END_ANGLE): lv_angle_degrees,
|
||||
}
|
||||
).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.process(config[CONF_START_ANGLE])
|
||||
end_angle = await lv_angle.process(config[CONF_END_ANGLE])
|
||||
start_angle = await lv_angle_degrees.process(config[CONF_START_ANGLE])
|
||||
end_angle = await lv_angle_degrees.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)
|
||||
|
@@ -14,7 +14,6 @@ from esphome.const import (
|
||||
CONF_VALUE,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.cpp_generator import IntLiteral
|
||||
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
@@ -32,7 +31,7 @@ from ..helpers import add_lv_use, lvgl_components_required
|
||||
from ..lv_validation import (
|
||||
get_end_value,
|
||||
get_start_value,
|
||||
lv_angle,
|
||||
lv_angle_degrees,
|
||||
lv_bool,
|
||||
lv_color,
|
||||
lv_float,
|
||||
@@ -163,7 +162,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,
|
||||
cv.Optional(CONF_ROTATION): lv_angle_degrees,
|
||||
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
|
||||
}
|
||||
)
|
||||
@@ -188,9 +187,7 @@ 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.process(scale_conf[CONF_ROTATION])
|
||||
if isinstance(rotation, IntLiteral):
|
||||
rotation = int(str(rotation)) // 10
|
||||
rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION])
|
||||
with LocalVariable(
|
||||
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
||||
) as meter_var:
|
||||
|
@@ -201,7 +201,7 @@ def _validate_manifest_version(manifest_data):
|
||||
else:
|
||||
raise cv.Invalid("Invalid manifest version")
|
||||
else:
|
||||
raise cv.Invalid("Invalid manifest file, missing 'version' key")
|
||||
raise cv.Invalid("Invalid manifest file, missing 'version' key.")
|
||||
|
||||
|
||||
def _process_http_source(config):
|
||||
@@ -421,7 +421,7 @@ def _feature_step_size_validate(config):
|
||||
if features_step_size is None:
|
||||
features_step_size = model_step_size
|
||||
elif features_step_size != model_step_size:
|
||||
raise cv.Invalid("Cannot load models with different features step sizes")
|
||||
raise cv.Invalid("Cannot load models with different features step sizes.")
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
|
@@ -764,7 +764,8 @@ void Nextion::process_nextion_commands_() {
|
||||
variable_name = to_process.substr(0, index);
|
||||
++index;
|
||||
|
||||
text_value = to_process.substr(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);
|
||||
|
||||
ESP_LOGN(TAG, "Text sensor: %s='%s'", variable_name.c_str(), text_value.c_str());
|
||||
|
||||
|
@@ -23,20 +23,18 @@ void Pipsolar::loop() {
|
||||
// Read message
|
||||
if (this->state_ == STATE_IDLE) {
|
||||
this->empty_uart_buffer_();
|
||||
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_command_()) {
|
||||
// command sent
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->send_next_poll_()) {
|
||||
// poll sent
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (this->state_ == STATE_COMMAND_COMPLETE) {
|
||||
if (this->check_incoming_length_(4)) {
|
||||
@@ -530,7 +528,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_ = true;
|
||||
this->value_faults_present_ = false;
|
||||
|
||||
for (size_t i = 1; i < strlen(tmp); i++) {
|
||||
enabled = tmp[i] == '1';
|
||||
@@ -708,6 +706,7 @@ void Pipsolar::loop() {
|
||||
return;
|
||||
}
|
||||
// crc ok
|
||||
this->used_polling_commands_[this->last_polling_command_].needs_update = false;
|
||||
this->state_ = STATE_POLL_CHECKED;
|
||||
return;
|
||||
} else {
|
||||
@@ -788,7 +787,7 @@ uint8_t Pipsolar::check_incoming_crc_() {
|
||||
}
|
||||
|
||||
// send next command used
|
||||
uint8_t Pipsolar::send_next_command_() {
|
||||
bool 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();
|
||||
@@ -809,37 +808,43 @@ uint8_t Pipsolar::send_next_command_() {
|
||||
// end Byte
|
||||
this->write(0x0D);
|
||||
ESP_LOGD(TAG, "Sending command from queue: %s with length %d", command, length);
|
||||
return 1;
|
||||
return true;
|
||||
}
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
void Pipsolar::send_next_poll_() {
|
||||
bool Pipsolar::send_next_poll_() {
|
||||
uint16_t crc16;
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
|
||||
void Pipsolar::queue_command_(const char *command, uint8_t length) {
|
||||
@@ -869,7 +874,13 @@ void Pipsolar::dump_config() {
|
||||
}
|
||||
}
|
||||
}
|
||||
void Pipsolar::update() {}
|
||||
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::add_polling_command_(const char *command, ENUMPollingCommand polling_command) {
|
||||
for (auto &used_polling_command : this->used_polling_commands_) {
|
||||
@@ -891,6 +902,7 @@ 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;
|
||||
}
|
||||
}
|
||||
|
@@ -25,6 +25,7 @@ struct PollingCommand {
|
||||
uint8_t length = 0;
|
||||
uint8_t errors;
|
||||
ENUMPollingCommand identifier;
|
||||
bool needs_update;
|
||||
};
|
||||
|
||||
#define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \
|
||||
@@ -189,14 +190,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;
|
||||
uint32_t last_poll_ = 0;
|
||||
static const size_t POLLING_COMMANDS_MAX = 15;
|
||||
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);
|
||||
uint8_t send_next_command_();
|
||||
void send_next_poll_();
|
||||
bool send_next_command_();
|
||||
bool 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;
|
||||
@@ -216,7 +217,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
||||
};
|
||||
|
||||
uint8_t last_polling_command_ = 0;
|
||||
PollingCommand used_polling_commands_[15];
|
||||
PollingCommand used_polling_commands_[POLLING_COMMANDS_MAX];
|
||||
};
|
||||
|
||||
} // namespace pipsolar
|
||||
|
@@ -46,10 +46,32 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t
|
||||
}
|
||||
this->connection_established_ = true;
|
||||
this->char_handle_ = chr->handle;
|
||||
#ifdef USE_TIME
|
||||
this->sync_time_();
|
||||
#endif
|
||||
this->display();
|
||||
|
||||
// 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());
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -127,6 +149,13 @@ 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)
|
||||
|
@@ -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,
|
||||
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; }
|
||||
@@ -112,6 +113,7 @@ 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};
|
||||
|
@@ -18,6 +18,14 @@ static const uint8_t QMP6988_TEMPERATURE_MSB_REG = 0xFA; /* Temperature MSB Reg
|
||||
static const uint8_t QMP6988_CALIBRATION_DATA_START = 0xA0; /* QMP6988 compensation coefficients */
|
||||
static const uint8_t QMP6988_CALIBRATION_DATA_LENGTH = 25;
|
||||
|
||||
static const uint8_t SHIFT_RIGHT_4_POSITION = 4;
|
||||
static const uint8_t SHIFT_LEFT_2_POSITION = 2;
|
||||
static const uint8_t SHIFT_LEFT_4_POSITION = 4;
|
||||
static const uint8_t SHIFT_LEFT_5_POSITION = 5;
|
||||
static const uint8_t SHIFT_LEFT_8_POSITION = 8;
|
||||
static const uint8_t SHIFT_LEFT_12_POSITION = 12;
|
||||
static const uint8_t SHIFT_LEFT_16_POSITION = 16;
|
||||
|
||||
/* power mode */
|
||||
static const uint8_t QMP6988_SLEEP_MODE = 0x00;
|
||||
static const uint8_t QMP6988_FORCED_MODE = 0x01;
|
||||
@@ -87,45 +95,64 @@ static const char *iir_filter_to_str(QMP6988IIRFilter filter) {
|
||||
}
|
||||
|
||||
bool QMP6988Component::device_check_() {
|
||||
if (this->read_register(QMP6988_CHIP_ID_REG, &(qmp6988_data_.chip_id), 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Read chip ID (0xD1) failed");
|
||||
return false;
|
||||
uint8_t ret = 0;
|
||||
|
||||
ret = this->read_register(QMP6988_CHIP_ID_REG, &(qmp6988_data_.chip_id), 1);
|
||||
if (ret != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "%s: read chip ID (0xD1) failed", __func__);
|
||||
}
|
||||
ESP_LOGV(TAG, "Read chip ID = 0x%x", qmp6988_data_.chip_id);
|
||||
ESP_LOGD(TAG, "qmp6988 read chip id = 0x%x", qmp6988_data_.chip_id);
|
||||
|
||||
return qmp6988_data_.chip_id == QMP6988_CHIP_ID;
|
||||
}
|
||||
|
||||
bool QMP6988Component::get_calibration_data_() {
|
||||
uint8_t status = 0;
|
||||
// BITFIELDS temp_COE;
|
||||
uint8_t a_data_uint8_tr[QMP6988_CALIBRATION_DATA_LENGTH] = {0};
|
||||
int len;
|
||||
|
||||
for (uint8_t len = 0; len < QMP6988_CALIBRATION_DATA_LENGTH; len += 1) {
|
||||
if (this->read_register(QMP6988_CALIBRATION_DATA_START + len, &a_data_uint8_tr[len], 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Read calibration data (0xA0) error");
|
||||
for (len = 0; len < QMP6988_CALIBRATION_DATA_LENGTH; len += 1) {
|
||||
status = this->read_register(QMP6988_CALIBRATION_DATA_START + len, &a_data_uint8_tr[len], 1);
|
||||
if (status != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "qmp6988 read calibration data (0xA0) error!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
qmp6988_data_.qmp6988_cali.COE_a0 =
|
||||
(int32_t) encode_uint32(a_data_uint8_tr[18], a_data_uint8_tr[19], (a_data_uint8_tr[24] & 0x0f) << 4, 0);
|
||||
(QMP6988_S32_t) (((a_data_uint8_tr[18] << SHIFT_LEFT_12_POSITION) |
|
||||
(a_data_uint8_tr[19] << SHIFT_LEFT_4_POSITION) | (a_data_uint8_tr[24] & 0x0f))
|
||||
<< 12);
|
||||
qmp6988_data_.qmp6988_cali.COE_a0 = qmp6988_data_.qmp6988_cali.COE_a0 >> 12;
|
||||
|
||||
qmp6988_data_.qmp6988_cali.COE_a1 = (int16_t) encode_uint16(a_data_uint8_tr[20], a_data_uint8_tr[21]);
|
||||
qmp6988_data_.qmp6988_cali.COE_a2 = (int16_t) encode_uint16(a_data_uint8_tr[22], a_data_uint8_tr[23]);
|
||||
qmp6988_data_.qmp6988_cali.COE_a1 =
|
||||
(QMP6988_S16_t) (((a_data_uint8_tr[20]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[21]);
|
||||
qmp6988_data_.qmp6988_cali.COE_a2 =
|
||||
(QMP6988_S16_t) (((a_data_uint8_tr[22]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[23]);
|
||||
|
||||
qmp6988_data_.qmp6988_cali.COE_b00 =
|
||||
(int32_t) encode_uint32(a_data_uint8_tr[0], a_data_uint8_tr[1], a_data_uint8_tr[24] & 0xf0, 0);
|
||||
(QMP6988_S32_t) (((a_data_uint8_tr[0] << SHIFT_LEFT_12_POSITION) | (a_data_uint8_tr[1] << SHIFT_LEFT_4_POSITION) |
|
||||
((a_data_uint8_tr[24] & 0xf0) >> SHIFT_RIGHT_4_POSITION))
|
||||
<< 12);
|
||||
qmp6988_data_.qmp6988_cali.COE_b00 = qmp6988_data_.qmp6988_cali.COE_b00 >> 12;
|
||||
|
||||
qmp6988_data_.qmp6988_cali.COE_bt1 = (int16_t) encode_uint16(a_data_uint8_tr[2], a_data_uint8_tr[3]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bt2 = (int16_t) encode_uint16(a_data_uint8_tr[4], a_data_uint8_tr[5]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bp1 = (int16_t) encode_uint16(a_data_uint8_tr[6], a_data_uint8_tr[7]);
|
||||
qmp6988_data_.qmp6988_cali.COE_b11 = (int16_t) encode_uint16(a_data_uint8_tr[8], a_data_uint8_tr[9]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bp2 = (int16_t) encode_uint16(a_data_uint8_tr[10], a_data_uint8_tr[11]);
|
||||
qmp6988_data_.qmp6988_cali.COE_b12 = (int16_t) encode_uint16(a_data_uint8_tr[12], a_data_uint8_tr[13]);
|
||||
qmp6988_data_.qmp6988_cali.COE_b21 = (int16_t) encode_uint16(a_data_uint8_tr[14], a_data_uint8_tr[15]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bp3 = (int16_t) encode_uint16(a_data_uint8_tr[16], a_data_uint8_tr[17]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bt1 =
|
||||
(QMP6988_S16_t) (((a_data_uint8_tr[2]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[3]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bt2 =
|
||||
(QMP6988_S16_t) (((a_data_uint8_tr[4]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[5]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bp1 =
|
||||
(QMP6988_S16_t) (((a_data_uint8_tr[6]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[7]);
|
||||
qmp6988_data_.qmp6988_cali.COE_b11 =
|
||||
(QMP6988_S16_t) (((a_data_uint8_tr[8]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[9]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bp2 =
|
||||
(QMP6988_S16_t) (((a_data_uint8_tr[10]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[11]);
|
||||
qmp6988_data_.qmp6988_cali.COE_b12 =
|
||||
(QMP6988_S16_t) (((a_data_uint8_tr[12]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[13]);
|
||||
qmp6988_data_.qmp6988_cali.COE_b21 =
|
||||
(QMP6988_S16_t) (((a_data_uint8_tr[14]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[15]);
|
||||
qmp6988_data_.qmp6988_cali.COE_bp3 =
|
||||
(QMP6988_S16_t) (((a_data_uint8_tr[16]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[17]);
|
||||
|
||||
ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n");
|
||||
ESP_LOGV(TAG, "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_a0,
|
||||
@@ -139,17 +166,17 @@ bool QMP6988Component::get_calibration_data_() {
|
||||
qmp6988_data_.ik.a0 = qmp6988_data_.qmp6988_cali.COE_a0; // 20Q4
|
||||
qmp6988_data_.ik.b00 = qmp6988_data_.qmp6988_cali.COE_b00; // 20Q4
|
||||
|
||||
qmp6988_data_.ik.a1 = 3608L * (int32_t) qmp6988_data_.qmp6988_cali.COE_a1 - 1731677965L; // 31Q23
|
||||
qmp6988_data_.ik.a2 = 16889L * (int32_t) qmp6988_data_.qmp6988_cali.COE_a2 - 87619360L; // 30Q47
|
||||
qmp6988_data_.ik.a1 = 3608L * (QMP6988_S32_t) qmp6988_data_.qmp6988_cali.COE_a1 - 1731677965L; // 31Q23
|
||||
qmp6988_data_.ik.a2 = 16889L * (QMP6988_S32_t) qmp6988_data_.qmp6988_cali.COE_a2 - 87619360L; // 30Q47
|
||||
|
||||
qmp6988_data_.ik.bt1 = 2982L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bt1 + 107370906L; // 28Q15
|
||||
qmp6988_data_.ik.bt2 = 329854L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bt2 + 108083093L; // 34Q38
|
||||
qmp6988_data_.ik.bp1 = 19923L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp1 + 1133836764L; // 31Q20
|
||||
qmp6988_data_.ik.b11 = 2406L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b11 + 118215883L; // 28Q34
|
||||
qmp6988_data_.ik.bp2 = 3079L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp2 - 181579595L; // 29Q43
|
||||
qmp6988_data_.ik.b12 = 6846L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b12 + 85590281L; // 29Q53
|
||||
qmp6988_data_.ik.b21 = 13836L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60
|
||||
qmp6988_data_.ik.bp3 = 2915L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65
|
||||
qmp6988_data_.ik.bt1 = 2982L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bt1 + 107370906L; // 28Q15
|
||||
qmp6988_data_.ik.bt2 = 329854L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bt2 + 108083093L; // 34Q38
|
||||
qmp6988_data_.ik.bp1 = 19923L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp1 + 1133836764L; // 31Q20
|
||||
qmp6988_data_.ik.b11 = 2406L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b11 + 118215883L; // 28Q34
|
||||
qmp6988_data_.ik.bp2 = 3079L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp2 - 181579595L; // 29Q43
|
||||
qmp6988_data_.ik.b12 = 6846L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b12 + 85590281L; // 29Q53
|
||||
qmp6988_data_.ik.b21 = 13836L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60
|
||||
qmp6988_data_.ik.bp3 = 2915L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65
|
||||
ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n");
|
||||
ESP_LOGV(TAG, "a0[%d] a1[%d] a2[%d] b00[%d]\r\n", qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2,
|
||||
qmp6988_data_.ik.b00);
|
||||
@@ -161,55 +188,55 @@ bool QMP6988Component::get_calibration_data_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
int16_t QMP6988Component::get_compensated_temperature_(qmp6988_ik_data_t *ik, int32_t dt) {
|
||||
int16_t ret;
|
||||
int64_t wk1, wk2;
|
||||
QMP6988_S16_t QMP6988Component::get_compensated_temperature_(qmp6988_ik_data_t *ik, QMP6988_S32_t dt) {
|
||||
QMP6988_S16_t ret;
|
||||
QMP6988_S64_t wk1, wk2;
|
||||
|
||||
// wk1: 60Q4 // bit size
|
||||
wk1 = ((int64_t) ik->a1 * (int64_t) dt); // 31Q23+24-1=54 (54Q23)
|
||||
wk2 = ((int64_t) ik->a2 * (int64_t) dt) >> 14; // 30Q47+24-1=53 (39Q33)
|
||||
wk2 = (wk2 * (int64_t) dt) >> 10; // 39Q33+24-1=62 (52Q23)
|
||||
wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04)
|
||||
ret = (int16_t) ((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0
|
||||
wk1 = ((QMP6988_S64_t) ik->a1 * (QMP6988_S64_t) dt); // 31Q23+24-1=54 (54Q23)
|
||||
wk2 = ((QMP6988_S64_t) ik->a2 * (QMP6988_S64_t) dt) >> 14; // 30Q47+24-1=53 (39Q33)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dt) >> 10; // 39Q33+24-1=62 (52Q23)
|
||||
wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04)
|
||||
ret = (QMP6988_S16_t) ((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0
|
||||
return ret;
|
||||
}
|
||||
|
||||
int32_t QMP6988Component::get_compensated_pressure_(qmp6988_ik_data_t *ik, int32_t dp, int16_t tx) {
|
||||
int32_t ret;
|
||||
int64_t wk1, wk2, wk3;
|
||||
QMP6988_S32_t QMP6988Component::get_compensated_pressure_(qmp6988_ik_data_t *ik, QMP6988_S32_t dp, QMP6988_S16_t tx) {
|
||||
QMP6988_S32_t ret;
|
||||
QMP6988_S64_t wk1, wk2, wk3;
|
||||
|
||||
// wk1 = 48Q16 // bit size
|
||||
wk1 = ((int64_t) ik->bt1 * (int64_t) tx); // 28Q15+16-1=43 (43Q15)
|
||||
wk2 = ((int64_t) ik->bp1 * (int64_t) dp) >> 5; // 31Q20+24-1=54 (49Q15)
|
||||
wk1 += wk2; // 43,49->50Q15
|
||||
wk2 = ((int64_t) ik->bt2 * (int64_t) tx) >> 1; // 34Q38+16-1=49 (48Q37)
|
||||
wk2 = (wk2 * (int64_t) tx) >> 8; // 48Q37+16-1=63 (55Q29)
|
||||
wk3 = wk2; // 55Q29
|
||||
wk2 = ((int64_t) ik->b11 * (int64_t) tx) >> 4; // 28Q34+16-1=43 (39Q30)
|
||||
wk2 = (wk2 * (int64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29)
|
||||
wk3 += wk2; // 55,61->62Q29
|
||||
wk2 = ((int64_t) ik->bp2 * (int64_t) dp) >> 13; // 29Q43+24-1=52 (39Q30)
|
||||
wk2 = (wk2 * (int64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29)
|
||||
wk3 += wk2; // 62,61->63Q29
|
||||
wk1 += wk3 >> 14; // Q29 >> 14 -> Q15
|
||||
wk2 = ((int64_t) ik->b12 * (int64_t) tx); // 29Q53+16-1=45 (45Q53)
|
||||
wk2 = (wk2 * (int64_t) tx) >> 22; // 45Q53+16-1=61 (39Q31)
|
||||
wk2 = (wk2 * (int64_t) dp) >> 1; // 39Q31+24-1=62 (61Q30)
|
||||
wk3 = wk2; // 61Q30
|
||||
wk2 = ((int64_t) ik->b21 * (int64_t) tx) >> 6; // 29Q60+16-1=45 (39Q54)
|
||||
wk2 = (wk2 * (int64_t) dp) >> 23; // 39Q54+24-1=62 (39Q31)
|
||||
wk2 = (wk2 * (int64_t) dp) >> 1; // 39Q31+24-1=62 (61Q20)
|
||||
wk3 += wk2; // 61,61->62Q30
|
||||
wk2 = ((int64_t) ik->bp3 * (int64_t) dp) >> 12; // 28Q65+24-1=51 (39Q53)
|
||||
wk2 = (wk2 * (int64_t) dp) >> 23; // 39Q53+24-1=62 (39Q30)
|
||||
wk2 = (wk2 * (int64_t) dp); // 39Q30+24-1=62 (62Q30)
|
||||
wk3 += wk2; // 62,62->63Q30
|
||||
wk1 += wk3 >> 15; // Q30 >> 15 = Q15
|
||||
wk1 = ((QMP6988_S64_t) ik->bt1 * (QMP6988_S64_t) tx); // 28Q15+16-1=43 (43Q15)
|
||||
wk2 = ((QMP6988_S64_t) ik->bp1 * (QMP6988_S64_t) dp) >> 5; // 31Q20+24-1=54 (49Q15)
|
||||
wk1 += wk2; // 43,49->50Q15
|
||||
wk2 = ((QMP6988_S64_t) ik->bt2 * (QMP6988_S64_t) tx) >> 1; // 34Q38+16-1=49 (48Q37)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) tx) >> 8; // 48Q37+16-1=63 (55Q29)
|
||||
wk3 = wk2; // 55Q29
|
||||
wk2 = ((QMP6988_S64_t) ik->b11 * (QMP6988_S64_t) tx) >> 4; // 28Q34+16-1=43 (39Q30)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29)
|
||||
wk3 += wk2; // 55,61->62Q29
|
||||
wk2 = ((QMP6988_S64_t) ik->bp2 * (QMP6988_S64_t) dp) >> 13; // 29Q43+24-1=52 (39Q30)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29)
|
||||
wk3 += wk2; // 62,61->63Q29
|
||||
wk1 += wk3 >> 14; // Q29 >> 14 -> Q15
|
||||
wk2 = ((QMP6988_S64_t) ik->b12 * (QMP6988_S64_t) tx); // 29Q53+16-1=45 (45Q53)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) tx) >> 22; // 45Q53+16-1=61 (39Q31)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q31+24-1=62 (61Q30)
|
||||
wk3 = wk2; // 61Q30
|
||||
wk2 = ((QMP6988_S64_t) ik->b21 * (QMP6988_S64_t) tx) >> 6; // 29Q60+16-1=45 (39Q54)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 23; // 39Q54+24-1=62 (39Q31)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q31+24-1=62 (61Q20)
|
||||
wk3 += wk2; // 61,61->62Q30
|
||||
wk2 = ((QMP6988_S64_t) ik->bp3 * (QMP6988_S64_t) dp) >> 12; // 28Q65+24-1=51 (39Q53)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp) >> 23; // 39Q53+24-1=62 (39Q30)
|
||||
wk2 = (wk2 * (QMP6988_S64_t) dp); // 39Q30+24-1=62 (62Q30)
|
||||
wk3 += wk2; // 62,62->63Q30
|
||||
wk1 += wk3 >> 15; // Q30 >> 15 = Q15
|
||||
wk1 /= 32767L;
|
||||
wk1 >>= 11; // Q15 >> 7 = Q4
|
||||
wk1 += ik->b00; // Q4 + 20Q4
|
||||
// wk1 >>= 4; // 28Q4 -> 24Q0
|
||||
ret = (int32_t) wk1;
|
||||
ret = (QMP6988_S32_t) wk1;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -247,7 +274,7 @@ void QMP6988Component::set_power_mode_(uint8_t power_mode) {
|
||||
delay(10);
|
||||
}
|
||||
|
||||
void QMP6988Component::write_filter_(QMP6988IIRFilter filter) {
|
||||
void QMP6988Component::write_filter_(unsigned char filter) {
|
||||
uint8_t data;
|
||||
|
||||
data = (filter & 0x03);
|
||||
@@ -255,7 +282,7 @@ void QMP6988Component::write_filter_(QMP6988IIRFilter filter) {
|
||||
delay(10);
|
||||
}
|
||||
|
||||
void QMP6988Component::write_oversampling_pressure_(QMP6988Oversampling oversampling_p) {
|
||||
void QMP6988Component::write_oversampling_pressure_(unsigned char oversampling_p) {
|
||||
uint8_t data;
|
||||
|
||||
this->read_register(QMP6988_CTRLMEAS_REG, &data, 1);
|
||||
@@ -265,7 +292,7 @@ void QMP6988Component::write_oversampling_pressure_(QMP6988Oversampling oversamp
|
||||
delay(10);
|
||||
}
|
||||
|
||||
void QMP6988Component::write_oversampling_temperature_(QMP6988Oversampling oversampling_t) {
|
||||
void QMP6988Component::write_oversampling_temperature_(unsigned char oversampling_t) {
|
||||
uint8_t data;
|
||||
|
||||
this->read_register(QMP6988_CTRLMEAS_REG, &data, 1);
|
||||
@@ -275,6 +302,16 @@ void QMP6988Component::write_oversampling_temperature_(QMP6988Oversampling overs
|
||||
delay(10);
|
||||
}
|
||||
|
||||
void QMP6988Component::set_temperature_oversampling(QMP6988Oversampling oversampling_t) {
|
||||
this->temperature_oversampling_ = oversampling_t;
|
||||
}
|
||||
|
||||
void QMP6988Component::set_pressure_oversampling(QMP6988Oversampling oversampling_p) {
|
||||
this->pressure_oversampling_ = oversampling_p;
|
||||
}
|
||||
|
||||
void QMP6988Component::set_iir_filter(QMP6988IIRFilter iirfilter) { this->iir_filter_ = iirfilter; }
|
||||
|
||||
void QMP6988Component::calculate_altitude_(float pressure, float temp) {
|
||||
float altitude;
|
||||
altitude = (pow((101325 / pressure), 1 / 5.257) - 1) * (temp + 273.15) / 0.0065;
|
||||
@@ -283,10 +320,10 @@ void QMP6988Component::calculate_altitude_(float pressure, float temp) {
|
||||
|
||||
void QMP6988Component::calculate_pressure_() {
|
||||
uint8_t err = 0;
|
||||
uint32_t p_read, t_read;
|
||||
int32_t p_raw, t_raw;
|
||||
QMP6988_U32_t p_read, t_read;
|
||||
QMP6988_S32_t p_raw, t_raw;
|
||||
uint8_t a_data_uint8_tr[6] = {0};
|
||||
int32_t t_int, p_int;
|
||||
QMP6988_S32_t t_int, p_int;
|
||||
this->qmp6988_data_.temperature = 0;
|
||||
this->qmp6988_data_.pressure = 0;
|
||||
|
||||
@@ -295,11 +332,13 @@ void QMP6988Component::calculate_pressure_() {
|
||||
ESP_LOGE(TAG, "Error reading raw pressure/temp values");
|
||||
return;
|
||||
}
|
||||
p_read = encode_uint24(a_data_uint8_tr[0], a_data_uint8_tr[1], a_data_uint8_tr[2]);
|
||||
p_raw = (int32_t) (p_read - SUBTRACTOR);
|
||||
p_read = (QMP6988_U32_t) ((((QMP6988_U32_t) (a_data_uint8_tr[0])) << SHIFT_LEFT_16_POSITION) |
|
||||
(((QMP6988_U16_t) (a_data_uint8_tr[1])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[2]));
|
||||
p_raw = (QMP6988_S32_t) (p_read - SUBTRACTOR);
|
||||
|
||||
t_read = encode_uint24(a_data_uint8_tr[3], a_data_uint8_tr[4], a_data_uint8_tr[5]);
|
||||
t_raw = (int32_t) (t_read - SUBTRACTOR);
|
||||
t_read = (QMP6988_U32_t) ((((QMP6988_U32_t) (a_data_uint8_tr[3])) << SHIFT_LEFT_16_POSITION) |
|
||||
(((QMP6988_U16_t) (a_data_uint8_tr[4])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[5]));
|
||||
t_raw = (QMP6988_S32_t) (t_read - SUBTRACTOR);
|
||||
|
||||
t_int = this->get_compensated_temperature_(&(qmp6988_data_.ik), t_raw);
|
||||
p_int = this->get_compensated_pressure_(&(qmp6988_data_.ik), p_raw, t_int);
|
||||
@@ -309,9 +348,10 @@ void QMP6988Component::calculate_pressure_() {
|
||||
}
|
||||
|
||||
void QMP6988Component::setup() {
|
||||
if (!this->device_check_()) {
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
return;
|
||||
bool ret;
|
||||
ret = this->device_check_();
|
||||
if (!ret) {
|
||||
ESP_LOGCONFIG(TAG, "Setup failed - device not found");
|
||||
}
|
||||
|
||||
this->software_reset_();
|
||||
@@ -325,6 +365,9 @@ void QMP6988Component::setup() {
|
||||
void QMP6988Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "QMP6988:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
@@ -334,6 +377,8 @@ void QMP6988Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_));
|
||||
}
|
||||
|
||||
float QMP6988Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void QMP6988Component::update() {
|
||||
this->calculate_pressure_();
|
||||
float pressurehectopascals = this->qmp6988_data_.pressure / 100;
|
||||
|
@@ -1,17 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace qmp6988 {
|
||||
|
||||
#define QMP6988_U16_t unsigned short
|
||||
#define QMP6988_S16_t short
|
||||
#define QMP6988_U32_t unsigned int
|
||||
#define QMP6988_S32_t int
|
||||
#define QMP6988_U64_t unsigned long long
|
||||
#define QMP6988_S64_t long long
|
||||
|
||||
/* oversampling */
|
||||
enum QMP6988Oversampling : uint8_t {
|
||||
enum QMP6988Oversampling {
|
||||
QMP6988_OVERSAMPLING_SKIPPED = 0x00,
|
||||
QMP6988_OVERSAMPLING_1X = 0x01,
|
||||
QMP6988_OVERSAMPLING_2X = 0x02,
|
||||
@@ -23,7 +30,7 @@ enum QMP6988Oversampling : uint8_t {
|
||||
};
|
||||
|
||||
/* filter */
|
||||
enum QMP6988IIRFilter : uint8_t {
|
||||
enum QMP6988IIRFilter {
|
||||
QMP6988_IIR_FILTER_OFF = 0x00,
|
||||
QMP6988_IIR_FILTER_2X = 0x01,
|
||||
QMP6988_IIR_FILTER_4X = 0x02,
|
||||
@@ -33,18 +40,18 @@ enum QMP6988IIRFilter : uint8_t {
|
||||
};
|
||||
|
||||
using qmp6988_cali_data_t = struct Qmp6988CaliData {
|
||||
int32_t COE_a0;
|
||||
int16_t COE_a1;
|
||||
int16_t COE_a2;
|
||||
int32_t COE_b00;
|
||||
int16_t COE_bt1;
|
||||
int16_t COE_bt2;
|
||||
int16_t COE_bp1;
|
||||
int16_t COE_b11;
|
||||
int16_t COE_bp2;
|
||||
int16_t COE_b12;
|
||||
int16_t COE_b21;
|
||||
int16_t COE_bp3;
|
||||
QMP6988_S32_t COE_a0;
|
||||
QMP6988_S16_t COE_a1;
|
||||
QMP6988_S16_t COE_a2;
|
||||
QMP6988_S32_t COE_b00;
|
||||
QMP6988_S16_t COE_bt1;
|
||||
QMP6988_S16_t COE_bt2;
|
||||
QMP6988_S16_t COE_bp1;
|
||||
QMP6988_S16_t COE_b11;
|
||||
QMP6988_S16_t COE_bp2;
|
||||
QMP6988_S16_t COE_b12;
|
||||
QMP6988_S16_t COE_b21;
|
||||
QMP6988_S16_t COE_bp3;
|
||||
};
|
||||
|
||||
using qmp6988_fk_data_t = struct Qmp6988FkData {
|
||||
@@ -53,9 +60,9 @@ using qmp6988_fk_data_t = struct Qmp6988FkData {
|
||||
};
|
||||
|
||||
using qmp6988_ik_data_t = struct Qmp6988IkData {
|
||||
int32_t a0, b00;
|
||||
int32_t a1, a2;
|
||||
int64_t bt1, bt2, bp1, b11, bp2, b12, b21, bp3;
|
||||
QMP6988_S32_t a0, b00;
|
||||
QMP6988_S32_t a1, a2;
|
||||
QMP6988_S64_t bt1, bt2, bp1, b11, bp2, b12, b21, bp3;
|
||||
};
|
||||
|
||||
using qmp6988_data_t = struct Qmp6988Data {
|
||||
@@ -70,18 +77,17 @@ using qmp6988_data_t = struct Qmp6988Data {
|
||||
|
||||
class QMP6988Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
||||
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
|
||||
void set_iir_filter(QMP6988IIRFilter iirfilter) { this->iir_filter_ = iirfilter; }
|
||||
void set_temperature_oversampling(QMP6988Oversampling oversampling_t) {
|
||||
this->temperature_oversampling_ = oversampling_t;
|
||||
}
|
||||
void set_pressure_oversampling(QMP6988Oversampling oversampling_p) { this->pressure_oversampling_ = oversampling_p; }
|
||||
void set_iir_filter(QMP6988IIRFilter iirfilter);
|
||||
void set_temperature_oversampling(QMP6988Oversampling oversampling_t);
|
||||
void set_pressure_oversampling(QMP6988Oversampling oversampling_p);
|
||||
|
||||
protected:
|
||||
qmp6988_data_t qmp6988_data_;
|
||||
@@ -96,14 +102,14 @@ class QMP6988Component : public PollingComponent, public i2c::I2CDevice {
|
||||
bool get_calibration_data_();
|
||||
bool device_check_();
|
||||
void set_power_mode_(uint8_t power_mode);
|
||||
void write_oversampling_temperature_(QMP6988Oversampling oversampling_t);
|
||||
void write_oversampling_pressure_(QMP6988Oversampling oversampling_p);
|
||||
void write_filter_(QMP6988IIRFilter filter);
|
||||
void write_oversampling_temperature_(unsigned char oversampling_t);
|
||||
void write_oversampling_pressure_(unsigned char oversampling_p);
|
||||
void write_filter_(unsigned char filter);
|
||||
void calculate_pressure_();
|
||||
void calculate_altitude_(float pressure, float temp);
|
||||
|
||||
int32_t get_compensated_pressure_(qmp6988_ik_data_t *ik, int32_t dp, int16_t tx);
|
||||
int16_t get_compensated_temperature_(qmp6988_ik_data_t *ik, int32_t dt);
|
||||
QMP6988_S32_t get_compensated_pressure_(qmp6988_ik_data_t *ik, QMP6988_S32_t dp, QMP6988_S16_t tx);
|
||||
QMP6988_S16_t get_compensated_temperature_(qmp6988_ik_data_t *ik, QMP6988_S32_t dt);
|
||||
};
|
||||
|
||||
} // namespace qmp6988
|
||||
|
@@ -53,10 +53,14 @@ void SenseAirComponent::update() {
|
||||
|
||||
this->status_clear_warning();
|
||||
const uint8_t length = response[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]);
|
||||
const uint16_t status = encode_uint16(response[3], response[4]);
|
||||
const uint16_t ppm = encode_uint16(response[length + 1], response[length + 2]);
|
||||
|
||||
ESP_LOGD(TAG, "SenseAir Received CO₂=%dppm Status=0x%02X", ppm, status);
|
||||
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;
|
||||
}
|
||||
if (this->co2_sensor_ != nullptr)
|
||||
this->co2_sensor_->publish_state(ppm);
|
||||
}
|
||||
|
@@ -8,6 +8,17 @@
|
||||
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; }
|
||||
|
@@ -147,7 +147,7 @@ def _read_audio_file_and_type(file_config):
|
||||
elif file_source == TYPE_WEB:
|
||||
path = _compute_local_file_path(conf_file)
|
||||
else:
|
||||
raise cv.Invalid("Unsupported file source")
|
||||
raise cv.Invalid("Unsupported file source.")
|
||||
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
@@ -219,7 +219,7 @@ def _validate_supported_local_file(config):
|
||||
for file_config in config.get(CONF_FILES, []):
|
||||
_, media_file_type = _read_audio_file_and_type(file_config)
|
||||
if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]):
|
||||
raise cv.Invalid("Unsupported local media file")
|
||||
raise cv.Invalid("Unsupported local media file.")
|
||||
if not config[CONF_CODEC_SUPPORT_ENABLED] and str(media_file_type) != str(
|
||||
audio.AUDIO_FILE_TYPE_ENUM["WAV"]
|
||||
):
|
||||
|
@@ -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://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:
|
||||
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 CONF_CSS_URL not in config:
|
||||
config[CONF_CSS_URL] = ""
|
||||
|
@@ -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://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.
|
||||
*/
|
||||
void set_css_url(const char *css_url);
|
||||
|
||||
/** 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.
|
||||
*/
|
||||
|
@@ -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 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 {
|
||||
|
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.9.0-dev"
|
||||
__version__ = "2025.8.1"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
@@ -475,16 +475,11 @@ bool Application::register_socket_fd(int fd) {
|
||||
if (fd < 0)
|
||||
return false;
|
||||
|
||||
#ifndef USE_ESP32
|
||||
// Only check on non-ESP32 platforms
|
||||
// On ESP32 (both Arduino and ESP-IDF), CONFIG_LWIP_MAX_SOCKETS is always <= FD_SETSIZE by design
|
||||
// (LWIP_SOCKET_OFFSET = FD_SETSIZE - CONFIG_LWIP_MAX_SOCKETS per lwipopts.h)
|
||||
// Other platforms may not have this guarantee
|
||||
if (fd >= FD_SETSIZE) {
|
||||
ESP_LOGE(TAG, "fd %d exceeds FD_SETSIZE %d", fd, FD_SETSIZE);
|
||||
ESP_LOGE(TAG, "Cannot monitor socket fd %d: exceeds FD_SETSIZE (%d)", fd, FD_SETSIZE);
|
||||
ESP_LOGE(TAG, "Socket will not be monitored for data - may cause performance issues!");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->socket_fds_.push_back(fd);
|
||||
this->socket_fds_changed_ = true;
|
||||
|
@@ -10,6 +10,7 @@
|
||||
#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"
|
||||
@@ -248,6 +249,8 @@ 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_; }
|
||||
|
@@ -5,6 +5,8 @@
|
||||
#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>
|
||||
|
||||
@@ -158,7 +160,16 @@ 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_++;
|
||||
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; }
|
||||
|
||||
|
@@ -236,10 +236,21 @@ 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}. "
|
||||
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
|
||||
|
@@ -65,14 +65,17 @@ 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) {
|
||||
const void *name_ptr, uint32_t delay, std::function<void()> func, bool is_retry,
|
||||
bool skip_cancel) {
|
||||
// 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
|
||||
LockGuard guard{this->lock_};
|
||||
this->cancel_item_locked_(component, name_cstr, type);
|
||||
if (!skip_cancel) {
|
||||
LockGuard guard{this->lock_};
|
||||
this->cancel_item_locked_(component, name_cstr, type);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,7 +85,13 @@ 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
|
||||
@@ -91,7 +100,9 @@ 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_};
|
||||
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));
|
||||
return;
|
||||
}
|
||||
@@ -144,9 +155,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
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
|
||||
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_
|
||||
// since we have the lock held
|
||||
this->to_add_.push_back(std::move(item));
|
||||
@@ -398,6 +411,31 @@ 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 ")",
|
||||
@@ -518,7 +556,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)) {
|
||||
item->remove = true;
|
||||
this->mark_item_removed_(item.get());
|
||||
total_cancelled++;
|
||||
}
|
||||
}
|
||||
@@ -528,7 +566,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)) {
|
||||
item->remove = true;
|
||||
this->mark_item_removed_(item.get());
|
||||
total_cancelled++;
|
||||
this->to_remove_++; // Track removals for heap items
|
||||
}
|
||||
@@ -537,7 +575,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)) {
|
||||
item->remove = true;
|
||||
this->mark_item_removed_(item.get());
|
||||
total_cancelled++;
|
||||
// Don't track removals for to_add_ items
|
||||
}
|
||||
|
@@ -21,8 +21,13 @@ struct RetryArgs;
|
||||
void retry_handler(const std::shared_ptr<RetryArgs> &args);
|
||||
|
||||
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);
|
||||
// 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
|
||||
@@ -97,22 +102,42 @@ class Scheduler {
|
||||
|
||||
std::function<void()> callback;
|
||||
|
||||
// Bit-packed fields to minimize padding
|
||||
#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)
|
||||
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
|
||||
// 4 bits padding
|
||||
#endif
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -164,7 +189,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);
|
||||
uint32_t delay, std::function<void()> func, bool is_retry = false, bool skip_cancel = false);
|
||||
|
||||
// Common implementation for retry
|
||||
void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time,
|
||||
@@ -219,6 +244,37 @@ 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,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
pylint==3.3.8
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.12.9 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.12.8 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
|
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
|
||||
constant_brightness: true
|
||||
effects:
|
||||
# Use default parameters:
|
||||
- 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
|
||||
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:
|
||||
@@ -73,6 +92,17 @@ 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"
|
||||
|
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);
|
139
tests/integration/fixtures/scheduler_removed_item_race.yaml
Normal file
139
tests/integration/fixtures/scheduler_removed_item_race.yaml
Normal file
@@ -0,0 +1,139 @@
|
||||
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 ===");
|
@@ -89,3 +89,73 @@ 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"
|
||||
)
|
||||
|
@@ -108,14 +108,51 @@ async def test_light_calls(
|
||||
# Wait for flash to end
|
||||
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
|
||||
client.light_command(key=rgbcw_light.key, state=True)
|
||||
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)
|
||||
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
|
||||
client.light_command(key=rgbcw_light.key, effect="None")
|
||||
|
102
tests/integration/test_scheduler_removed_item_race.py
Normal file
102
tests/integration/test_scheduler_removed_item_race.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""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}"
|
||||
)
|
@@ -705,3 +705,48 @@ def test_empty_or_null_device_id_on_entity() -> None:
|
||||
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)
|
||||
|
Reference in New Issue
Block a user