1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-14 17:22:20 +01:00

Merge branch 'dev' into proxy_memory

This commit is contained in:
J. Nick Koston
2025-06-18 12:16:12 +02:00
committed by GitHub
70 changed files with 1255 additions and 312 deletions

View File

@@ -4,7 +4,7 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.11.10 rev: v0.12.0
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View File

@@ -520,6 +520,7 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xiaomi_rtcgq02lm/* @jesserockz
esphome/components/xiaomi_xmwsdj04mmc/* @medusalix
esphome/components/xl9535/* @mreditor97 esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow esphome/components/xxtea/* @clydebarrow

View File

@@ -17,7 +17,11 @@ void Anova::setup() {
this->current_request_ = 0; this->current_request_ = 0;
} }
void Anova::loop() {} void Anova::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE callbacks so loop isn't needed
this->disable_loop();
}
void Anova::control(const ClimateCall &call) { void Anova::control(const ClimateCall &call) {
if (call.get_mode().has_value()) { if (call.get_mode().has_value()) {

View File

@@ -1643,6 +1643,7 @@ enum VoiceAssistantEvent {
VOICE_ASSISTANT_STT_VAD_END = 12; VOICE_ASSISTANT_STT_VAD_END = 12;
VOICE_ASSISTANT_TTS_STREAM_START = 98; VOICE_ASSISTANT_TTS_STREAM_START = 98;
VOICE_ASSISTANT_TTS_STREAM_END = 99; VOICE_ASSISTANT_TTS_STREAM_END = 99;
VOICE_ASSISTANT_INTENT_PROGRESS = 100;
} }
message VoiceAssistantEventData { message VoiceAssistantEventData {

View File

@@ -516,6 +516,8 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V
return "VOICE_ASSISTANT_TTS_STREAM_START"; return "VOICE_ASSISTANT_TTS_STREAM_START";
case enums::VOICE_ASSISTANT_TTS_STREAM_END: case enums::VOICE_ASSISTANT_TTS_STREAM_END:
return "VOICE_ASSISTANT_TTS_STREAM_END"; return "VOICE_ASSISTANT_TTS_STREAM_END";
case enums::VOICE_ASSISTANT_INTENT_PROGRESS:
return "VOICE_ASSISTANT_INTENT_PROGRESS";
default: default:
return "UNKNOWN"; return "UNKNOWN";
} }

View File

@@ -208,6 +208,7 @@ enum VoiceAssistantEvent : uint32_t {
VOICE_ASSISTANT_STT_VAD_END = 12, VOICE_ASSISTANT_STT_VAD_END = 12,
VOICE_ASSISTANT_TTS_STREAM_START = 98, VOICE_ASSISTANT_TTS_STREAM_START = 98,
VOICE_ASSISTANT_TTS_STREAM_END = 99, VOICE_ASSISTANT_TTS_STREAM_END = 99,
VOICE_ASSISTANT_INTENT_PROGRESS = 100,
}; };
enum VoiceAssistantTimerEvent : uint32_t { enum VoiceAssistantTimerEvent : uint32_t {
VOICE_ASSISTANT_TIMER_STARTED = 0, VOICE_ASSISTANT_TIMER_STARTED = 0,

View File

@@ -21,8 +21,8 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(200.0) @coroutine_with_priority(200.0)
async def to_code(config): async def to_code(config):
if CORE.is_esp32 or CORE.is_libretiny: if CORE.is_esp32 or CORE.is_libretiny:
# https://github.com/esphome/AsyncTCP/blob/master/library.json # https://github.com/ESP32Async/AsyncTCP
cg.add_library("esphome/AsyncTCP-esphome", "2.1.4") cg.add_library("ESP32Async/AsyncTCP", "3.4.4")
elif CORE.is_esp8266: elif CORE.is_esp8266:
# https://github.com/esphome/ESPAsyncTCP # https://github.com/ESP32Async/ESPAsyncTCP
cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0") cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")

View File

@@ -480,7 +480,11 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
/* Internal */ /* Internal */
void BedJetHub::loop() {} void BedJetHub::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE callbacks so loop isn't needed
this->disable_loop();
}
void BedJetHub::update() { this->dispatch_status_(); } void BedJetHub::update() { this->dispatch_status_(); }
void BedJetHub::dump_config() { void BedJetHub::dump_config() {

View File

@@ -83,7 +83,11 @@ void BedJetClimate::reset_state_() {
this->publish_state(); this->publish_state();
} }
void BedJetClimate::loop() {} void BedJetClimate::loop() {
// This component is controlled via the parent BedJetHub
// Empty loop not needed, disable to save CPU cycles
this->disable_loop();
}
void BedJetClimate::control(const ClimateCall &call) { void BedJetClimate::control(const ClimateCall &call) {
ESP_LOGD(TAG, "Received BedJetClimate::control"); ESP_LOGD(TAG, "Received BedJetClimate::control");

View File

@@ -11,7 +11,11 @@ namespace ble_client {
static const char *const TAG = "ble_rssi_sensor"; static const char *const TAG = "ble_rssi_sensor";
void BLEClientRSSISensor::loop() {} void BLEClientRSSISensor::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE GAP callbacks so loop isn't needed
this->disable_loop();
}
void BLEClientRSSISensor::dump_config() { void BLEClientRSSISensor::dump_config() {
LOG_SENSOR("", "BLE Client RSSI Sensor", this); LOG_SENSOR("", "BLE Client RSSI Sensor", this);

View File

@@ -11,7 +11,11 @@ namespace ble_client {
static const char *const TAG = "ble_sensor"; static const char *const TAG = "ble_sensor";
void BLESensor::loop() {} void BLESensor::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE callbacks so loop isn't needed
this->disable_loop();
}
void BLESensor::dump_config() { void BLESensor::dump_config() {
LOG_SENSOR("", "BLE Sensor", this); LOG_SENSOR("", "BLE Sensor", this);

View File

@@ -14,7 +14,11 @@ static const char *const TAG = "ble_text_sensor";
static const std::string EMPTY = ""; static const std::string EMPTY = "";
void BLETextSensor::loop() {} void BLETextSensor::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE callbacks so loop isn't needed
this->disable_loop();
}
void BLETextSensor::dump_config() { void BLETextSensor::dump_config() {
LOG_TEXT_SENSOR("", "BLE Text Sensor", this); LOG_TEXT_SENSOR("", "BLE Text Sensor", this);

View File

@@ -12,8 +12,8 @@ from esphome.const import (
CONF_OVERSAMPLING, CONF_OVERSAMPLING,
CONF_PRESSURE, CONF_PRESSURE,
CONF_TEMPERATURE, CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
ICON_GAS_CYLINDER, ICON_GAS_CYLINDER,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,

View File

@@ -37,7 +37,12 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
request->redirect("/?save"); request->redirect("/?save");
} }
void CaptivePortal::setup() {} void CaptivePortal::setup() {
#ifndef USE_ARDUINO
// No DNS server needed for non-Arduino frameworks
this->disable_loop();
#endif
}
void CaptivePortal::start() { void CaptivePortal::start() {
this->base_->init(); this->base_->init();
if (!this->initialized_) { if (!this->initialized_) {
@@ -50,6 +55,8 @@ void CaptivePortal::start() {
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
this->dns_server_->start(53, "*", ip); this->dns_server_->start(53, "*", ip);
// Re-enable loop() when DNS server is started
this->enable_loop();
#endif #endif
this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
@@ -68,7 +75,11 @@ void CaptivePortal::start() {
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
if (req->url() == "/") { if (req->url() == "/") {
#ifndef USE_ESP8266
auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
#else
auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
#endif
response->addHeader("Content-Encoding", "gzip"); response->addHeader("Content-Encoding", "gzip");
req->send(response); req->send(response);
return; return;

View File

@@ -21,8 +21,11 @@ class CaptivePortal : public AsyncWebHandler, public Component {
void dump_config() override; void dump_config() override;
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
void loop() override { void loop() override {
if (this->dns_server_ != nullptr) if (this->dns_server_ != nullptr) {
this->dns_server_->processNextRequest(); this->dns_server_->processNextRequest();
} else {
this->disable_loop();
}
} }
#endif #endif
float get_setup_priority() const override; float get_setup_priority() const override;
@@ -37,7 +40,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
#endif #endif
} }
bool canHandle(AsyncWebServerRequest *request) override { bool canHandle(AsyncWebServerRequest *request) const override {
if (!this->active_) if (!this->active_)
return false; return false;

View File

@@ -1,6 +1,7 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "ble.h" #include "ble.h"
#include "ble_event_pool.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -23,9 +24,6 @@ namespace esp32_ble {
static const char *const TAG = "esp32_ble"; static const char *const TAG = "esp32_ble";
static RAMAllocator<BLEEvent> EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
void ESP32BLE::setup() { void ESP32BLE::setup() {
global_ble = this; global_ble = this;
ESP_LOGCONFIG(TAG, "Running setup"); ESP_LOGCONFIG(TAG, "Running setup");
@@ -349,9 +347,8 @@ void ESP32BLE::loop() {
default: default:
break; break;
} }
// Destructor will clean up external allocations for GATTC/GATTS // Return the event to the pool
ble_event->~BLEEvent(); this->ble_event_pool_.release(ble_event);
EVENT_ALLOCATOR.deallocate(ble_event, 1);
ble_event = this->ble_events_.pop(); ble_event = this->ble_events_.pop();
} }
if (this->advertising_ != nullptr) { if (this->advertising_ != nullptr) {
@@ -359,37 +356,41 @@ void ESP32BLE::loop() {
} }
// Log dropped events periodically // Log dropped events periodically
size_t dropped = this->ble_events_.get_and_reset_dropped_count(); uint16_t dropped = this->ble_events_.get_and_reset_dropped_count();
if (dropped > 0) { if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %zu BLE events due to buffer overflow", dropped); ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped);
} }
} }
// Helper function to load new event data based on type
void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
event->load_gap_event(e, p);
}
void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
event->load_gattc_event(e, i, p);
}
void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
event->load_gatts_event(e, i, p);
}
template<typename... Args> void enqueue_ble_event(Args... args) { template<typename... Args> void enqueue_ble_event(Args... args) {
// Check if queue is full before allocating // Allocate an event from the pool
if (global_ble->ble_events_.full()) { BLEEvent *event = global_ble->ble_event_pool_.allocate();
// Queue is full, drop the event if (event == nullptr) {
// No events available - queue is full or we're out of memory
global_ble->ble_events_.increment_dropped_count(); global_ble->ble_events_.increment_dropped_count();
return; return;
} }
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); // Load new event data (replaces previous event)
if (new_event == nullptr) { load_ble_event(event, args...);
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
global_ble->ble_events_.increment_dropped_count();
return;
}
new (new_event) BLEEvent(args...);
// Push the event - since we're the only producer and we checked full() above, // Push the event to the queue
// this should always succeed unless we have a bug global_ble->ble_events_.push(event);
if (!global_ble->ble_events_.push(new_event)) { // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
// This should not happen in SPSC queue with single producer }
ESP_LOGE(TAG, "BLE queue push failed unexpectedly");
new_event->~BLEEvent();
EVENT_ALLOCATOR.deallocate(new_event, 1);
}
} // NOLINT(clang-analyzer-unix.Malloc)
// Explicit template instantiations for the friend function // Explicit template instantiations for the friend function
template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *); template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *);

View File

@@ -12,6 +12,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "ble_event.h" #include "ble_event.h"
#include "ble_event_pool.h"
#include "queue.h" #include "queue.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -148,6 +149,7 @@ class ESP32BLE : public Component {
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_; LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
BLEEventPool<MAX_BLE_QUEUE_SIZE> ble_event_pool_;
BLEAdvertising *advertising_{}; BLEAdvertising *advertising_{};
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
uint32_t advertising_cycle_time_{}; uint32_t advertising_cycle_time_{};

View File

@@ -51,6 +51,13 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) ==
// - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring // - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring
// the data remains valid even after the BLE callback returns. The original // the data remains valid even after the BLE callback returns. The original
// param pointer from ESP-IDF is only valid during the callback. // param pointer from ESP-IDF is only valid during the callback.
//
// CRITICAL DESIGN NOTE:
// The heap allocations for GATTC/GATTS events are REQUIRED for memory safety.
// DO NOT attempt to optimize by removing these allocations or storing pointers
// to the original ESP-IDF data. The ESP-IDF callback data has a different lifetime
// than our event processing, and accessing it after the callback returns would
// result in use-after-free bugs and crashes.
class BLEEvent { class BLEEvent {
public: public:
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
@@ -63,125 +70,74 @@ class BLEEvent {
// Constructor for GAP events - no external allocations needed // Constructor for GAP events - no external allocations needed
BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->type_ = GAP; this->type_ = GAP;
this->event_.gap.gap_event = e; this->init_gap_data_(e, p);
if (p == nullptr) {
return; // Invalid event, but we can't log in header file
}
// Only copy the data we actually use for each GAP event type
switch (e) {
case ESP_GAP_BLE_SCAN_RESULT_EVT:
// Copy only the fields we use from scan results
memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len;
this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len;
this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt;
memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv,
ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX);
break;
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_param_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_start_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
break;
default:
// We only handle 4 GAP event types, others are dropped
break;
}
} }
// Constructor for GATTC events - uses heap allocation // Constructor for GATTC events - uses heap allocation
// Creates a copy of the param struct since the original is only valid during the callback // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
// The param pointer from ESP-IDF is only valid during the callback execution.
// Since BLE events are processed asynchronously in the main loop, we must create
// our own copy to ensure the data remains valid until the event is processed.
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->type_ = GATTC; this->type_ = GATTC;
this->event_.gattc.gattc_event = e; this->init_gattc_data_(e, i, p);
this->event_.gattc.gattc_if = i;
if (p == nullptr) {
this->event_.gattc.gattc_param = nullptr;
this->event_.gattc.data = nullptr;
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
this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
// Copy data for events that need it
switch (e) {
case ESP_GATTC_NOTIFY_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
break;
case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_READ_DESCR_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
break;
default:
this->event_.gattc.data = nullptr;
break;
}
} }
// Constructor for GATTS events - uses heap allocation // Constructor for GATTS events - uses heap allocation
// Creates a copy of the param struct since the original is only valid during the callback // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
// The param pointer from ESP-IDF is only valid during the callback execution.
// Since BLE events are processed asynchronously in the main loop, we must create
// our own copy to ensure the data remains valid until the event is processed.
BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->type_ = GATTS; this->type_ = GATTS;
this->event_.gatts.gatts_event = e; this->init_gatts_data_(e, i, p);
this->event_.gatts.gatts_if = i;
if (p == nullptr) {
this->event_.gatts.gatts_param = nullptr;
this->event_.gatts.data = nullptr;
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
this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
// Copy data for events that need it
switch (e) {
case ESP_GATTS_WRITE_EVT:
this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
break;
default:
this->event_.gatts.data = nullptr;
break;
}
} }
// Destructor to clean up heap allocations // Destructor to clean up heap allocations
~BLEEvent() { ~BLEEvent() { this->cleanup_heap_data(); }
switch (this->type_) {
case GATTC: // Default constructor for pre-allocation in pool
BLEEvent() : type_(GAP) {}
// Clean up any heap-allocated data
void cleanup_heap_data() {
if (this->type_ == GAP) {
return;
}
if (this->type_ == GATTC) {
delete this->event_.gattc.gattc_param; delete this->event_.gattc.gattc_param;
delete this->event_.gattc.data; delete this->event_.gattc.data;
break; this->event_.gattc.gattc_param = nullptr;
case GATTS: this->event_.gattc.data = nullptr;
return;
}
if (this->type_ == GATTS) {
delete this->event_.gatts.gatts_param; delete this->event_.gatts.gatts_param;
delete this->event_.gatts.data; delete this->event_.gatts.data;
break; this->event_.gatts.gatts_param = nullptr;
default: this->event_.gatts.data = nullptr;
break;
} }
} }
// Load new event data for reuse (replaces previous event data)
void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->cleanup_heap_data();
this->type_ = GAP;
this->init_gap_data_(e, p);
}
void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->cleanup_heap_data();
this->type_ = GATTC;
this->init_gattc_data_(e, i, p);
}
void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->cleanup_heap_data();
this->type_ = GATTS;
this->init_gatts_data_(e, i, p);
}
// Disable copy to prevent double-delete // Disable copy to prevent double-delete
BLEEvent(const BLEEvent &) = delete; BLEEvent(const BLEEvent &) = delete;
BLEEvent &operator=(const BLEEvent &) = delete; BLEEvent &operator=(const BLEEvent &) = delete;
@@ -224,6 +180,119 @@ class BLEEvent {
esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; } esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; }
const BLEScanResult &scan_result() const { return event_.gap.scan_result; } const BLEScanResult &scan_result() const { return event_.gap.scan_result; }
esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; } esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; }
private:
// 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;
if (p == nullptr) {
return; // Invalid event, but we can't log in header file
}
// Copy data based on event type
switch (e) {
case ESP_GAP_BLE_SCAN_RESULT_EVT:
memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len;
this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len;
this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt;
memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv,
ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX);
break;
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_param_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_start_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
break;
default:
// We only handle 4 GAP event types, others are dropped
break;
}
}
// Initialize GATTC event data
void init_gattc_data_(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->event_.gattc.gattc_event = e;
this->event_.gattc.gattc_if = i;
if (p == nullptr) {
this->event_.gattc.gattc_param = nullptr;
this->event_.gattc.data = nullptr;
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 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 = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
break;
case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_READ_DESCR_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
break;
default:
this->event_.gattc.data = nullptr;
break;
}
}
// Initialize GATTS event data
void init_gatts_data_(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->event_.gatts.gatts_event = e;
this->event_.gatts.gatts_if = i;
if (p == nullptr) {
this->event_.gatts.gatts_param = nullptr;
this->event_.gatts.data = nullptr;
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 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 = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
break;
default:
this->event_.gatts.data = nullptr;
break;
}
}
}; };
// BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding)

