mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/dev' into light-addr
This commit is contained in:
		| @@ -2,11 +2,11 @@ | ||||
|  | ||||
| #include <cinttypes> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -92,8 +92,8 @@ class DoubleClickTrigger : public Trigger<> { | ||||
|  | ||||
| class MultiClickTrigger : public Trigger<>, public Component { | ||||
|  public: | ||||
|   explicit MultiClickTrigger(BinarySensor *parent, std::vector<MultiClickTriggerEvent> timing) | ||||
|       : parent_(parent), timing_(std::move(timing)) {} | ||||
|   explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing) | ||||
|       : parent_(parent), timing_(timing) {} | ||||
|  | ||||
|   void setup() override { | ||||
|     this->last_state_ = this->parent_->get_state_default(false); | ||||
| @@ -115,7 +115,7 @@ class MultiClickTrigger : public Trigger<>, public Component { | ||||
|   void trigger_(); | ||||
|  | ||||
|   BinarySensor *parent_; | ||||
|   std::vector<MultiClickTriggerEvent> timing_; | ||||
|   FixedVector<MultiClickTriggerEvent> timing_; | ||||
|   uint32_t invalid_cooldown_{1000}; | ||||
|   optional<size_t> at_index_{}; | ||||
|   bool last_state_{false}; | ||||
|   | ||||
| @@ -8,12 +8,19 @@ namespace event { | ||||
| static const char *const TAG = "event"; | ||||
|  | ||||
| void Event::trigger(const std::string &event_type) { | ||||
|   auto found = types_.find(event_type); | ||||
|   if (found == types_.end()) { | ||||
|   // Linear search - faster than std::set for small datasets (1-5 items typical) | ||||
|   const std::string *found = nullptr; | ||||
|   for (const auto &type : this->types_) { | ||||
|     if (type == event_type) { | ||||
|       found = &type; | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|   if (found == nullptr) { | ||||
|     ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); | ||||
|     return; | ||||
|   } | ||||
|   last_event_type = &(*found); | ||||
|   last_event_type = found; | ||||
|   ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); | ||||
|   this->event_callback_.call(event_type); | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <set> | ||||
| #include <string> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| @@ -26,13 +25,13 @@ class Event : public EntityBase, public EntityBase_DeviceClass { | ||||
|   const std::string *last_event_type; | ||||
|  | ||||
|   void trigger(const std::string &event_type); | ||||
|   void set_event_types(const std::set<std::string> &event_types) { this->types_ = event_types; } | ||||
|   std::set<std::string> get_event_types() const { return this->types_; } | ||||
|   void set_event_types(const std::initializer_list<std::string> &event_types) { this->types_ = event_types; } | ||||
|   const FixedVector<std::string> &get_event_types() const { return this->types_; } | ||||
|   void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback); | ||||
|  | ||||
|  protected: | ||||
|   CallbackManager<void(const std::string &event_type)> event_callback_; | ||||
|   std::set<std::string> types_; | ||||
|   FixedVector<std::string> types_; | ||||
| }; | ||||
|  | ||||
| }  // namespace event | ||||
|   | ||||
| @@ -378,14 +378,19 @@ async def to_code(config): | ||||
|     # Track if any network uses Enterprise authentication | ||||
|     has_eap = False | ||||
|  | ||||
|     def add_sta(ap, network): | ||||
|         ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) | ||||
|         cg.add(var.add_sta(wifi_network(network, ap, ip_config))) | ||||
|     # Initialize FixedVector with the count of networks | ||||
|     networks = config.get(CONF_NETWORKS, []) | ||||
|     if networks: | ||||
|         cg.add(var.init_sta(len(networks))) | ||||
|  | ||||
|     for network in config.get(CONF_NETWORKS, []): | ||||
|         if CONF_EAP in network: | ||||
|             has_eap = True | ||||
|         cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network) | ||||
|         def add_sta(ap: cg.MockObj, network: dict) -> None: | ||||
|             ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) | ||||
|             cg.add(var.add_sta(wifi_network(network, ap, ip_config))) | ||||
|  | ||||
|         for network in networks: | ||||
|             if CONF_EAP in network: | ||||
|                 has_eap = True | ||||
|             cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network) | ||||
|  | ||||
|     if CONF_AP in config: | ||||
|         conf = config[CONF_AP] | ||||
|   | ||||
| @@ -330,9 +330,11 @@ float WiFiComponent::get_loop_priority() const { | ||||
|   return 10.0f;  // before other loop components | ||||
| } | ||||
|  | ||||
| void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); } | ||||
| void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } | ||||
| void WiFiComponent::set_sta(const WiFiAP &ap) { | ||||
|   this->clear_sta(); | ||||
|   this->init_sta(1); | ||||
|   this->add_sta(ap); | ||||
| } | ||||
| void WiFiComponent::clear_sta() { this->sta_.clear(); } | ||||
|   | ||||
| @@ -219,6 +219,7 @@ class WiFiComponent : public Component { | ||||
|  | ||||
|   void set_sta(const WiFiAP &ap); | ||||
|   WiFiAP get_sta() { return this->selected_ap_; } | ||||
|   void init_sta(size_t count); | ||||
|   void add_sta(const WiFiAP &ap); | ||||
|   void clear_sta(); | ||||
|  | ||||
| @@ -393,7 +394,7 @@ class WiFiComponent : public Component { | ||||
| #endif | ||||
|  | ||||
|   std::string use_address_; | ||||
|   std::vector<WiFiAP> sta_; | ||||
|   FixedVector<WiFiAP> sta_; | ||||
|   std::vector<WiFiSTAPriority> sta_priorities_; | ||||
|   wifi_scan_vector_t<WiFiScanResult> scan_result_; | ||||
|   WiFiAP selected_ap_; | ||||
|   | ||||
| @@ -194,12 +194,8 @@ template<typename T> class FixedVector { | ||||
|     size_ = 0; | ||||
|   } | ||||
|  | ||||
|  public: | ||||
|   FixedVector() = default; | ||||
|  | ||||
|   /// Constructor from initializer list - allocates exact size needed | ||||
|   /// This enables brace initialization: FixedVector<int> v = {1, 2, 3}; | ||||
|   FixedVector(std::initializer_list<T> init_list) { | ||||
|   // Helper to assign from initializer list (shared by constructor and assignment operator) | ||||
|   void assign_from_initializer_list_(std::initializer_list<T> init_list) { | ||||
|     init(init_list.size()); | ||||
|     size_t idx = 0; | ||||
|     for (const auto &item : init_list) { | ||||
| @@ -209,6 +205,13 @@ template<typename T> class FixedVector { | ||||
|     size_ = init_list.size(); | ||||
|   } | ||||
|  | ||||
|  public: | ||||
|   FixedVector() = default; | ||||
|  | ||||
|   /// Constructor from initializer list - allocates exact size needed | ||||
|   /// This enables brace initialization: FixedVector<int> v = {1, 2, 3}; | ||||
|   FixedVector(std::initializer_list<T> init_list) { assign_from_initializer_list_(init_list); } | ||||
|  | ||||
|   ~FixedVector() { cleanup_(); } | ||||
|  | ||||
|   // Disable copy operations (avoid accidental expensive copies) | ||||
| @@ -234,6 +237,15 @@ template<typename T> class FixedVector { | ||||
|     return *this; | ||||
|   } | ||||
|  | ||||
|   /// Assignment from initializer list - avoids temporary and move overhead | ||||
|   /// This enables: FixedVector<int> v; v = {1, 2, 3}; | ||||
|   FixedVector &operator=(std::initializer_list<T> init_list) { | ||||
|     cleanup_(); | ||||
|     reset_(); | ||||
|     assign_from_initializer_list_(init_list); | ||||
|     return *this; | ||||
|   } | ||||
|  | ||||
|   // Allocate capacity - can be called multiple times to reinit | ||||
|   void init(size_t n) { | ||||
|     cleanup_(); | ||||
|   | ||||
| @@ -606,21 +606,23 @@ def main() -> None: | ||||
|     #   [list]: Changed components (already includes dependencies) | ||||
|     changed_components_result = get_changed_components() | ||||
|  | ||||
|     # Always analyze component files, even if core files changed | ||||
|     # This is needed for component testing and memory impact analysis | ||||
|     changed = changed_files(args.branch) | ||||
|     component_files = [f for f in changed if filter_component_and_test_files(f)] | ||||
|  | ||||
|     directly_changed_components = get_components_with_dependencies( | ||||
|         component_files, False | ||||
|     ) | ||||
|  | ||||
|     if changed_components_result is None: | ||||
|         # Core files changed - will trigger full clang-tidy scan | ||||
|         # No specific components to test | ||||
|         changed_components = [] | ||||
|         directly_changed_components = [] | ||||
|         # But we still need to track changed components for testing and memory analysis | ||||
|         changed_components = get_components_with_dependencies(component_files, True) | ||||
|         is_core_change = True | ||||
|     else: | ||||
|         # Get both directly changed and all changed (with dependencies) | ||||
|         changed = changed_files(args.branch) | ||||
|         component_files = [f for f in changed if filter_component_and_test_files(f)] | ||||
|  | ||||
|         directly_changed_components = get_components_with_dependencies( | ||||
|             component_files, False | ||||
|         ) | ||||
|         changed_components = get_components_with_dependencies(component_files, True) | ||||
|         # Use the result from get_changed_components() which includes dependencies | ||||
|         changed_components = changed_components_result | ||||
|         is_core_change = False | ||||
|  | ||||
|     # Filter to only components that have test files | ||||
|   | ||||
| @@ -12,5 +12,8 @@ esphome: | ||||
|             - logger.log: "Failed to connect to WiFi!" | ||||
|  | ||||
| wifi: | ||||
|   ssid: MySSID | ||||
|   password: password1 | ||||
|   networks: | ||||
|     - ssid: MySSID | ||||
|       password: password1 | ||||
|     - ssid: MySSID2 | ||||
|       password: password2 | ||||
|   | ||||
| @@ -910,3 +910,60 @@ def test_clang_tidy_mode_targeted_scan( | ||||
|     output = json.loads(captured.out) | ||||
|  | ||||
|     assert output["clang_tidy_mode"] == expected_mode | ||||
|  | ||||
|  | ||||
| def test_main_core_files_changed_still_detects_components( | ||||
|     mock_should_run_integration_tests: Mock, | ||||
|     mock_should_run_clang_tidy: Mock, | ||||
|     mock_should_run_clang_format: Mock, | ||||
|     mock_should_run_python_linters: Mock, | ||||
|     mock_changed_files: Mock, | ||||
|     mock_determine_cpp_unit_tests: Mock, | ||||
|     capsys: pytest.CaptureFixture[str], | ||||
|     monkeypatch: pytest.MonkeyPatch, | ||||
| ) -> None: | ||||
|     """Test that component changes are detected even when core files change.""" | ||||
|     monkeypatch.delenv("GITHUB_ACTIONS", raising=False) | ||||
|  | ||||
|     mock_should_run_integration_tests.return_value = True | ||||
|     mock_should_run_clang_tidy.return_value = True | ||||
|     mock_should_run_clang_format.return_value = True | ||||
|     mock_should_run_python_linters.return_value = True | ||||
|     mock_determine_cpp_unit_tests.return_value = (True, []) | ||||
|  | ||||
|     mock_changed_files.return_value = [ | ||||
|         "esphome/core/helpers.h", | ||||
|         "esphome/components/select/select_traits.h", | ||||
|         "esphome/components/select/select_traits.cpp", | ||||
|         "esphome/components/api/api.proto", | ||||
|     ] | ||||
|  | ||||
|     with ( | ||||
|         patch("sys.argv", ["determine-jobs.py"]), | ||||
|         patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), | ||||
|         patch.object(determine_jobs, "get_changed_components", return_value=None), | ||||
|         patch.object( | ||||
|             determine_jobs, | ||||
|             "filter_component_and_test_files", | ||||
|             side_effect=lambda f: f.startswith("esphome/components/"), | ||||
|         ), | ||||
|         patch.object( | ||||
|             determine_jobs, | ||||
|             "get_components_with_dependencies", | ||||
|             side_effect=lambda files, deps: ( | ||||
|                 ["select", "api"] | ||||
|                 if not deps | ||||
|                 else ["select", "api", "bluetooth_proxy", "logger"] | ||||
|             ), | ||||
|         ), | ||||
|     ): | ||||
|         determine_jobs.main() | ||||
|  | ||||
|     captured = capsys.readouterr() | ||||
|     output = json.loads(captured.out) | ||||
|  | ||||
|     assert output["clang_tidy"] is True | ||||
|     assert output["clang_tidy_mode"] == "split" | ||||
|     assert "select" in output["changed_components"] | ||||
|     assert "api" in output["changed_components"] | ||||
|     assert len(output["changed_components"]) > 0 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user