mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'dev' into proxy_memory
This commit is contained in:
		| @@ -4,7 +4,7 @@ | ||||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
|     rev: v0.11.10 | ||||
|     rev: v0.12.0 | ||||
|     hooks: | ||||
|       # Run the linter. | ||||
|       - id: ruff | ||||
|   | ||||
| @@ -520,6 +520,7 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl | ||||
| esphome/components/xiaomi_mhoc303/* @drug123 | ||||
| esphome/components/xiaomi_mhoc401/* @vevsvevs | ||||
| esphome/components/xiaomi_rtcgq02lm/* @jesserockz | ||||
| esphome/components/xiaomi_xmwsdj04mmc/* @medusalix | ||||
| esphome/components/xl9535/* @mreditor97 | ||||
| esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 | ||||
| esphome/components/xxtea/* @clydebarrow | ||||
|   | ||||
| @@ -17,7 +17,11 @@ void Anova::setup() { | ||||
|   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) { | ||||
|   if (call.get_mode().has_value()) { | ||||
|   | ||||
| @@ -1643,6 +1643,7 @@ enum VoiceAssistantEvent { | ||||
|   VOICE_ASSISTANT_STT_VAD_END = 12; | ||||
|   VOICE_ASSISTANT_TTS_STREAM_START = 98; | ||||
|   VOICE_ASSISTANT_TTS_STREAM_END = 99; | ||||
|   VOICE_ASSISTANT_INTENT_PROGRESS = 100; | ||||
| } | ||||
|  | ||||
| message VoiceAssistantEventData { | ||||
|   | ||||
| @@ -516,6 +516,8 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V | ||||
|       return "VOICE_ASSISTANT_TTS_STREAM_START"; | ||||
|     case enums::VOICE_ASSISTANT_TTS_STREAM_END: | ||||
|       return "VOICE_ASSISTANT_TTS_STREAM_END"; | ||||
|     case enums::VOICE_ASSISTANT_INTENT_PROGRESS: | ||||
|       return "VOICE_ASSISTANT_INTENT_PROGRESS"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
|   | ||||
| @@ -208,6 +208,7 @@ enum VoiceAssistantEvent : uint32_t { | ||||
|   VOICE_ASSISTANT_STT_VAD_END = 12, | ||||
|   VOICE_ASSISTANT_TTS_STREAM_START = 98, | ||||
|   VOICE_ASSISTANT_TTS_STREAM_END = 99, | ||||
|   VOICE_ASSISTANT_INTENT_PROGRESS = 100, | ||||
| }; | ||||
| enum VoiceAssistantTimerEvent : uint32_t { | ||||
|   VOICE_ASSISTANT_TIMER_STARTED = 0, | ||||
|   | ||||
| @@ -21,8 +21,8 @@ CONFIG_SCHEMA = cv.All( | ||||
| @coroutine_with_priority(200.0) | ||||
| async def to_code(config): | ||||
|     if CORE.is_esp32 or CORE.is_libretiny: | ||||
|         # https://github.com/esphome/AsyncTCP/blob/master/library.json | ||||
|         cg.add_library("esphome/AsyncTCP-esphome", "2.1.4") | ||||
|         # https://github.com/ESP32Async/AsyncTCP | ||||
|         cg.add_library("ESP32Async/AsyncTCP", "3.4.4") | ||||
|     elif CORE.is_esp8266: | ||||
|         # https://github.com/esphome/ESPAsyncTCP | ||||
|         cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0") | ||||
|         # https://github.com/ESP32Async/ESPAsyncTCP | ||||
|         cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") | ||||
|   | ||||
| @@ -480,7 +480,11 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) { | ||||
|  | ||||
| /* 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::dump_config() { | ||||
|   | ||||
| @@ -83,7 +83,11 @@ void BedJetClimate::reset_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) { | ||||
|   ESP_LOGD(TAG, "Received BedJetClimate::control"); | ||||
|   | ||||
| @@ -11,7 +11,11 @@ namespace ble_client { | ||||
|  | ||||
| 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() { | ||||
|   LOG_SENSOR("", "BLE Client RSSI Sensor", this); | ||||
|   | ||||
| @@ -11,7 +11,11 @@ namespace ble_client { | ||||
|  | ||||
| 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() { | ||||
|   LOG_SENSOR("", "BLE Sensor", this); | ||||
|   | ||||
| @@ -14,7 +14,11 @@ static const char *const TAG = "ble_text_sensor"; | ||||
|  | ||||
| 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() { | ||||
|   LOG_TEXT_SENSOR("", "BLE Text Sensor", this); | ||||
|   | ||||
| @@ -12,8 +12,8 @@ from esphome.const import ( | ||||
|     CONF_OVERSAMPLING, | ||||
|     CONF_PRESSURE, | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_ATMOSPHERIC_PRESSURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     ICON_GAS_CYLINDER, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|   | ||||
| @@ -37,7 +37,12 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { | ||||
|   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() { | ||||
|   this->base_->init(); | ||||
|   if (!this->initialized_) { | ||||
| @@ -50,6 +55,8 @@ void CaptivePortal::start() { | ||||
|   this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); | ||||
|   network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); | ||||
|   this->dns_server_->start(53, "*", ip); | ||||
|   // Re-enable loop() when DNS server is started | ||||
|   this->enable_loop(); | ||||
| #endif | ||||
|  | ||||
|   this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { | ||||
| @@ -68,7 +75,11 @@ void CaptivePortal::start() { | ||||
|  | ||||
| void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { | ||||
|   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)); | ||||
| #endif | ||||
|     response->addHeader("Content-Encoding", "gzip"); | ||||
|     req->send(response); | ||||
|     return; | ||||
|   | ||||
| @@ -21,8 +21,11 @@ class CaptivePortal : public AsyncWebHandler, public Component { | ||||
|   void dump_config() override; | ||||
| #ifdef USE_ARDUINO | ||||
|   void loop() override { | ||||
|     if (this->dns_server_ != nullptr) | ||||
|     if (this->dns_server_ != nullptr) { | ||||
|       this->dns_server_->processNextRequest(); | ||||
|     } else { | ||||
|       this->disable_loop(); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|   float get_setup_priority() const override; | ||||
| @@ -37,7 +40,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { | ||||
| #endif | ||||
|   } | ||||
|  | ||||
|   bool canHandle(AsyncWebServerRequest *request) override { | ||||
|   bool canHandle(AsyncWebServerRequest *request) const override { | ||||
|     if (!this->active_) | ||||
|       return false; | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "ble.h" | ||||
| #include "ble_event_pool.h" | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/log.h" | ||||
| @@ -23,9 +24,6 @@ namespace 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() { | ||||
|   global_ble = this; | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
| @@ -349,9 +347,8 @@ void ESP32BLE::loop() { | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
|     // Destructor will clean up external allocations for GATTC/GATTS | ||||
|     ble_event->~BLEEvent(); | ||||
|     EVENT_ALLOCATOR.deallocate(ble_event, 1); | ||||
|     // Return the event to the pool | ||||
|     this->ble_event_pool_.release(ble_event); | ||||
|     ble_event = this->ble_events_.pop(); | ||||
|   } | ||||
|   if (this->advertising_ != nullptr) { | ||||
| @@ -359,37 +356,41 @@ void ESP32BLE::loop() { | ||||
|   } | ||||
|  | ||||
|   // 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) { | ||||
|     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) { | ||||
|   // Check if queue is full before allocating | ||||
|   if (global_ble->ble_events_.full()) { | ||||
|     // Queue is full, drop the event | ||||
|   // Allocate an event from the pool | ||||
|   BLEEvent *event = global_ble->ble_event_pool_.allocate(); | ||||
|   if (event == nullptr) { | ||||
|     // No events available - queue is full or we're out of memory | ||||
|     global_ble->ble_events_.increment_dropped_count(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); | ||||
|   if (new_event == nullptr) { | ||||
|     // 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...); | ||||
|   // Load new event data (replaces previous event) | ||||
|   load_ble_event(event, args...); | ||||
|  | ||||
|   // Push the event - since we're the only producer and we checked full() above, | ||||
|   // this should always succeed unless we have a bug | ||||
|   if (!global_ble->ble_events_.push(new_event)) { | ||||
|     // 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); | ||||
|   // Push the event to the queue | ||||
|   global_ble->ble_events_.push(event); | ||||
|   // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size | ||||
| } | ||||
| }  // NOLINT(clang-analyzer-unix.Malloc) | ||||
|  | ||||
| // Explicit template instantiations for the friend function | ||||
| 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 "ble_event.h" | ||||
| #include "ble_event_pool.h" | ||||
| #include "queue.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| @@ -148,6 +149,7 @@ class ESP32BLE : public Component { | ||||
|   BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; | ||||
|  | ||||
|   LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_; | ||||
|   BLEEventPool<MAX_BLE_QUEUE_SIZE> ble_event_pool_; | ||||
|   BLEAdvertising *advertising_{}; | ||||
|   esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; | ||||
|   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 | ||||
| //   the data remains valid even after the BLE callback returns. The original | ||||
| //   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 { | ||||
|  public: | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
| @@ -63,125 +70,74 @@ class BLEEvent { | ||||
|   // Constructor for GAP events - no external allocations needed | ||||
|   BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { | ||||
|     this->type_ = GAP; | ||||
|     this->event_.gap.gap_event = e; | ||||
|  | ||||
|     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; | ||||
|     } | ||||
|     this->init_gap_data_(e, p); | ||||
|   } | ||||
|  | ||||
|   // 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) { | ||||
|     this->type_ = GATTC; | ||||
|     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 | ||||
|     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; | ||||
|     } | ||||
|     this->init_gattc_data_(e, i, p); | ||||
|   } | ||||
|  | ||||
|   // 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) { | ||||
|     this->type_ = GATTS; | ||||
|     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 | ||||
|     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; | ||||
|     } | ||||
|     this->init_gatts_data_(e, i, p); | ||||
|   } | ||||
|  | ||||
|   // Destructor to clean up heap allocations | ||||
|   ~BLEEvent() { | ||||
|     switch (this->type_) { | ||||
|       case GATTC: | ||||
|   ~BLEEvent() { this->cleanup_heap_data(); } | ||||
|  | ||||
|   // Default constructor for pre-allocation in pool | ||||
|   BLEEvent() : type_(GAP) {} | ||||
|  | ||||
|   // Clean up any heap-allocated data | ||||
|   void cleanup_heap_data() { | ||||
|     if (this->type_ == GAP) { | ||||
|       return; | ||||
|     } | ||||
|     if (this->type_ == GATTC) { | ||||
|       delete this->event_.gattc.gattc_param; | ||||
|       delete this->event_.gattc.data; | ||||
|         break; | ||||
|       case GATTS: | ||||
|       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; | ||||
|         break; | ||||
|       default: | ||||
|         break; | ||||
|       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 | ||||
|   BLEEvent(const BLEEvent &) = delete; | ||||
|   BLEEvent &operator=(const BLEEvent &) = delete; | ||||
| @@ -224,6 +180,119 @@ class BLEEvent { | ||||
|   esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; } | ||||
|   const BLEScanResult &scan_result() const { return event_.gap.scan_result; } | ||||
|   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) | ||||
|   | ||||
							
								
								
									
										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 esp32_ble { | ||||
|  | ||||
| template<class T, size_t SIZE> class LockFreeQueue { | ||||
| template<class T, uint8_t SIZE> class LockFreeQueue { | ||||
|  public: | ||||
|   LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} | ||||
|  | ||||
| @@ -26,8 +26,8 @@ template<class T, size_t SIZE> class LockFreeQueue { | ||||
|     if (element == nullptr) | ||||
|       return false; | ||||
|  | ||||
|     size_t current_tail = tail_.load(std::memory_order_relaxed); | ||||
|     size_t next_tail = (current_tail + 1) % SIZE; | ||||
|     uint8_t current_tail = tail_.load(std::memory_order_relaxed); | ||||
|     uint8_t next_tail = (current_tail + 1) % SIZE; | ||||
|  | ||||
|     if (next_tail == head_.load(std::memory_order_acquire)) { | ||||
|       // Buffer full | ||||
| @@ -41,7 +41,7 @@ template<class T, size_t SIZE> class LockFreeQueue { | ||||
|   } | ||||
|  | ||||
|   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)) { | ||||
|       return nullptr;  // Empty | ||||
| @@ -53,27 +53,30 @@ template<class T, size_t SIZE> class LockFreeQueue { | ||||
|   } | ||||
|  | ||||
|   size_t size() const { | ||||
|     size_t tail = tail_.load(std::memory_order_acquire); | ||||
|     size_t head = head_.load(std::memory_order_acquire); | ||||
|     uint8_t tail = tail_.load(std::memory_order_acquire); | ||||
|     uint8_t head = head_.load(std::memory_order_acquire); | ||||
|     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); } | ||||
|  | ||||
|   bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } | ||||
|  | ||||
|   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); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   T *buffer_[SIZE]; | ||||
|   std::atomic<size_t> head_; | ||||
|   std::atomic<size_t> tail_; | ||||
|   std::atomic<size_t> dropped_count_; | ||||
|   // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) | ||||
|   std::atomic<uint16_t> dropped_count_;  // 65535 max - more than enough for drop tracking | ||||
|   // 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 | ||||
|   | ||||
| @@ -22,6 +22,16 @@ void BLEClientBase::setup() { | ||||
|   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() { | ||||
|   if (!esp32_ble::global_ble->is_active()) { | ||||
|     this->set_state(espbt::ClientState::INIT); | ||||
| @@ -37,9 +47,14 @@ void BLEClientBase::loop() { | ||||
|   } | ||||
|   // READY_TO_CONNECT means we have discovered the device | ||||
|   // 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(); | ||||
|   } | ||||
|   // 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; } | ||||
|   | ||||
| @@ -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; } | ||||
|  | ||||
|   void set_state(espbt::ClientState st) override; | ||||
|  | ||||
|  protected: | ||||
|   // Memory optimized layout for 32-bit systems | ||||
|   // 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: { | ||||
|       this->incoming_data_.clear(); | ||||
|       this->set_status_indicator_state_(false); | ||||
|       // Provisioning complete, no further loop execution needed | ||||
|       this->disable_loop(); | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
| @@ -254,6 +256,7 @@ void ESP32ImprovComponent::start() { | ||||
|  | ||||
|   ESP_LOGD(TAG, "Setting Improv to start"); | ||||
|   this->should_start_ = true; | ||||
|   this->enable_loop(); | ||||
| } | ||||
|  | ||||
| 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_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: | ||||
|         CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") | ||||
|   | ||||
| @@ -116,5 +116,5 @@ async def to_code(config): | ||||
|  | ||||
|     cg.add_library("WiFiClientSecure", 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") | ||||
|   | ||||
| @@ -17,7 +17,7 @@ namespace light { | ||||
|  | ||||
| class LightOutput; | ||||
|  | ||||
| enum LightRestoreMode { | ||||
| enum LightRestoreMode : uint8_t { | ||||
|   LIGHT_RESTORE_DEFAULT_OFF, | ||||
|   LIGHT_RESTORE_DEFAULT_ON, | ||||
|   LIGHT_ALWAYS_OFF, | ||||
| @@ -212,12 +212,18 @@ class LightState : public EntityBase, public Component { | ||||
|  | ||||
|   /// Store the output to allow effects to have more access. | ||||
|   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). | ||||
|   std::unique_ptr<LightTransformer> transformer_{nullptr}; | ||||
|   /// Whether the light value should be written in the next cycle. | ||||
|   bool next_write_{true}; | ||||
|   /// List of effects for this light. | ||||
|   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. | ||||
|   ESPPreferenceObject rtc_; | ||||
| @@ -236,19 +242,13 @@ class LightState : public EntityBase, public Component { | ||||
|    */ | ||||
|   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. | ||||
|   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. | ||||
|   bool is_transformer_active_ = false; | ||||
| }; | ||||
|   | ||||
| @@ -178,18 +178,21 @@ void OnlineImage::update() { | ||||
|   if (this->format_ == ImageFormat::BMP) { | ||||
|     ESP_LOGD(TAG, "Allocating BMP decoder"); | ||||
|     this->decoder_ = make_unique<BmpDecoder>(this); | ||||
|     this->enable_loop(); | ||||
|   } | ||||
| #endif  // USE_ONLINE_IMAGE_BMP_SUPPORT | ||||
| #ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT | ||||
|   if (this->format_ == ImageFormat::JPEG) { | ||||
|     ESP_LOGD(TAG, "Allocating JPEG decoder"); | ||||
|     this->decoder_ = esphome::make_unique<JpegDecoder>(this); | ||||
|     this->enable_loop(); | ||||
|   } | ||||
| #endif  // USE_ONLINE_IMAGE_JPEG_SUPPORT | ||||
| #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT | ||||
|   if (this->format_ == ImageFormat::PNG) { | ||||
|     ESP_LOGD(TAG, "Allocating PNG decoder"); | ||||
|     this->decoder_ = make_unique<PngDecoder>(this); | ||||
|     this->enable_loop(); | ||||
|   } | ||||
| #endif  // USE_ONLINE_IMAGE_PNG_SUPPORT | ||||
|  | ||||
| @@ -212,6 +215,7 @@ void OnlineImage::update() { | ||||
| void OnlineImage::loop() { | ||||
|   if (!this->decoder_) { | ||||
|     // Not decoding at the moment => nothing to do. | ||||
|     this->disable_loop(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->downloader_ || this->decoder_->is_finished()) { | ||||
|   | ||||
| @@ -12,6 +12,8 @@ class IntervalSyncer : public Component { | ||||
|   void setup() override { | ||||
|     if (this->write_interval_ != 0) { | ||||
|       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 { | ||||
|   | ||||
| @@ -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}); } | ||||
|  | ||||
|   bool canHandle(AsyncWebServerRequest *request) override { | ||||
|   bool canHandle(AsyncWebServerRequest *request) const override { | ||||
|     if (request->method() == HTTP_GET) { | ||||
|       if (request->url() == "/metrics") | ||||
|         return true; | ||||
|   | ||||
| @@ -142,8 +142,10 @@ void Rtttl::stop() { | ||||
| } | ||||
|  | ||||
| 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; | ||||
|   } | ||||
|  | ||||
| #ifdef USE_SPEAKER | ||||
|   if (this->speaker_ != nullptr) { | ||||
| @@ -391,6 +393,11 @@ void Rtttl::set_state_(State state) { | ||||
|   this->state_ = 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))); | ||||
|  | ||||
|   // 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 | ||||
|   | ||||
| @@ -42,6 +42,8 @@ void SafeModeComponent::loop() { | ||||
|     ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); | ||||
|     this->clean_rtc(); | ||||
|     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); | ||||
|   this->time_sync_callback_.call(); | ||||
|   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 | ||||
|   | ||||
| @@ -21,7 +21,7 @@ const int RESTORE_MODE_PERSISTENT_MASK = 0x02; | ||||
| const int RESTORE_MODE_INVERTED_MASK = 0x04; | ||||
| const int RESTORE_MODE_DISABLED_MASK = 0x08; | ||||
|  | ||||
| enum SwitchRestoreMode { | ||||
| enum SwitchRestoreMode : uint8_t { | ||||
|   SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK, | ||||
|   SWITCH_ALWAYS_ON = RESTORE_MODE_ON_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); | ||||
|  | ||||
|   /// The current reported state of the binary sensor. | ||||
|   bool state; | ||||
|  | ||||
|   /// Indicates whether or not state is to be retrieved from flash and how | ||||
|   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. | ||||
|    * | ||||
|    * 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; | ||||
|  | ||||
|   CallbackManager<void(bool)> state_callback_{}; | ||||
|   bool inverted_{false}; | ||||
|   Deduplicator<bool> publish_dedup_; | ||||
|   // Pointer first (4 bytes) | ||||
|   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)) | ||||
|   | ||||
| @@ -24,8 +24,10 @@ void TLC5971::dump_config() { | ||||
| } | ||||
|  | ||||
| void TLC5971::loop() { | ||||
|   if (!this->update_) | ||||
|   if (!this->update_) { | ||||
|     this->disable_loop(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   uint32_t command; | ||||
|  | ||||
| @@ -93,6 +95,7 @@ void TLC5971::set_channel_value(uint16_t channel, uint16_t value) { | ||||
|     return; | ||||
|   if (this->pwm_amounts_[channel] != value) { | ||||
|     this->update_ = true; | ||||
|     this->enable_loop(); | ||||
|   } | ||||
|   this->pwm_amounts_[channel] = value; | ||||
| } | ||||
|   | ||||
| @@ -91,7 +91,7 @@ void DeferredUpdateEventSource::process_deferred_queue_() { | ||||
|   while (!deferred_queue_.empty()) { | ||||
|     DeferredEvent &de = deferred_queue_.front(); | ||||
|     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 | ||||
|       deferred_queue_.erase(deferred_queue_.begin()); | ||||
|     } else { | ||||
| @@ -131,7 +131,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char * | ||||
|     deq_push_back_with_dedup_(source, message_generator); | ||||
|   } else { | ||||
|     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); | ||||
|     } | ||||
|   } | ||||
| @@ -171,8 +171,8 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer | ||||
|     ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); }); | ||||
|   }); | ||||
|  | ||||
|   es->onDisconnect([this, ws](AsyncEventSource *source, AsyncEventSourceClient *client) { | ||||
|     ws->defer([this, source]() { this->on_client_disconnect_((DeferredUpdateEventSource *) source); }); | ||||
|   es->onDisconnect([this, ws, es](AsyncEventSourceClient *client) { | ||||
|     ws->defer([this, es]() { this->on_client_disconnect_((DeferredUpdateEventSource *) es); }); | ||||
|   }); | ||||
|  | ||||
|   es->handleRequest(request); | ||||
| @@ -291,14 +291,23 @@ float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f | ||||
|  | ||||
| #ifdef USE_WEBSERVER_LOCAL | ||||
| 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)); | ||||
| #endif | ||||
|   response->addHeader("Content-Encoding", "gzip"); | ||||
|   request->send(response); | ||||
| } | ||||
| #elif USE_WEBSERVER_VERSION >= 2 | ||||
| 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 = | ||||
|       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 | ||||
|   request->send(response); | ||||
| } | ||||
| @@ -317,8 +326,13 @@ void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { | ||||
|  | ||||
| #ifdef USE_WEBSERVER_CSS_INCLUDE | ||||
| 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 = | ||||
|       request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); | ||||
| #endif | ||||
|   response->addHeader("Content-Encoding", "gzip"); | ||||
|   request->send(response); | ||||
| } | ||||
| @@ -326,8 +340,13 @@ void WebServer::handle_css_request(AsyncWebServerRequest *request) { | ||||
|  | ||||
| #ifdef USE_WEBSERVER_JS_INCLUDE | ||||
| 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 = | ||||
|       request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); | ||||
| #endif | ||||
|   response->addHeader("Content-Encoding", "gzip"); | ||||
|   request->send(response); | ||||
| } | ||||
| @@ -1837,7 +1856,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c | ||||
| } | ||||
| #endif | ||||
|  | ||||
| bool WebServer::canHandle(AsyncWebServerRequest *request) { | ||||
| bool WebServer::canHandle(AsyncWebServerRequest *request) const { | ||||
|   if (request->url() == "/") | ||||
|     return true; | ||||
|  | ||||
| @@ -1859,12 +1878,6 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { | ||||
|  | ||||
| #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS | ||||
|   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; | ||||
|   } | ||||
| #endif | ||||
| @@ -2145,7 +2158,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { | ||||
| #endif | ||||
| } | ||||
|  | ||||
| bool WebServer::isRequestHandlerTrivial() { return false; } | ||||
| bool WebServer::isRequestHandlerTrivial() const { return false; } | ||||
|  | ||||
| void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) { | ||||
|   this->sorting_entitys_[entity] = SortingComponents{weight, group}; | ||||
|   | ||||
| @@ -99,7 +99,7 @@ class DeferredUpdateEventSource : public AsyncEventSource { | ||||
|  protected: | ||||
|   // surface a couple methods from the base class | ||||
|   using AsyncEventSource::handleRequest; | ||||
|   using AsyncEventSource::try_send; | ||||
|   using AsyncEventSource::send; | ||||
|  | ||||
|   ListEntitiesIterator entities_iterator_; | ||||
|   // 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 | ||||
|  | ||||
|   /// 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. | ||||
|   void handleRequest(AsyncWebServerRequest *request) override; | ||||
|   /// 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_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("FS", None) | ||||
|             cg.add_library("Update", None) | ||||
|         # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json | ||||
|         cg.add_library("esphome/ESPAsyncWebServer-esphome", "3.3.0") | ||||
|         if CORE.is_esp8266: | ||||
|             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: | ||||
|   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 handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, | ||||
|                     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 { | ||||
|     next_->handleBody(request, data, len, index, total); | ||||
|   } | ||||
|   bool isRequestHandlerTrivial() override { return next_->isRequestHandlerTrivial(); } | ||||
|   bool isRequestHandlerTrivial() const override { return next_->isRequestHandlerTrivial(); } | ||||
|  | ||||
|  protected: | ||||
|   AsyncWebHandler *next_; | ||||
| @@ -131,12 +131,12 @@ class OTARequestHandler : public AsyncWebHandler { | ||||
|   void handleRequest(AsyncWebServerRequest *request) override; | ||||
|   void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, | ||||
|                     bool final) override; | ||||
|   bool canHandle(AsyncWebServerRequest *request) override { | ||||
|   bool canHandle(AsyncWebServerRequest *request) const override { | ||||
|     return request->url() == "/update" && request->method() == HTTP_POST; | ||||
|   } | ||||
|  | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   bool isRequestHandlerTrivial() override { return false; } | ||||
|   bool isRequestHandlerTrivial() const override { return false; } | ||||
|  | ||||
|  protected: | ||||
|   uint32_t last_ota_progress_{0}; | ||||
|   | ||||
| @@ -135,7 +135,7 @@ class AsyncWebServerRequest { | ||||
|     return res; | ||||
|   } | ||||
|   // 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) { | ||||
|     auto *res = new AsyncWebServerResponseProgmem(this, data, data_size);  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|     this->init_response_(res, code, content_type); | ||||
| @@ -211,7 +211,7 @@ class AsyncWebHandler { | ||||
|  public: | ||||
|   virtual ~AsyncWebHandler() {} | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   virtual bool canHandle(AsyncWebServerRequest *request) { return false; } | ||||
|   virtual bool canHandle(AsyncWebServerRequest *request) const { return false; } | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   virtual void handleRequest(AsyncWebServerRequest *request) {} | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
| @@ -220,7 +220,7 @@ class AsyncWebHandler { | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {} | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   virtual bool isRequestHandlerTrivial() { return true; } | ||||
|   virtual bool isRequestHandlerTrivial() const { return true; } | ||||
| }; | ||||
|  | ||||
| #ifdef USE_WEBSERVER | ||||
| @@ -290,7 +290,7 @@ class AsyncEventSource : public AsyncWebHandler { | ||||
|   ~AsyncEventSource() override; | ||||
|  | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   bool canHandle(AsyncWebServerRequest *request) override { | ||||
|   bool canHandle(AsyncWebServerRequest *request) const override { | ||||
|     return request->method() == HTTP_GET && request->url() == this->url_; | ||||
|   } | ||||
|   // 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 % | ||||
|   else if ((value_type == 0x4C02) && (value_length == 1)) { | ||||
|     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 { | ||||
|     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 | ||||
|     result.type = XiaomiParseResult::TYPE_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 | ||||
|     result.type = XiaomiParseResult::TYPE_MJYD02YLA; | ||||
|     result.name = "MJYD02YLA"; | ||||
|   | ||||
| @@ -20,6 +20,7 @@ struct XiaomiParseResult { | ||||
|     TYPE_LYWSD02MMC, | ||||
|     TYPE_CGG1, | ||||
|     TYPE_LYWSD03MMC, | ||||
|     TYPE_XMWSDJ04MMC, | ||||
|     TYPE_CGD1, | ||||
|     TYPE_CGDK2, | ||||
|     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 | ||||
|   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 | ||||
|     this->loop_component_start_time_ = last_op_end_time; | ||||
|  | ||||
| @@ -112,6 +118,8 @@ void Application::loop() { | ||||
|     this->app_state_ |= new_app_state; | ||||
|     this->feed_wdt(last_op_end_time); | ||||
|   } | ||||
|  | ||||
|   this->in_loop_ = false; | ||||
|   this->app_state_ = new_app_state; | ||||
|  | ||||
|   // Use the last component's end time instead of calling millis() again | ||||
| @@ -235,12 +243,69 @@ void Application::teardown_components(uint32_t timeout_ms) { | ||||
| } | ||||
|  | ||||
| void Application::calculate_looping_components_() { | ||||
|   // First add all active 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_active_end_ = this->looping_components_.size(); | ||||
|  | ||||
|   // Then add all inactive (LOOP_DONE) components | ||||
|   // This handles components that called disable_loop() during setup, before this method runs | ||||
|   for (auto *obj : this->components_) { | ||||
|     if (obj->has_overridden_loop() && | ||||
|         (obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { | ||||
|       this->looping_components_.push_back(obj); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Application::disable_component_loop_(Component *component) { | ||||
|   // This method must be reentrant - components can disable themselves during their own loop() call | ||||
|   // Linear search to find component in active section | ||||
|   // Most configs have 10-30 looping components (30 is on the high end) | ||||
|   // O(n) is acceptable here as we optimize for memory, not complexity | ||||
|   for (uint16_t i = 0; i < this->looping_components_active_end_; i++) { | ||||
|     if (this->looping_components_[i] == component) { | ||||
|       // Move last active component to this position | ||||
|       this->looping_components_active_end_--; | ||||
|       if (i != this->looping_components_active_end_) { | ||||
|         std::swap(this->looping_components_[i], this->looping_components_[this->looping_components_active_end_]); | ||||
|  | ||||
|         // If we're currently iterating and just swapped the current position | ||||
|         if (this->in_loop_ && i == this->current_loop_index_) { | ||||
|           // Decrement so we'll process the swapped component next | ||||
|           this->current_loop_index_--; | ||||
|         } | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Application::enable_component_loop_(Component *component) { | ||||
|   // This method must be reentrant - components can re-enable themselves during their own loop() call | ||||
|   // Single pass through all components to find and move if needed | ||||
|   // With typical 10-30 components, O(n) is faster than maintaining a map | ||||
|   const uint16_t size = this->looping_components_.size(); | ||||
|   for (uint16_t i = 0; i < size; i++) { | ||||
|     if (this->looping_components_[i] == component) { | ||||
|       if (i < this->looping_components_active_end_) { | ||||
|         return;  // Already active | ||||
|       } | ||||
|       // Found in inactive section - move to active | ||||
|       if (i != this->looping_components_active_end_) { | ||||
|         std::swap(this->looping_components_[i], this->looping_components_[this->looping_components_active_end_]); | ||||
|       } | ||||
|       this->looping_components_active_end_++; | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| #ifdef USE_SOCKET_SELECT_SUPPORT | ||||
| bool Application::register_socket_fd(int fd) { | ||||
|   // WARNING: This function is NOT thread-safe and must only be called from the main loop | ||||
|   | ||||
| @@ -572,13 +572,41 @@ class Application { | ||||
|  | ||||
|   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_(); | ||||
|  | ||||
|   /// Perform a delay while also monitoring socket file descriptors for readiness | ||||
|   void yield_with_select_(uint32_t delay_ms); | ||||
|  | ||||
|   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_{}; | ||||
|   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 | ||||
|   std::vector<binary_sensor::BinarySensor *> binary_sensors_{}; | ||||
|   | ||||
| @@ -30,17 +30,18 @@ const float LATE = -100.0f; | ||||
|  | ||||
| }  // namespace setup_priority | ||||
|  | ||||
| // Component state uses bits 0-1 (4 states) | ||||
| const uint8_t COMPONENT_STATE_MASK = 0x03; | ||||
| // Component state uses bits 0-2 (8 states, 5 used) | ||||
| const uint8_t COMPONENT_STATE_MASK = 0x07; | ||||
| const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; | ||||
| const uint8_t COMPONENT_STATE_SETUP = 0x01; | ||||
| const uint8_t COMPONENT_STATE_LOOP = 0x02; | ||||
| const uint8_t COMPONENT_STATE_FAILED = 0x03; | ||||
| // Status LED uses bits 2-3 | ||||
| const uint8_t STATUS_LED_MASK = 0x0C; | ||||
| const uint8_t COMPONENT_STATE_LOOP_DONE = 0x04; | ||||
| // 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_WARNING = 0x04;  // Bit 2 | ||||
| const uint8_t STATUS_LED_ERROR = 0x08;    // Bit 3 | ||||
| const uint8_t STATUS_LED_WARNING = 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_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) | ||||
|       // State failed: Do nothing | ||||
|       break; | ||||
|     case COMPONENT_STATE_LOOP_DONE:  // NOLINT(bugprone-branch-clone) | ||||
|       // State loop done: Do nothing, component has finished its work | ||||
|       break; | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| @@ -136,14 +140,30 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) { | ||||
|   return false; | ||||
| } | ||||
| 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_FAILED; | ||||
|   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() { | ||||
|   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_CONSTRUCTION; | ||||
|     // Clear error status when resetting | ||||
| @@ -276,8 +296,8 @@ uint32_t WarnIfComponentBlockingGuard::finish() { | ||||
|   } | ||||
|   if (should_warn) { | ||||
|     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, "Components should block for at most 30 ms."); | ||||
|     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"); | ||||
|   } | ||||
|  | ||||
|   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_LOOP; | ||||
| 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_OK; | ||||
| extern const uint8_t STATUS_LED_WARNING; | ||||
| @@ -150,6 +151,26 @@ class Component { | ||||
|     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_ready() const; | ||||
|   | ||||
| @@ -29,7 +29,9 @@ Component = esphome_ns.class_("Component") | ||||
| ComponentPtr = Component.operator("ptr") | ||||
| PollingComponent = esphome_ns.class_("PollingComponent", Component) | ||||
| 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") | ||||
| JsonObject = arduino_json_ns.class_("JsonObject") | ||||
| JsonObjectConst = arduino_json_ns.class_("JsonObjectConst") | ||||
|   | ||||
| @@ -65,14 +65,14 @@ lib_deps = | ||||
|     SPI                                                   ; spi (Arduino built-in) | ||||
|     Wire                                                  ; i2c (Arduino built-int) | ||||
|     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 | ||||
|     mikalhart/TinyGPSPlus@1.1.0                           ; gps | ||||
|     freekode/TM1651@1.0.1                                 ; tm1651 | ||||
|     glmnet/Dsmr@0.7                                       ; dsmr | ||||
|     rweather/Crypto@0.4.0                                 ; dsmr | ||||
|     dudanov/MideaUART@1.1.9                               ; midea | ||||
|     tonia/HeatpumpIR@1.0.32                               ; heatpumpir | ||||
|     tonia/HeatpumpIR@1.0.35                               ; heatpumpir | ||||
| build_flags = | ||||
|     ${common.build_flags} | ||||
|     -DUSE_ARDUINO | ||||
| @@ -100,7 +100,7 @@ lib_deps = | ||||
|     ${common:arduino.lib_deps} | ||||
|     ESP8266WiFi                           ; wifi (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) | ||||
|     ESP8266mDNS                           ; mdns (Arduino built-in) | ||||
|     DNSServer                             ; captive_portal (Arduino built-in) | ||||
| @@ -130,12 +130,12 @@ lib_deps = | ||||
|     WiFi                                 ; wifi,web_server_base,ethernet (Arduino built-in) | ||||
|     Update                               ; ota,web_server_base (Arduino built-in) | ||||
|     ${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) | ||||
|     HTTPClient                           ; http_request,nextion (Arduino built-in) | ||||
|     ESPmDNS                              ; mdns (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 | ||||
|     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. | ||||
| [common:libretiny-arduino] | ||||
| extends = common:arduino | ||||
| platform = libretiny | ||||
| platform = libretiny@1.9.1 | ||||
| framework = arduino | ||||
| lib_deps = | ||||
|     droscy/esp_wireguard@0.4.2    ; wireguard | ||||
|   | ||||
| @@ -120,10 +120,12 @@ select = [ | ||||
|  | ||||
| ignore = [ | ||||
|   "E501", # line too long | ||||
|   "PLC0415", # `import` should be at the top-level of a file | ||||
|   "PLR0911", # Too many return statements ({returns} > {max_returns}) | ||||
|   "PLR0912", # Too many branches ({branches} > {max_branches}) | ||||
|   "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) | ||||
|   "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 | ||||
|   "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 | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| pylint==3.3.7 | ||||
| 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 | ||||
| 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") | ||||
|  | ||||
|     # 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 | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
| import asyncio | ||||
| from collections.abc import AsyncGenerator, Generator | ||||
| from collections.abc import AsyncGenerator, Callable, Generator | ||||
| from contextlib import AbstractAsyncContextManager, asynccontextmanager | ||||
| import logging | ||||
| import os | ||||
| @@ -46,6 +46,7 @@ if platform.system() == "Windows": | ||||
|         "Integration tests are not supported on Windows", allow_module_level=True | ||||
|     ) | ||||
|  | ||||
|  | ||||
| import pty  # not available on Windows | ||||
|  | ||||
|  | ||||
| @@ -362,7 +363,10 @@ async def api_client_connected( | ||||
|  | ||||
|  | ||||
| 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: | ||||
|     """Read lines from a stream, append to list, and echo to output stream.""" | ||||
|     log_parser = LogParser() | ||||
| @@ -380,6 +384,9 @@ async def _read_stream_lines( | ||||
|             file=output_stream, | ||||
|             flush=True, | ||||
|         ) | ||||
|         # Call the callback if provided | ||||
|         if line_callback: | ||||
|             line_callback(decoded_line.rstrip()) | ||||
|  | ||||
|  | ||||
| @asynccontextmanager | ||||
| @@ -388,6 +395,7 @@ async def run_binary_and_wait_for_port( | ||||
|     host: str, | ||||
|     port: int, | ||||
|     timeout: float = PORT_WAIT_TIMEOUT, | ||||
|     line_callback: Callable[[str], None] | None = None, | ||||
| ) -> AsyncGenerator[None]: | ||||
|     """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 | ||||
| @@ -435,7 +443,9 @@ async def run_binary_and_wait_for_port( | ||||
|         # Read from output stream | ||||
|         output_tasks = [ | ||||
|             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, | ||||
|     port: int, | ||||
|     port_socket: socket.socket | None = None, | ||||
|     line_callback: Callable[[str], None] | None = None, | ||||
| ) -> AsyncGenerator[None]: | ||||
|     """Context manager to write, compile and run an ESPHome configuration.""" | ||||
|     # Write the YAML config | ||||
| @@ -528,7 +539,9 @@ async def run_compiled_context( | ||||
|         port_socket.close() | ||||
|  | ||||
|     # 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 | ||||
|  | ||||
|  | ||||
| @@ -542,7 +555,9 @@ async def run_compiled( | ||||
|     port, port_socket = reserved_tcp_port | ||||
|  | ||||
|     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]: | ||||
|         return run_compiled_context( | ||||
|             yaml_content, | ||||
| @@ -551,6 +566,7 @@ async def run_compiled( | ||||
|             compile_esphome, | ||||
|             port, | ||||
|             port_socket, | ||||
|             line_callback=line_callback, | ||||
|         ) | ||||
|  | ||||
|     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]] | ||||
| CompileFunction = Callable[[Path], Awaitable[Path]] | ||||
| 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]] | ||||
|  | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user