View File

@@ -0,0 +1,72 @@
#pragma once
#ifdef USE_ESP32
#include <atomic>
#include <cstddef>
#include "ble_event.h"
#include "queue.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace esp32_ble {
// BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation
// Events are allocated on first use and reused thereafter, growing to peak usage
template<uint8_t SIZE> class BLEEventPool {
public:
BLEEventPool() : total_created_(0) {}
~BLEEventPool() {
// Clean up any remaining events in the free list
BLEEvent *event;
while ((event = this->free_list_.pop()) != nullptr) {
delete event;
}
}
// Allocate an event from the pool
// Returns nullptr if pool is full
BLEEvent *allocate() {
// Try to get from free list first
BLEEvent *event = this->free_list_.pop();
if (event != nullptr)
return event;
// Need to create a new event
if (this->total_created_ >= SIZE) {
// Pool is at capacity
return nullptr;
}
// Use internal RAM for better performance
RAMAllocator<BLEEvent> allocator(RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
event = allocator.allocate(1);
if (event == nullptr) {
// Memory allocation failed
return nullptr;
}
// Placement new to construct the object
new (event) BLEEvent();
this->total_created_++;
return event;
}
// Return an event to the pool for reuse
void release(BLEEvent *event) {
if (event != nullptr) {
this->free_list_.push(event);
}
}
private:
LockFreeQueue<BLEEvent, SIZE> free_list_; // Free events ready for reuse
uint8_t total_created_; // Total events created (high water mark)
};
} // namespace esp32_ble
} // namespace esphome
#endif

View File

@@ -18,7 +18,7 @@
namespace esphome { namespace esphome {
namespace esp32_ble { namespace esp32_ble {
template<class T, size_t SIZE> class LockFreeQueue { template<class T, uint8_t SIZE> class LockFreeQueue {
public: public:
LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
@@ -26,8 +26,8 @@ template<class T, size_t SIZE> class LockFreeQueue {
if (element == nullptr) if (element == nullptr)
return false; return false;
size_t current_tail = tail_.load(std::memory_order_relaxed); uint8_t current_tail = tail_.load(std::memory_order_relaxed);
size_t next_tail = (current_tail + 1) % SIZE; uint8_t next_tail = (current_tail + 1) % SIZE;
if (next_tail == head_.load(std::memory_order_acquire)) { if (next_tail == head_.load(std::memory_order_acquire)) {
// Buffer full // Buffer full
@@ -41,7 +41,7 @@ template<class T, size_t SIZE> class LockFreeQueue {
} }
T *pop() { T *pop() {
size_t current_head = head_.load(std::memory_order_relaxed); uint8_t current_head = head_.load(std::memory_order_relaxed);
if (current_head == tail_.load(std::memory_order_acquire)) { if (current_head == tail_.load(std::memory_order_acquire)) {
return nullptr; // Empty return nullptr; // Empty
@@ -53,27 +53,30 @@ template<class T, size_t SIZE> class LockFreeQueue {
} }
size_t size() const { size_t size() const {
size_t tail = tail_.load(std::memory_order_acquire); uint8_t tail = tail_.load(std::memory_order_acquire);
size_t head = head_.load(std::memory_order_acquire); uint8_t head = head_.load(std::memory_order_acquire);
return (tail - head + SIZE) % SIZE; return (tail - head + SIZE) % SIZE;
} }
size_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); }
void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); }
bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); }
bool full() const { bool full() const {
size_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE;
return next_tail == head_.load(std::memory_order_acquire); return next_tail == head_.load(std::memory_order_acquire);
} }
protected: protected:
T *buffer_[SIZE]; T *buffer_[SIZE];
std::atomic<size_t> head_; // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
std::atomic<size_t> tail_; std::atomic<uint16_t> dropped_count_; // 65535 max - more than enough for drop tracking
std::atomic<size_t> dropped_count_; // Atomic: written by consumer (pop), read by producer (push) to check if full
std::atomic<uint8_t> head_;
// Atomic: written by producer (push), read by consumer (pop) to check if empty
std::atomic<uint8_t> tail_;
}; };
} // namespace esp32_ble } // namespace esp32_ble

View File

@@ -22,6 +22,16 @@ void BLEClientBase::setup() {
this->connection_index_ = connection_index++; this->connection_index_ = connection_index++;
} }
void BLEClientBase::set_state(espbt::ClientState st) {
ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st);
ESPBTClient::set_state(st);
if (st == espbt::ClientState::READY_TO_CONNECT) {
// Enable loop when we need to connect
this->enable_loop();
}
}
void BLEClientBase::loop() { void BLEClientBase::loop() {
if (!esp32_ble::global_ble->is_active()) { if (!esp32_ble::global_ble->is_active()) {
this->set_state(espbt::ClientState::INIT); this->set_state(espbt::ClientState::INIT);
@@ -37,9 +47,14 @@ void BLEClientBase::loop() {
} }
// READY_TO_CONNECT means we have discovered the device // READY_TO_CONNECT means we have discovered the device
// and the scanner has been stopped by the tracker. // and the scanner has been stopped by the tracker.
if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
this->connect(); this->connect();
} }
// If its idle, we can disable the loop as set_state
// will enable it again when we need to connect.
else if (this->state_ == espbt::ClientState::IDLE) {
this->disable_loop();
}
} }
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }

