mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'dev' into proxy_memory
This commit is contained in:
		| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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()) { | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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"; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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() { | ||||||
|   | |||||||
| @@ -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"); | ||||||
|   | |||||||
| @@ -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); | ||||||
|   | |||||||
| @@ -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); | ||||||
|   | |||||||
| @@ -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); | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 *); | ||||||
|   | |||||||
| @@ -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_{}; | ||||||
|   | |||||||
| @@ -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,123 +70,72 @@ 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 | ||||||
|         delete this->event_.gattc.gattc_param; |   BLEEvent() : type_(GAP) {} | ||||||
|         delete this->event_.gattc.data; |  | ||||||
|         break; |   // Clean up any heap-allocated data | ||||||
|       case GATTS: |   void cleanup_heap_data() { | ||||||
|         delete this->event_.gatts.gatts_param; |     if (this->type_ == GAP) { | ||||||
|         delete this->event_.gatts.data; |       return; | ||||||
|         break; |  | ||||||
|       default: |  | ||||||
|         break; |  | ||||||
|     } |     } | ||||||
|  |     if (this->type_ == GATTC) { | ||||||
|  |       delete this->event_.gattc.gattc_param; | ||||||
|  |       delete this->event_.gattc.data; | ||||||
|  |       this->event_.gattc.gattc_param = nullptr; | ||||||
|  |       this->event_.gattc.data = nullptr; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (this->type_ == GATTS) { | ||||||
|  |       delete this->event_.gatts.gatts_param; | ||||||
|  |       delete this->event_.gatts.data; | ||||||
|  |       this->event_.gatts.gatts_param = nullptr; | ||||||
|  |       this->event_.gatts.data = nullptr; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // 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 | ||||||
| @@ -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) | ||||||
|   | |||||||
							
								
								
									
										72
									
								
								esphome/components/esp32_ble/ble_event_pool.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								esphome/components/esp32_ble/ble_event_pool.h
									
									
									
									
									
										Normal 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 | ||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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; } | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 |  | ||||||
| @@ -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 |  | ||||||
| @@ -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) |  | ||||||
| @@ -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() { | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -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()) { | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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)) | ||||||
|   | |||||||
| @@ -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; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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}; | ||||||
|   | |||||||
| @@ -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); | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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}; | ||||||
|   | |||||||
| @@ -135,8 +135,8 @@ 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); | ||||||
|     return res; |     return res; | ||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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"; | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
							
								
								
									
										77
									
								
								esphome/components/xiaomi_xmwsdj04mmc/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								esphome/components/xiaomi_xmwsdj04mmc/sensor.py
									
									
									
									
									
										Normal 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)) | ||||||
							
								
								
									
										77
									
								
								esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										37
									
								
								esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h
									
									
									
									
									
										Normal 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 | ||||||
| @@ -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,9 +243,66 @@ 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; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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_{}; | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -1,3 +0,0 @@ | |||||||
| sensor: |  | ||||||
|   - platform: esp32_hall |  | ||||||
|     name: ESP32 Hall Sensor |  | ||||||
							
								
								
									
										12
									
								
								tests/components/xiaomi_xmwsdj04mmc/common.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								tests/components/xiaomi_xmwsdj04mmc/common.yaml
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										1
									
								
								tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										1
									
								
								tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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] | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
| @@ -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 | ||||||
| @@ -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 | ||||||
							
								
								
									
										48
									
								
								tests/integration/fixtures/loop_disable_enable.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								tests/integration/fixtures/loop_disable_enable.yaml
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										150
									
								
								tests/integration/test_loop_disable_enable.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								tests/integration/test_loop_disable_enable.py
									
									
									
									
									
										Normal 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" | ||||||
|  |         ) | ||||||
| @@ -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]] | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user