mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Merge branch 'integration' into memory_api
This commit is contained in:
		| @@ -1 +1 @@ | ||||
| a3cdfc378d28b53b416a1d5bf0ab9077ee18867f0d39436ea8013cf5a4ead87a | ||||
| 07f621354fe1350ba51953c80273cd44a04aa44f15cc30bd7b8fe2a641427b7a | ||||
|   | ||||
							
								
								
									
										92
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| name: Report an issue with ESPHome | ||||
| description: Report an issue with ESPHome. | ||||
| body: | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: | | ||||
|         This issue form is for reporting bugs only! | ||||
|  | ||||
|         If you have a feature request or enhancement, please [request them here instead][fr]. | ||||
|  | ||||
|         [fr]: https://github.com/orgs/esphome/discussions | ||||
|   - type: textarea | ||||
|     validations: | ||||
|       required: true | ||||
|     id: problem | ||||
|     attributes: | ||||
|       label: The problem | ||||
|       description: >- | ||||
|         Describe the issue you are experiencing here to communicate to the | ||||
|         maintainers. Tell us what you were trying to do and what happened. | ||||
|  | ||||
|         Provide a clear and concise description of what the problem is. | ||||
|  | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: | | ||||
|         ## Environment | ||||
|   - type: input | ||||
|     id: version | ||||
|     validations: | ||||
|       required: true | ||||
|     attributes: | ||||
|       label: Which version of ESPHome has the issue? | ||||
|       description: > | ||||
|         ESPHome version like 1.19, 2025.6.0 or 2025.XX.X-dev. | ||||
|   - type: dropdown | ||||
|     validations: | ||||
|       required: true | ||||
|     id: installation | ||||
|     attributes: | ||||
|       label: What type of installation are you using? | ||||
|       options: | ||||
|         - Home Assistant Add-on | ||||
|         - Docker | ||||
|         - pip | ||||
|   - type: dropdown | ||||
|     validations: | ||||
|       required: true | ||||
|     id: platform | ||||
|     attributes: | ||||
|       label: What platform are you using? | ||||
|       options: | ||||
|         - ESP8266 | ||||
|         - ESP32 | ||||
|         - RP2040 | ||||
|         - BK72XX | ||||
|         - RTL87XX | ||||
|         - LN882X | ||||
|         - Host | ||||
|         - Other | ||||
|   - type: input | ||||
|     id: component_name | ||||
|     attributes: | ||||
|       label: Component causing the issue | ||||
|       description: > | ||||
|         The name of the component or platform. For example, api/i2c or ultrasonic. | ||||
|  | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: | | ||||
|         # Details | ||||
|   - type: textarea | ||||
|     id: config | ||||
|     attributes: | ||||
|       label: YAML Config | ||||
|       description: | | ||||
|         Include a complete YAML configuration file demonstrating the problem here. Preferably post the *entire* file - don't make assumptions about what is unimportant. However, if it's a large or complicated config then you will need to reduce it to the smallest possible file *that still demonstrates the problem*. If you don't provide enough information to *easily* reproduce the problem, it's unlikely your bug report will get any attention. Logs do not belong here, attach them below. | ||||
|       render: yaml | ||||
|   - type: textarea | ||||
|     id: logs | ||||
|     attributes: | ||||
|       label: Anything in the logs that might be useful for us? | ||||
|       description: For example, error message, or stack traces. Serial or USB logs are much more useful than WiFi logs. | ||||
|       render: txt | ||||
|   - type: textarea | ||||
|     id: additional | ||||
|     attributes: | ||||
|       label: Additional information | ||||
|       description: > | ||||
|         If you have any additional information for us, use the field below. | ||||
|         Please note, you can attach screenshots or screen recordings here, by | ||||
|         dragging and dropping files in the field below. | ||||
							
								
								
									
										26
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,15 +1,21 @@ | ||||
| --- | ||||
| blank_issues_enabled: false | ||||
| contact_links: | ||||
|   - name: Issue Tracker | ||||
|     url: https://github.com/esphome/issues | ||||
|     about: Please create bug reports in the dedicated issue tracker. | ||||
|   - name: Feature Request Tracker | ||||
|     url: https://github.com/esphome/feature-requests | ||||
|     about: | | ||||
|       Please create feature requests in the dedicated feature request tracker. | ||||
|   - name: Report an issue with the ESPHome documentation | ||||
|     url: https://github.com/esphome/esphome-docs/issues/new/choose | ||||
|     about: Report an issue with the ESPHome documentation. | ||||
|   - name: Report an issue with the ESPHome web server | ||||
|     url: https://github.com/esphome/esphome-webserver/issues/new/choose | ||||
|     about: Report an issue with the ESPHome web server. | ||||
|   - name: Report an issue with the ESPHome Builder / Dashboard | ||||
|     url: https://github.com/esphome/dashboard/issues/new/choose | ||||
|     about: Report an issue with the ESPHome Builder / Dashboard. | ||||
|   - name: Report an issue with the ESPHome API client | ||||
|     url: https://github.com/esphome/aioesphomeapi/issues/new/choose | ||||
|     about: Report an issue with the ESPHome API client. | ||||
|   - name: Make a Feature Request | ||||
|     url: https://github.com/orgs/esphome/discussions | ||||
|     about: Please create feature requests in the dedicated feature request tracker. | ||||
|   - name: Frequently Asked Question | ||||
|     url: https://esphome.io/guides/faq.html | ||||
|     about: | | ||||
|       Please view the FAQ for common questions and what | ||||
|       to include in a bug report. | ||||
|     about: Please view the FAQ for common questions and what to include in a bug report. | ||||
|   | ||||
| @@ -31,7 +31,6 @@ APIServer::APIServer() { | ||||
| } | ||||
|  | ||||
| void APIServer::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   this->setup_controller(); | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
| @@ -205,16 +204,16 @@ void APIServer::loop() { | ||||
|  | ||||
| void APIServer::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "API Server:\n" | ||||
|                 "Server:\n" | ||||
|                 "  Address: %s:%u", | ||||
|                 network::get_use_address().c_str(), this->port_); | ||||
| #ifdef USE_API_NOISE | ||||
|   ESP_LOGCONFIG(TAG, "  Using noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); | ||||
|   ESP_LOGCONFIG(TAG, "  Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); | ||||
|   if (!this->noise_ctx_->has_psk()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Supports noise encryption: YES"); | ||||
|     ESP_LOGCONFIG(TAG, "  Supports encryption: YES"); | ||||
|   } | ||||
| #else | ||||
|   ESP_LOGCONFIG(TAG, "  Using noise encryption: NO"); | ||||
|   ESP_LOGCONFIG(TAG, "  Noise encryption: NO"); | ||||
| #endif | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -389,10 +389,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * | ||||
|  | ||||
| #ifdef USE_WEBSERVER_SORTING | ||||
|   for (auto &group : ws->sorting_groups_) { | ||||
|     // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||
|     message = json::build_json([group](JsonObject root) { | ||||
|       root["name"] = group.second.name; | ||||
|       root["sorting_weight"] = group.second.weight; | ||||
|     }); | ||||
|     // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||
|  | ||||
|     // a (very) large number of these should be able to be queued initially without defer | ||||
|     // since the only thing in the send buffer at this point is the initial ping/config | ||||
|   | ||||
| @@ -71,7 +71,7 @@ void Application::setup() { | ||||
|  | ||||
|     do { | ||||
|       uint8_t new_app_state = STATUS_LED_WARNING; | ||||
|       this->scheduler.call(); | ||||
|       this->scheduler.call(millis()); | ||||
|       this->feed_wdt(); | ||||
|       for (uint32_t j = 0; j <= i; j++) { | ||||
|         // Update loop_component_start_time_ right before calling each component | ||||
| @@ -97,11 +97,11 @@ void Application::setup() { | ||||
| void Application::loop() { | ||||
|   uint8_t new_app_state = 0; | ||||
|  | ||||
|   this->scheduler.call(); | ||||
|  | ||||
|   // Get the initial loop time at the start | ||||
|   uint32_t last_op_end_time = millis(); | ||||
|  | ||||
|   this->scheduler.call(last_op_end_time); | ||||
|  | ||||
|   // Feed WDT with time | ||||
|   this->feed_wdt(last_op_end_time); | ||||
|  | ||||
| @@ -160,7 +160,7 @@ void Application::loop() { | ||||
|     this->yield_with_select_(0); | ||||
|   } else { | ||||
|     uint32_t delay_time = this->loop_interval_ - elapsed; | ||||
|     uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time); | ||||
|     uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time); | ||||
|     // next_schedule is max 0.5*delay_time | ||||
|     // otherwise interval=0 schedules result in constant looping with almost no sleep | ||||
|     next_schedule = std::max(next_schedule, delay_time / 2); | ||||
|   | ||||
| @@ -267,6 +267,7 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: | ||||
| bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } | ||||
| bool Component::is_ready() const { | ||||
|   return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || | ||||
|          (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || | ||||
|          (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; | ||||
| } | ||||
| bool Component::can_proceed() { return true; } | ||||
|   | ||||
| @@ -91,7 +91,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   const auto now = this->millis_(); | ||||
|   const auto now = this->millis_64_(millis()); | ||||
|  | ||||
|   // Type-specific setup | ||||
|   if (type == SchedulerItem::INTERVAL) { | ||||
| @@ -193,9 +193,7 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin | ||||
|             name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor); | ||||
|  | ||||
|   if (backoff_increase_factor < 0.0001) { | ||||
|     ESP_LOGE(TAG, | ||||
|              "set_retry(name='%s'): backoff_factor cannot be close to zero nor negative (%0.1f). Using 1.0 instead", | ||||
|              name.c_str(), backoff_increase_factor); | ||||
|     ESP_LOGE(TAG, "backoff_factor %0.1f too small, using 1.0", backoff_increase_factor); | ||||
|     backoff_increase_factor = 1; | ||||
|   } | ||||
|  | ||||
| @@ -215,19 +213,19 @@ bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) | ||||
|   return this->cancel_timeout(component, "retry$" + name); | ||||
| } | ||||
|  | ||||
| optional<uint32_t> HOT Scheduler::next_schedule_in() { | ||||
| optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) { | ||||
|   // IMPORTANT: This method should only be called from the main thread (loop task). | ||||
|   // It calls empty_() and accesses items_[0] without holding a lock, which is only | ||||
|   // safe when called from the main thread. Other threads must not call this method. | ||||
|   if (this->empty_()) | ||||
|     return {}; | ||||
|   auto &item = this->items_[0]; | ||||
|   const auto now = this->millis_(); | ||||
|   if (item->next_execution_ < now) | ||||
|   const auto now_64 = this->millis_64_(now); | ||||
|   if (item->next_execution_ < now_64) | ||||
|     return 0; | ||||
|   return item->next_execution_ - now; | ||||
|   return item->next_execution_ - now_64; | ||||
| } | ||||
| void HOT Scheduler::call() { | ||||
| void HOT Scheduler::call(uint32_t now) { | ||||
| #if !defined(USE_ESP8266) && !defined(USE_RP2040) | ||||
|   // Process defer queue first to guarantee FIFO execution order for deferred items. | ||||
|   // Previously, defer() used the heap which gave undefined order for equal timestamps, | ||||
| @@ -256,22 +254,22 @@ void HOT Scheduler::call() { | ||||
|     // Execute callback without holding lock to prevent deadlocks | ||||
|     // if the callback tries to call defer() again | ||||
|     if (!this->should_skip_item_(item.get())) { | ||||
|       this->execute_item_(item.get()); | ||||
|       this->execute_item_(item.get(), now); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   const auto now = this->millis_(); | ||||
|   const auto now_64 = this->millis_64_(now); | ||||
|   this->process_to_add(); | ||||
|  | ||||
| #ifdef ESPHOME_DEBUG_SCHEDULER | ||||
|   static uint64_t last_print = 0; | ||||
|  | ||||
|   if (now - last_print > 2000) { | ||||
|     last_print = now; | ||||
|   if (now_64 - last_print > 2000) { | ||||
|     last_print = now_64; | ||||
|     std::vector<std::unique_ptr<SchedulerItem>> old_items; | ||||
|     ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, | ||||
|              this->last_millis_); | ||||
|     ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64, | ||||
|              this->millis_major_, this->last_millis_); | ||||
|     while (!this->empty_()) { | ||||
|       std::unique_ptr<SchedulerItem> item; | ||||
|       { | ||||
| @@ -283,7 +281,7 @@ void HOT Scheduler::call() { | ||||
|       const char *name = item->get_name(); | ||||
|       ESP_LOGD(TAG, "  %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, | ||||
|                item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, | ||||
|                item->next_execution_ - now, item->next_execution_); | ||||
|                item->next_execution_ - now_64, item->next_execution_); | ||||
|  | ||||
|       old_items.push_back(std::move(item)); | ||||
|     } | ||||
| @@ -328,7 +326,7 @@ void HOT Scheduler::call() { | ||||
|     { | ||||
|       // Don't copy-by value yet | ||||
|       auto &item = this->items_[0]; | ||||
|       if (item->next_execution_ > now) { | ||||
|       if (item->next_execution_ > now_64) { | ||||
|         // Not reached timeout yet, done for this call | ||||
|         break; | ||||
|       } | ||||
| @@ -342,13 +340,13 @@ void HOT Scheduler::call() { | ||||
|       const char *item_name = item->get_name(); | ||||
|       ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", | ||||
|                item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval, | ||||
|                item->next_execution_, now); | ||||
|                item->next_execution_, now_64); | ||||
| #endif | ||||
|  | ||||
|       // Warning: During callback(), a lot of stuff can happen, including: | ||||
|       //  - timeouts/intervals get added, potentially invalidating vector pointers | ||||
|       //  - timeouts/intervals get cancelled | ||||
|       this->execute_item_(item.get()); | ||||
|       this->execute_item_(item.get(), now); | ||||
|     } | ||||
|  | ||||
|     { | ||||
| @@ -367,7 +365,7 @@ void HOT Scheduler::call() { | ||||
|       } | ||||
|  | ||||
|       if (item->type == SchedulerItem::INTERVAL) { | ||||
|         item->next_execution_ = now + item->interval; | ||||
|         item->next_execution_ = now_64 + item->interval; | ||||
|         // Add new item directly to to_add_ | ||||
|         // since we have the lock held | ||||
|         this->to_add_.push_back(std::move(item)); | ||||
| @@ -423,11 +421,9 @@ void HOT Scheduler::pop_raw_() { | ||||
| } | ||||
|  | ||||
| // Helper to execute a scheduler item | ||||
| void HOT Scheduler::execute_item_(SchedulerItem *item) { | ||||
| void HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { | ||||
|   App.set_current_component(item->component); | ||||
|  | ||||
|   uint32_t now_ms = millis(); | ||||
|   WarnIfComponentBlockingGuard guard{item->component, now_ms}; | ||||
|   WarnIfComponentBlockingGuard guard{item->component, now}; | ||||
|   item->callback(); | ||||
|   guard.finish(); | ||||
| } | ||||
| @@ -486,15 +482,15 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c | ||||
|   return total_cancelled > 0; | ||||
| } | ||||
|  | ||||
| uint64_t Scheduler::millis_() { | ||||
|   // Get the current 32-bit millis value | ||||
|   const uint32_t now = millis(); | ||||
| uint64_t Scheduler::millis_64_(uint32_t now) { | ||||
|   // Check for rollover by comparing with last value | ||||
|   if (now < this->last_millis_) { | ||||
|     // Detected rollover (happens every ~49.7 days) | ||||
|     this->millis_major_++; | ||||
| #ifdef ESPHOME_DEBUG_SCHEDULER | ||||
|     ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms", | ||||
|              now + (static_cast<uint64_t>(this->millis_major_) << 32)); | ||||
| #endif | ||||
|   } | ||||
|   this->last_millis_ = now; | ||||
|   // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time | ||||
|   | ||||
| @@ -52,9 +52,9 @@ class Scheduler { | ||||
|                  std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f); | ||||
|   bool cancel_retry(Component *component, const std::string &name); | ||||
|  | ||||
|   optional<uint32_t> next_schedule_in(); | ||||
|   optional<uint32_t> next_schedule_in(uint32_t now); | ||||
|  | ||||
|   void call(); | ||||
|   void call(uint32_t now); | ||||
|  | ||||
|   void process_to_add(); | ||||
|  | ||||
| @@ -137,7 +137,7 @@ class Scheduler { | ||||
|   void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, | ||||
|                          uint32_t delay, std::function<void()> func); | ||||
|  | ||||
|   uint64_t millis_(); | ||||
|   uint64_t millis_64_(uint32_t now); | ||||
|   void cleanup_(); | ||||
|   void pop_raw_(); | ||||
|  | ||||
| @@ -175,7 +175,7 @@ class Scheduler { | ||||
|   } | ||||
|  | ||||
|   // Helper to execute a scheduler item | ||||
|   void execute_item_(SchedulerItem *item); | ||||
|   void execute_item_(SchedulerItem *item, uint32_t now); | ||||
|  | ||||
|   // Helper to check if item should be skipped | ||||
|   bool should_skip_item_(const SchedulerItem *item) const { | ||||
|   | ||||
| @@ -163,7 +163,7 @@ def get_ini_content(): | ||||
|     CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) | ||||
|  | ||||
|     # Add extra script for C++ flags | ||||
|     CORE.add_platformio_option("extra_scripts", ["pre:cxx_flags.py"]) | ||||
|     CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"]) | ||||
|  | ||||
|     content = "[platformio]\n" | ||||
|     content += f"description = ESPHome {__version__}\n" | ||||
| @@ -402,14 +402,18 @@ def write_gitignore(): | ||||
|             f.write(GITIGNORE_CONTENT) | ||||
|  | ||||
|  | ||||
| CXX_FLAGS_SCRIPT = """# Auto-generated ESPHome script for C++ specific compiler flags | ||||
| CXX_FLAGS_FILE_NAME = "cxx_flags.py" | ||||
| CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags | ||||
| Import("env") | ||||
|  | ||||
| # Add C++ specific warning flags | ||||
| env.Append(CXXFLAGS=["-Wno-volatile"]) | ||||
| # Add C++ specific flags | ||||
| """ | ||||
|  | ||||
|  | ||||
| def write_cxx_flags_script() -> None: | ||||
|     path = CORE.relative_build_path("cxx_flags.py") | ||||
|     write_file_if_changed(path, CXX_FLAGS_SCRIPT) | ||||
|     path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME) | ||||
|     contents = CXX_FLAGS_FILE_CONTENTS | ||||
|     if not CORE.is_host: | ||||
|         contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])' | ||||
|         contents += "\n" | ||||
|     write_file_if_changed(path, contents) | ||||
|   | ||||
| @@ -62,26 +62,6 @@ def get_clang_tidy_version_from_requirements() -> str: | ||||
|     return "clang-tidy version not found" | ||||
|  | ||||
|  | ||||
| def extract_platformio_flags() -> str: | ||||
|     """Extract clang-tidy related flags from platformio.ini""" | ||||
|     flags: list[str] = [] | ||||
|     in_clangtidy_section = False | ||||
|  | ||||
|     platformio_path = Path(__file__).parent.parent / "platformio.ini" | ||||
|     lines = read_file_lines(platformio_path) | ||||
|     for line in lines: | ||||
|         line = line.strip() | ||||
|         if line.startswith("[flags:clangtidy]"): | ||||
|             in_clangtidy_section = True | ||||
|             continue | ||||
|         elif line.startswith("[") and in_clangtidy_section: | ||||
|             break | ||||
|         elif in_clangtidy_section and line and not line.startswith("#"): | ||||
|             flags.append(line) | ||||
|  | ||||
|     return "\n".join(sorted(flags)) | ||||
|  | ||||
|  | ||||
| def read_file_bytes(path: Path) -> bytes: | ||||
|     """Read bytes from a file.""" | ||||
|     with open(path, "rb") as f: | ||||
| @@ -101,9 +81,10 @@ def calculate_clang_tidy_hash() -> str: | ||||
|     version = get_clang_tidy_version_from_requirements() | ||||
|     hasher.update(version.encode()) | ||||
|  | ||||
|     # Hash relevant platformio.ini sections | ||||
|     pio_flags = extract_platformio_flags() | ||||
|     hasher.update(pio_flags.encode()) | ||||
|     # Hash the entire platformio.ini file | ||||
|     platformio_path = Path(__file__).parent.parent / "platformio.ini" | ||||
|     platformio_content = read_file_bytes(platformio_path) | ||||
|     hasher.update(platformio_content) | ||||
|  | ||||
|     return hasher.hexdigest() | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| 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 | ||||
| from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME, CONF_UPDATE_INTERVAL | ||||
|  | ||||
| CODEOWNERS = ["@esphome/tests"] | ||||
|  | ||||
| @@ -10,10 +10,15 @@ LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Compon | ||||
| LoopTestISRComponent = loop_test_component_ns.class_( | ||||
|     "LoopTestISRComponent", cg.Component | ||||
| ) | ||||
| LoopTestUpdateComponent = loop_test_component_ns.class_( | ||||
|     "LoopTestUpdateComponent", cg.PollingComponent | ||||
| ) | ||||
|  | ||||
| CONF_DISABLE_AFTER = "disable_after" | ||||
| CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations" | ||||
| CONF_ISR_COMPONENTS = "isr_components" | ||||
| CONF_UPDATE_COMPONENTS = "update_components" | ||||
| CONF_DISABLE_LOOP_AFTER = "disable_loop_after" | ||||
|  | ||||
| COMPONENT_CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
| @@ -31,11 +36,23 @@ ISR_COMPONENT_CONFIG_SCHEMA = cv.Schema( | ||||
|     } | ||||
| ) | ||||
|  | ||||
| UPDATE_COMPONENT_CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(LoopTestUpdateComponent), | ||||
|         cv.Required(CONF_NAME): cv.string, | ||||
|         cv.Optional(CONF_DISABLE_LOOP_AFTER, default=0): cv.int_, | ||||
|         cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(LoopTestComponent), | ||||
|         cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA), | ||||
|         cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA), | ||||
|         cv.Optional(CONF_UPDATE_COMPONENTS): cv.ensure_list( | ||||
|             UPDATE_COMPONENT_CONFIG_SCHEMA | ||||
|         ), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
| @@ -94,3 +111,12 @@ async def to_code(config): | ||||
|         var = cg.new_Pvariable(isr_config[CONF_ID]) | ||||
|         await cg.register_component(var, isr_config) | ||||
|         cg.add(var.set_name(isr_config[CONF_NAME])) | ||||
|  | ||||
|     # Create update test components | ||||
|     for update_config in config.get(CONF_UPDATE_COMPONENTS, []): | ||||
|         var = cg.new_Pvariable(update_config[CONF_ID]) | ||||
|         await cg.register_component(var, update_config) | ||||
|  | ||||
|         cg.add(var.set_name(update_config[CONF_NAME])) | ||||
|         cg.add(var.set_disable_loop_after(update_config[CONF_DISABLE_LOOP_AFTER])) | ||||
|         cg.add(var.set_update_interval(update_config[CONF_UPDATE_INTERVAL])) | ||||
|   | ||||
| @@ -39,5 +39,29 @@ void LoopTestComponent::service_disable() { | ||||
|   this->disable_loop(); | ||||
| } | ||||
|  | ||||
| // LoopTestUpdateComponent implementation | ||||
| void LoopTestUpdateComponent::setup() { | ||||
|   ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent setup called", this->name_.c_str()); | ||||
| } | ||||
|  | ||||
| void LoopTestUpdateComponent::loop() { | ||||
|   this->loop_count_++; | ||||
|   ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent loop count: %d", this->name_.c_str(), this->loop_count_); | ||||
|  | ||||
|   // Disable loop after specified count to test component.update when loop is disabled | ||||
|   if (this->disable_loop_after_ > 0 && this->loop_count_ == this->disable_loop_after_) { | ||||
|     ESP_LOGI(TAG, "[%s] Disabling loop after %d iterations", this->name_.c_str(), this->disable_loop_after_); | ||||
|     this->disable_loop(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void LoopTestUpdateComponent::update() { | ||||
|   this->update_count_++; | ||||
|   // Check if loop is disabled by testing component state | ||||
|   bool loop_disabled = this->component_state_ == COMPONENT_STATE_LOOP_DONE; | ||||
|   ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent update() called, count: %d, loop_disabled: %s", this->name_.c_str(), | ||||
|            this->update_count_, loop_disabled ? "YES" : "NO"); | ||||
| } | ||||
|  | ||||
| }  // namespace loop_test_component | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace loop_test_component { | ||||
| @@ -54,5 +55,29 @@ template<typename... Ts> class DisableAction : public Action<Ts...> { | ||||
|   LoopTestComponent *parent_; | ||||
| }; | ||||
|  | ||||
| // Component with update() method to test component.update action | ||||
| class LoopTestUpdateComponent : public PollingComponent { | ||||
|  public: | ||||
|   LoopTestUpdateComponent() : PollingComponent(1000) {}  // Default 1s update interval | ||||
|  | ||||
|   void set_name(const std::string &name) { this->name_ = name; } | ||||
|   void set_disable_loop_after(int count) { this->disable_loop_after_ = count; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   void update() override; | ||||
|  | ||||
|   int get_update_count() const { return this->update_count_; } | ||||
|   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 update_count_{0}; | ||||
|   int disable_loop_after_{0}; | ||||
| }; | ||||
|  | ||||
| }  // namespace loop_test_component | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -40,6 +40,13 @@ loop_test_component: | ||||
|     - id: isr_test | ||||
|       name: "isr_test" | ||||
|  | ||||
|   # Update test component to test component.update when loop is disabled | ||||
|   update_components: | ||||
|     - id: update_test_component | ||||
|       name: "update_test" | ||||
|       disable_loop_after: 3  # Disable loop after 3 iterations | ||||
|       update_interval: 0.1s  # Fast update interval for testing | ||||
|  | ||||
| # Interval to re-enable the self_disable_10 component after some time | ||||
| interval: | ||||
|   - interval: 0.5s | ||||
| @@ -51,3 +58,28 @@ interval: | ||||
|             - logger.log: "Re-enabling self_disable_10 via service" | ||||
|             - loop_test_component.enable: | ||||
|                 id: self_disable_10 | ||||
|  | ||||
|   # Test component.update on a component with disabled loop | ||||
|   - interval: 0.1s | ||||
|     then: | ||||
|       - lambda: |- | ||||
|           static bool manual_update_done = false; | ||||
|           if (!manual_update_done && | ||||
|               id(update_test_component).get_loop_count() == 3 && | ||||
|               id(update_test_component).get_update_count() >= 3) { | ||||
|             ESP_LOGI("main", "Manually calling component.update on update_test_component with disabled loop"); | ||||
|             manual_update_done = true; | ||||
|           } | ||||
|       - if: | ||||
|           condition: | ||||
|             lambda: |- | ||||
|               static bool manual_update_triggered = false; | ||||
|               if (!manual_update_triggered && | ||||
|                   id(update_test_component).get_loop_count() == 3 && | ||||
|                   id(update_test_component).get_update_count() >= 3) { | ||||
|                 manual_update_triggered = true; | ||||
|                 return true; | ||||
|               } | ||||
|               return false; | ||||
|           then: | ||||
|             - component.update: update_test_component | ||||
|   | ||||
| @@ -45,11 +45,18 @@ async def test_loop_disable_enable( | ||||
|     isr_component_disabled = asyncio.Event() | ||||
|     isr_component_re_enabled = asyncio.Event() | ||||
|     isr_component_pure_re_enabled = asyncio.Event() | ||||
|     # Events for update component testing | ||||
|     update_component_loop_disabled = asyncio.Event() | ||||
|     update_component_manual_update_called = asyncio.Event() | ||||
|  | ||||
|     # Track loop counts for components | ||||
|     self_disable_10_counts: list[int] = [] | ||||
|     normal_component_counts: list[int] = [] | ||||
|     isr_component_counts: list[int] = [] | ||||
|     # Track update component behavior | ||||
|     update_component_loop_count = 0 | ||||
|     update_component_update_count = 0 | ||||
|     update_component_manual_update_count = 0 | ||||
|  | ||||
|     def on_log_line(line: str) -> None: | ||||
|         """Process each log line from the process output.""" | ||||
| @@ -59,6 +66,7 @@ async def test_loop_disable_enable( | ||||
|         if ( | ||||
|             "loop_test_component" not in clean_line | ||||
|             and "loop_test_isr_component" not in clean_line | ||||
|             and "Manually calling component.update" not in clean_line | ||||
|         ): | ||||
|             return | ||||
|  | ||||
| @@ -112,6 +120,23 @@ async def test_loop_disable_enable( | ||||
|             elif "Running after pure ISR re-enable!" in clean_line: | ||||
|                 isr_component_pure_re_enabled.set() | ||||
|  | ||||
|         # Update component events | ||||
|         elif "[update_test]" in clean_line: | ||||
|             if "LoopTestUpdateComponent loop count:" in clean_line: | ||||
|                 nonlocal update_component_loop_count | ||||
|                 update_component_loop_count = int( | ||||
|                     clean_line.split("LoopTestUpdateComponent loop count: ")[1] | ||||
|                 ) | ||||
|             elif "LoopTestUpdateComponent update() called" in clean_line: | ||||
|                 nonlocal update_component_update_count | ||||
|                 update_component_update_count += 1 | ||||
|                 if "Manually calling component.update" in " ".join(log_messages[-5:]): | ||||
|                     nonlocal update_component_manual_update_count | ||||
|                     update_component_manual_update_count += 1 | ||||
|                     update_component_manual_update_called.set() | ||||
|             elif "Disabling loop after" in clean_line: | ||||
|                 update_component_loop_disabled.set() | ||||
|  | ||||
|     # Write, compile and run the ESPHome device with log callback | ||||
|     async with ( | ||||
|         run_compiled(yaml_config, line_callback=on_log_line), | ||||
| @@ -205,3 +230,28 @@ async def test_loop_disable_enable( | ||||
|         assert final_count > 10, ( | ||||
|             f"Component didn't run after pure ISR enable: got {final_count} counts total" | ||||
|         ) | ||||
|  | ||||
|         # Test component.update functionality when loop is disabled | ||||
|         # Wait for update component to disable its loop | ||||
|         try: | ||||
|             await asyncio.wait_for(update_component_loop_disabled.wait(), timeout=3.0) | ||||
|         except asyncio.TimeoutError: | ||||
|             pytest.fail("Update component did not disable its loop within 3 seconds") | ||||
|  | ||||
|         # Verify it ran exactly 3 loops before disabling | ||||
|         assert update_component_loop_count == 3, ( | ||||
|             f"Expected 3 loop iterations before disable, got {update_component_loop_count}" | ||||
|         ) | ||||
|  | ||||
|         # Wait for manual component.update to be called | ||||
|         try: | ||||
|             await asyncio.wait_for( | ||||
|                 update_component_manual_update_called.wait(), timeout=5.0 | ||||
|             ) | ||||
|         except asyncio.TimeoutError: | ||||
|             pytest.fail("Manual component.update was not called within 5 seconds") | ||||
|  | ||||
|         # The key test: verify that manual component.update worked after loop was disabled | ||||
|         assert update_component_manual_update_count >= 1, ( | ||||
|             "component.update did not fire after loop was disabled" | ||||
|         ) | ||||
|   | ||||
| @@ -44,67 +44,36 @@ def test_get_clang_tidy_version_from_requirements( | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     ("platformio_content", "expected_flags"), | ||||
|     [ | ||||
|         ( | ||||
|             "[env:esp32]\n" | ||||
|             "platform = espressif32\n" | ||||
|             "\n" | ||||
|             "[flags:clangtidy]\n" | ||||
|             "build_flags = -Wall\n" | ||||
|             "extra_flags = -Wextra\n" | ||||
|             "\n" | ||||
|             "[env:esp8266]\n", | ||||
|             "build_flags = -Wall\nextra_flags = -Wextra", | ||||
|         ), | ||||
|         ( | ||||
|             "[flags:clangtidy]\n# Comment line\nbuild_flags = -O2\n\n[next_section]\n", | ||||
|             "build_flags = -O2", | ||||
|         ), | ||||
|         ( | ||||
|             "[flags:clangtidy]\nflag_c = -std=c99\nflag_b = -Wall\nflag_a = -O2\n", | ||||
|             "flag_a = -O2\nflag_b = -Wall\nflag_c = -std=c99",  # Sorted | ||||
|         ), | ||||
|         ( | ||||
|             "[env:esp32]\nplatform = espressif32\n",  # No clangtidy section | ||||
|             "", | ||||
|         ), | ||||
|     ], | ||||
| ) | ||||
| def test_extract_platformio_flags(platformio_content: str, expected_flags: str) -> None: | ||||
|     """Test extracting clang-tidy flags from platformio.ini.""" | ||||
|     # Mock read_file_lines to return our test content | ||||
|     with patch("clang_tidy_hash.read_file_lines") as mock_read: | ||||
|         mock_read.return_value = platformio_content.splitlines(keepends=True) | ||||
|  | ||||
|         result = clang_tidy_hash.extract_platformio_flags() | ||||
|  | ||||
|     assert result == expected_flags | ||||
|  | ||||
|  | ||||
| def test_calculate_clang_tidy_hash() -> None: | ||||
|     """Test calculating hash from all configuration sources.""" | ||||
|     clang_tidy_content = b"Checks: '-*,readability-*'\n" | ||||
|     requirements_version = "clang-tidy==18.1.5" | ||||
|     pio_flags = "build_flags = -Wall" | ||||
|     platformio_content = b"[env:esp32]\nplatform = espressif32\n" | ||||
|  | ||||
|     # Expected hash calculation | ||||
|     expected_hasher = hashlib.sha256() | ||||
|     expected_hasher.update(clang_tidy_content) | ||||
|     expected_hasher.update(requirements_version.encode()) | ||||
|     expected_hasher.update(pio_flags.encode()) | ||||
|     expected_hasher.update(platformio_content) | ||||
|     expected_hash = expected_hasher.hexdigest() | ||||
|  | ||||
|     # Mock the dependencies | ||||
|     with ( | ||||
|         patch("clang_tidy_hash.read_file_bytes", return_value=clang_tidy_content), | ||||
|         patch("clang_tidy_hash.read_file_bytes") as mock_read_bytes, | ||||
|         patch( | ||||
|             "clang_tidy_hash.get_clang_tidy_version_from_requirements", | ||||
|             return_value=requirements_version, | ||||
|         ), | ||||
|         patch("clang_tidy_hash.extract_platformio_flags", return_value=pio_flags), | ||||
|     ): | ||||
|         # Set up mock to return different content based on the file being read | ||||
|         def read_file_mock(path: Path) -> bytes: | ||||
|             if ".clang-tidy" in str(path): | ||||
|                 return clang_tidy_content | ||||
|             elif "platformio.ini" in str(path): | ||||
|                 return platformio_content | ||||
|             return b"" | ||||
|  | ||||
|         mock_read_bytes.side_effect = read_file_mock | ||||
|         result = clang_tidy_hash.calculate_clang_tidy_hash() | ||||
|  | ||||
|     assert result == expected_hash | ||||
|   | ||||
		Reference in New Issue
	
	Block a user