View File

@@ -93,6 +93,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; } bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; }
void set_state(espbt::ClientState st) override;
protected: protected:
// Memory optimized layout for 32-bit systems // Memory optimized layout for 32-bit systems
// Group 1: 8-byte types // Group 1: 8-byte types

View File

@@ -1,25 +0,0 @@
#ifdef USE_ESP32
#include "esp32_hall.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <driver/adc.h>
namespace esphome {
namespace esp32_hall {
static const char *const TAG = "esp32_hall";
void ESP32HallSensor::update() {
adc1_config_width(ADC_WIDTH_BIT_12);
int value_int = hall_sensor_read();
float value = (value_int / 4095.0f) * 10000.0f;
ESP_LOGD(TAG, "'%s': Got reading %.0f µT", this->name_.c_str(), value);
this->publish_state(value);
}
std::string ESP32HallSensor::unique_id() { return get_mac_address() + "-hall"; }
void ESP32HallSensor::dump_config() { LOG_SENSOR("", "ESP32 Hall Sensor", this); }
} // namespace esp32_hall
} // namespace esphome
#endif

View File

@@ -1,23 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#ifdef USE_ESP32
namespace esphome {
namespace esp32_hall {
class ESP32HallSensor : public sensor::Sensor, public PollingComponent {
public:
void dump_config() override;
void update() override;
std::string unique_id() override;
};
} // namespace esp32_hall
} // namespace esphome
#endif

View File

@@ -1,24 +0,0 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import ICON_MAGNET, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA
DEPENDENCIES = ["esp32"]
esp32_hall_ns = cg.esphome_ns.namespace("esp32_hall")
ESP32HallSensor = esp32_hall_ns.class_(
"ESP32HallSensor", sensor.Sensor, cg.PollingComponent
)
CONFIG_SCHEMA = sensor.sensor_schema(
ESP32HallSensor,
unit_of_measurement=UNIT_MICROTESLA,
icon=ICON_MAGNET,
accuracy_decimals=1,
state_class=STATE_CLASS_MEASUREMENT,
).extend(cv.polling_component_schema("60s"))
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)

View File

@@ -168,6 +168,8 @@ void ESP32ImprovComponent::loop() {
case improv::STATE_PROVISIONED: { case improv::STATE_PROVISIONED: {
this->incoming_data_.clear(); this->incoming_data_.clear();
this->set_status_indicator_state_(false); this->set_status_indicator_state_(false);
// Provisioning complete, no further loop execution needed
this->disable_loop();
break; break;
} }
} }
@@ -254,6 +256,7 @@ void ESP32ImprovComponent::start() {
ESP_LOGD(TAG, "Setting Improv to start"); ESP_LOGD(TAG, "Setting Improv to start");
this->should_start_ = true; this->should_start_ = true;
this->enable_loop();
} }
void ESP32ImprovComponent::stop() { void ESP32ImprovComponent::stop() {

View File

@@ -125,6 +125,6 @@ async def to_code(config):
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
cg.add_library("tonia/HeatpumpIR", "1.0.32") cg.add_library("tonia/HeatpumpIR", "1.0.35")
if CORE.is_libretiny: if CORE.is_libretiny:
CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")

View File

@@ -116,5 +116,5 @@ async def to_code(config):
cg.add_library("WiFiClientSecure", None) cg.add_library("WiFiClientSecure", None)
cg.add_library("HTTPClient", None) cg.add_library("HTTPClient", None)
cg.add_library("esphome/ESP32-audioI2S", "2.2.0") cg.add_library("esphome/ESP32-audioI2S", "2.3.0")
cg.add_build_flag("-DAUDIO_NO_SD_FS") cg.add_build_flag("-DAUDIO_NO_SD_FS")

View File

@@ -17,7 +17,7 @@ namespace light {
class LightOutput; class LightOutput;
enum LightRestoreMode { enum LightRestoreMode : uint8_t {
LIGHT_RESTORE_DEFAULT_OFF, LIGHT_RESTORE_DEFAULT_OFF,
LIGHT_RESTORE_DEFAULT_ON, LIGHT_RESTORE_DEFAULT_ON,
LIGHT_ALWAYS_OFF, LIGHT_ALWAYS_OFF,
@@ -212,12 +212,18 @@ class LightState : public EntityBase, public Component {
/// Store the output to allow effects to have more access. /// Store the output to allow effects to have more access.
LightOutput *output_; LightOutput *output_;
/// Value for storing the index of the currently active effect. 0 if no effect is active
uint32_t active_effect_index_{};
/// The currently active transformer for this light (transition/flash). /// The currently active transformer for this light (transition/flash).
std::unique_ptr<LightTransformer> transformer_{nullptr}; std::unique_ptr<LightTransformer> transformer_{nullptr};
/// Whether the light value should be written in the next cycle. /// List of effects for this light.
bool next_write_{true}; std::vector<LightEffect *> effects_;
/// Value for storing the index of the currently active effect. 0 if no effect is active
uint32_t active_effect_index_{};
/// Default transition length for all transitions in ms.
uint32_t default_transition_length_{};
/// Transition length to use for flash transitions.
uint32_t flash_transition_length_{};
/// Gamma correction factor for the light.
float gamma_correct_{};
/// Object used to store the persisted values of the light. /// Object used to store the persisted values of the light.
ESPPreferenceObject rtc_; ESPPreferenceObject rtc_;
@@ -236,19 +242,13 @@ class LightState : public EntityBase, public Component {
*/ */
CallbackManager<void()> target_state_reached_callback_{}; CallbackManager<void()> target_state_reached_callback_{};
/// Default transition length for all transitions in ms.
uint32_t default_transition_length_{};
/// Transition length to use for flash transitions.
uint32_t flash_transition_length_{};
/// Gamma correction factor for the light.
float gamma_correct_{};
/// Restore mode of the light.
LightRestoreMode restore_mode_;
/// Initial state of the light. /// Initial state of the light.
optional<LightStateRTCState> initial_state_{}; optional<LightStateRTCState> initial_state_{};
/// List of effects for this light.
std::vector<LightEffect *> effects_;
/// Restore mode of the light.
LightRestoreMode restore_mode_;
/// Whether the light value should be written in the next cycle.
bool next_write_{true};
// for effects, true if a transformer (transition) is active. // for effects, true if a transformer (transition) is active.
bool is_transformer_active_ = false; bool is_transformer_active_ = false;
}; };

View File

@@ -178,18 +178,21 @@ void OnlineImage::update() {
if (this->format_ == ImageFormat::BMP) { if (this->format_ == ImageFormat::BMP) {
ESP_LOGD(TAG, "Allocating BMP decoder"); ESP_LOGD(TAG, "Allocating BMP decoder");
this->decoder_ = make_unique<BmpDecoder>(this); this->decoder_ = make_unique<BmpDecoder>(this);
this->enable_loop();
} }
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT #endif // USE_ONLINE_IMAGE_BMP_SUPPORT
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT #ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
if (this->format_ == ImageFormat::JPEG) { if (this->format_ == ImageFormat::JPEG) {
ESP_LOGD(TAG, "Allocating JPEG decoder"); ESP_LOGD(TAG, "Allocating JPEG decoder");
this->decoder_ = esphome::make_unique<JpegDecoder>(this); this->decoder_ = esphome::make_unique<JpegDecoder>(this);
this->enable_loop();
} }
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT #endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
if (this->format_ == ImageFormat::PNG) { if (this->format_ == ImageFormat::PNG) {
ESP_LOGD(TAG, "Allocating PNG decoder"); ESP_LOGD(TAG, "Allocating PNG decoder");
this->decoder_ = make_unique<PngDecoder>(this); this->decoder_ = make_unique<PngDecoder>(this);
this->enable_loop();
} }
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT #endif // USE_ONLINE_IMAGE_PNG_SUPPORT
@@ -212,6 +215,7 @@ void OnlineImage::update() {
void OnlineImage::loop() { void OnlineImage::loop() {
if (!this->decoder_) { if (!this->decoder_) {
// Not decoding at the moment => nothing to do. // Not decoding at the moment => nothing to do.
this->disable_loop();
return; return;
} }
if (!this->downloader_ || this->decoder_->is_finished()) { if (!this->downloader_ || this->decoder_->is_finished()) {

View File

@@ -12,6 +12,8 @@ class IntervalSyncer : public Component {
void setup() override { void setup() override {
if (this->write_interval_ != 0) { if (this->write_interval_ != 0) {
set_interval(this->write_interval_, []() { global_preferences->sync(); }); set_interval(this->write_interval_, []() { global_preferences->sync(); });
// When using interval-based syncing, we don't need the loop
this->disable_loop();
} }
} }
void loop() override { void loop() override {

View File

@@ -40,7 +40,7 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
*/ */
void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); } void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); }
bool canHandle(AsyncWebServerRequest *request) override { bool canHandle(AsyncWebServerRequest *request) const override {
if (request->method() == HTTP_GET) { if (request->method() == HTTP_GET) {
if (request->url() == "/metrics") if (request->url() == "/metrics")
return true; return true;

View File

@@ -142,8 +142,10 @@ void Rtttl::stop() {
} }
void Rtttl::loop() { void Rtttl::loop() {
if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) {
this->disable_loop();
return; return;
}
#ifdef USE_SPEAKER #ifdef USE_SPEAKER
if (this->speaker_ != nullptr) { if (this->speaker_ != nullptr) {
@@ -391,6 +393,11 @@ void Rtttl::set_state_(State state) {
this->state_ = state; this->state_ = state;
ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)), ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)),
LOG_STR_ARG(state_to_string(state))); LOG_STR_ARG(state_to_string(state)));
// Clear loop_done when transitioning from STOPPED to any other state
if (old_state == State::STATE_STOPPED && state != State::STATE_STOPPED) {
this->enable_loop();
}
} }
} // namespace rtttl } // namespace rtttl

View File

@@ -42,6 +42,8 @@ void SafeModeComponent::loop() {
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
this->clean_rtc(); this->clean_rtc();
this->boot_successful_ = true; this->boot_successful_ = true;
// Disable loop since we no longer need to check
this->disable_loop();
} }
} }

View File

@@ -67,6 +67,12 @@ void SNTPComponent::loop() {
time.minute, time.second); time.minute, time.second);
this->time_sync_callback_.call(); this->time_sync_callback_.call();
this->has_time_ = true; this->has_time_ = true;
#ifdef USE_ESP_IDF
// On ESP-IDF, time sync is permanent and update() doesn't force resync
// Time is now synchronized, no need to check anymore
this->disable_loop();
#endif
} }
} // namespace sntp } // namespace sntp

View File

@@ -21,7 +21,7 @@ const int RESTORE_MODE_PERSISTENT_MASK = 0x02;
const int RESTORE_MODE_INVERTED_MASK = 0x04; const int RESTORE_MODE_INVERTED_MASK = 0x04;
const int RESTORE_MODE_DISABLED_MASK = 0x08; const int RESTORE_MODE_DISABLED_MASK = 0x08;
enum SwitchRestoreMode { enum SwitchRestoreMode : uint8_t {
SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK, SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK,
SWITCH_ALWAYS_ON = RESTORE_MODE_ON_MASK, SWITCH_ALWAYS_ON = RESTORE_MODE_ON_MASK,
SWITCH_RESTORE_DEFAULT_OFF = RESTORE_MODE_PERSISTENT_MASK, SWITCH_RESTORE_DEFAULT_OFF = RESTORE_MODE_PERSISTENT_MASK,
@@ -49,12 +49,12 @@ class Switch : public EntityBase, public EntityBase_DeviceClass {
*/ */
void publish_state(bool state); void publish_state(bool state);
/// The current reported state of the binary sensor.
bool state;
/// Indicates whether or not state is to be retrieved from flash and how /// Indicates whether or not state is to be retrieved from flash and how
SwitchRestoreMode restore_mode{SWITCH_RESTORE_DEFAULT_OFF}; SwitchRestoreMode restore_mode{SWITCH_RESTORE_DEFAULT_OFF};
/// The current reported state of the binary sensor.
bool state;
/** Turn this switch on. This is called by the front-end. /** Turn this switch on. This is called by the front-end.
* *
* For implementing switches, please override write_state. * For implementing switches, please override write_state.
@@ -123,10 +123,16 @@ class Switch : public EntityBase, public EntityBase_DeviceClass {
*/ */
virtual void write_state(bool state) = 0; virtual void write_state(bool state) = 0;
CallbackManager<void(bool)> state_callback_{}; // Pointer first (4 bytes)
bool inverted_{false};
Deduplicator<bool> publish_dedup_;
ESPPreferenceObject rtc_; ESPPreferenceObject rtc_;
// CallbackManager (12 bytes on 32-bit - contains vector)
CallbackManager<void(bool)> state_callback_{};
// Small types grouped together
Deduplicator<bool> publish_dedup_; // 2 bytes (bool has_value_ + bool last_value_)
bool inverted_{false}; // 1 byte
// Total: 3 bytes, 1 byte padding
}; };
#define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj)) #define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj))

View File

@@ -24,8 +24,10 @@ void TLC5971::dump_config() {
} }
void TLC5971::loop() { void TLC5971::loop() {
if (!this->update_) if (!this->update_) {
this->disable_loop();
return; return;
}
uint32_t command; uint32_t command;
@@ -93,6 +95,7 @@ void TLC5971::set_channel_value(uint16_t channel, uint16_t value) {
return; return;
if (this->pwm_amounts_[channel] != value) { if (this->pwm_amounts_[channel] != value) {
this->update_ = true; this->update_ = true;
this->enable_loop();
} }
this->pwm_amounts_[channel] = value; this->pwm_amounts_[channel] = value;
} }

View File

@@ -91,7 +91,7 @@ void DeferredUpdateEventSource::process_deferred_queue_() {
while (!deferred_queue_.empty()) { while (!deferred_queue_.empty()) {
DeferredEvent &de = deferred_queue_.front(); DeferredEvent &de = deferred_queue_.front();
std::string message = de.message_generator_(web_server_, de.source_); std::string message = de.message_generator_(web_server_, de.source_);
if (this->try_send(message.c_str(), "state")) { if (this->send(message.c_str(), "state") != DISCARDED) {
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
deferred_queue_.erase(deferred_queue_.begin()); deferred_queue_.erase(deferred_queue_.begin());
} else { } else {
@@ -131,7 +131,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *
deq_push_back_with_dedup_(source, message_generator); deq_push_back_with_dedup_(source, message_generator);
} else { } else {
std::string message = message_generator(web_server_, source); std::string message = message_generator(web_server_, source);
if (!this->try_send(message.c_str(), "state")) { if (this->send(message.c_str(), "state") == DISCARDED) {
deq_push_back_with_dedup_(source, message_generator); deq_push_back_with_dedup_(source, message_generator);
} }
} }
@@ -171,8 +171,8 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer
ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); }); ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); });
}); });
es->onDisconnect([this, ws](AsyncEventSource *source, AsyncEventSourceClient *client) { es->onDisconnect([this, ws, es](AsyncEventSourceClient *client) {
ws->defer([this, source]() { this->on_client_disconnect_((DeferredUpdateEventSource *) source); }); ws->defer([this, es]() { this->on_client_disconnect_((DeferredUpdateEventSource *) es); });
}); });
es->handleRequest(request); es->handleRequest(request);
@@ -291,14 +291,23 @@ float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f
#ifdef USE_WEBSERVER_LOCAL #ifdef USE_WEBSERVER_LOCAL
void WebServer::handle_index_request(AsyncWebServerRequest *request) { void WebServer::handle_index_request(AsyncWebServerRequest *request) {
#ifndef USE_ESP8266
AsyncWebServerResponse *response = request->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
#else
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
#endif
response->addHeader("Content-Encoding", "gzip"); response->addHeader("Content-Encoding", "gzip");
request->send(response); request->send(response);
} }
#elif USE_WEBSERVER_VERSION >= 2 #elif USE_WEBSERVER_VERSION >= 2
void WebServer::handle_index_request(AsyncWebServerRequest *request) { void WebServer::handle_index_request(AsyncWebServerRequest *request) {
#ifndef USE_ESP8266
AsyncWebServerResponse *response =
request->beginResponse(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE);
#else
AsyncWebServerResponse *response = AsyncWebServerResponse *response =
request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE); request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE);
#endif
// No gzip header here because the HTML file is so small // No gzip header here because the HTML file is so small
request->send(response); request->send(response);
} }
@@ -317,8 +326,13 @@ void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) {
#ifdef USE_WEBSERVER_CSS_INCLUDE #ifdef USE_WEBSERVER_CSS_INCLUDE
void WebServer::handle_css_request(AsyncWebServerRequest *request) { void WebServer::handle_css_request(AsyncWebServerRequest *request) {
#ifndef USE_ESP8266
AsyncWebServerResponse *response =
request->beginResponse(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE);
#else
AsyncWebServerResponse *response = AsyncWebServerResponse *response =
request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE);
#endif
response->addHeader("Content-Encoding", "gzip"); response->addHeader("Content-Encoding", "gzip");
request->send(response); request->send(response);
} }
@@ -326,8 +340,13 @@ void WebServer::handle_css_request(AsyncWebServerRequest *request) {
#ifdef USE_WEBSERVER_JS_INCLUDE #ifdef USE_WEBSERVER_JS_INCLUDE
void WebServer::handle_js_request(AsyncWebServerRequest *request) { void WebServer::handle_js_request(AsyncWebServerRequest *request) {
#ifndef USE_ESP8266
AsyncWebServerResponse *response =
request->beginResponse(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE);
#else
AsyncWebServerResponse *response = AsyncWebServerResponse *response =
request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE);
#endif
response->addHeader("Content-Encoding", "gzip"); response->addHeader("Content-Encoding", "gzip");
request->send(response); request->send(response);
} }
@@ -1837,7 +1856,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
} }
#endif #endif
bool WebServer::canHandle(AsyncWebServerRequest *request) { bool WebServer::canHandle(AsyncWebServerRequest *request) const {
if (request->url() == "/") if (request->url() == "/")
return true; return true;
@@ -1859,12 +1878,6 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) { if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) {
#ifdef USE_ARDUINO
// Header needs to be added to interesting header list for it to not be
// nuked by the time we handle the request later.
// Only required in Arduino framework.
request->addInterestingHeader(HEADER_CORS_REQ_PNA);
#endif
return true; return true;
} }
#endif #endif
@@ -2145,7 +2158,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
#endif #endif
} }
bool WebServer::isRequestHandlerTrivial() { return false; } bool WebServer::isRequestHandlerTrivial() const { return false; }
void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) { void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) {
this->sorting_entitys_[entity] = SortingComponents{weight, group}; this->sorting_entitys_[entity] = SortingComponents{weight, group};

View File

@@ -99,7 +99,7 @@ class DeferredUpdateEventSource : public AsyncEventSource {
protected: protected:
// surface a couple methods from the base class // surface a couple methods from the base class
using AsyncEventSource::handleRequest; using AsyncEventSource::handleRequest;
using AsyncEventSource::try_send; using AsyncEventSource::send;
ListEntitiesIterator entities_iterator_; ListEntitiesIterator entities_iterator_;
// vector is used very specifically for its zero memory overhead even though items are popped from the front (memory // vector is used very specifically for its zero memory overhead even though items are popped from the front (memory
@@ -468,11 +468,11 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
#endif #endif
/// Override the web handler's canHandle method. /// Override the web handler's canHandle method.
bool canHandle(AsyncWebServerRequest *request) override; bool canHandle(AsyncWebServerRequest *request) const override;
/// Override the web handler's handleRequest method. /// Override the web handler's handleRequest method.
void handleRequest(AsyncWebServerRequest *request) override; void handleRequest(AsyncWebServerRequest *request) override;
/// This web handle is not trivial. /// This web handle is not trivial.
bool isRequestHandlerTrivial() override; // NOLINT(readability-identifier-naming) bool isRequestHandlerTrivial() const override; // NOLINT(readability-identifier-naming)
void add_entity_config(EntityBase *entity, float weight, uint64_t group); void add_entity_config(EntityBase *entity, float weight, uint64_t group);
void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight); void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight);

View File

@@ -36,5 +36,7 @@ async def to_code(config):
cg.add_library("WiFi", None) cg.add_library("WiFi", None)
cg.add_library("FS", None) cg.add_library("FS", None)
cg.add_library("Update", None) cg.add_library("Update", None)
# https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json if CORE.is_esp8266:
cg.add_library("esphome/ESPAsyncWebServer-esphome", "3.3.0") cg.add_library("ESP8266WiFi", None)
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8")

View File

@@ -23,7 +23,7 @@ class MiddlewareHandler : public AsyncWebHandler {
public: public:
MiddlewareHandler(AsyncWebHandler *next) : next_(next) {} MiddlewareHandler(AsyncWebHandler *next) : next_(next) {}
bool canHandle(AsyncWebServerRequest *request) override { return next_->canHandle(request); } bool canHandle(AsyncWebServerRequest *request) const override { return next_->canHandle(request); }
void handleRequest(AsyncWebServerRequest *request) override { next_->handleRequest(request); } void handleRequest(AsyncWebServerRequest *request) override { next_->handleRequest(request); }
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
bool final) override { bool final) override {
@@ -32,7 +32,7 @@ class MiddlewareHandler : public AsyncWebHandler {
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override { void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override {
next_->handleBody(request, data, len, index, total); next_->handleBody(request, data, len, index, total);
} }
bool isRequestHandlerTrivial() override { return next_->isRequestHandlerTrivial(); } bool isRequestHandlerTrivial() const override { return next_->isRequestHandlerTrivial(); }
protected: protected:
AsyncWebHandler *next_; AsyncWebHandler *next_;
@@ -131,12 +131,12 @@ class OTARequestHandler : public AsyncWebHandler {
void handleRequest(AsyncWebServerRequest *request) override; void handleRequest(AsyncWebServerRequest *request) override;
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
bool final) override; bool final) override;
bool canHandle(AsyncWebServerRequest *request) override { bool canHandle(AsyncWebServerRequest *request) const override {
return request->url() == "/update" && request->method() == HTTP_POST; return request->url() == "/update" && request->method() == HTTP_POST;
} }
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
bool isRequestHandlerTrivial() override { return false; } bool isRequestHandlerTrivial() const override { return false; }
protected: protected:
uint32_t last_ota_progress_{0}; uint32_t last_ota_progress_{0};

View File

@@ -135,7 +135,7 @@ class AsyncWebServerRequest {
return res; return res;
} }
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
AsyncWebServerResponse *beginResponse_P(int code, const char *content_type, const uint8_t *data, AsyncWebServerResponse *beginResponse(int code, const char *content_type, const uint8_t *data,
const size_t data_size) { const size_t data_size) {
auto *res = new AsyncWebServerResponseProgmem(this, data, data_size); // NOLINT(cppcoreguidelines-owning-memory) auto *res = new AsyncWebServerResponseProgmem(this, data, data_size); // NOLINT(cppcoreguidelines-owning-memory)
this->init_response_(res, code, content_type); this->init_response_(res, code, content_type);
@@ -211,7 +211,7 @@ class AsyncWebHandler {
public: public:
virtual ~AsyncWebHandler() {} virtual ~AsyncWebHandler() {}
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
virtual bool canHandle(AsyncWebServerRequest *request) { return false; } virtual bool canHandle(AsyncWebServerRequest *request) const { return false; }
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
virtual void handleRequest(AsyncWebServerRequest *request) {} virtual void handleRequest(AsyncWebServerRequest *request) {}
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
@@ -220,7 +220,7 @@ class AsyncWebHandler {
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {} virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {}
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
virtual bool isRequestHandlerTrivial() { return true; } virtual bool isRequestHandlerTrivial() const { return true; }
}; };
#ifdef USE_WEBSERVER #ifdef USE_WEBSERVER
@@ -290,7 +290,7 @@ class AsyncEventSource : public AsyncWebHandler {
~AsyncEventSource() override; ~AsyncEventSource() override;
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
bool canHandle(AsyncWebServerRequest *request) override { bool canHandle(AsyncWebServerRequest *request) const override {
return request->method() == HTTP_GET && request->url() == this->url_; return request->method() == HTTP_GET && request->url() == this->url_;
} }
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)

View File

@@ -91,6 +91,13 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_
// MiaoMiaoce humidity, 1 byte, 8-bit unsigned integer, 1 % // MiaoMiaoce humidity, 1 byte, 8-bit unsigned integer, 1 %
else if ((value_type == 0x4C02) && (value_length == 1)) { else if ((value_type == 0x4C02) && (value_length == 1)) {
result.humidity = data[0]; result.humidity = data[0];
}
// XMWSDJ04MMC humidity, 4 bytes, float, 0.1 °C
else if ((value_type == 0x4C08) && (value_length == 4)) {
const uint32_t int_number = encode_uint32(data[3], data[2], data[1], data[0]);
float humidity;
std::memcpy(&humidity, &int_number, sizeof(humidity));
result.humidity = humidity;
} else { } else {
return false; return false;
} }
@@ -219,6 +226,11 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service
} else if (device_uuid == 0x055b) { // small square body, segment LCD, encrypted } else if (device_uuid == 0x055b) { // small square body, segment LCD, encrypted
result.type = XiaomiParseResult::TYPE_LYWSD03MMC; result.type = XiaomiParseResult::TYPE_LYWSD03MMC;
result.name = "LYWSD03MMC"; result.name = "LYWSD03MMC";
} else if (device_uuid == 0x1203) { // small square body, e-ink display, encrypted
result.type = XiaomiParseResult::TYPE_XMWSDJ04MMC;
result.name = "XMWSDJ04MMC";
if (raw.size() == 19)
result.raw_offset -= 6;
} else if (device_uuid == 0x07f6) { // Xiaomi-Yeelight BLE nightlight } else if (device_uuid == 0x07f6) { // Xiaomi-Yeelight BLE nightlight
result.type = XiaomiParseResult::TYPE_MJYD02YLA; result.type = XiaomiParseResult::TYPE_MJYD02YLA;
result.name = "MJYD02YLA"; result.name = "MJYD02YLA";

View File

@@ -20,6 +20,7 @@ struct XiaomiParseResult {
TYPE_LYWSD02MMC, TYPE_LYWSD02MMC,
TYPE_CGG1, TYPE_CGG1,
TYPE_LYWSD03MMC, TYPE_LYWSD03MMC,
TYPE_XMWSDJ04MMC,
TYPE_CGD1, TYPE_CGD1,
TYPE_CGDK2, TYPE_CGDK2,
TYPE_JQJCY01YM, TYPE_JQJCY01YM,

View File

@@ -0,0 +1,77 @@
import esphome.codegen as cg
from esphome.components import esp32_ble_tracker, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_BATTERY_LEVEL,
CONF_BINDKEY,
CONF_HUMIDITY,
CONF_ID,
CONF_MAC_ADDRESS,
CONF_TEMPERATURE,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PERCENT,
)
AUTO_LOAD = ["xiaomi_ble"]
CODEOWNERS = ["@medusalix"]
DEPENDENCIES = ["esp32_ble_tracker"]
xiaomi_xmwsdj04mmc_ns = cg.esphome_ns.namespace("xiaomi_xmwsdj04mmc")
XiaomiXMWSDJ04MMC = xiaomi_xmwsdj04mmc_ns.class_(
"XiaomiXMWSDJ04MMC", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(XiaomiXMWSDJ04MMC),
cv.Required(CONF_BINDKEY): cv.bind_key,
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_BATTERY,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await esp32_ble_tracker.register_ble_device(var, config)
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
cg.add(var.set_bindkey(config[CONF_BINDKEY]))
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity(sens))
if battery_level_config := config.get(CONF_BATTERY_LEVEL):
sens = await sensor.new_sensor(battery_level_config)
cg.add(var.set_battery_level(sens))

View File

@@ -0,0 +1,77 @@
#include "xiaomi_xmwsdj04mmc.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32
namespace esphome {
namespace xiaomi_xmwsdj04mmc {
static const char *const TAG = "xiaomi_xmwsdj04mmc";
void XiaomiXMWSDJ04MMC::dump_config() {
ESP_LOGCONFIG(TAG, "Xiaomi XMWSDJ04MMC");
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str());
LOG_SENSOR(" ", "Temperature", this->temperature_);
LOG_SENSOR(" ", "Humidity", this->humidity_);
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
}
bool XiaomiXMWSDJ04MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
if (device.address_uint64() != this->address_) {
ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
return false;
}
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
bool success = false;
for (auto &service_data : device.get_service_datas()) {
auto res = xiaomi_ble::parse_xiaomi_header(service_data);
if (!res.has_value()) {
continue;
}
if (res->is_duplicate) {
continue;
}
if (res->has_encryption &&
(!(xiaomi_ble::decrypt_xiaomi_payload(const_cast<std::vector<uint8_t> &>(service_data.data), this->bindkey_,
this->address_)))) {
continue;
}
if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
continue;
}
if (res->humidity.has_value() && this->humidity_ != nullptr) {
// see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254
*res->humidity = trunc(*res->humidity);
}
if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
continue;
}
if (res->temperature.has_value() && this->temperature_ != nullptr)
this->temperature_->publish_state(*res->temperature);
if (res->humidity.has_value() && this->humidity_ != nullptr)
this->humidity_->publish_state(*res->humidity);
if (res->battery_level.has_value() && this->battery_level_ != nullptr)
this->battery_level_->publish_state(*res->battery_level);
success = true;
}
return success;
}
void XiaomiXMWSDJ04MMC::set_bindkey(const std::string &bindkey) {
memset(this->bindkey_, 0, 16);
if (bindkey.size() != 32) {
return;
}
char temp[3] = {0};
for (int i = 0; i < 16; i++) {
strncpy(temp, &(bindkey.c_str()[i * 2]), 2);
this->bindkey_[i] = std::strtoul(temp, nullptr, 16);
}
}
} // namespace xiaomi_xmwsdj04mmc
} // namespace esphome
#endif

View File

@@ -0,0 +1,37 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
#ifdef USE_ESP32
namespace esphome {
namespace xiaomi_xmwsdj04mmc {
class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { this->address_ = address; }
void set_bindkey(const std::string &bindkey);
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; }
protected:
uint64_t address_;
uint8_t bindkey_[16];
sensor::Sensor *temperature_{nullptr};
sensor::Sensor *humidity_{nullptr};
sensor::Sensor *battery_level_{nullptr};
};
} // namespace xiaomi_xmwsdj04mmc
} // namespace esphome
#endif

View File

@@ -97,7 +97,13 @@ void Application::loop() {
// Feed WDT with time // Feed WDT with time
this->feed_wdt(last_op_end_time); this->feed_wdt(last_op_end_time);
for (Component *component : this->looping_components_) { // Mark that we're in the loop for safe reentrant modifications
this->in_loop_ = true;
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
this->current_loop_index_++) {
Component *component = this->looping_components_[this->current_loop_index_];
// Update the cached time before each component runs // Update the cached time before each component runs
this->loop_component_start_time_ = last_op_end_time; this->loop_component_start_time_ = last_op_end_time;
@@ -112,6 +118,8 @@ void Application::loop() {
this->app_state_ |= new_app_state; this->app_state_ |= new_app_state;
this->feed_wdt(last_op_end_time); this->feed_wdt(last_op_end_time);
} }
this->in_loop_ = false;
this->app_state_ = new_app_state; this->app_state_ = new_app_state;
// Use the last component's end time instead of calling millis() again // Use the last component's end time instead of calling millis() again
@@ -235,10 +243,67 @@ void Application::teardown_components(uint32_t timeout_ms) {
} }
void Application::calculate_looping_components_() { void Application::calculate_looping_components_() {
// First add all active components
for (auto *obj : this->components_) { for (auto *obj : this->components_) {
if (obj->has_overridden_loop()) if (obj->has_overridden_loop() &&
(obj->get_component_state() & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) {
this->looping_components_.push_back(obj); this->looping_components_.push_back(obj);
} }
}
this->looping_components_active_end_ = this->looping_components_.size();
// Then add all inactive (LOOP_DONE) components
// This handles components that called disable_loop() during setup, before this method runs
for (auto *obj : this->components_) {
if (obj->has_overridden_loop() &&
(obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) {
this->looping_components_.push_back(obj);
}
}
}
void Application::disable_component_loop_(Component *component) {
// This method must be reentrant - components can disable themselves during their own loop() call
// Linear search to find component in active section
// Most configs have 10-30 looping components (30 is on the high end)
// O(n) is acceptable here as we optimize for memory, not complexity
for (uint16_t i = 0; i < this->looping_components_active_end_; i++) {
if (this->looping_components_[i] == component) {
// Move last active component to this position
this->looping_components_active_end_--;
if (i != this->looping_components_active_end_) {
std::swap(this->looping_components_[i], this->looping_components_[this->looping_components_active_end_]);
// If we're currently iterating and just swapped the current position
if (this->in_loop_ && i == this->current_loop_index_) {
// Decrement so we'll process the swapped component next
this->current_loop_index_--;
}
}
return;
}
}
}
void Application::enable_component_loop_(Component *component) {
// This method must be reentrant - components can re-enable themselves during their own loop() call
// Single pass through all components to find and move if needed
// With typical 10-30 components, O(n) is faster than maintaining a map
const uint16_t size = this->looping_components_.size();
for (uint16_t i = 0; i < size; i++) {
if (this->looping_components_[i] == component) {
if (i < this->looping_components_active_end_) {
return; // Already active
}
// Found in inactive section - move to active
if (i != this->looping_components_active_end_) {
std::swap(this->looping_components_[i], this->looping_components_[this->looping_components_active_end_]);
}
this->looping_components_active_end_++;
return;
}
}
} }
#ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_SOCKET_SELECT_SUPPORT

View File

@@ -572,13 +572,41 @@ class Application {
void calculate_looping_components_(); void calculate_looping_components_();
// These methods are called by Component::disable_loop() and Component::enable_loop()
// Components should not call these directly - use this->disable_loop() or this->enable_loop()
// to ensure component state is properly updated along with the loop partition
void disable_component_loop_(Component *component);
void enable_component_loop_(Component *component);
void feed_wdt_arch_(); void feed_wdt_arch_();
/// Perform a delay while also monitoring socket file descriptors for readiness /// Perform a delay while also monitoring socket file descriptors for readiness
void yield_with_select_(uint32_t delay_ms); void yield_with_select_(uint32_t delay_ms);
std::vector<Component *> components_{}; std::vector<Component *> components_{};
// Partitioned vector design for looping components
// =================================================
// Components are partitioned into [active | inactive] sections:
//
// looping_components_: [A, B, C, D | E, F]
// ^
// looping_components_active_end_ (4)
//
// - Components A,B,C,D are active and will be called in loop()
// - Components E,F are inactive (disabled/failed) and won't be called
// - No flag checking needed during iteration - just loop 0 to active_end_
// - When a component is disabled, it's swapped with the last active component
// and active_end_ is decremented
// - When a component is enabled, it's swapped with the first inactive component
// and active_end_ is incremented
// - This eliminates branch mispredictions from flag checking in the hot loop
std::vector<Component *> looping_components_{}; std::vector<Component *> looping_components_{};
uint16_t looping_components_active_end_{0};
// For safe reentrant modifications during iteration
uint16_t current_loop_index_{0};
bool in_loop_{false};
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
std::vector<binary_sensor::BinarySensor *> binary_sensors_{}; std::vector<binary_sensor::BinarySensor *> binary_sensors_{};

View File

@@ -30,17 +30,18 @@ const float LATE = -100.0f;
} // namespace setup_priority } // namespace setup_priority
// Component state uses bits 0-1 (4 states) // Component state uses bits 0-2 (8 states, 5 used)
const uint8_t COMPONENT_STATE_MASK = 0x03; const uint8_t COMPONENT_STATE_MASK = 0x07;
const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00;
const uint8_t COMPONENT_STATE_SETUP = 0x01; const uint8_t COMPONENT_STATE_SETUP = 0x01;
const uint8_t COMPONENT_STATE_LOOP = 0x02; const uint8_t COMPONENT_STATE_LOOP = 0x02;
const uint8_t COMPONENT_STATE_FAILED = 0x03; const uint8_t COMPONENT_STATE_FAILED = 0x03;
// Status LED uses bits 2-3 const uint8_t COMPONENT_STATE_LOOP_DONE = 0x04;
const uint8_t STATUS_LED_MASK = 0x0C; // Status LED uses bits 3-4
const uint8_t STATUS_LED_MASK = 0x18;
const uint8_t STATUS_LED_OK = 0x00; const uint8_t STATUS_LED_OK = 0x00;
const uint8_t STATUS_LED_WARNING = 0x04; // Bit 2 const uint8_t STATUS_LED_WARNING = 0x08; // Bit 3
const uint8_t STATUS_LED_ERROR = 0x08; // Bit 3 const uint8_t STATUS_LED_ERROR = 0x10; // Bit 4
const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning
const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again
@@ -113,6 +114,9 @@ void Component::call() {
case COMPONENT_STATE_FAILED: // NOLINT(bugprone-branch-clone) case COMPONENT_STATE_FAILED: // NOLINT(bugprone-branch-clone)
// State failed: Do nothing // State failed: Do nothing
break; break;
case COMPONENT_STATE_LOOP_DONE: // NOLINT(bugprone-branch-clone)
// State loop done: Do nothing, component has finished its work
break;
default: default:
break; break;
} }
@@ -136,14 +140,30 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) {
return false; return false;
} }
void Component::mark_failed() { void Component::mark_failed() {
ESP_LOGE(TAG, "Component %s was marked as failed.", this->get_component_source()); ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source());
this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_FAILED; this->component_state_ |= COMPONENT_STATE_FAILED;
this->status_set_error(); this->status_set_error();
// Also remove from loop since failed components shouldn't loop
App.disable_component_loop_(this);
}
void Component::disable_loop() {
ESP_LOGD(TAG, "%s loop disabled", this->get_component_source());
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_LOOP_DONE;
App.disable_component_loop_(this);
}
void Component::enable_loop() {
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) {
ESP_LOGD(TAG, "%s loop enabled", this->get_component_source());
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_LOOP;
App.enable_component_loop_(this);
}
} }
void Component::reset_to_construction_state() { void Component::reset_to_construction_state() {
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
ESP_LOGI(TAG, "Component %s is being reset to construction state.", this->get_component_source()); ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source());
this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
// Clear error status when resetting // Clear error status when resetting
@@ -276,8 +296,8 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
} }
if (should_warn) { if (should_warn) {
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source(); const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms).", src, blocking_time); ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time);
ESP_LOGW(TAG, "Components should block for at most 30 ms."); ESP_LOGW(TAG, "Components should block for at most 30 ms");
} }
return curr_time; return curr_time;

View File

@@ -58,6 +58,7 @@ extern const uint8_t COMPONENT_STATE_CONSTRUCTION;
extern const uint8_t COMPONENT_STATE_SETUP; extern const uint8_t COMPONENT_STATE_SETUP;
extern const uint8_t COMPONENT_STATE_LOOP; extern const uint8_t COMPONENT_STATE_LOOP;
extern const uint8_t COMPONENT_STATE_FAILED; extern const uint8_t COMPONENT_STATE_FAILED;
extern const uint8_t COMPONENT_STATE_LOOP_DONE;
extern const uint8_t STATUS_LED_MASK; extern const uint8_t STATUS_LED_MASK;
extern const uint8_t STATUS_LED_OK; extern const uint8_t STATUS_LED_OK;
extern const uint8_t STATUS_LED_WARNING; extern const uint8_t STATUS_LED_WARNING;
@@ -150,6 +151,26 @@ class Component {
this->mark_failed(); this->mark_failed();
} }
/** Disable this component's loop. The loop() method will no longer be called.
*
* This is useful for components that only need to run for a certain period of time
* or when inactive, saving CPU cycles.
*
* @note Components should call this->disable_loop() on themselves, not on other components.
* This ensures the component's state is properly updated along with the loop partition.
*/
void disable_loop();
/** Enable this component's loop. The loop() method will be called normally.
*
* This is useful for components that transition between active and inactive states
* and need to re-enable their loop() method when becoming active again.
*
* @note Components should call this->enable_loop() on themselves, not on other components.
* This ensures the component's state is properly updated along with the loop partition.
*/
void enable_loop();
bool is_failed() const; bool is_failed() const;
bool is_ready() const; bool is_ready() const;

View File

@@ -29,7 +29,9 @@ Component = esphome_ns.class_("Component")
ComponentPtr = Component.operator("ptr") ComponentPtr = Component.operator("ptr")
PollingComponent = esphome_ns.class_("PollingComponent", Component) PollingComponent = esphome_ns.class_("PollingComponent", Component)
Application = esphome_ns.class_("Application") Application = esphome_ns.class_("Application")
optional = esphome_ns.class_("optional") # Create optional with explicit namespace to avoid ambiguity with std::optional
# The generated code will use esphome::optional instead of just optional
optional = global_ns.namespace("esphome").class_("optional")
arduino_json_ns = global_ns.namespace("ArduinoJson") arduino_json_ns = global_ns.namespace("ArduinoJson")
JsonObject = arduino_json_ns.class_("JsonObject") JsonObject = arduino_json_ns.class_("JsonObject")
JsonObjectConst = arduino_json_ns.class_("JsonObjectConst") JsonObjectConst = arduino_json_ns.class_("JsonObjectConst")

View File

@@ -65,14 +65,14 @@ lib_deps =
SPI ; spi (Arduino built-in) SPI ; spi (Arduino built-in)
Wire ; i2c (Arduino built-int) Wire ; i2c (Arduino built-int)
heman/AsyncMqttClient-esphome@1.0.0 ; mqtt heman/AsyncMqttClient-esphome@1.0.0 ; mqtt
esphome/ESPAsyncWebServer-esphome@3.3.0 ; web_server_base ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
fastled/FastLED@3.9.16 ; fastled_base fastled/FastLED@3.9.16 ; fastled_base
mikalhart/TinyGPSPlus@1.1.0 ; gps mikalhart/TinyGPSPlus@1.1.0 ; gps
freekode/TM1651@1.0.1 ; tm1651 freekode/TM1651@1.0.1 ; tm1651
glmnet/Dsmr@0.7 ; dsmr glmnet/Dsmr@0.7 ; dsmr
rweather/Crypto@0.4.0 ; dsmr rweather/Crypto@0.4.0 ; dsmr
dudanov/MideaUART@1.1.9 ; midea dudanov/MideaUART@1.1.9 ; midea
tonia/HeatpumpIR@1.0.32 ; heatpumpir tonia/HeatpumpIR@1.0.35 ; heatpumpir
build_flags = build_flags =
${common.build_flags} ${common.build_flags}
-DUSE_ARDUINO -DUSE_ARDUINO
@@ -100,7 +100,7 @@ lib_deps =
${common:arduino.lib_deps} ${common:arduino.lib_deps}
ESP8266WiFi ; wifi (Arduino built-in) ESP8266WiFi ; wifi (Arduino built-in)
Update ; ota (Arduino built-in) Update ; ota (Arduino built-in)
esphome/ESPAsyncTCP-esphome@2.0.0 ; async_tcp ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp
ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266HTTPClient ; http_request (Arduino built-in)
ESP8266mDNS ; mdns (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in)
DNSServer ; captive_portal (Arduino built-in) DNSServer ; captive_portal (Arduino built-in)
@@ -130,12 +130,12 @@ lib_deps =
WiFi ; wifi,web_server_base,ethernet (Arduino built-in) WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
Update ; ota,web_server_base (Arduino built-in) Update ; ota,web_server_base (Arduino built-in)
${common:arduino.lib_deps} ${common:arduino.lib_deps}
esphome/AsyncTCP-esphome@2.1.4 ; async_tcp ESP32Async/AsyncTCP@3.4.4 ; async_tcp
WiFiClientSecure ; http_request,nextion (Arduino built-in) WiFiClientSecure ; http_request,nextion (Arduino built-in)
HTTPClient ; http_request,nextion (Arduino built-in) HTTPClient ; http_request,nextion (Arduino built-in)
ESPmDNS ; mdns (Arduino built-in) ESPmDNS ; mdns (Arduino built-in)
DNSServer ; captive_portal (Arduino built-in) DNSServer ; captive_portal (Arduino built-in)
esphome/ESP32-audioI2S@2.2.0 ; i2s_audio esphome/ESP32-audioI2S@2.3.0 ; i2s_audio
droscy/esp_wireguard@0.4.2 ; wireguard droscy/esp_wireguard@0.4.2 ; wireguard
esphome/esp-audio-libs@1.1.4 ; audio esphome/esp-audio-libs@1.1.4 ; audio
@@ -200,7 +200,7 @@ build_unflags =
; This are common settings for the LibreTiny (all variants) using Arduino. ; This are common settings for the LibreTiny (all variants) using Arduino.
[common:libretiny-arduino] [common:libretiny-arduino]
extends = common:arduino extends = common:arduino
platform = libretiny platform = libretiny@1.9.1
framework = arduino framework = arduino
lib_deps = lib_deps =
droscy/esp_wireguard@0.4.2 ; wireguard droscy/esp_wireguard@0.4.2 ; wireguard

View File

@@ -120,10 +120,12 @@ select = [
ignore = [ ignore = [
"E501", # line too long "E501", # line too long
"PLC0415", # `import` should be at the top-level of a file
"PLR0911", # Too many return statements ({returns} > {max_returns}) "PLR0911", # Too many return statements ({returns} > {max_returns})
"PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0912", # Too many branches ({branches} > {max_branches})
"PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args})
"PLR0915", # Too many statements ({statements} > {max_statements}) "PLR0915", # Too many statements ({statements} > {max_statements})
"PLW1641", # Object does not implement `__hash__` method
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
"PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
"UP038", # https://github.com/astral-sh/ruff/issues/7871 https://github.com/astral-sh/ruff/pull/16681 "UP038", # https://github.com/astral-sh/ruff/issues/7871 https://github.com/astral-sh/ruff/pull/16681

View File

@@ -1,6 +1,6 @@
pylint==3.3.7 pylint==3.3.7
flake8==7.2.0 # also change in .pre-commit-config.yaml when updating flake8==7.2.0 # also change in .pre-commit-config.yaml when updating
ruff==0.11.13 # also change in .pre-commit-config.yaml when updating ruff==0.12.0 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating
pre-commit pre-commit

View File

@@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main):
main_cpp = generate_main("tests/component_tests/text/test_text.yaml") main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
# Then # Then
assert "it_4->set_template([=]() -> optional<std::string> {" in main_cpp assert "it_4->set_template([=]() -> esphome::optional<std::string> {" in main_cpp
assert 'return std::string{"Hello"};' in main_cpp assert 'return std::string{"Hello"};' in main_cpp

View File

@@ -1,3 +0,0 @@
sensor:
- platform: esp32_hall
name: ESP32 Hall Sensor

View File

@@ -0,0 +1,12 @@
esp32_ble_tracker:
sensor:
- platform: xiaomi_xmwsdj04mmc
mac_address: 84:B4:DB:5D:A3:8F
bindkey: d8ca2ed09bb5541dc8f045ca360b00ea
temperature:
name: Xiaomi XMWSDJ04MMC Temperature
humidity:
name: Xiaomi XMWSDJ04MMC Humidity
battery_level:
name: Xiaomi XMWSDJ04MMC Battery Level

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import AsyncGenerator, Generator from collections.abc import AsyncGenerator, Callable, Generator
from contextlib import AbstractAsyncContextManager, asynccontextmanager from contextlib import AbstractAsyncContextManager, asynccontextmanager
import logging import logging
import os import os
@@ -46,6 +46,7 @@ if platform.system() == "Windows":
"Integration tests are not supported on Windows", allow_module_level=True "Integration tests are not supported on Windows", allow_module_level=True
) )
import pty # not available on Windows import pty # not available on Windows
@@ -362,7 +363,10 @@ async def api_client_connected(
async def _read_stream_lines( async def _read_stream_lines(
stream: asyncio.StreamReader, lines: list[str], output_stream: TextIO stream: asyncio.StreamReader,
lines: list[str],
output_stream: TextIO,
line_callback: Callable[[str], None] | None = None,
) -> None: ) -> None:
"""Read lines from a stream, append to list, and echo to output stream.""" """Read lines from a stream, append to list, and echo to output stream."""
log_parser = LogParser() log_parser = LogParser()
@@ -380,6 +384,9 @@ async def _read_stream_lines(
file=output_stream, file=output_stream,
flush=True, flush=True,
) )
# Call the callback if provided
if line_callback:
line_callback(decoded_line.rstrip())
@asynccontextmanager @asynccontextmanager
@@ -388,6 +395,7 @@ async def run_binary_and_wait_for_port(
host: str, host: str,
port: int, port: int,
timeout: float = PORT_WAIT_TIMEOUT, timeout: float = PORT_WAIT_TIMEOUT,
line_callback: Callable[[str], None] | None = None,
) -> AsyncGenerator[None]: ) -> AsyncGenerator[None]:
"""Run a binary, wait for it to open a port, and clean up on exit.""" """Run a binary, wait for it to open a port, and clean up on exit."""
# Create a pseudo-terminal to make the binary think it's running interactively # Create a pseudo-terminal to make the binary think it's running interactively
@@ -435,7 +443,9 @@ async def run_binary_and_wait_for_port(
# Read from output stream # Read from output stream
output_tasks = [ output_tasks = [
asyncio.create_task( asyncio.create_task(
_read_stream_lines(output_reader, stdout_lines, sys.stdout) _read_stream_lines(
output_reader, stdout_lines, sys.stdout, line_callback
)
) )
] ]
@@ -515,6 +525,7 @@ async def run_compiled_context(
compile_esphome: CompileFunction, compile_esphome: CompileFunction,
port: int, port: int,
port_socket: socket.socket | None = None, port_socket: socket.socket | None = None,
line_callback: Callable[[str], None] | None = None,
) -> AsyncGenerator[None]: ) -> AsyncGenerator[None]:
"""Context manager to write, compile and run an ESPHome configuration.""" """Context manager to write, compile and run an ESPHome configuration."""
# Write the YAML config # Write the YAML config
@@ -528,7 +539,9 @@ async def run_compiled_context(
port_socket.close() port_socket.close()
# Run the binary and wait for the API server to start # Run the binary and wait for the API server to start
async with run_binary_and_wait_for_port(binary_path, LOCALHOST, port): async with run_binary_and_wait_for_port(
binary_path, LOCALHOST, port, line_callback=line_callback
):
yield yield
@@ -542,7 +555,9 @@ async def run_compiled(
port, port_socket = reserved_tcp_port port, port_socket = reserved_tcp_port
def _run_compiled( def _run_compiled(
yaml_content: str, filename: str | None = None yaml_content: str,
filename: str | None = None,
line_callback: Callable[[str], None] | None = None,
) -> AbstractAsyncContextManager[asyncio.subprocess.Process]: ) -> AbstractAsyncContextManager[asyncio.subprocess.Process]:
return run_compiled_context( return run_compiled_context(
yaml_content, yaml_content,
@@ -551,6 +566,7 @@ async def run_compiled(
compile_esphome, compile_esphome,
port, port,
port_socket, port_socket,
line_callback=line_callback,
) )
yield _run_compiled yield _run_compiled

View File

@@ -0,0 +1,78 @@
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME
CODEOWNERS = ["@esphome/tests"]
loop_test_component_ns = cg.esphome_ns.namespace("loop_test_component")
LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Component)
CONF_DISABLE_AFTER = "disable_after"
CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations"
COMPONENT_CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(LoopTestComponent),
cv.Required(CONF_NAME): cv.string,
cv.Optional(CONF_DISABLE_AFTER, default=0): cv.int_,
cv.Optional(CONF_TEST_REDUNDANT_OPERATIONS, default=False): cv.boolean,
}
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(LoopTestComponent),
cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA),
}
).extend(cv.COMPONENT_SCHEMA)
# Define actions
EnableAction = loop_test_component_ns.class_("EnableAction", automation.Action)
DisableAction = loop_test_component_ns.class_("DisableAction", automation.Action)
@automation.register_action(
"loop_test_component.enable",
EnableAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(LoopTestComponent),
}
),
)
async def enable_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, parent)
return var
@automation.register_action(
"loop_test_component.disable",
DisableAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(LoopTestComponent),
}
),
)
async def disable_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, parent)
return var
async def to_code(config):
# The parent config doesn't actually create a component
# We just create each sub-component
for comp_config in config[CONF_COMPONENTS]:
var = cg.new_Pvariable(comp_config[CONF_ID])
await cg.register_component(var, comp_config)
cg.add(var.set_name(comp_config[CONF_NAME]))
cg.add(var.set_disable_after(comp_config[CONF_DISABLE_AFTER]))
cg.add(
var.set_test_redundant_operations(
comp_config[CONF_TEST_REDUNDANT_OPERATIONS]
)
)

View File

@@ -0,0 +1,43 @@
#include "loop_test_component.h"
namespace esphome {
namespace loop_test_component {
void LoopTestComponent::setup() { ESP_LOGI(TAG, "[%s] Setup called", this->name_.c_str()); }
void LoopTestComponent::loop() {
this->loop_count_++;
ESP_LOGI(TAG, "[%s] Loop count: %d", this->name_.c_str(), this->loop_count_);
// Test self-disable after specified count
if (this->disable_after_ > 0 && this->loop_count_ == this->disable_after_) {
ESP_LOGI(TAG, "[%s] Disabling self after %d loops", this->name_.c_str(), this->disable_after_);
this->disable_loop();
}
// Test redundant operations
if (this->test_redundant_operations_ && this->loop_count_ == 5) {
if (this->name_ == "redundant_enable") {
ESP_LOGI(TAG, "[%s] Testing enable when already enabled", this->name_.c_str());
this->enable_loop();
} else if (this->name_ == "redundant_disable") {
ESP_LOGI(TAG, "[%s] Testing disable when will be disabled", this->name_.c_str());
// We'll disable at count 10, but try to disable again at 5
this->disable_loop();
ESP_LOGI(TAG, "[%s] First disable complete", this->name_.c_str());
}
}
}
void LoopTestComponent::service_enable() {
ESP_LOGI(TAG, "[%s] Service enable called", this->name_.c_str());
this->enable_loop();
}
void LoopTestComponent::service_disable() {
ESP_LOGI(TAG, "[%s] Service disable called", this->name_.c_str());
this->disable_loop();
}
} // namespace loop_test_component
} // namespace esphome

View File

@@ -0,0 +1,58 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/automation.h"
namespace esphome {
namespace loop_test_component {
static const char *const TAG = "loop_test_component";
class LoopTestComponent : public Component {
public:
void set_name(const std::string &name) { this->name_ = name; }
void set_disable_after(int count) { this->disable_after_ = count; }
void set_test_redundant_operations(bool test) { this->test_redundant_operations_ = test; }
void setup() override;
void loop() override;
// Service methods for external control
void service_enable();
void service_disable();
int get_loop_count() const { return this->loop_count_; }
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
std::string name_;
int loop_count_{0};
int disable_after_{0};
bool test_redundant_operations_{false};
};
template<typename... Ts> class EnableAction : public Action<Ts...> {
public:
EnableAction(LoopTestComponent *parent) : parent_(parent) {}
void play(Ts... x) override { this->parent_->service_enable(); }
protected:
LoopTestComponent *parent_;
};
template<typename... Ts> class DisableAction : public Action<Ts...> {
public:
DisableAction(LoopTestComponent *parent) : parent_(parent) {}
void play(Ts... x) override { this->parent_->service_disable(); }
protected:
LoopTestComponent *parent_;
};
} // namespace loop_test_component
} // namespace esphome

View File

@@ -0,0 +1,48 @@
esphome:
name: loop-test
host:
api:
logger:
level: DEBUG
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
loop_test_component:
components:
# Component that disables itself after 10 loops
- id: self_disable_10
name: "self_disable_10"
disable_after: 10
# Component that never disables itself (for re-enable test)
- id: normal_component
name: "normal_component"
disable_after: 0
# Component that tests enable when already enabled
- id: redundant_enable
name: "redundant_enable"
test_redundant_operations: true
disable_after: 0
# Component that tests disable when already disabled
- id: redundant_disable
name: "redundant_disable"
test_redundant_operations: true
disable_after: 10
# Interval to re-enable the self_disable_10 component after some time
interval:
- interval: 0.5s
then:
- if:
condition:
lambda: 'return id(self_disable_10).get_loop_count() == 10;'
then:
- logger.log: "Re-enabling self_disable_10 via service"
- loop_test_component.enable:
id: self_disable_10

View File

@@ -0,0 +1,150 @@
"""Integration test for loop disable/enable functionality."""
from __future__ import annotations
import asyncio
from pathlib import Path
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_loop_disable_enable(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that components can disable and enable their loop() method."""
# Get the absolute path to the external components directory
external_components_path = str(
Path(__file__).parent / "fixtures" / "external_components"
)
# Replace the placeholder in the YAML config with the actual path
yaml_config = yaml_config.replace(
"EXTERNAL_COMPONENT_PATH", external_components_path
)
# Track log messages and events
log_messages: list[str] = []
# Event fired when self_disable_10 component disables itself after 10 loops
self_disable_10_disabled = asyncio.Event()
# Event fired when normal_component reaches 10 loops
normal_component_10_loops = asyncio.Event()
# Event fired when redundant_enable component tests enabling when already enabled
redundant_enable_tested = asyncio.Event()
# Event fired when redundant_disable component tests disabling when already disabled
redundant_disable_tested = asyncio.Event()
# Event fired when self_disable_10 component is re-enabled and runs again (count > 10)
self_disable_10_re_enabled = asyncio.Event()
# Track loop counts for components
self_disable_10_counts: list[int] = []
normal_component_counts: list[int] = []
def on_log_line(line: str) -> None:
"""Process each log line from the process output."""
# Strip ANSI color codes
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
if "loop_test_component" not in clean_line:
return
log_messages.append(clean_line)
# Track specific events using the cleaned line
if "[self_disable_10]" in clean_line:
if "Loop count:" in clean_line:
# Extract loop count
try:
count = int(clean_line.split("Loop count: ")[1])
self_disable_10_counts.append(count)
# Check if component was re-enabled (count > 10)
if count > 10:
self_disable_10_re_enabled.set()
except (IndexError, ValueError):
pass
elif "Disabling self after 10 loops" in clean_line:
self_disable_10_disabled.set()
elif "[normal_component]" in clean_line and "Loop count:" in clean_line:
try:
count = int(clean_line.split("Loop count: ")[1])
normal_component_counts.append(count)
if count >= 10:
normal_component_10_loops.set()
except (IndexError, ValueError):
pass
elif (
"[redundant_enable]" in clean_line
and "Testing enable when already enabled" in clean_line
):
redundant_enable_tested.set()
elif (
"[redundant_disable]" in clean_line
and "Testing disable when will be disabled" in clean_line
):
redundant_disable_tested.set()
# Write, compile and run the ESPHome device with log callback
async with (
run_compiled(yaml_config, line_callback=on_log_line),
api_client_connected() as client,
):
# Verify we can connect and get device info
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "loop-test"
# Wait for self_disable_10 to disable itself
try:
await asyncio.wait_for(self_disable_10_disabled.wait(), timeout=10.0)
except asyncio.TimeoutError:
pytest.fail("self_disable_10 did not disable itself within 10 seconds")
# Verify it ran at least 10 times before disabling
assert len([c for c in self_disable_10_counts if c <= 10]) == 10, (
f"Expected exactly 10 loops before disable, got {[c for c in self_disable_10_counts if c <= 10]}"
)
assert self_disable_10_counts[:10] == list(range(1, 11)), (
f"Expected first 10 counts to be 1-10, got {self_disable_10_counts[:10]}"
)
# Wait for normal_component to run at least 10 times
try:
await asyncio.wait_for(normal_component_10_loops.wait(), timeout=10.0)
except asyncio.TimeoutError:
pytest.fail(
f"normal_component did not reach 10 loops within timeout, got {len(normal_component_counts)}"
)
# Wait for redundant operation tests
try:
await asyncio.wait_for(redundant_enable_tested.wait(), timeout=10.0)
except asyncio.TimeoutError:
pytest.fail("redundant_enable did not test enabling when already enabled")
try:
await asyncio.wait_for(redundant_disable_tested.wait(), timeout=10.0)
except asyncio.TimeoutError:
pytest.fail(
"redundant_disable did not test disabling when will be disabled"
)
# Wait to see if self_disable_10 gets re-enabled
try:
await asyncio.wait_for(self_disable_10_re_enabled.wait(), timeout=5.0)
except asyncio.TimeoutError:
pytest.fail("self_disable_10 was not re-enabled within 5 seconds")
# Component was re-enabled - verify it ran more times
later_self_disable_counts = [c for c in self_disable_10_counts if c > 10]
assert later_self_disable_counts, (
"self_disable_10 was re-enabled but did not run additional times"
)

View File

@@ -13,7 +13,19 @@ from aioesphomeapi import APIClient
ConfigWriter = Callable[[str, str | None], Awaitable[Path]] ConfigWriter = Callable[[str, str | None], Awaitable[Path]]
CompileFunction = Callable[[Path], Awaitable[Path]] CompileFunction = Callable[[Path], Awaitable[Path]]
RunFunction = Callable[[Path], Awaitable[asyncio.subprocess.Process]] RunFunction = Callable[[Path], Awaitable[asyncio.subprocess.Process]]
RunCompiledFunction = Callable[[str, str | None], AbstractAsyncContextManager[None]]
class RunCompiledFunction(Protocol):
"""Protocol for run_compiled function with optional line callback."""
def __call__( # noqa: E704
self,
yaml_content: str,
filename: str | None = None,
line_callback: Callable[[str], None] | None = None,
) -> AbstractAsyncContextManager[None]: ...
WaitFunction = Callable[[APIClient, float], Awaitable[bool]] WaitFunction = Callable[[APIClient, float], Awaitable[bool]]