From eb97781f684166d68872fe68db6a96db4aa000b8 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 20 Jun 2025 08:38:40 +0200 Subject: [PATCH 001/115] [nextion] Add command queuing to prevent command loss when spacing is active (#9139) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 56 +++++++++++++++++++ esphome/components/nextion/nextion.h | 31 ++++++++++ .../nextion/nextion_component_base.h | 3 + 3 files changed, 90 insertions(+) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index e6fee10173..1e14831515 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -325,8 +325,31 @@ void Nextion::loop() { this->nextion_reports_is_setup_ = true; } } + +#ifdef USE_NEXTION_COMMAND_SPACING + // Try to send any pending commands if spacing allows + this->process_pending_in_queue_(); +#endif // USE_NEXTION_COMMAND_SPACING } +#ifdef USE_NEXTION_COMMAND_SPACING +void Nextion::process_pending_in_queue_() { + if (this->nextion_queue_.empty() || !this->command_pacer_.can_send()) { + return; + } + + // Check if first item in queue has a pending command + auto *front_item = this->nextion_queue_.front(); + if (front_item && !front_item->pending_command.empty()) { + if (this->send_command_(front_item->pending_command)) { + // Command sent successfully, clear the pending command + front_item->pending_command.clear(); + ESP_LOGVV(TAG, "Pending command sent: %s", front_item->component->get_variable_name().c_str()); + } + } +} +#endif // USE_NEXTION_COMMAND_SPACING + bool Nextion::remove_from_q_(bool report_empty) { if (this->nextion_queue_.empty()) { if (report_empty) { @@ -1034,9 +1057,42 @@ void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_n if (this->send_command_(command)) { this->add_no_result_to_queue_(variable_name); +#ifdef USE_NEXTION_COMMAND_SPACING + } else { + // Command blocked by spacing, add to queue WITH the command for retry + this->add_no_result_to_queue_with_pending_command_(variable_name, command); +#endif // USE_NEXTION_COMMAND_SPACING } } +#ifdef USE_NEXTION_COMMAND_SPACING +void Nextion::add_no_result_to_queue_with_pending_command_(const std::string &variable_name, + const std::string &command) { +#ifdef USE_NEXTION_MAX_QUEUE_SIZE + if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) { + ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str()); + return; + } +#endif + + RAMAllocator allocator; + nextion::NextionQueue *nextion_queue = allocator.allocate(1); + if (nextion_queue == nullptr) { + ESP_LOGW(TAG, "Queue alloc failed"); + return; + } + new (nextion_queue) nextion::NextionQueue(); + + nextion_queue->component = new nextion::NextionComponentBase; + nextion_queue->component->set_variable_name(variable_name); + nextion_queue->queue_time = millis(); + nextion_queue->pending_command = command; // Store command for retry + + this->nextion_queue_.push_back(nextion_queue); + ESP_LOGVV(TAG, "Queue with pending command: %s", variable_name.c_str()); +} +#endif // USE_NEXTION_COMMAND_SPACING + bool Nextion::add_no_result_to_queue_with_ignore_sleep_printf_(const std::string &variable_name, const char *format, ...) { if ((!this->is_setup() && !this->ignore_is_setup_)) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 036fbe6c6d..0cd559d251 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1309,9 +1309,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe #ifdef USE_NEXTION_MAX_QUEUE_SIZE size_t max_queue_size_{0}; #endif // USE_NEXTION_MAX_QUEUE_SIZE + #ifdef USE_NEXTION_COMMAND_SPACING NextionCommandPacer command_pacer_{0}; + + /** + * @brief Process any commands in the queue that are pending due to command spacing + * + * This method checks if the first item in the nextion_queue_ has a pending command + * that was previously blocked by command spacing. If spacing now allows and a + * pending command exists, it attempts to send the command. Once successfully sent, + * the pending command is cleared and the queue item continues normal processing. + * + * Called from loop() to retry sending commands that were delayed by spacing. + */ + void process_pending_in_queue_(); #endif // USE_NEXTION_COMMAND_SPACING + std::deque nextion_queue_; std::deque waveform_queue_; uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); @@ -1348,6 +1362,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe __attribute__((format(printf, 3, 4))); void add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command); +#ifdef USE_NEXTION_COMMAND_SPACING + /** + * @brief Add a command to the Nextion queue with a pending command for retry + * + * This method creates a queue entry for a command that was blocked by command spacing. + * The command string is stored in the queue item's pending_command field so it can + * be retried later when spacing allows. This ensures commands are not lost when + * sent too quickly. + * + * If the max_queue_size limit is configured and reached, the command will be dropped. + * + * @param variable_name Name of the variable or component associated with the command + * @param command The actual command string to be sent when spacing allows + */ + void add_no_result_to_queue_with_pending_command_(const std::string &variable_name, const std::string &command); +#endif // USE_NEXTION_COMMAND_SPACING + bool add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) __attribute__((format(printf, 3, 4))); diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index 42e1b00998..fe0692b875 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -25,6 +25,9 @@ class NextionQueue { virtual ~NextionQueue() = default; NextionComponentBase *component; uint32_t queue_time = 0; + + // Store command for retry if spacing blocked it + std::string pending_command; // Empty if command was sent successfully }; class NextionComponentBase { From 7dbad424705ae3c7dafa477a2f815173c4359151 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:46:12 +0200 Subject: [PATCH 002/115] [nextion] Cached timing optimization (#9150) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Keith Burzinski --- esphome/components/nextion/nextion.cpp | 26 +++++++++---------- .../nextion/nextion_upload_arduino.cpp | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 1e14831515..4f08fcb393 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -71,13 +71,13 @@ bool Nextion::check_connect_() { } this->send_command_("connect"); - this->comok_sent_ = millis(); + this->comok_sent_ = App.get_loop_component_start_time(); this->ignore_is_setup_ = false; return false; } - if (millis() - this->comok_sent_ <= 500) // Wait 500 ms + if (App.get_loop_component_start_time() - this->comok_sent_ <= 500) // Wait 500 ms return false; std::string response; @@ -318,9 +318,9 @@ void Nextion::loop() { if (!this->nextion_reports_is_setup_) { if (this->started_ms_ == 0) - this->started_ms_ = millis(); + this->started_ms_ = App.get_loop_component_start_time(); - if (this->started_ms_ + this->startup_override_ms_ < millis()) { + if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) { ESP_LOGD(TAG, "Manual ready set"); this->nextion_reports_is_setup_ = true; } @@ -432,7 +432,7 @@ void Nextion::process_nextion_commands_() { case 0x01: // instruction sent by user was successful ESP_LOGVV(TAG, "Cmd OK"); - ESP_LOGN(TAG, "this->nextion_queue_.empty() %s", this->nextion_queue_.empty() ? "True" : "False"); + ESP_LOGN(TAG, "this->nextion_queue_.empty() %s", YESNO(this->nextion_queue_.empty())); this->remove_from_q_(); if (!this->is_setup_) { @@ -444,7 +444,7 @@ void Nextion::process_nextion_commands_() { } #ifdef USE_NEXTION_COMMAND_SPACING this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent - ESP_LOGN(TAG, "Command spacing: marked command sent at %u ms", millis()); + ESP_LOGN(TAG, "Command spacing: marked command sent"); #endif break; case 0x02: // invalid Component ID or name was used @@ -828,7 +828,7 @@ void Nextion::process_nextion_commands_() { this->command_data_.erase(0, to_process_length + COMMAND_DELIMITER.length() + 1); } - uint32_t ms = millis(); + uint32_t ms = App.get_loop_component_start_time(); if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { for (size_t i = 0; i < this->nextion_queue_.size(); i++) { @@ -967,9 +967,9 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool bool exit_flag = false; bool ff_flag = false; - start = millis(); + start = App.get_loop_component_start_time(); - while ((timeout == 0 && this->available()) || millis() - start <= timeout) { + while ((timeout == 0 && this->available()) || App.get_loop_component_start_time() - start <= timeout) { if (!this->available()) { App.feed_wdt(); delay(1); @@ -1038,7 +1038,7 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { nextion_queue->component = new nextion::NextionComponentBase; nextion_queue->component->set_variable_name(variable_name); - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); this->nextion_queue_.push_back(nextion_queue); @@ -1085,7 +1085,7 @@ void Nextion::add_no_result_to_queue_with_pending_command_(const std::string &va nextion_queue->component = new nextion::NextionComponentBase; nextion_queue->component->set_variable_name(variable_name); - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); nextion_queue->pending_command = command; // Store command for retry this->nextion_queue_.push_back(nextion_queue); @@ -1224,7 +1224,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { new (nextion_queue) nextion::NextionQueue(); nextion_queue->component = component; - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); ESP_LOGN(TAG, "Queue %s: %s", component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); @@ -1256,7 +1256,7 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { new (nextion_queue) nextion::NextionQueue(); nextion_queue->component = component; - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); this->waveform_queue_.push_back(nextion_queue); if (this->waveform_queue_.size() == 1) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index aa7350bb57..c2d0f2a22d 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -67,8 +67,8 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { ESP_LOGV(TAG, "Fetch %" PRIu16 " bytes", buffer_size); uint16_t read_len = 0; int partial_read_len = 0; - const uint32_t start_time = millis(); - while (read_len < buffer_size && millis() - start_time < 5000) { + const uint32_t start_time = App.get_loop_component_start_time(); + while (read_len < buffer_size && App.get_loop_component_start_time() - start_time < 5000) { if (http_client.getStreamPtr()->available() > 0) { partial_read_len = http_client.getStreamPtr()->readBytes(reinterpret_cast(buffer) + read_len, buffer_size - read_len); From 46d962dcf1ddff27ce925b9a483e511a95af18d5 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 20 Jun 2025 05:02:36 -0500 Subject: [PATCH 003/115] [wifi, wifi_info] Tidy up/shorten more log messages (#9151) --- esphome/components/wifi/wifi_component.cpp | 34 +++---- .../wifi/wifi_component_esp32_arduino.cpp | 82 ++++++++--------- .../wifi/wifi_component_esp8266.cpp | 91 +++++++++---------- .../wifi/wifi_component_esp_idf.cpp | 67 +++++++------- .../wifi/wifi_component_libretiny.cpp | 49 +++++----- .../components/wifi/wifi_component_pico_w.cpp | 6 +- .../wifi_info/wifi_info_text_sensor.cpp | 12 +-- 7 files changed, 168 insertions(+), 173 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index f2f9d712fc..51ae1c9f8e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -73,7 +73,7 @@ void WiFiComponent::start() { SavedWifiSettings save{}; if (this->pref_.load(&save)) { - ESP_LOGD(TAG, "Loaded saved settings: %s", save.ssid); + ESP_LOGD(TAG, "Loaded settings: %s", save.ssid); WiFiAP sta{}; sta.set_ssid(save.ssid); @@ -84,11 +84,11 @@ void WiFiComponent::start() { if (this->has_sta()) { this->wifi_sta_pre_setup_(); if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { - ESP_LOGV(TAG, "Setting Output Power Option failed!"); + ESP_LOGV(TAG, "Setting Output Power Option failed"); } if (!this->wifi_apply_power_save_()) { - ESP_LOGV(TAG, "Setting Power Save Option failed!"); + ESP_LOGV(TAG, "Setting Power Save Option failed"); } if (this->fast_connect_) { @@ -102,7 +102,7 @@ void WiFiComponent::start() { } else if (this->has_ap()) { this->setup_ap_config_(); if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { - ESP_LOGV(TAG, "Setting Output Power Option failed!"); + ESP_LOGV(TAG, "Setting Output Power Option failed"); } #ifdef USE_CAPTIVE_PORTAL if (captive_portal::global_captive_portal != nullptr) { @@ -181,7 +181,7 @@ void WiFiComponent::loop() { #ifdef USE_WIFI_AP if (this->has_ap() && !this->ap_setup_) { if (this->ap_timeout_ != 0 && (now - this->last_connected_ > this->ap_timeout_)) { - ESP_LOGI(TAG, "Starting fallback AP!"); + ESP_LOGI(TAG, "Starting fallback AP"); this->setup_ap_config_(); #ifdef USE_CAPTIVE_PORTAL if (captive_portal::global_captive_portal != nullptr) @@ -359,7 +359,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { if (ap.get_channel().has_value()) { ESP_LOGV(TAG, " Channel: %u", *ap.get_channel()); } else { - ESP_LOGV(TAG, " Channel: Not Set"); + ESP_LOGV(TAG, " Channel not set"); } if (ap.get_manual_ip().has_value()) { ManualIP m = *ap.get_manual_ip(); @@ -372,7 +372,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { #endif if (!this->wifi_sta_connect_(ap)) { - ESP_LOGE(TAG, "wifi_sta_connect_ failed!"); + ESP_LOGE(TAG, "wifi_sta_connect_ failed"); this->retry_connect(); return; } @@ -500,20 +500,20 @@ void WiFiComponent::start_scanning() { void WiFiComponent::check_scanning_finished() { if (!this->scan_done_) { if (millis() - this->action_started_ > 30000) { - ESP_LOGE(TAG, "Scan timeout!"); + ESP_LOGE(TAG, "Scan timeout"); this->retry_connect(); } return; } this->scan_done_ = false; - ESP_LOGD(TAG, "Found networks:"); if (this->scan_result_.empty()) { - ESP_LOGD(TAG, " No network found!"); + ESP_LOGW(TAG, "No networks found"); this->retry_connect(); return; } + ESP_LOGD(TAG, "Found networks:"); for (auto &res : this->scan_result_) { for (auto &ap : this->sta_) { if (res.matches(ap)) { @@ -561,7 +561,7 @@ void WiFiComponent::check_scanning_finished() { } if (!this->scan_result_[0].get_matches()) { - ESP_LOGW(TAG, "No matching network found!"); + ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); return; } @@ -619,7 +619,7 @@ void WiFiComponent::check_connecting_finished() { if (status == WiFiSTAConnectStatus::CONNECTED) { if (wifi_ssid().empty()) { - ESP_LOGW(TAG, "Incomplete connection."); + ESP_LOGW(TAG, "Connection incomplete"); this->retry_connect(); return; } @@ -663,7 +663,7 @@ void WiFiComponent::check_connecting_finished() { } if (this->error_from_callback_) { - ESP_LOGW(TAG, "Error while connecting to network."); + ESP_LOGW(TAG, "Connecting to network failed"); this->retry_connect(); return; } @@ -679,7 +679,7 @@ void WiFiComponent::check_connecting_finished() { } if (status == WiFiSTAConnectStatus::ERROR_CONNECT_FAILED) { - ESP_LOGW(TAG, "Connection failed. Check credentials"); + ESP_LOGW(TAG, "Connecting to network failed"); this->retry_connect(); return; } @@ -700,7 +700,7 @@ void WiFiComponent::retry_connect() { (this->num_retried_ > 3 || this->error_from_callback_)) { if (this->num_retried_ > 5) { // If retry failed for more than 5 times, let's restart STA - ESP_LOGW(TAG, "Restarting WiFi adapter"); + ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); delay(100); // NOLINT this->num_retried_ = 0; @@ -770,7 +770,7 @@ void WiFiComponent::load_fast_connect_settings_() { this->selected_ap_.set_bssid(bssid); this->selected_ap_.set_channel(fast_connect_save.channel); - ESP_LOGD(TAG, "Loaded saved fast_connect wifi settings"); + ESP_LOGD(TAG, "Loaded fast_connect settings"); } } @@ -786,7 +786,7 @@ void WiFiComponent::save_fast_connect_settings_() { this->fast_connect_pref_.save(&fast_connect_save); - ESP_LOGD(TAG, "Saved fast_connect wifi settings"); + ESP_LOGD(TAG, "Saved fast_connect settings"); } } diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index d9e45242a8..3fc2c009db 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -78,14 +78,14 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return true; if (set_sta && !current_sta) { - ESP_LOGV(TAG, "Enabling STA."); + ESP_LOGV(TAG, "Enabling STA"); } else if (!set_sta && current_sta) { - ESP_LOGV(TAG, "Disabling STA."); + ESP_LOGV(TAG, "Disabling STA"); } if (set_ap && !current_ap) { - ESP_LOGV(TAG, "Enabling AP."); + ESP_LOGV(TAG, "Enabling AP"); } else if (!set_ap && current_ap) { - ESP_LOGV(TAG, "Disabling AP."); + ESP_LOGV(TAG, "Disabling AP"); } bool ret = WiFiClass::mode(set_mode); @@ -147,11 +147,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) { - ESP_LOGE(TAG, "SSID is too long"); + ESP_LOGE(TAG, "SSID too long"); return false; } if (ap.get_password().size() > sizeof(conf.sta.password)) { - ESP_LOGE(TAG, "password is too long"); + ESP_LOGE(TAG, "Password too long"); return false; } memcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); @@ -230,7 +230,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { EAPAuth eap = ap.get_eap().value(); err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_identity failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_identity failed: %d", err); } int ca_cert_len = strlen(eap.ca_cert); int client_cert_len = strlen(eap.client_cert); @@ -238,7 +238,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ca_cert_len) { err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_ca_cert failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_ca_cert failed: %d", err); } } // workout what type of EAP this is @@ -249,22 +249,22 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { (uint8_t *) eap.client_key, client_key_len + 1, (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_certificate_and_key failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_certificate_and_key failed: %d", err); } } else { // in the absence of certs, assume this is username/password based err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_username failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_username failed: %d", err); } err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_eap_client_set_password failed! %d", err); + ESP_LOGV(TAG, "esp_eap_client_set_password failed: %d", err); } } err = esp_wifi_sta_enterprise_enable(); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_enterprise_enable failed! %d", err); + ESP_LOGV(TAG, "esp_wifi_sta_enterprise_enable failed: %d", err); } } #endif // USE_WIFI_WPA2_EAP @@ -319,7 +319,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { if (dhcp_status != ESP_NETIF_DHCP_STARTED) { err = esp_netif_dhcpc_start(s_sta_netif); if (err != ESP_OK) { - ESP_LOGV(TAG, "Starting DHCP client failed! %d", err); + ESP_LOGV(TAG, "Starting DHCP client failed: %d", err); } return err == ESP_OK; } @@ -332,12 +332,12 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { info.netmask = manual_ip->subnet; err = esp_netif_dhcpc_stop(s_sta_netif); if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { - ESP_LOGV(TAG, "Stopping DHCP client failed! %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err)); } err = esp_netif_set_ip_info(s_sta_netif, &info); if (err != ESP_OK) { - ESP_LOGV(TAG, "Setting manual IP info failed! %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "Setting manual IP info failed: %s", esp_err_to_name(err)); } esp_netif_dns_info_t dns; @@ -520,18 +520,18 @@ using esphome_wifi_event_info_t = arduino_event_info_t; void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) { switch (event) { case ESPHOME_EVENT_ID_WIFI_READY: { - ESP_LOGV(TAG, "Event: WiFi ready"); + ESP_LOGV(TAG, "Ready"); break; } case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: { auto it = info.wifi_scan_done; - ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); + ESP_LOGV(TAG, "Scan done: status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); this->wifi_scan_done_callback_(); break; } case ESPHOME_EVENT_ID_WIFI_STA_START: { - ESP_LOGV(TAG, "Event: WiFi STA start"); + ESP_LOGV(TAG, "STA start"); // apply hostname s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); esp_err_t err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str()); @@ -541,7 +541,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ break; } case ESPHOME_EVENT_ID_WIFI_STA_STOP: { - ESP_LOGV(TAG, "Event: WiFi STA stop"); + ESP_LOGV(TAG, "STA stop"); break; } case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { @@ -549,7 +549,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, + ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); #if USE_NETWORK_IPV6 this->set_timeout(100, [] { WiFi.enableIPv6(); }); @@ -563,9 +563,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; if (it.reason == WIFI_REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); } @@ -585,8 +585,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { auto it = info.wifi_sta_authmode_change; - ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), - get_auth_mode_str(it.new_mode)); + ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); // Mitigate CVE-2020-12638 // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) { @@ -603,8 +602,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: { auto it = info.got_ip.ip_info; - ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), - format_ip4_addr(it.gw).c_str()); + ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), format_ip4_addr(it.gw).c_str()); this->got_ipv4_address_ = true; #if USE_NETWORK_IPV6 s_sta_connecting = this->num_ipv6_addresses_ < USE_NETWORK_MIN_IPV6_ADDR_COUNT; @@ -616,44 +614,44 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ #if USE_NETWORK_IPV6 case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { auto it = info.got_ip6.ip6_info; - ESP_LOGV(TAG, "Got IPv6 address=" IPV6STR, IPV62STR(it.ip)); + ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip)); this->num_ipv6_addresses_++; s_sta_connecting = !(this->got_ipv4_address_ & (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT)); break; } #endif /* USE_NETWORK_IPV6 */ case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { - ESP_LOGV(TAG, "Event: Lost IP"); + ESP_LOGV(TAG, "Lost IP"); this->got_ipv4_address_ = false; break; } case ESPHOME_EVENT_ID_WIFI_AP_START: { - ESP_LOGV(TAG, "Event: WiFi AP start"); + ESP_LOGV(TAG, "AP start"); break; } case ESPHOME_EVENT_ID_WIFI_AP_STOP: { - ESP_LOGV(TAG, "Event: WiFi AP stop"); + ESP_LOGV(TAG, "AP stop"); break; } case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { auto it = info.wifi_sta_connected; auto &mac = it.bssid; - ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { auto it = info.wifi_sta_disconnected; auto &mac = it.bssid; - ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: { - ESP_LOGV(TAG, "Event: AP client assigned IP"); + ESP_LOGV(TAG, "AP client assigned IP"); break; } case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { auto it = info.wifi_ap_probereqrecved; - ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } default: @@ -685,7 +683,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { // need to use WiFi because of WiFiScanClass allocations :( int16_t err = WiFi.scanNetworks(true, true, passive, 200); if (err != WIFI_SCAN_RUNNING) { - ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err); + ESP_LOGV(TAG, "WiFi.scanNetworks failed: %d", err); return false; } @@ -741,7 +739,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { err = esp_netif_set_ip_info(s_ap_netif, &info); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_set_ip_info failed! %d", err); + ESP_LOGE(TAG, "esp_netif_set_ip_info failed: %d", err); return false; } @@ -757,14 +755,14 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_dhcps_option failed! %d", err); + ESP_LOGE(TAG, "esp_netif_dhcps_option failed: %d", err); return false; } err = esp_netif_dhcps_start(s_ap_netif); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_dhcps_start failed! %d", err); + ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err); return false; } @@ -779,7 +777,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) { - ESP_LOGE(TAG, "AP SSID is too long"); + ESP_LOGE(TAG, "AP SSID too long"); return false; } memcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); @@ -794,7 +792,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; if (ap.get_password().size() > sizeof(conf.ap.password)) { - ESP_LOGE(TAG, "AP password is too long"); + ESP_LOGE(TAG, "AP password too long"); return false; } memcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), ap.get_password().size()); @@ -805,14 +803,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf); if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err); + ESP_LOGV(TAG, "esp_wifi_set_config failed: %d", err); return false; } yield(); if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { - ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 3e121098e7..594bc79e54 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -59,17 +59,17 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return true; if (target_sta && !current_sta) { - ESP_LOGV(TAG, "Enabling STA."); + ESP_LOGV(TAG, "Enabling STA"); } else if (!target_sta && current_sta) { - ESP_LOGV(TAG, "Disabling STA."); + ESP_LOGV(TAG, "Disabling STA"); // Stop DHCP client when disabling STA // See https://github.com/esp8266/Arduino/pull/5703 wifi_station_dhcpc_stop(); } if (target_ap && !current_ap) { - ESP_LOGV(TAG, "Enabling AP."); + ESP_LOGV(TAG, "Enabling AP"); } else if (!target_ap && current_ap) { - ESP_LOGV(TAG, "Disabling AP."); + ESP_LOGV(TAG, "Disabling AP"); } ETS_UART_INTR_DISABLE(); @@ -82,7 +82,7 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { ETS_UART_INTR_ENABLE(); if (!ret) { - ESP_LOGW(TAG, "Setting WiFi mode failed!"); + ESP_LOGW(TAG, "Set mode failed"); } return ret; @@ -133,7 +133,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { if (dhcp_status != DHCP_STARTED) { bool ret = wifi_station_dhcpc_start(); if (!ret) { - ESP_LOGV(TAG, "Starting DHCP client failed!"); + ESP_LOGV(TAG, "Starting DHCP client failed"); } return ret; } @@ -157,13 +157,13 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { if (dhcp_status == DHCP_STARTED) { bool dhcp_stop_ret = wifi_station_dhcpc_stop(); if (!dhcp_stop_ret) { - ESP_LOGV(TAG, "Stopping DHCP client failed!"); + ESP_LOGV(TAG, "Stopping DHCP client failed"); ret = false; } } bool wifi_set_info_ret = wifi_set_ip_info(STATION_IF, &info); if (!wifi_set_info_ret) { - ESP_LOGV(TAG, "Setting manual IP info failed!"); + ESP_LOGV(TAG, "Set manual IP info failed"); ret = false; } @@ -202,7 +202,7 @@ bool WiFiComponent::wifi_apply_hostname_() { const std::string &hostname = App.get_name(); bool ret = wifi_station_set_hostname(const_cast(hostname.c_str())); if (!ret) { - ESP_LOGV(TAG, "Setting WiFi Hostname failed!"); + ESP_LOGV(TAG, "Set hostname failed"); } // inform dhcp server of hostname change using dhcp_renew() @@ -237,11 +237,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { struct station_config conf {}; memset(&conf, 0, sizeof(conf)); if (ap.get_ssid().size() > sizeof(conf.ssid)) { - ESP_LOGE(TAG, "SSID is too long"); + ESP_LOGE(TAG, "SSID too long"); return false; } if (ap.get_password().size() > sizeof(conf.password)) { - ESP_LOGE(TAG, "password is too long"); + ESP_LOGE(TAG, "Password too long"); return false; } memcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); @@ -269,7 +269,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ETS_UART_INTR_ENABLE(); if (!ret) { - ESP_LOGV(TAG, "Setting WiFi Station config failed!"); + ESP_LOGV(TAG, "Set Station config failed"); return false; } @@ -284,7 +284,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { EAPAuth eap = ap.get_eap().value(); ret = wifi_station_set_enterprise_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); if (ret) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", ret); + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed: %d", ret); } int ca_cert_len = strlen(eap.ca_cert); int client_cert_len = strlen(eap.client_cert); @@ -292,7 +292,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ca_cert_len) { ret = wifi_station_set_enterprise_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); if (ret) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", ret); + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed: %d", ret); } } // workout what type of EAP this is @@ -303,22 +303,22 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { (uint8_t *) eap.client_key, client_key_len + 1, (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); if (ret) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", ret); + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed: %d", ret); } } else { // in the absence of certs, assume this is username/password based ret = wifi_station_set_enterprise_username((uint8_t *) eap.username.c_str(), eap.username.length()); if (ret) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", ret); + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed: %d", ret); } ret = wifi_station_set_enterprise_password((uint8_t *) eap.password.c_str(), eap.password.length()); if (ret) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", ret); + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed: %d", ret); } } ret = wifi_station_set_wpa2_enterprise_auth(true); if (ret) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", ret); + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed: %d", ret); } } #endif // USE_WIFI_WPA2_EAP @@ -337,7 +337,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ret = wifi_station_connect(); ETS_UART_INTR_ENABLE(); if (!ret) { - ESP_LOGV(TAG, "wifi_station_connect failed!"); + ESP_LOGV(TAG, "wifi_station_connect failed"); return false; } @@ -359,7 +359,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ap.get_channel().has_value()) { ret = wifi_set_channel(*ap.get_channel()); if (!ret) { - ESP_LOGV(TAG, "wifi_set_channel failed!"); + ESP_LOGV(TAG, "wifi_set_channel failed"); return false; } } @@ -496,8 +496,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_addr(it.bssid).c_str(), - it.channel); + ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_addr(it.bssid).c_str(), it.channel); s_sta_connected = true; break; } @@ -507,10 +506,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; if (it.reason == REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); s_sta_connect_not_found = true; } else { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, format_mac_addr(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); s_sta_connect_error = true; } @@ -520,7 +519,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } case EVENT_STAMODE_AUTHMODE_CHANGE: { auto it = event->event_info.auth_change; - ESP_LOGV(TAG, "Event: Changed AuthMode old=%s new=%s", LOG_STR_ARG(get_auth_mode_str(it.old_mode)), + ESP_LOGV(TAG, "Changed Authmode old=%s new=%s", LOG_STR_ARG(get_auth_mode_str(it.old_mode)), LOG_STR_ARG(get_auth_mode_str(it.new_mode))); // Mitigate CVE-2020-12638 // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors @@ -535,40 +534,40 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } case EVENT_STAMODE_GOT_IP: { auto it = event->event_info.got_ip; - ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), - format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str()); + ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(), + format_ip_addr(it.mask).c_str()); s_sta_got_ip = true; break; } case EVENT_STAMODE_DHCP_TIMEOUT: { - ESP_LOGW(TAG, "Event: Getting IP address timeout"); + ESP_LOGW(TAG, "DHCP request timeout"); break; } case EVENT_SOFTAPMODE_STACONNECTED: { auto it = event->event_info.sta_connected; - ESP_LOGV(TAG, "Event: AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); break; } case EVENT_SOFTAPMODE_STADISCONNECTED: { auto it = event->event_info.sta_disconnected; - ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); break; } case EVENT_SOFTAPMODE_PROBEREQRECVED: { auto it = event->event_info.ap_probereqrecved; - ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) case EVENT_OPMODE_CHANGED: { auto it = event->event_info.opmode_changed; - ESP_LOGV(TAG, "Event: Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)), + ESP_LOGV(TAG, "Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)), LOG_STR_ARG(get_op_mode_str(it.new_opmode))); break; } case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: { auto it = event->event_info.distribute_sta_ip; - ESP_LOGV(TAG, "Event: AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_addr(it.mac).c_str(), + ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_addr(it.mac).c_str(), format_ip_addr(it.ip).c_str(), it.aid); break; } @@ -600,7 +599,7 @@ bool WiFiComponent::wifi_sta_pre_setup_() { ETS_UART_INTR_ENABLE(); if (!ret1 || !ret2) { - ESP_LOGV(TAG, "Disabling Auto-Connect failed!"); + ESP_LOGV(TAG, "Disabling Auto-Connect failed"); } delay(10); @@ -666,7 +665,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { first_scan = false; bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); if (!ret) { - ESP_LOGV(TAG, "wifi_station_scan failed!"); + ESP_LOGV(TAG, "wifi_station_scan failed"); return false; } @@ -692,7 +691,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { this->scan_result_.clear(); if (status != OK) { - ESP_LOGV(TAG, "Scan failed! %d", status); + ESP_LOGV(TAG, "Scan failed: %d", status); this->retry_connect(); return; } @@ -725,12 +724,12 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { if (wifi_softap_dhcps_status() == DHCP_STARTED) { if (!wifi_softap_dhcps_stop()) { - ESP_LOGW(TAG, "Stopping DHCP server failed!"); + ESP_LOGW(TAG, "Stopping DHCP server failed"); } } if (!wifi_set_ip_info(SOFTAP_IF, &info)) { - ESP_LOGE(TAG, "Setting SoftAP info failed!"); + ESP_LOGE(TAG, "Set SoftAP info failed"); return false; } @@ -748,13 +747,13 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { lease.end_ip = start_address; ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str()); if (!wifi_softap_set_dhcps_lease(&lease)) { - ESP_LOGE(TAG, "Setting SoftAP DHCP lease failed!"); + ESP_LOGE(TAG, "Set SoftAP DHCP lease failed"); return false; } // lease time 1440 minutes (=24 hours) if (!wifi_softap_set_dhcps_lease_time(1440)) { - ESP_LOGE(TAG, "Setting SoftAP DHCP lease time failed!"); + ESP_LOGE(TAG, "Set SoftAP DHCP lease time failed"); return false; } @@ -764,13 +763,13 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { uint8_t mode = 1; // bit0, 1 enables router information from ESP8266 SoftAP DHCP server. if (!wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &mode)) { - ESP_LOGE(TAG, "wifi_softap_set_dhcps_offer_option failed!"); + ESP_LOGE(TAG, "wifi_softap_set_dhcps_offer_option failed"); return false; } #endif if (!wifi_softap_dhcps_start()) { - ESP_LOGE(TAG, "Starting SoftAP DHCPS failed!"); + ESP_LOGE(TAG, "Starting SoftAP DHCPS failed"); return false; } @@ -784,7 +783,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { struct softap_config conf {}; if (ap.get_ssid().size() > sizeof(conf.ssid)) { - ESP_LOGE(TAG, "AP SSID is too long"); + ESP_LOGE(TAG, "AP SSID too long"); return false; } memcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); @@ -800,7 +799,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } else { conf.authmode = AUTH_WPA2_PSK; if (ap.get_password().size() > sizeof(conf.password)) { - ESP_LOGE(TAG, "AP password is too long"); + ESP_LOGE(TAG, "AP password too long"); return false; } memcpy(reinterpret_cast(conf.password), ap.get_password().c_str(), ap.get_password().size()); @@ -811,12 +810,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { ETS_UART_INTR_ENABLE(); if (!ret) { - ESP_LOGV(TAG, "wifi_softap_set_config_current failed!"); + ESP_LOGV(TAG, "wifi_softap_set_config_current failed"); return false; } if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { - ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 1af271345f..e767e7ffc1 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -219,14 +219,14 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return true; if (set_sta && !current_sta) { - ESP_LOGV(TAG, "Enabling STA."); + ESP_LOGV(TAG, "Enabling STA"); } else if (!set_sta && current_sta) { - ESP_LOGV(TAG, "Disabling STA."); + ESP_LOGV(TAG, "Disabling STA"); } if (set_ap && !current_ap) { - ESP_LOGV(TAG, "Enabling AP."); + ESP_LOGV(TAG, "Enabling AP"); } else if (!set_ap && current_ap) { - ESP_LOGV(TAG, "Disabling AP."); + ESP_LOGV(TAG, "Disabling AP"); } if (set_mode == WIFI_MODE_NULL && s_wifi_started) { @@ -290,11 +290,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) { - ESP_LOGE(TAG, "SSID is too long"); + ESP_LOGE(TAG, "SSID too long"); return false; } if (ap.get_password().size() > sizeof(conf.sta.password)) { - ESP_LOGE(TAG, "password is too long"); + ESP_LOGE(TAG, "Password too long"); return false; } memcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); @@ -490,7 +490,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { if (dhcp_status != ESP_NETIF_DHCP_STARTED) { err = esp_netif_dhcpc_start(s_sta_netif); if (err != ESP_OK) { - ESP_LOGV(TAG, "Starting DHCP client failed! %d", err); + ESP_LOGV(TAG, "Starting DHCP client failed: %d", err); } return err == ESP_OK; } @@ -503,12 +503,12 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { info.netmask = manual_ip->subnet; err = esp_netif_dhcpc_stop(s_sta_netif); if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { - ESP_LOGV(TAG, "Stopping DHCP client failed! %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err)); } err = esp_netif_set_ip_info(s_sta_netif, &info); if (err != ESP_OK) { - ESP_LOGV(TAG, "Setting manual IP info failed! %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "Setting manual IP info failed: %s", esp_err_to_name(err)); } esp_netif_dns_info_t dns; @@ -665,7 +665,7 @@ void WiFiComponent::wifi_loop_() { void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { esp_err_t err; if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) { - ESP_LOGV(TAG, "Event: WiFi STA start"); + ESP_LOGV(TAG, "STA start"); // apply hostname err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str()); if (err != ERR_OK) { @@ -677,13 +677,12 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { wifi_apply_power_save_(); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { - ESP_LOGV(TAG, "Event: WiFi STA stop"); + ESP_LOGV(TAG, "STA stop"); s_sta_started = false; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) { const auto &it = data->data.sta_authmode_change; - ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), - get_auth_mode_str(it.new_mode)); + ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_CONNECTED) { const auto &it = data->data.sta_connected; @@ -691,7 +690,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { assert(it.ssid_len <= 32); memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, + ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); s_sta_connected = true; @@ -702,13 +701,13 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; if (it.reason == WIFI_REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); s_sta_connect_not_found = true; } else if (it.reason == WIFI_REASON_ROAMING) { - ESP_LOGI(TAG, "Event: Disconnected ssid='%s' reason='Station Roaming'", buf); + ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf); return; } else { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); s_sta_connect_error = true; } @@ -721,24 +720,24 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #if USE_NETWORK_IPV6 esp_netif_create_ip6_linklocal(s_sta_netif); #endif /* USE_NETWORK_IPV6 */ - ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), + ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), format_ip4_addr(it.ip_info.gw).c_str()); this->got_ipv4_address_ = true; #if USE_NETWORK_IPV6 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { const auto &it = data->data.ip_got_ip6; - ESP_LOGV(TAG, "Event: Got IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); + ESP_LOGV(TAG, "IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); this->num_ipv6_addresses_++; #endif /* USE_NETWORK_IPV6 */ } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) { - ESP_LOGV(TAG, "Event: Lost IP"); + ESP_LOGV(TAG, "Lost IP"); this->got_ipv4_address_ = false; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_SCAN_DONE) { const auto &it = data->data.sta_scan_done; - ESP_LOGV(TAG, "Event: WiFi Scan Done status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); + ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); scan_result_.clear(); this->scan_done_ = true; @@ -772,28 +771,28 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { - ESP_LOGV(TAG, "Event: WiFi AP start"); + ESP_LOGV(TAG, "AP start"); s_ap_started = true; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STOP) { - ESP_LOGV(TAG, "Event: WiFi AP stop"); + ESP_LOGV(TAG, "AP stop"); s_ap_started = false; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) { const auto &it = data->data.ap_probe_req_rx; - ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STACONNECTED) { const auto &it = data->data.ap_staconnected; - ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(it.mac).c_str()); + ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(it.mac).c_str()); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STADISCONNECTED) { const auto &it = data->data.ap_stadisconnected; - ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(it.mac).c_str()); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(it.mac).c_str()); } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) { const auto &it = data->data.ip_ap_staipassigned; - ESP_LOGV(TAG, "Event: AP client assigned IP %s", format_ip4_addr(it.ip).c_str()); + ESP_LOGV(TAG, "AP client assigned IP %s", format_ip4_addr(it.ip).c_str()); } } @@ -873,7 +872,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { err = esp_netif_set_ip_info(s_ap_netif, &info); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_set_ip_info failed! %d", err); + ESP_LOGE(TAG, "esp_netif_set_ip_info failed: %d", err); return false; } @@ -889,14 +888,14 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_dhcps_option failed! %d", err); + ESP_LOGE(TAG, "esp_netif_dhcps_option failed: %d", err); return false; } err = esp_netif_dhcps_start(s_ap_netif); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_netif_dhcps_start failed! %d", err); + ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err); return false; } @@ -911,7 +910,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) { - ESP_LOGE(TAG, "AP SSID is too long"); + ESP_LOGE(TAG, "AP SSID too long"); return false; } memcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); @@ -926,7 +925,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; if (ap.get_password().size() > sizeof(conf.ap.password)) { - ESP_LOGE(TAG, "AP password is too long"); + ESP_LOGE(TAG, "AP password too long"); return false; } memcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), ap.get_password().size()); @@ -937,12 +936,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_wifi_set_config failed! %d", err); + ESP_LOGE(TAG, "esp_wifi_set_config failed: %d", err); return false; } if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { - ESP_LOGE(TAG, "wifi_ap_ip_config_ failed!"); + ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:"); return false; } diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index eb88ed81ad..0f7b688290 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -32,14 +32,14 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return true; if (enable_sta && !current_sta) { - ESP_LOGV(TAG, "Enabling STA."); + ESP_LOGV(TAG, "Enabling STA"); } else if (!enable_sta && current_sta) { - ESP_LOGV(TAG, "Disabling STA."); + ESP_LOGV(TAG, "Disabling STA"); } if (enable_ap && !current_ap) { - ESP_LOGV(TAG, "Enabling AP."); + ESP_LOGV(TAG, "Enabling AP"); } else if (!enable_ap && current_ap) { - ESP_LOGV(TAG, "Disabling AP."); + ESP_LOGV(TAG, "Disabling AP"); } uint8_t mode = 0; @@ -124,7 +124,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ap.get_channel().has_value() ? *ap.get_channel() : 0, ap.get_bssid().has_value() ? ap.get_bssid()->data() : NULL); if (status != WL_CONNECTED) { - ESP_LOGW(TAG, "esp_wifi_connect failed! %d", status); + ESP_LOGW(TAG, "esp_wifi_connect failed: %d", status); return false; } @@ -256,23 +256,23 @@ using esphome_wifi_event_info_t = arduino_event_info_t; void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) { switch (event) { case ESPHOME_EVENT_ID_WIFI_READY: { - ESP_LOGV(TAG, "Event: WiFi ready"); + ESP_LOGV(TAG, "Ready"); break; } case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: { auto it = info.wifi_scan_done; - ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); + ESP_LOGV(TAG, "Scan done: status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); this->wifi_scan_done_callback_(); break; } case ESPHOME_EVENT_ID_WIFI_STA_START: { - ESP_LOGV(TAG, "Event: WiFi STA start"); + ESP_LOGV(TAG, "STA start"); WiFi.setHostname(App.get_name().c_str()); break; } case ESPHOME_EVENT_ID_WIFI_STA_STOP: { - ESP_LOGV(TAG, "Event: WiFi STA stop"); + ESP_LOGV(TAG, "STA stop"); break; } case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { @@ -280,7 +280,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, + ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); break; @@ -291,9 +291,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; if (it.reason == WIFI_REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { - ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); } @@ -310,8 +310,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { auto it = info.wifi_sta_authmode_change; - ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), - get_auth_mode_str(it.new_mode)); + ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); // Mitigate CVE-2020-12638 // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) { @@ -325,47 +324,47 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: { // auto it = info.got_ip.ip_info; - ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), + ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), format_ip4_addr(WiFi.gatewayIP()).c_str()); s_sta_connecting = false; break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { // auto it = info.got_ip.ip_info; - ESP_LOGV(TAG, "Event: Got IPv6"); + ESP_LOGV(TAG, "Got IPv6"); break; } case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { - ESP_LOGV(TAG, "Event: Lost IP"); + ESP_LOGV(TAG, "Lost IP"); break; } case ESPHOME_EVENT_ID_WIFI_AP_START: { - ESP_LOGV(TAG, "Event: WiFi AP start"); + ESP_LOGV(TAG, "AP start"); break; } case ESPHOME_EVENT_ID_WIFI_AP_STOP: { - ESP_LOGV(TAG, "Event: WiFi AP stop"); + ESP_LOGV(TAG, "AP stop"); break; } case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { auto it = info.wifi_sta_connected; auto &mac = it.bssid; - ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { auto it = info.wifi_sta_disconnected; auto &mac = it.bssid; - ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: { - ESP_LOGV(TAG, "Event: AP client assigned IP"); + ESP_LOGV(TAG, "AP client assigned IP"); break; } case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { auto it = info.wifi_ap_probereqrecved; - ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } default: @@ -399,7 +398,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { // need to use WiFi because of WiFiScanClass allocations :( int16_t err = WiFi.scanNetworks(true, true, passive, 200); if (err != WIFI_SCAN_RUNNING) { - ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err); + ESP_LOGV(TAG, "WiFi.scanNetworks failed: %d", err); return false; } @@ -447,7 +446,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { - ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 23fd766abe..bf15892cd5 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -134,7 +134,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { scan_options.scan_type = passive ? 1 : 0; int err = cyw43_wifi_scan(&cyw43_state, &scan_options, nullptr, &s_wifi_scan_result); if (err) { - ESP_LOGV(TAG, "cyw43_wifi_scan failed!"); + ESP_LOGV(TAG, "cyw43_wifi_scan failed"); } return err == 0; return true; @@ -162,7 +162,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { if (!this->wifi_mode_({}, true)) return false; if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { - ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } @@ -209,7 +209,7 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { void WiFiComponent::wifi_loop_() { if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; - ESP_LOGV(TAG, "Scan done!"); + ESP_LOGV(TAG, "Scan done"); } } diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 150c7229f8..2612e4af8d 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -7,12 +7,12 @@ namespace wifi_info { static const char *const TAG = "wifi_info"; -void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo IPAddress", this); } -void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Scan Results", this); } -void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); } -void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); } -void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Mac Address", this); } -void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo DNS Address", this); } +void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); } +void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } +void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } +void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } +void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); } +void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); } } // namespace wifi_info } // namespace esphome From 3e98cceb007179e695a22fe13d044b3bef402c7d Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 20 Jun 2025 05:33:46 -0500 Subject: [PATCH 004/115] [bh1750] Remove redundant platform name from logging (#9153) --- esphome/components/bh1750/bh1750.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 4b51794907..267a728fdd 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -50,7 +50,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< // turn on (after one-shot sensor automatically powers down) uint8_t turn_on = BH1750_COMMAND_POWER_ON; if (this->write(&turn_on, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Turning on BH1750 failed"); + ESP_LOGW(TAG, "Power on failed"); f(NAN); return; } @@ -60,7 +60,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111); uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111); if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Setting measurement time for BH1750 failed"); + ESP_LOGW(TAG, "Set measurement time failed"); active_mtreg_ = 0; f(NAN); return; @@ -88,7 +88,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< return; } if (this->write(&cmd, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Starting measurement for BH1750 failed"); + ESP_LOGW(TAG, "Start measurement failed"); f(NAN); return; } @@ -99,7 +99,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< this->set_timeout("read", meas_time, [this, mode, mtreg, f]() { uint16_t raw_value; if (this->read(reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Reading BH1750 data failed"); + ESP_LOGW(TAG, "Read data failed"); f(NAN); return; } @@ -156,7 +156,7 @@ void BH1750Sensor::update() { this->publish_state(NAN); return; } - ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), val); + ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val); this->status_clear_warning(); this->publish_state(val); }); From b693b8ccb1a5fafdcbb33e96771cd0e761cc1b05 Mon Sep 17 00:00:00 2001 From: RoganDawes Date: Fri, 20 Jun 2025 14:03:15 +0200 Subject: [PATCH 005/115] [usb-host] Add support for USB Hubs (#9154) --- esphome/components/usb_host/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index b6ca779706..3204562dc8 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -19,6 +19,7 @@ USBClient = usb_host_ns.class_("USBClient", Component) CONF_DEVICES = "devices" CONF_VID = "vid" CONF_PID = "pid" +CONF_ENABLE_HUBS = "enable_hubs" def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.Schema: @@ -42,6 +43,7 @@ CONFIG_SCHEMA = cv.All( cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(USBHost), + cv.Optional(CONF_ENABLE_HUBS, default=False): cv.boolean, cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()), } ), @@ -58,6 +60,8 @@ async def register_usb_client(config): async def to_code(config): add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024) + if config.get(CONF_ENABLE_HUBS): + add_idf_sdkconfig_option("CONFIG_USB_HOST_HUBS_SUPPORTED", True) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for device in config.get(CONF_DEVICES) or (): From 169db9cc0acc4adbc61e77fec62828661ffe7b3c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 21 Jun 2025 07:55:08 +1000 Subject: [PATCH 006/115] [spi] Enable >6 devices with ESP-IDF (#9128) --- esphome/components/mipi_spi/display.py | 1 + esphome/components/spi/__init__.py | 16 +++-- esphome/components/spi/spi.cpp | 5 +- esphome/components/spi/spi.h | 12 +++- esphome/components/spi/spi_arduino.cpp | 8 +-- esphome/components/spi/spi_esp_idf.cpp | 61 ++++++++++++------- tests/components/spi_device/common.yaml | 8 +-- .../components/spi_device/test.esp32-idf.yaml | 5 ++ 8 files changed, 75 insertions(+), 41 deletions(-) diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index e9ed97a2a2..061257e859 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -472,3 +472,4 @@ async def to_code(config): cg.add(var.set_writer(lambda_)) await display.register_display(var, config) await spi.register_spi_device(var, config) + cg.add(var.set_write_only(True)) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index ffb5e11f79..55a4b9c8f6 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -79,6 +79,7 @@ CONF_SPI_MODE = "spi_mode" CONF_FORCE_SW = "force_sw" CONF_INTERFACE = "interface" CONF_INTERFACE_INDEX = "interface_index" +CONF_RELEASE_DEVICE = "release_device" TYPE_SINGLE = "single" TYPE_QUAD = "quad" TYPE_OCTAL = "octal" @@ -378,6 +379,7 @@ def spi_device_schema( cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum( SPI_MODE_OPTIONS, upper=True ), + cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_with_esp_idf), } if cs_pin_required: schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema @@ -389,13 +391,15 @@ def spi_device_schema( async def register_spi_device(var, config): parent = await cg.get_variable(config[CONF_SPI_ID]) cg.add(var.set_spi_parent(parent)) - if CONF_CS_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_CS_PIN]) + if cs_pin := config.get(CONF_CS_PIN): + pin = await cg.gpio_pin_expression(cs_pin) cg.add(var.set_cs_pin(pin)) - if CONF_DATA_RATE in config: - cg.add(var.set_data_rate(config[CONF_DATA_RATE])) - if CONF_SPI_MODE in config: - cg.add(var.set_mode(config[CONF_SPI_MODE])) + if data_rate := config.get(CONF_DATA_RATE): + cg.add(var.set_data_rate(data_rate)) + if spi_mode := config.get(CONF_SPI_MODE): + cg.add(var.set_mode(spi_mode)) + if release_device := config.get(CONF_RELEASE_DEVICE): + cg.add(var.set_release_device(release_device)) def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso: bool): diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 76d9d8ae86..805a774ceb 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -16,12 +16,13 @@ bool SPIDelegate::is_ready() { return true; } GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) SPIDelegate *SPIComponent::register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate, - GPIOPin *cs_pin) { + GPIOPin *cs_pin, bool release_device, bool write_only) { if (this->devices_.count(device) != 0) { ESP_LOGE(TAG, "Device already registered"); return this->devices_[device]; } - SPIDelegate *delegate = this->spi_bus_->get_delegate(data_rate, bit_order, mode, cs_pin); // NOLINT + SPIDelegate *delegate = + this->spi_bus_->get_delegate(data_rate, bit_order, mode, cs_pin, release_device, write_only); // NOLINT this->devices_[device] = delegate; return delegate; } diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index f96d3da251..5bc80350da 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -317,7 +317,8 @@ class SPIBus { SPIBus(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) : clk_pin_(clk), sdo_pin_(sdo), sdi_pin_(sdi) {} - virtual SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) { + virtual SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, + bool release_device, bool write_only) { return new SPIDelegateBitBash(data_rate, bit_order, mode, cs_pin, this->clk_pin_, this->sdo_pin_, this->sdi_pin_); } @@ -334,7 +335,7 @@ class SPIClient; class SPIComponent : public Component { public: SPIDelegate *register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate, - GPIOPin *cs_pin); + GPIOPin *cs_pin, bool release_device, bool write_only); void unregister_device(SPIClient *device); void set_clk(GPIOPin *clk) { this->clk_pin_ = clk; } @@ -390,7 +391,8 @@ class SPIClient { virtual void spi_setup() { esph_log_d("spi_device", "mode %u, data_rate %ukHz", (unsigned) this->mode_, (unsigned) (this->data_rate_ / 1000)); - this->delegate_ = this->parent_->register_device(this, this->mode_, this->bit_order_, this->data_rate_, this->cs_); + this->delegate_ = this->parent_->register_device(this, this->mode_, this->bit_order_, this->data_rate_, this->cs_, + this->release_device_, this->write_only_); } virtual void spi_teardown() { @@ -399,6 +401,8 @@ class SPIClient { } bool spi_is_ready() { return this->delegate_->is_ready(); } + void set_release_device(bool release) { this->release_device_ = release; } + void set_write_only(bool write_only) { this->write_only_ = write_only; } protected: SPIBitOrder bit_order_{BIT_ORDER_MSB_FIRST}; @@ -406,6 +410,8 @@ class SPIClient { uint32_t data_rate_{1000000}; SPIComponent *parent_{nullptr}; GPIOPin *cs_{nullptr}; + bool release_device_{false}; + bool write_only_{false}; SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE}; }; diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp index 432f7cf2cd..a34e3c3c82 100644 --- a/esphome/components/spi/spi_arduino.cpp +++ b/esphome/components/spi/spi_arduino.cpp @@ -43,10 +43,7 @@ class SPIDelegateHw : public SPIDelegate { return; } #ifdef USE_RP2040 - // avoid overwriting the supplied buffer. Use vector for automatic deallocation - auto rxbuf = std::vector(length); - memcpy(rxbuf.data(), ptr, length); - this->channel_->transfer((void *) rxbuf.data(), length); + this->channel_->transfer(ptr, nullptr, length); #elif defined(USE_ESP8266) // ESP8266 SPI library requires the pointer to be word aligned, but the data may not be // so we need to copy the data to a temporary buffer @@ -89,7 +86,8 @@ class SPIBusHw : public SPIBus { #endif } - SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) override { + SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, + bool release_device, bool write_only) override { return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin); } diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp index a78da2cd9a..549f516eb1 100644 --- a/esphome/components/spi/spi_esp_idf.cpp +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -11,34 +11,26 @@ static const size_t MAX_TRANSFER_SIZE = 4092; // dictated by ESP-IDF API. class SPIDelegateHw : public SPIDelegate { public: SPIDelegateHw(SPIInterface channel, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, - bool write_only) - : SPIDelegate(data_rate, bit_order, mode, cs_pin), channel_(channel), write_only_(write_only) { - spi_device_interface_config_t config = {}; - config.mode = static_cast(mode); - config.clock_speed_hz = static_cast(data_rate); - config.spics_io_num = -1; - config.flags = 0; - config.queue_size = 1; - config.pre_cb = nullptr; - config.post_cb = nullptr; - if (bit_order == BIT_ORDER_LSB_FIRST) - config.flags |= SPI_DEVICE_BIT_LSBFIRST; - if (write_only) - config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY; - esp_err_t const err = spi_bus_add_device(channel, &config, &this->handle_); - if (err != ESP_OK) - ESP_LOGE(TAG, "Add device failed - err %X", err); + bool release_device, bool write_only) + : SPIDelegate(data_rate, bit_order, mode, cs_pin), + channel_(channel), + release_device_(release_device), + write_only_(write_only) { + if (!this->release_device_) + add_device_(); } bool is_ready() override { return this->handle_ != nullptr; } void begin_transaction() override { + if (this->release_device_) + this->add_device_(); if (this->is_ready()) { if (spi_device_acquire_bus(this->handle_, portMAX_DELAY) != ESP_OK) ESP_LOGE(TAG, "Failed to acquire SPI bus"); SPIDelegate::begin_transaction(); } else { - ESP_LOGW(TAG, "spi_setup called before initialisation"); + ESP_LOGW(TAG, "SPI device not ready, cannot begin transaction"); } } @@ -46,6 +38,10 @@ class SPIDelegateHw : public SPIDelegate { if (this->is_ready()) { SPIDelegate::end_transaction(); spi_device_release_bus(this->handle_); + if (this->release_device_) { + spi_bus_remove_device(this->handle_); + this->handle_ = nullptr; // reset handle to indicate no device is registered + } } } @@ -189,8 +185,30 @@ class SPIDelegateHw : public SPIDelegate { void read_array(uint8_t *ptr, size_t length) override { this->transfer(nullptr, ptr, length); } protected: + bool add_device_() { + spi_device_interface_config_t config = {}; + config.mode = static_cast(this->mode_); + config.clock_speed_hz = static_cast(this->data_rate_); + config.spics_io_num = -1; + config.flags = 0; + config.queue_size = 1; + config.pre_cb = nullptr; + config.post_cb = nullptr; + if (this->bit_order_ == BIT_ORDER_LSB_FIRST) + config.flags |= SPI_DEVICE_BIT_LSBFIRST; + if (this->write_only_) + config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY; + esp_err_t const err = spi_bus_add_device(this->channel_, &config, &this->handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Add device failed - err %X", err); + return false; + } + return true; + } + SPIInterface channel_{}; spi_device_handle_t handle_{}; + bool release_device_{false}; bool write_only_{false}; }; @@ -231,9 +249,10 @@ class SPIBusHw : public SPIBus { ESP_LOGE(TAG, "Bus init failed - err %X", err); } - SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) override { - return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin, - Utility::get_pin_no(this->sdi_pin_) == -1); + SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, + bool release_device, bool write_only) override { + return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin, release_device, + write_only || Utility::get_pin_no(this->sdi_pin_) == -1); } protected: diff --git a/tests/components/spi_device/common.yaml b/tests/components/spi_device/common.yaml index 636d82202b..0f6a5038fb 100644 --- a/tests/components/spi_device/common.yaml +++ b/tests/components/spi_device/common.yaml @@ -5,7 +5,7 @@ spi: miso_pin: ${miso_pin} spi_device: - id: spi_device_test - data_rate: 2MHz - spi_mode: 3 - bit_order: lsb_first + - id: spi_device_test + data_rate: 2MHz + spi_mode: 3 + bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp32-idf.yaml b/tests/components/spi_device/test.esp32-idf.yaml index 448e54fea6..c4989cccbf 100644 --- a/tests/components/spi_device/test.esp32-idf.yaml +++ b/tests/components/spi_device/test.esp32-idf.yaml @@ -4,3 +4,8 @@ substitutions: miso_pin: GPIO15 <<: !include common.yaml +spi_device: + - id: spi_device_test + release_device: true + data_rate: 1MHz + spi_mode: 0 From 4ef0264ed3d1632b35677dfa59f5dce1e57da428 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 21 Jun 2025 19:32:24 +1200 Subject: [PATCH 007/115] Clean up RAMAllocators in light related code (#9142) --- esphome/components/beken_spi_led_strip/led_strip.cpp | 6 ++++-- .../m5stack_8angle/light/m5stack_8angle_light.cpp | 2 +- esphome/components/rp2040_pio_led_strip/led_strip.cpp | 4 ++-- esphome/components/spi_led_strip/spi_led_strip.cpp | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp index d4585d7d36..17b2dd1808 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.cpp +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -7,11 +7,13 @@ extern "C" { #include "rtos_pub.h" -#include "spi.h" +// rtos_pub.h must be included before the rest of the includes + #include "arm_arch.h" #include "general_dma_pub.h" #include "gpio_pub.h" #include "icu_pub.h" +#include "spi.h" #undef SPI_DAT #undef SPI_BASE }; @@ -124,7 +126,7 @@ void BekenSPILEDStripLightOutput::setup() { size_t buffer_size = this->get_buffer_size_(); size_t dma_buffer_size = (buffer_size * 8) + (2 * 64); - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buf_ = allocator.allocate(buffer_size); if (this->buf_ == nullptr) { ESP_LOGE(TAG, "Cannot allocate LED buffer!"); diff --git a/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp index 95fd8cb98f..0e7b902919 100644 --- a/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp +++ b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp @@ -8,7 +8,7 @@ namespace m5stack_8angle { static const char *const TAG = "m5stack_8angle.light"; void M5Stack8AngleLightOutput::setup() { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buf_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); if (this->buf_ == nullptr) { ESP_LOGE(TAG, "Failed to allocate buffer of size %u", M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index a6ff037d88..42f7e9cf52 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -9,8 +9,8 @@ #include #include #include -#include #include +#include namespace esphome { namespace rp2040_pio_led_strip { @@ -44,7 +44,7 @@ void RP2040PIOLEDStripLightOutput::setup() { size_t buffer_size = this->get_buffer_size_(); - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buf_ = allocator.allocate(buffer_size); if (this->buf_ == nullptr) { ESP_LOGE(TAG, "Failed to allocate buffer of size %u", buffer_size); diff --git a/esphome/components/spi_led_strip/spi_led_strip.cpp b/esphome/components/spi_led_strip/spi_led_strip.cpp index 46243c0686..85c10ee87d 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.cpp +++ b/esphome/components/spi_led_strip/spi_led_strip.cpp @@ -5,7 +5,7 @@ namespace spi_led_strip { SpiLedStrip::SpiLedStrip(uint16_t num_leds) { this->num_leds_ = num_leds; - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buffer_size_ = num_leds * 4 + 8; this->buf_ = allocator.allocate(this->buffer_size_); if (this->buf_ == nullptr) { From a6c20853ca1c9a47f7220fb7615c21e719533ced Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sat, 21 Jun 2025 11:26:14 +0200 Subject: [PATCH 008/115] [nextion] Extract common `upload_end_` function to shared file (#9155) --- esphome/components/nextion/nextion_upload.cpp | 36 +++++++++++++++++++ .../nextion/nextion_upload_arduino.cpp | 25 ------------- .../components/nextion/nextion_upload_idf.cpp | 24 ------------- 3 files changed, 36 insertions(+), 49 deletions(-) create mode 100644 esphome/components/nextion/nextion_upload.cpp diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp new file mode 100644 index 0000000000..6a54abfed4 --- /dev/null +++ b/esphome/components/nextion/nextion_upload.cpp @@ -0,0 +1,36 @@ +#include "nextion.h" + +#ifdef USE_NEXTION_TFT_UPLOAD + +#include "esphome/core/application.h" + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion.upload"; + +bool Nextion::upload_end_(bool successful) { + if (successful) { + ESP_LOGD(TAG, "Upload successful"); + delay(1500); // NOLINT + App.safe_reboot(); + } else { + ESP_LOGE(TAG, "Upload failed"); + + this->is_updating_ = false; + this->ignore_is_setup_ = false; + + uint32_t baud_rate = this->parent_->get_baud_rate(); + if (baud_rate != this->original_baud_rate_) { + ESP_LOGD(TAG, "Baud: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); + this->parent_->set_baud_rate(this->original_baud_rate_); + this->parent_->load_settings(); + } + } + + return successful; +} + +} // namespace nextion +} // namespace esphome + +#endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index c2d0f2a22d..6cd03118d2 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -335,31 +335,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return upload_end_(true); } -bool Nextion::upload_end_(bool successful) { - ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful)); - - if (successful) { - ESP_LOGD(TAG, "Restart"); - delay(1500); // NOLINT - App.safe_reboot(); - delay(1500); // NOLINT - } else { - ESP_LOGE(TAG, "TFT upload failed"); - - this->is_updating_ = false; - this->ignore_is_setup_ = false; - - uint32_t baud_rate = this->parent_->get_baud_rate(); - if (baud_rate != this->original_baud_rate_) { - ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); - this->parent_->set_baud_rate(this->original_baud_rate_); - this->parent_->load_settings(); - } - } - - return successful; -} - #ifdef USE_ESP8266 WiFiClient *Nextion::get_wifi_client_() { if (this->tft_url_.compare(0, 6, "https:") == 0) { diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 43b80f7761..14ce46d0a0 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -335,30 +335,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return this->upload_end_(true); } -bool Nextion::upload_end_(bool successful) { - ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful)); - - if (successful) { - ESP_LOGD(TAG, "Restart"); - delay(1500); // NOLINT - App.safe_reboot(); - } else { - ESP_LOGE(TAG, "TFT upload failed"); - - this->is_updating_ = false; - this->ignore_is_setup_ = false; - - uint32_t baud_rate = this->parent_->get_baud_rate(); - if (baud_rate != this->original_baud_rate_) { - ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); - this->parent_->set_baud_rate(this->original_baud_rate_); - this->parent_->load_settings(); - } - } - - return successful; -} - } // namespace nextion } // namespace esphome From ac9c608542829974e09b6766583f84811955bae8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 18:13:07 +0200 Subject: [PATCH 009/115] Bump esptool from 4.8.1 to 4.9.0 (#9158) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76a58bf622..4932fb48f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile -esptool==4.8.1 +esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 aioesphomeapi==32.2.4 From c81dbf9d59b8b5a04cf76d4cdd2b01c207c75387 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sun, 22 Jun 2025 12:09:38 +0200 Subject: [PATCH 010/115] Improve on C++17 (#9170) --- esphome/components/host/__init__.py | 2 +- esphome/cpp_generator.py | 6 ++++++ platformio.ini | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index e275adafa9..b59d8ebd03 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -41,6 +41,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add_build_flag("-DUSE_HOST") cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) - cg.add_build_flag("-std=c++17") + cg.add_build_flag("-std=gnu++17") cg.add_define("ESPHOME_BOARD", "host") cg.add_platformio_option("platform", "platformio/native") diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 4641f69bdd..2a7b7fe057 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -616,6 +616,12 @@ def add_build_unflag(build_unflag: str) -> None: def set_cpp_standard(standard: str) -> None: """Set C++ standard with compiler flag `-std={standard}`.""" CORE.add_build_unflag("-std=gnu++11") + CORE.add_build_unflag("-std=gnu++14") + CORE.add_build_unflag("-std=gnu++20") + CORE.add_build_unflag("-std=gnu++23") + CORE.add_build_unflag("-std=gnu++2a") + CORE.add_build_unflag("-std=gnu++2b") + CORE.add_build_unflag("-std=gnu++2c") CORE.add_build_flag(f"-std={standard}") diff --git a/platformio.ini b/platformio.ini index f67226d657..6da9fc1338 100644 --- a/platformio.ini +++ b/platformio.ini @@ -50,6 +50,12 @@ build_flags = -std=gnu++17 build_unflags = -std=gnu++11 + -std=gnu++14 + -std=gnu++20 + -std=gnu++23 + -std=gnu++2a + -std=gnu++2b + -std=gnu++2c src_filter = +<./> +<../tests/dummy_main.cpp> From cbfd904b9f9fb178dfe119d2d1e287ee0273be8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 11:00:42 +0000 Subject: [PATCH 011/115] Bump aioesphomeapi from 32.2.4 to 33.1.0 (#9173) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4932fb48f5..01bbfa91c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==32.2.4 +aioesphomeapi==33.1.0 zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.14 # dashboard_import From 788803d5884185b2a8b8d078b7579e2ff0016396 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 19:05:54 +0000 Subject: [PATCH 012/115] Bump flake8 from 7.2.0 to 7.3.0 (#9172) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 634c474571..96efee7020 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: # Run the formatter. - id: ruff-format - repo: https://github.com/PyCQA/flake8 - rev: 7.2.0 + rev: 7.3.0 hooks: - id: flake8 additional_dependencies: diff --git a/requirements_test.txt b/requirements_test.txt index 9263d165ac..89aba702b9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,5 +1,5 @@ pylint==3.3.7 -flake8==7.2.0 # also change in .pre-commit-config.yaml when updating +flake8==7.3.0 # 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 From 7ab9083d778d47b7a88db8ec1d0c56c13027677b Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sun, 22 Jun 2025 22:56:50 +0200 Subject: [PATCH 013/115] [nextion] Revert to `millis()` on `recv_ret_string_` (#9168) --- esphome/components/nextion/nextion.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 4f08fcb393..042a595ff8 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -963,13 +963,12 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool uint16_t ret = 0; uint8_t c = 0; uint8_t nr_of_ff_bytes = 0; - uint64_t start; bool exit_flag = false; bool ff_flag = false; - start = App.get_loop_component_start_time(); + const uint32_t start = millis(); - while ((timeout == 0 && this->available()) || App.get_loop_component_start_time() - start <= timeout) { + while ((timeout == 0 && this->available()) || millis() - start <= timeout) { if (!this->available()) { App.feed_wdt(); delay(1); @@ -1038,7 +1037,7 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { nextion_queue->component = new nextion::NextionComponentBase; nextion_queue->component->set_variable_name(variable_name); - nextion_queue->queue_time = App.get_loop_component_start_time(); + nextion_queue->queue_time = millis(); this->nextion_queue_.push_back(nextion_queue); From dc5cbd4df8e658c48218f93e43dd1ecfc4d14d61 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 23 Jun 2025 09:54:49 +1200 Subject: [PATCH 014/115] [const] Move ``CONF_DEVICES`` to ``const.py`` (#9179) --- esphome/components/usb_host/__init__.py | 3 +-- esphome/const.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index 3204562dc8..0fe3310127 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -6,7 +6,7 @@ from esphome.components.esp32 import ( only_on_variant, ) import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_DEVICES, CONF_ID from esphome.cpp_types import Component AUTO_LOAD = ["bytebuffer"] @@ -16,7 +16,6 @@ usb_host_ns = cg.esphome_ns.namespace("usb_host") USBHost = usb_host_ns.class_("USBHost", Component) USBClient = usb_host_ns.class_("USBClient", Component) -CONF_DEVICES = "devices" CONF_VID = "vid" CONF_PID = "pid" CONF_ENABLE_HUBS = "enable_hubs" diff --git a/esphome/const.py b/esphome/const.py index 69d75c81ce..e61af6c5b5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -217,6 +217,7 @@ CONF_DEST = "dest" CONF_DEVICE = "device" CONF_DEVICE_CLASS = "device_class" CONF_DEVICE_FACTOR = "device_factor" +CONF_DEVICES = "devices" CONF_DIELECTRIC_CONSTANT = "dielectric_constant" CONF_DIMENSIONS = "dimensions" CONF_DIO_PIN = "dio_pin" From 59889a6286eed421c33880a1f931c0bc72733a95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 04:06:02 +0200 Subject: [PATCH 015/115] Reduce Logger memory usage by optimizing variable sizes (#9161) --- esphome/components/logger/__init__.py | 4 +- esphome/components/logger/logger.cpp | 20 ++--- esphome/components/logger/logger.h | 111 ++++++++++++++------------ esphome/core/log.cpp | 4 +- 4 files changed, 76 insertions(+), 63 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 26516e1506..af62d8a73f 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -184,7 +184,9 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(Logger), cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int, - cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes, + cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.All( + cv.validate_bytes, cv.int_range(min=160, max=65535) + ), cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean, cv.SplitDefault( CONF_TASK_LOG_BUFFER_SIZE, diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 28a66b23b7..b42496af66 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -24,7 +24,7 @@ static const char *const TAG = "logger"; // - Messages are serialized through main loop for proper console output // - Fallback to emergency console logging only if ring buffer is full // - WITHOUT task log buffer: Only emergency console output, no callbacks -void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT +void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag)) return; @@ -46,8 +46,8 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * bool message_sent = false; #ifdef USE_ESPHOME_TASK_LOG_BUFFER // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered - message_sent = this->log_buffer_->send_message_thread_safe(static_cast(level), tag, - static_cast(line), current_task, format, args); + message_sent = + this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), current_task, format, args); #endif // USE_ESPHOME_TASK_LOG_BUFFER // Emergency console logging for non-main tasks when ring buffer is full or disabled @@ -58,7 +58,7 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * // Maximum size for console log messages (includes null terminator) static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety - int buffer_at = 0; // Initialize buffer position + uint16_t buffer_at = 0; // Initialize buffer position this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); this->write_msg_(console_buffer); @@ -69,7 +69,7 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * } #else // Implementation for all other platforms -void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT +void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag) || global_recursion_guard_) return; @@ -85,7 +85,7 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * #ifdef USE_STORE_LOG_STR_IN_FLASH // Implementation for ESP8266 with flash string support. // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. -void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, +void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT if (level > this->level_for(tag) || global_recursion_guard_) return; @@ -122,7 +122,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr } #endif // USE_STORE_LOG_STR_IN_FLASH -inline int Logger::level_for(const char *tag) { +inline uint8_t Logger::level_for(const char *tag) { auto it = this->log_levels_.find(tag); if (it != this->log_levels_.end()) return it->second; @@ -195,13 +195,13 @@ void Logger::loop() { #endif void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } -void Logger::set_log_level(const std::string &tag, int log_level) { this->log_levels_[tag] = log_level; } +void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) UARTSelection Logger::get_uart() const { return this->uart_; } #endif -void Logger::add_on_log_callback(std::function &&callback) { +void Logger::add_on_log_callback(std::function &&callback) { this->log_callback_.add(std::move(callback)); } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } @@ -230,7 +230,7 @@ void Logger::dump_config() { } } -void Logger::set_log_level(int level) { +void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 9f09208b66..ea82764393 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -61,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = { * * Advanced configuration (pin selection, etc) is not supported. */ -enum UARTSelection { +enum UARTSelection : uint8_t { #ifdef USE_LIBRETINY UART_SELECTION_DEFAULT = 0, UART_SELECTION_UART0, @@ -129,10 +129,10 @@ class Logger : public Component { #endif /// Set the default log level for this logger. - void set_log_level(int level); + void set_log_level(uint8_t level); /// Set the log level of the specified tag. - void set_log_level(const std::string &tag, int log_level); - int get_log_level() { return this->current_level_; } + void set_log_level(const std::string &tag, uint8_t log_level); + uint8_t get_log_level() { return this->current_level_; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -140,19 +140,20 @@ class Logger : public Component { void pre_setup(); void dump_config() override; - inline int level_for(const char *tag); + inline uint8_t level_for(const char *tag); /// Register a callback that will be called for every log message sent - void add_on_log_callback(std::function &&callback); + void add_on_log_callback(std::function &&callback); // add a listener for log level changes - void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } + void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } float get_setup_priority() const override; - void log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT + void log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args); // NOLINT #ifdef USE_STORE_LOG_STR_IN_FLASH - void log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); // NOLINT + void log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, + va_list args); // NOLINT #endif protected: @@ -160,8 +161,9 @@ class Logger : public Component { // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // It's the caller's responsibility to initialize buffer_at (typically to 0) - inline void HOT format_log_to_buffer_with_terminator_(int level, const char *tag, int line, const char *format, - va_list args, char *buffer, int *buffer_at, int buffer_size) { + inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, + va_list args, char *buffer, uint16_t *buffer_at, + uint16_t buffer_size) { #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); #else @@ -180,7 +182,7 @@ class Logger : public Component { } // Helper to format and send a log message to both console and callbacks - inline void HOT log_message_to_buffer_and_send_(int level, const char *tag, int line, const char *format, + inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // Format to tx_buffer and prepare for output this->tx_buffer_at_ = 0; // Initialize buffer position @@ -194,11 +196,12 @@ class Logger : public Component { } // Write the body of the log message to the buffer - inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, int *buffer_at, int buffer_size) { + inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, uint16_t *buffer_at, + uint16_t buffer_size) { // Calculate available space - const int available = buffer_size - *buffer_at; - if (available <= 0) + if (*buffer_at >= buffer_size) return; + const uint16_t available = buffer_size - *buffer_at; // Determine copy length (minimum of remaining capacity and string length) const size_t copy_len = (length < static_cast(available)) ? length : available; @@ -211,7 +214,7 @@ class Logger : public Component { } // Format string to explicit buffer with varargs - inline void printf_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format, ...) { + inline void printf_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, ...) { va_list arg; va_start(arg, format); this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, arg); @@ -222,41 +225,50 @@ class Logger : public Component { const char *get_uart_selection_(); #endif + // Group 4-byte aligned members first uint32_t baud_rate_; char *tx_buffer_{nullptr}; - int tx_buffer_at_{0}; - int tx_buffer_size_{0}; +#ifdef USE_ARDUINO + Stream *hw_serial_{nullptr}; +#endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + void *main_task_ = nullptr; // Only used for thread name identification +#endif +#ifdef USE_ESP32 + // Task-specific recursion guards: + // - Main task uses a dedicated member variable for efficiency + // - Other tasks use pthread TLS with a dynamically created key via pthread_key_create + pthread_key_t log_recursion_key_; // 4 bytes +#endif +#ifdef USE_ESP_IDF + uart_port_t uart_num_; // 4 bytes (enum defaults to int size) +#endif + + // Large objects (internally aligned) + std::map log_levels_{}; + CallbackManager log_callback_{}; + CallbackManager level_callback_{}; +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer +#endif + + // Group smaller types together at the end + uint16_t tx_buffer_at_{0}; + uint16_t tx_buffer_size_{0}; + uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) UARTSelection uart_{UART_SELECTION_UART0}; #endif #ifdef USE_LIBRETINY UARTSelection uart_{UART_SELECTION_DEFAULT}; #endif -#ifdef USE_ARDUINO - Stream *hw_serial_{nullptr}; -#endif -#ifdef USE_ESP_IDF - uart_port_t uart_num_; -#endif - std::map log_levels_{}; - CallbackManager log_callback_{}; - int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer -#endif #ifdef USE_ESP32 - // Task-specific recursion guards: - // - Main task uses a dedicated member variable for efficiency - // - Other tasks use pthread TLS with a dynamically created key via pthread_key_create bool main_task_recursion_guard_{false}; - pthread_key_t log_recursion_key_; #else bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif - CallbackManager level_callback_{}; #if defined(USE_ESP32) || defined(USE_LIBRETINY) - void *main_task_ = nullptr; // Only used for thread name identification const char *HOT get_thread_name_() { TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); if (current_task == main_task_) { @@ -297,11 +309,10 @@ class Logger : public Component { } #endif - inline void HOT write_header_to_buffer_(int level, const char *tag, int line, const char *thread_name, char *buffer, - int *buffer_at, int buffer_size) { + inline void HOT write_header_to_buffer_(uint8_t level, const char *tag, int line, const char *thread_name, + char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { // Format header - if (level < 0) - level = 0; + // uint8_t level is already bounded 0-255, just ensure it's <= 7 if (level > 7) level = 7; @@ -320,12 +331,12 @@ class Logger : public Component { this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]: ", color, letter, tag, line); } - inline void HOT format_body_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format, + inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, va_list args) { // Get remaining capacity in the buffer - const int remaining = buffer_size - *buffer_at; - if (remaining <= 0) + if (*buffer_at >= buffer_size) return; + const uint16_t remaining = buffer_size - *buffer_at; const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args); @@ -334,7 +345,7 @@ class Logger : public Component { } // Update buffer_at with the formatted length (handle truncation) - int formatted_len = (ret >= remaining) ? remaining : ret; + uint16_t formatted_len = (ret >= remaining) ? remaining : ret; *buffer_at += formatted_len; // Remove all trailing newlines right after formatting @@ -343,18 +354,18 @@ class Logger : public Component { } } - inline void HOT write_footer_to_buffer_(char *buffer, int *buffer_at, int buffer_size) { - static const int RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); + inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { + static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); } }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger { +class LoggerMessageTrigger : public Trigger { public: - explicit LoggerMessageTrigger(Logger *parent, int level) { + explicit LoggerMessageTrigger(Logger *parent, uint8_t level) { this->level_ = level; - parent->add_on_log_callback([this](int level, const char *tag, const char *message) { + parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message) { if (level <= this->level_) { this->trigger(level, tag, message); } @@ -362,7 +373,7 @@ class LoggerMessageTrigger : public Trigger { } protected: - int level_; + uint8_t level_; }; } // namespace logger diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 424154d253..909319dd28 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -29,7 +29,7 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const char *form if (log == nullptr) return; - log->log_vprintf_(level, tag, line, format, args); + log->log_vprintf_(static_cast(level), tag, line, format, args); #endif } @@ -41,7 +41,7 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStr if (log == nullptr) return; - log->log_vprintf_(level, tag, line, format, args); + log->log_vprintf_(static_cast(level), tag, line, format, args); #endif } #endif From 04f592ba6d5e6d16b0978de04814abd3882f3666 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 04:07:53 +0200 Subject: [PATCH 016/115] Fix slow noise handshake by reading multiple messages per loop (#9130) --- esphome/components/api/api_connection.cpp | 60 ++++++++++++--------- esphome/components/api/api_frame_helper.cpp | 17 ++++-- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ca5689bdf6..ef791d462c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -28,6 +28,12 @@ namespace esphome { namespace api { +// Read a maximum of 5 messages per loop iteration to prevent starving other components. +// This is a balance between API responsiveness and allowing other components to run. +// Since each message could contain multiple protobuf messages when using packet batching, +// this limits the number of messages processed, not the number of TCP packets. +static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; + static const char *const TAG = "api.connection"; static const int ESP32_CAMERA_STOP_STREAM = 5000; @@ -109,33 +115,38 @@ void APIConnection::loop() { return; } + const uint32_t now = App.get_loop_component_start_time(); // Check if socket has data ready before attempting to read if (this->helper_->is_socket_ready()) { - ReadPacketBuffer buffer; - err = this->helper_->read_packet(&buffer); - if (err == APIError::WOULD_BLOCK) { - // pass - } else if (err != APIError::OK) { - on_fatal_error(); - if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); - } else if (err == APIError::CONNECTION_CLOSED) { - ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str()); - } else { - ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(), - api_error_to_str(err), errno); - } - return; - } else { - this->last_traffic_ = App.get_loop_component_start_time(); - // read a packet - if (buffer.data_len > 0) { - this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); - } else { - this->read_message(0, buffer.type, nullptr); - } - if (this->remove_) + // Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput + for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) { + ReadPacketBuffer buffer; + err = this->helper_->read_packet(&buffer); + if (err == APIError::WOULD_BLOCK) { + // No more data available + break; + } else if (err != APIError::OK) { + on_fatal_error(); + if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { + ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); + } else if (err == APIError::CONNECTION_CLOSED) { + ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str()); + } else { + ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(), + api_error_to_str(err), errno); + } return; + } else { + this->last_traffic_ = now; + // read a packet + if (buffer.data_len > 0) { + this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); + } else { + this->read_message(0, buffer.type, nullptr); + } + if (this->remove_) + return; + } } } @@ -152,7 +163,6 @@ void APIConnection::loop() { static uint8_t max_ping_retries = 60; static uint16_t ping_retry_interval = 1000; - const uint32_t now = App.get_loop_component_start_time(); if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) { diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index e0eb94836d..ff660f439e 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -274,12 +274,21 @@ APIError APINoiseFrameHelper::init() { } /// Run through handshake messages (if in that phase) APIError APINoiseFrameHelper::loop() { - APIError err = state_action_(); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - return err; + // During handshake phase, process as many actions as possible until we can't progress + // socket_->ready() stays true until next main loop, but state_action() will return + // WOULD_BLOCK when no more data is available to read + while (state_ != State::DATA && this->socket_->ready()) { + APIError err = state_action_(); + if (err != APIError::OK && err != APIError::WOULD_BLOCK) { + return err; + } + if (err == APIError::WOULD_BLOCK) { + break; + } } + if (!this->tx_buf_.empty()) { - err = try_send_tx_buf_(); + APIError err = try_send_tx_buf_(); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { return err; } From 7fc5bfd787794431cc386c6f1be09b42fef51d0e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 04:09:34 +0200 Subject: [PATCH 017/115] Reduce RAM usage for scheduled tasks (#9180) --- esphome/core/scheduler.cpp | 4 ++++ esphome/core/scheduler.h | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index eed222c974..8144435163 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -319,13 +319,17 @@ bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, return ret; } uint64_t Scheduler::millis_() { + // Get the current 32-bit millis value const uint32_t now = millis(); + // Check for rollover by comparing with last value if (now < this->last_millis_) { + // Detected rollover (happens every ~49.7 days) this->millis_major_++; ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms", now + (static_cast(this->millis_major_) << 32)); } this->last_millis_ = now; + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time return now + (static_cast(this->millis_major_) << 32); } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 872a8bd6f6..1284bcd4a7 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -29,12 +29,16 @@ class Scheduler { protected: struct SchedulerItem { + // Ordered by size to minimize padding Component *component; - std::string name; - enum Type { TIMEOUT, INTERVAL } type; uint32_t interval; + // 64-bit time to handle millis() rollover. The scheduler combines the 32-bit millis() + // with a 16-bit rollover counter to create a 64-bit time that won't roll over for + // billions of years. This ensures correct scheduling even when devices run for months. uint64_t next_execution_; + std::string name; std::function callback; + enum Type : uint8_t { TIMEOUT, INTERVAL } type; bool remove; static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); From 2a45467bf62fcde5933d55213a582d19627946b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Jun 2025 04:10:09 +0200 Subject: [PATCH 018/115] Pre-reserve looping components vector to reduce memory allocations (#9177) --- esphome/core/application.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 49c1e5fd61..f64070fa3d 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -257,6 +257,17 @@ void Application::teardown_components(uint32_t timeout_ms) { } void Application::calculate_looping_components_() { + // Count total components that need looping + size_t total_looping = 0; + for (auto *obj : this->components_) { + if (obj->has_overridden_loop()) { + total_looping++; + } + } + + // Pre-reserve vector to avoid reallocations + this->looping_components_.reserve(total_looping); + // First add all active components for (auto *obj : this->components_) { if (obj->has_overridden_loop() && From 78ec9856fbcf9a01e760f57ec5c2233e1cc8ef4d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:23:41 +1000 Subject: [PATCH 019/115] [lvgl] Add start_value to bar; make values templatable and updateable (#9056) --- esphome/components/lvgl/schemas.py | 8 ++- esphome/components/lvgl/widgets/lv_bar.py | 59 +++++++++++++++-------- tests/components/lvgl/lvgl-package.yaml | 3 ++ 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index fdc8750d1d..a0be65c928 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -454,9 +454,13 @@ def container_validator(schema, widget_type: WidgetType): """ def validator(value): - result = schema if w_sch := widget_type.schema: - result = result.extend(w_sch) + if isinstance(w_sch, dict): + w_sch = cv.Schema(w_sch) + # order is important here to preserve extras + result = w_sch.extend(schema) + else: + result = schema ltype = df.TYPE_NONE if value and (layout := value.get(df.CONF_LAYOUT)): if not isinstance(layout, dict): diff --git a/esphome/components/lvgl/widgets/lv_bar.py b/esphome/components/lvgl/widgets/lv_bar.py index 57209370c0..f0fdd6d278 100644 --- a/esphome/components/lvgl/widgets/lv_bar.py +++ b/esphome/components/lvgl/widgets/lv_bar.py @@ -1,8 +1,15 @@ import esphome.config_validation as cv from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE -from ..defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal -from ..lv_validation import animated, get_start_value, lv_float +from ..defines import ( + BAR_MODES, + CONF_ANIMATED, + CONF_INDICATOR, + CONF_MAIN, + CONF_START_VALUE, + literal, +) +from ..lv_validation import animated, lv_int from ..lvcode import lv from ..types import LvNumber, NumberType from . import Widget @@ -10,22 +17,30 @@ from . import Widget # Note this file cannot be called "bar.py" because that name is disallowed. CONF_BAR = "bar" -BAR_MODIFY_SCHEMA = cv.Schema( - { - cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_ANIMATED, default=True): animated, - } -) + + +def validate_bar(config): + if config.get(CONF_MODE) != "LV_BAR_MODE_RANGE" and CONF_START_VALUE in config: + raise cv.Invalid( + f"{CONF_START_VALUE} is only allowed when {CONF_MODE} is set to 'RANGE'" + ) + if (CONF_MIN_VALUE in config) != (CONF_MAX_VALUE in config): + raise cv.Invalid( + f"If either {CONF_MIN_VALUE} or {CONF_MAX_VALUE} is set, both must be set" + ) + return config + BAR_SCHEMA = cv.Schema( { - cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, - cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, - cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, + cv.Optional(CONF_VALUE): lv_int, + cv.Optional(CONF_START_VALUE): lv_int, + cv.Optional(CONF_MIN_VALUE): lv_int, + cv.Optional(CONF_MAX_VALUE): lv_int, + cv.Optional(CONF_MODE): BAR_MODES.one_of, cv.Optional(CONF_ANIMATED, default=True): animated, } -) +).add_extra(validate_bar) class BarType(NumberType): @@ -35,17 +50,23 @@ class BarType(NumberType): LvNumber("lv_bar_t"), parts=(CONF_MAIN, CONF_INDICATOR), schema=BAR_SCHEMA, - modify_schema=BAR_MODIFY_SCHEMA, ) async def to_code(self, w: Widget, config): var = w.obj + if mode := config.get(CONF_MODE): + lv.bar_set_mode(var, literal(mode)) + is_animated = literal(config[CONF_ANIMATED]) if CONF_MIN_VALUE in config: - lv.bar_set_range(var, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) - lv.bar_set_mode(var, literal(config[CONF_MODE])) - value = await get_start_value(config) - if value is not None: - lv.bar_set_value(var, value, literal(config[CONF_ANIMATED])) + lv.bar_set_range( + var, + await lv_int.process(config[CONF_MIN_VALUE]), + await lv_int.process(config[CONF_MAX_VALUE]), + ) + if value := await lv_int.process(config.get(CONF_VALUE)): + lv.bar_set_value(var, value, is_animated) + if start_value := await lv_int.process(config.get(CONF_START_VALUE)): + lv.bar_set_start_value(var, start_value, is_animated) @property def animated(self): diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index d8452bdd2a..f32b09d0e6 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -728,12 +728,15 @@ lvgl: value: 30 max_value: 100 min_value: 10 + start_value: 20 mode: range on_click: then: - lvgl.bar.update: id: bar_id value: !lambda return (int)((float)rand() / RAND_MAX * 100); + start_value: !lambda return (int)((float)rand() / RAND_MAX * 100); + mode: symmetrical - logger.log: format: "bar value %f" args: [x] From 41c78521280e0376f5f2e0967cba784f44428504 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:25:26 +1000 Subject: [PATCH 020/115] [lvgl] Use styles instead of object properties for themes (#9116) --- esphome/components/lvgl/styles.py | 40 ++++++++++++--------- esphome/components/lvgl/widgets/__init__.py | 14 ++++++-- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index b59ff513e2..426dd3f229 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -3,7 +3,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import ID -from esphome.cpp_generator import MockObj from .defines import ( CONF_STYLE_DEFINITIONS, @@ -13,12 +12,13 @@ from .defines import ( literal, ) from .helpers import add_lv_use -from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable +from .lvcode import LambdaContext, LocalVariable, lv from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, STYLE_REMAP -from .types import ObjUpdateAction, lv_lambda_t, lv_obj_t, lv_obj_t_ptr, lv_style_t +from .types import ObjUpdateAction, lv_obj_t, lv_style_t from .widgets import ( Widget, add_widgets, + collect_parts, set_obj_properties, theme_widget_map, wait_for_widgets, @@ -37,12 +37,18 @@ async def style_set(svar, style): lv.call(f"style_set_{remapped_prop}", svar, literal(value)) +async def create_style(style, id_name): + style_id = ID(id_name, True, lv_style_t) + svar = cg.new_Pvariable(style_id) + lv.style_init(svar) + await style_set(svar, style) + return svar + + async def styles_to_code(config): """Convert styles to C__ code.""" for style in config.get(CONF_STYLE_DEFINITIONS, ()): - svar = cg.new_Pvariable(style[CONF_ID]) - lv.style_init(svar) - await style_set(svar, style) + await create_style(style, style[CONF_ID].id) @automation.register_action( @@ -68,16 +74,18 @@ async def theme_to_code(config): if theme := config.get(CONF_THEME): add_lv_use(CONF_THEME) for w_name, style in theme.items(): - if not isinstance(style, dict): - continue - - lname = "lv_theme_apply_" + w_name - apply = lv_variable(lv_lambda_t, lname) - theme_widget_map[w_name] = apply - ow = Widget.create("obj", MockObj(ID("obj")), obj_spec) - async with LambdaContext([(lv_obj_t_ptr, "obj")], where=w_name) as context: - await set_obj_properties(ow, style) - lv_assign(apply, await context.get_lambda()) + # Work around Python 3.10 bug with nested async comprehensions + # With Python 3.11 this could be simplified + styles = {} + for part, states in collect_parts(style).items(): + styles[part] = { + state: await create_style( + props, + "_lv_theme_style_" + w_name + "_" + part + "_" + state, + ) + for state, props in states.items() + } + theme_widget_map[w_name] = styles async def add_top_layer(lv_component, config): diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 9d53c0df26..a8cb8dce33 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -6,7 +6,7 @@ from esphome.config_validation import Invalid from esphome.const import CONF_DEFAULT, CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE from esphome.core import ID, TimePeriod from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import CallExpression, MockObj +from esphome.cpp_generator import MockObj from ..defines import ( CONF_FLEX_ALIGN_CROSS, @@ -453,7 +453,17 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): w = Widget.create(wid, var, spec, w_cnfig) if theme := theme_widget_map.get(w_type): - lv_add(CallExpression(theme, w.obj)) + for part, states in theme.items(): + part = "LV_PART_" + part.upper() + for state, style in states.items(): + state = "LV_STATE_" + state.upper() + if state == "LV_STATE_DEFAULT": + lv_state = literal(part) + elif part == "LV_PART_MAIN": + lv_state = literal(state) + else: + lv_state = join_enums((state, part)) + lv.obj_add_style(w.obj, style, lv_state) await set_obj_properties(w, w_cnfig) await add_widgets(w, w_cnfig) await spec.to_code(w, w_cnfig) From 87df3596a2236ad3c503fc4b1c75bbbe843364d4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:41:06 +1200 Subject: [PATCH 021/115] [config validation] Add more ip address / network validators (#9181) --- esphome/config_validation.py | 45 +++++++++++++++++++++++++++++++++++- esphome/yaml_util.py | 3 ++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 964f533215..bf69b81bb5 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -3,7 +3,15 @@ from contextlib import contextmanager from dataclasses import dataclass from datetime import datetime -from ipaddress import AddressValueError, IPv4Address, ip_address +from ipaddress import ( + AddressValueError, + IPv4Address, + IPv4Network, + IPv6Address, + IPv6Network, + ip_address, + ip_network, +) import logging import os import re @@ -1176,6 +1184,14 @@ def ipv4address(value): return address +def ipv6address(value): + try: + address = IPv6Address(value) + except AddressValueError as exc: + raise Invalid(f"{value} is not a valid IPv6 address") from exc + return address + + def ipv4address_multi_broadcast(value): address = ipv4address(value) if not (address.is_multicast or (address == IPv4Address("255.255.255.255"))): @@ -1193,6 +1209,33 @@ def ipaddress(value): return address +def ipv4network(value): + """Validate that the value is a valid IPv4 network.""" + try: + network = IPv4Network(value, strict=False) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IPv4 network") from exc + return network + + +def ipv6network(value): + """Validate that the value is a valid IPv6 network.""" + try: + network = IPv6Network(value, strict=False) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IPv6 network") from exc + return network + + +def ipnetwork(value): + """Validate that the value is a valid IP network.""" + try: + network = ip_network(value, strict=False) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IP network") from exc + return network + + def _valid_topic(value): """Validate that this is a valid topic name/filter.""" if value is None: # Used to disable publishing and subscribing diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 78deec8e65..bd1806affc 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -5,7 +5,7 @@ import fnmatch import functools import inspect from io import BytesIO, TextIOBase, TextIOWrapper -from ipaddress import _BaseAddress +from ipaddress import _BaseAddress, _BaseNetwork import logging import math import os @@ -621,6 +621,7 @@ ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(int, ESPHomeDumper.represent_int) ESPHomeDumper.add_multi_representer(float, ESPHomeDumper.represent_float) ESPHomeDumper.add_multi_representer(_BaseAddress, ESPHomeDumper.represent_stringify) +ESPHomeDumper.add_multi_representer(_BaseNetwork, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda) From aecaffa2f5a9e35d1aa0773b17f07faa10dc8c98 Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Sun, 22 Jun 2025 23:41:29 -0400 Subject: [PATCH 022/115] Fixes for setup of OpenThread either using TLV or entering Credentials directly (#9157) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/openthread/__init__.py | 10 +++++----- esphome/components/openthread/openthread.cpp | 6 +++--- esphome/components/openthread/tlv.py | 7 +++++++ tests/components/openthread/test.esp32-c6-idf.yaml | 2 ++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 5b1ea491e3..393c47e720 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -46,7 +46,7 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", config[CONF_PAN_ID]) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", config[CONF_CHANNEL]) add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}" + "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}".lower() ) if network_name := config.get(CONF_NETWORK_NAME): @@ -54,14 +54,14 @@ def set_sdkconfig_options(config): if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None: add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}" + "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}".lower() ) if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None: add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix:X}" + "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix}".lower() ) if (pskc := config.get(CONF_PSKC)) is not None: - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}") + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower()) if CONF_FORCE_DATASET in config: if config[CONF_FORCE_DATASET]: @@ -98,7 +98,7 @@ _CONNECTION_SCHEMA = cv.Schema( cv.Optional(CONF_EXT_PAN_ID): cv.hex_int, cv.Optional(CONF_NETWORK_NAME): cv.string_strict, cv.Optional(CONF_PSKC): cv.hex_int, - cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.hex_int, + cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.ipv6network, } ) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index f40a56952a..24b3c23960 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -137,7 +137,7 @@ void OpenThreadSrpComponent::setup() { // Copy the mdns services to our local instance so that the c_str pointers remain valid for the lifetime of this // component this->mdns_services_ = this->mdns_->get_services(); - ESP_LOGW(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); + ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); for (const auto &service : this->mdns_services_) { otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance); if (!entry) { @@ -185,11 +185,11 @@ void OpenThreadSrpComponent::setup() { if (error != OT_ERROR_NONE) { ESP_LOGW(TAG, "Failed to add service: %s", otThreadErrorToString(error)); } - ESP_LOGW(TAG, "Added service: %s", full_service.c_str()); + ESP_LOGD(TAG, "Added service: %s", full_service.c_str()); } otSrpClientEnableAutoStartMode(instance, srp_start_callback, nullptr); - ESP_LOGW(TAG, "Finished SRP setup"); + ESP_LOGD(TAG, "Finished SRP setup"); } void *OpenThreadSrpComponent::pool_alloc_(size_t size) { diff --git a/esphome/components/openthread/tlv.py b/esphome/components/openthread/tlv.py index 45c8c47227..4a7d21c47d 100644 --- a/esphome/components/openthread/tlv.py +++ b/esphome/components/openthread/tlv.py @@ -1,5 +1,6 @@ # Sourced from https://gist.github.com/agners/0338576e0003318b63ec1ea75adc90f9 import binascii +import ipaddress from esphome.const import CONF_CHANNEL @@ -37,6 +38,12 @@ def parse_tlv(tlv) -> dict: if tag in TLV_TYPES: if tag == 3: output[TLV_TYPES[tag]] = val.decode("utf-8") + elif tag == 7: + mesh_local_prefix = binascii.hexlify(val).decode("utf-8") + mesh_local_prefix_str = f"{mesh_local_prefix}0000000000000000" + ipv6_bytes = bytes.fromhex(mesh_local_prefix_str) + ipv6_address = ipaddress.IPv6Address(ipv6_bytes) + output[TLV_TYPES[tag]] = f"{ipv6_address}/64" else: output[TLV_TYPES[tag]] = int.from_bytes(val) return output diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index 482fd1a453..f53b323bec 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -8,4 +8,6 @@ openthread: pan_id: 0x8f28 ext_pan_id: 0xd63e8e3e495ebbc3 pskc: 0xc23a76e98f1a6483639b1ac1271e2e27 + mesh_local_prefix: fd53:145f:ed22:ad81::/64 force_dataset: true + From cd22723623c78933764ff246fabd92eadeac87af Mon Sep 17 00:00:00 2001 From: myhomeiot <70070601+myhomeiot@users.noreply.github.com> Date: Mon, 23 Jun 2025 06:42:20 +0300 Subject: [PATCH 023/115] Restore access to BLEScanResult as get_scan_result (#9148) --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 1 + esphome/components/esp32_ble_tracker/esp32_ble_tracker.h | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 4785c29230..d950ccb5f1 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -522,6 +522,7 @@ optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData } void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { + this->scan_result_ = &scan_result; for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) this->address_[i] = scan_result.bda[i]; this->address_type_ = static_cast(scan_result.ble_addr_type); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 414c9f4b48..f5ed75a93e 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -85,6 +85,9 @@ class ESPBTDevice { const std::vector &get_service_datas() const { return service_datas_; } + // Exposed through a function for use in lambdas + const BLEScanResult &get_scan_result() const { return *scan_result_; } + bool resolve_irk(const uint8_t *irk) const; optional get_ibeacon() const { @@ -111,6 +114,7 @@ class ESPBTDevice { std::vector service_uuids_{}; std::vector manufacturer_datas_{}; std::vector service_datas_{}; + const BLEScanResult *scan_result_{nullptr}; }; class ESP32BLETracker; From 1a47164876cd59dc7e4f7aeafda5829916e07f6d Mon Sep 17 00:00:00 2001 From: JonasB2497 <45214989+JonasB2497@users.noreply.github.com> Date: Mon, 23 Jun 2025 06:47:47 +0200 Subject: [PATCH 024/115] Feature fontmetrics (#8978) --- esphome/components/font/__init__.py | 174 +++++++++++++++------------- esphome/components/font/font.cpp | 11 +- esphome/components/font/font.h | 21 +++- 3 files changed, 119 insertions(+), 87 deletions(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index be88fdb957..7d9a35647e 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -1,6 +1,7 @@ from collections.abc import MutableMapping import functools import hashlib +from itertools import accumulate import logging import os from pathlib import Path @@ -468,8 +469,9 @@ class EFont: class GlyphInfo: - def __init__(self, data_len, advance, offset_x, offset_y, width, height): - self.data_len = data_len + def __init__(self, glyph, data, advance, offset_x, offset_y, width, height): + self.glyph = glyph + self.bitmap_data = data self.advance = advance self.offset_x = offset_x self.offset_y = offset_y @@ -477,6 +479,62 @@ class GlyphInfo: self.height = height +def glyph_to_glyphinfo(glyph, font, size, bpp): + scale = 256 // (1 << bpp) + if not font.is_scalable: + sizes = [pt_to_px(x.size) for x in font.available_sizes] + if size in sizes: + font.select_size(sizes.index(size)) + else: + font.set_pixel_sizes(size, 0) + flags = FT_LOAD_RENDER + if bpp != 1: + flags |= FT_LOAD_NO_BITMAP + else: + flags |= FT_LOAD_TARGET_MONO + font.load_char(glyph, flags) + width = font.glyph.bitmap.width + height = font.glyph.bitmap.rows + buffer = font.glyph.bitmap.buffer + pitch = font.glyph.bitmap.pitch + glyph_data = [0] * ((height * width * bpp + 7) // 8) + src_mode = font.glyph.bitmap.pixel_mode + pos = 0 + for y in range(height): + for x in range(width): + if src_mode == ft_pixel_mode_mono: + pixel = ( + (1 << bpp) - 1 + if buffer[y * pitch + x // 8] & (1 << (7 - x % 8)) + else 0 + ) + else: + pixel = buffer[y * pitch + x] // scale + for bit_num in range(bpp): + if pixel & (1 << (bpp - bit_num - 1)): + glyph_data[pos // 8] |= 0x80 >> (pos % 8) + pos += 1 + ascender = pt_to_px(font.size.ascender) + if ascender == 0: + if not font.is_scalable: + ascender = size + else: + _LOGGER.error( + "Unable to determine ascender of font %s %s", + font.family_name, + font.style_name, + ) + return GlyphInfo( + glyph, + glyph_data, + pt_to_px(font.glyph.metrics.horiAdvance), + font.glyph.bitmap_left, + ascender - font.glyph.bitmap_top, + width, + height, + ) + + async def to_code(config): """ Collect all glyph codepoints, construct a map from a codepoint to a font file. @@ -506,98 +564,47 @@ async def to_code(config): codepoints = list(point_set) codepoints.sort(key=functools.cmp_to_key(glyph_comparator)) - glyph_args = {} - data = [] bpp = config[CONF_BPP] - scale = 256 // (1 << bpp) size = config[CONF_SIZE] # create the data array for all glyphs - for codepoint in codepoints: - font = point_font_map[codepoint] - if not font.is_scalable: - sizes = [pt_to_px(x.size) for x in font.available_sizes] - if size in sizes: - font.select_size(sizes.index(size)) - else: - font.set_pixel_sizes(size, 0) - flags = FT_LOAD_RENDER - if bpp != 1: - flags |= FT_LOAD_NO_BITMAP - else: - flags |= FT_LOAD_TARGET_MONO - font.load_char(codepoint, flags) - width = font.glyph.bitmap.width - height = font.glyph.bitmap.rows - buffer = font.glyph.bitmap.buffer - pitch = font.glyph.bitmap.pitch - glyph_data = [0] * ((height * width * bpp + 7) // 8) - src_mode = font.glyph.bitmap.pixel_mode - pos = 0 - for y in range(height): - for x in range(width): - if src_mode == ft_pixel_mode_mono: - pixel = ( - (1 << bpp) - 1 - if buffer[y * pitch + x // 8] & (1 << (7 - x % 8)) - else 0 - ) - else: - pixel = buffer[y * pitch + x] // scale - for bit_num in range(bpp): - if pixel & (1 << (bpp - bit_num - 1)): - glyph_data[pos // 8] |= 0x80 >> (pos % 8) - pos += 1 - ascender = pt_to_px(font.size.ascender) - if ascender == 0: - if not font.is_scalable: - ascender = size - else: - _LOGGER.error( - "Unable to determine ascender of font %s", config[CONF_FILE] - ) - glyph_args[codepoint] = GlyphInfo( - len(data), - pt_to_px(font.glyph.metrics.horiAdvance), - font.glyph.bitmap_left, - ascender - font.glyph.bitmap_top, - width, - height, - ) - data += glyph_data - - rhs = [HexInt(x) for x in data] + glyph_args = [ + glyph_to_glyphinfo(x, point_font_map[x], size, bpp) for x in codepoints + ] + rhs = [HexInt(x) for x in flatten([x.bitmap_data for x in glyph_args])] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) # Create the glyph table that points to data in the above array. - glyph_initializer = [] - for codepoint in codepoints: - glyph_initializer.append( - cg.StructInitializer( - GlyphData, - ( - "a_char", - cg.RawExpression( - f"(const uint8_t *){cpp_string_escape(codepoint)}" - ), - ), - ( - "data", - cg.RawExpression( - f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}" - ), - ), - ("advance", glyph_args[codepoint].advance), - ("offset_x", glyph_args[codepoint].offset_x), - ("offset_y", glyph_args[codepoint].offset_y), - ("width", glyph_args[codepoint].width), - ("height", glyph_args[codepoint].height), - ) + glyph_initializer = [ + cg.StructInitializer( + GlyphData, + ( + "a_char", + cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"), + ), + ( + "data", + cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"), + ), + ("advance", x.advance), + ("offset_x", x.offset_x), + ("offset_y", x.offset_y), + ("width", x.width), + ("height", x.height), ) + for (x, y) in zip( + glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args])) + ) + ] glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) font_height = pt_to_px(base_font.size.height) ascender = pt_to_px(base_font.size.ascender) + descender = abs(pt_to_px(base_font.size.descender)) + g = glyph_to_glyphinfo("x", base_font, size, bpp) + xheight = g.height if len(g.bitmap_data) > 1 else 0 + g = glyph_to_glyphinfo("X", base_font, size, bpp) + capheight = g.height if len(g.bitmap_data) > 1 else 0 if font_height == 0: if not base_font.is_scalable: font_height = size @@ -610,5 +617,8 @@ async def to_code(config): len(glyph_initializer), ascender, font_height, + descender, + xheight, + capheight, bpp, ) diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index 32464d87ee..8b2420ac07 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -45,8 +45,15 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { *height = this->glyph_data_->height; } -Font::Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp) - : baseline_(baseline), height_(height), bpp_(bpp) { +Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, + uint8_t bpp) + : baseline_(baseline), + height_(height), + descender_(descender), + linegap_(height - baseline - descender), + xheight_(xheight), + capheight_(capheight), + bpp_(bpp) { glyphs_.reserve(data_nr); for (int i = 0; i < data_nr; ++i) glyphs_.emplace_back(&data[i]); diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 992c77cb9f..28832d647d 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -50,11 +50,17 @@ class Font public: /** Construct the font with the given glyphs. * - * @param glyphs A vector of glyphs, must be sorted lexicographically. + * @param data A vector of glyphs, must be sorted lexicographically. + * @param data_nr The number of glyphs in data. * @param baseline The y-offset from the top of the text to the baseline. - * @param bottom The y-offset from the top of the text to the bottom (i.e. height). + * @param height The y-offset from the top of the text to the bottom. + * @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p). + * @param xheight The height of lowercase letters, usually measured at the "x" glyph. + * @param capheight The height of capital letters, usually measured at the "X" glyph. + * @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps. */ - Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp = 1); + Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, + uint8_t bpp = 1); int match_next_glyph(const uint8_t *str, int *match_length); @@ -65,6 +71,11 @@ class Font #endif inline int get_baseline() { return this->baseline_; } inline int get_height() { return this->height_; } + inline int get_ascender() { return this->baseline_; } + inline int get_descender() { return this->descender_; } + inline int get_linegap() { return this->linegap_; } + inline int get_xheight() { return this->xheight_; } + inline int get_capheight() { return this->capheight_; } inline int get_bpp() { return this->bpp_; } const std::vector> &get_glyphs() const { return glyphs_; } @@ -73,6 +84,10 @@ class Font std::vector> glyphs_; int baseline_; int height_; + int descender_; + int linegap_; + int xheight_; + int capheight_; uint8_t bpp_; // bits per pixel }; From 2ad266582f61db675f0bd4b84b9b1927bfd211b8 Mon Sep 17 00:00:00 2001 From: Gustavo Ambrozio Date: Mon, 23 Jun 2025 00:40:07 -1000 Subject: [PATCH 025/115] [online_image] Allow suppressing update on url change (#8885) --- esphome/components/online_image/__init__.py | 5 +++++ esphome/components/online_image/online_image.h | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 9380cf1b1b..3f15db6e50 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -34,6 +34,7 @@ MULTI_CONF = True CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" CONF_PLACEHOLDER = "placeholder" +CONF_UPDATE = "update" _LOGGER = logging.getLogger(__name__) @@ -167,6 +168,7 @@ SET_URL_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(OnlineImage), cv.Required(CONF_URL): cv.templatable(cv.url), + cv.Optional(CONF_UPDATE, default=True): cv.templatable(bool), } ) @@ -188,6 +190,9 @@ async def online_image_action_to_code(config, action_id, template_arg, args): if CONF_URL in config: template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) cg.add(var.set_url(template_)) + if CONF_UPDATE in config: + template_ = await cg.templatable(config[CONF_UPDATE], args, bool) + cg.add(var.set_update(template_)) return var diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 6ed9c7956f..6a2144538f 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -201,9 +201,12 @@ template class OnlineImageSetUrlAction : public Action { public: OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, url) + TEMPLATABLE_VALUE(bool, update) void play(Ts... x) override { this->parent_->set_url(this->url_.value(x...)); - this->parent_->update(); + if (this->update_.value(x...)) { + this->parent_->update(); + } } protected: From a1aebe6a2cec45290a7c8e51f58b32285ae0cdaa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Jun 2025 11:57:49 +0200 Subject: [PATCH 026/115] Eliminate memory fragmentation with BLE event pool (#9101) --- esphome/components/esp32_ble/ble.cpp | 55 ++-- esphome/components/esp32_ble/ble.h | 2 + esphome/components/esp32_ble/ble_event.h | 275 +++++++++++------- esphome/components/esp32_ble/ble_event_pool.h | 72 +++++ esphome/components/esp32_ble/queue.h | 25 +- 5 files changed, 288 insertions(+), 141 deletions(-) create mode 100644 esphome/components/esp32_ble/ble_event_pool.h diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 8adef79d2f..5a66f11d0f 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -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 EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - RAMAllocator::ALLOW_FAILURE | RAMAllocator::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 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); - } -} // NOLINT(clang-analyzer-unix.Malloc) + // 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 +} // Explicit template instantiations for the friend function template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *); diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 58c064a2ef..9fe996086e 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -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 ble_events_; + BLEEventPool ble_event_pool_; BLEAdvertising *advertising_{}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; uint32_t advertising_cycle_time_{}; diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index f51095effd..30118d2afd 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -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,123 +70,72 @@ 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(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(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(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: - delete this->event_.gattc.gattc_param; - delete this->event_.gattc.data; - break; - case GATTS: - delete this->event_.gatts.gatts_param; - delete this->event_.gatts.data; - break; - default: - break; + ~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; + this->event_.gattc.gattc_param = nullptr; + this->event_.gattc.data = nullptr; + return; + } + if (this->type_ == GATTS) { + delete this->event_.gatts.gatts_param; + delete this->event_.gatts.data; + this->event_.gatts.gatts_param = nullptr; + this->event_.gatts.data = nullptr; + } + } + + // Load new event data for reuse (replaces previous event data) + void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->cleanup_heap_data(); + this->type_ = GAP; + this->init_gap_data_(e, p); + } + + void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->cleanup_heap_data(); + this->type_ = GATTC; + this->init_gattc_data_(e, i, p); + } + + void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->cleanup_heap_data(); + this->type_ = GATTS; + this->init_gatts_data_(e, i, p); } // Disable copy to prevent double-delete @@ -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(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(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(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) diff --git a/esphome/components/esp32_ble/ble_event_pool.h b/esphome/components/esp32_ble/ble_event_pool.h new file mode 100644 index 0000000000..ef123b1325 --- /dev/null +++ b/esphome/components/esp32_ble/ble_event_pool.h @@ -0,0 +1,72 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include +#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 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 allocator(RAMAllocator::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 free_list_; // Free events ready for reuse + uint8_t total_created_; // Total events created (high water mark) +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h index 56d2efd18b..75bf1eef25 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/components/esp32_ble/queue.h @@ -18,7 +18,7 @@ namespace esphome { namespace esp32_ble { -template class LockFreeQueue { +template class LockFreeQueue { public: LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} @@ -26,8 +26,8 @@ template 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 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 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 head_; - std::atomic tail_; - std::atomic dropped_count_; + // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) + std::atomic 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 head_; + // Atomic: written by producer (push), read by consumer (pop) to check if empty + std::atomic tail_; }; } // namespace esp32_ble From 24587fe875fabbd061b92fc100c0846d3512fab1 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 19 Jun 2025 03:41:20 +0200 Subject: [PATCH 027/115] [nextion] Fix command spacing double timing and response blocking issues (#9134) --- esphome/components/nextion/nextion.cpp | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 3de32bfde9..24c31713bc 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -33,6 +33,7 @@ bool Nextion::send_command_(const std::string &command) { #ifdef USE_NEXTION_COMMAND_SPACING if (!this->ignore_is_setup_ && !this->command_pacer_.can_send()) { + ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str()); return false; } #endif // USE_NEXTION_COMMAND_SPACING @@ -43,10 +44,6 @@ bool Nextion::send_command_(const std::string &command) { const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF}; this->write_array(to_send, sizeof(to_send)); -#ifdef USE_NEXTION_COMMAND_SPACING - this->command_pacer_.mark_sent(); -#endif // USE_NEXTION_COMMAND_SPACING - return true; } @@ -377,12 +374,6 @@ void Nextion::process_nextion_commands_() { size_t commands_processed = 0; #endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP -#ifdef USE_NEXTION_COMMAND_SPACING - if (!this->command_pacer_.can_send()) { - return; // Will try again in next loop iteration - } -#endif - size_t to_process_length = 0; std::string to_process; @@ -430,6 +421,7 @@ void Nextion::process_nextion_commands_() { } #ifdef USE_NEXTION_COMMAND_SPACING this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent + ESP_LOGN(TAG, "Command spacing: marked command sent at %u ms", millis()); #endif break; case 0x02: // invalid Component ID or name was used From 6dfb9eba619b57be2c56dcf480d408fc9b09f1c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jun 2025 05:03:09 +0200 Subject: [PATCH 028/115] Fix missing BLE GAP events causing RSSI sensor and beacon failures (#9138) --- esphome/components/esp32_ble/ble.cpp | 97 ++++++++++++++---- esphome/components/esp32_ble/ble_event.h | 120 ++++++++++++++++++++--- 2 files changed, 188 insertions(+), 29 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 5a66f11d0f..cf63ad34d7 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -324,23 +324,69 @@ void ESP32BLE::loop() { } case BLEEvent::GAP: { esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event; - if (gap_event == ESP_GAP_BLE_SCAN_RESULT_EVT) { - // Use the new scan event handler - no memcpy! - for (auto *scan_handler : this->gap_scan_event_handlers_) { - scan_handler->gap_scan_event_handler(ble_event->scan_result()); - } - } else if (gap_event == ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT || - gap_event == ESP_GAP_BLE_SCAN_START_COMPLETE_EVT || - gap_event == ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT) { - // All three scan complete events have the same structure with just status - // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe - // This is verified at compile-time by static_assert checks in ble_event.h - // The struct already contains our copy of the status (copied in BLEEvent constructor) - ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.scan_complete)); - } + switch (gap_event) { + case ESP_GAP_BLE_SCAN_RESULT_EVT: + // Use the new scan event handler - no memcpy! + for (auto *scan_handler : this->gap_scan_event_handlers_) { + scan_handler->gap_scan_event_handler(ble_event->scan_result()); + } + break; + + // Scan complete events + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + // All three scan complete events have the same structure with just status + // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe + // This is verified at compile-time by static_assert checks in ble_event.h + // The struct already contains our copy of the status (copied in BLEEvent constructor) + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.scan_complete)); + } + break; + + // Advertising complete events + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + // All advertising complete events have the same structure with just status + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.adv_complete)); + } + break; + + // RSSI complete event + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.read_rssi_complete)); + } + break; + + // Security events + case ESP_GAP_BLE_AUTH_CMPL_EVT: + case ESP_GAP_BLE_SEC_REQ_EVT: + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: + case ESP_GAP_BLE_PASSKEY_REQ_EVT: + case ESP_GAP_BLE_NC_REQ_EVT: + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.security)); + } + break; + + default: + // Unknown/unhandled event + ESP_LOGW(TAG, "Unhandled GAP event type in loop: %d", gap_event); + break; } break; } @@ -399,11 +445,26 @@ template void enqueue_ble_event(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gat void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { - // Only queue the 4 GAP events we actually handle + // Queue GAP events that components need to handle + // Scanning events - used by esp32_ble_tracker case ESP_GAP_BLE_SCAN_RESULT_EVT: case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + // Advertising events - used by esp32_ble_beacon and esp32_ble server + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + // Connection events - used by ble_client + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + // Security events - used by ble_client and bluetooth_proxy + case ESP_GAP_BLE_AUTH_CMPL_EVT: + case ESP_GAP_BLE_SEC_REQ_EVT: + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: + case ESP_GAP_BLE_PASSKEY_REQ_EVT: + case ESP_GAP_BLE_NC_REQ_EVT: enqueue_ble_event(event, param); return; diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index 30118d2afd..dd3ec3da42 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -24,16 +24,45 @@ static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param) == si "ESP-IDF scan_stop_cmpl structure has unexpected size"); // Verify the status field is at offset 0 (first member) -static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == - offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl), +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == 0, "status must be first member of scan_param_cmpl"); -static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == - offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl), +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == 0, "status must be first member of scan_start_cmpl"); -static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == - offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl), +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == 0, "status must be first member of scan_stop_cmpl"); +// Compile-time verification for advertising complete events +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_data_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_rsp_data_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF scan_rsp_data_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_raw_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_data_raw_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_start_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_start_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_stop_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_stop_cmpl structure has unexpected size"); + +// Verify the status field is at offset 0 for advertising events +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_cmpl.status) == 0, + "status must be first member of adv_data_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_rsp_data_cmpl.status) == 0, + "status must be first member of scan_rsp_data_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_raw_cmpl.status) == 0, + "status must be first member of adv_data_raw_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_start_cmpl.status) == 0, + "status must be first member of adv_start_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_stop_cmpl.status) == 0, + "status must be first member of adv_stop_cmpl"); + +// Compile-time verification for RSSI complete event structure +static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.status) == 0, + "status must be first member of read_rssi_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(esp_bt_status_t), + "rssi must immediately follow status in read_rssi_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t), + "remote_addr must follow rssi in read_rssi_cmpl"); + // Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). // This class stores each event with minimal memory usage. // GAP events (99% of traffic) don't have the vector overhead. @@ -67,6 +96,17 @@ class BLEEvent { GATTS, }; + // Type definitions for cleaner method signatures + struct StatusOnlyData { + esp_bt_status_t status; + }; + + struct RSSICompleteData { + esp_bt_status_t status; + int8_t rssi; + esp_bd_addr_t remote_addr; + }; + // 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; @@ -147,12 +187,21 @@ class BLEEvent { struct gap_event { esp_gap_ble_cb_event_t gap_event; union { - BLEScanResult scan_result; // 73 bytes + BLEScanResult scan_result; // 73 bytes - Used by: esp32_ble_tracker // This matches ESP-IDF's scan complete event structures // All three (scan_param_cmpl, scan_start_cmpl, scan_stop_cmpl) have identical layout - struct { - esp_bt_status_t status; - } scan_complete; // 1 byte + // Used by: esp32_ble_tracker + StatusOnlyData scan_complete; // 1 byte + // Advertising complete events all have same structure + // Used by: esp32_ble_beacon, esp32_ble server components + // ADV_DATA_SET, SCAN_RSP_DATA_SET, ADV_DATA_RAW_SET, ADV_START, ADV_STOP + StatusOnlyData adv_complete; // 1 byte + // RSSI complete event + // Used by: ble_client (ble_rssi_sensor component) + RSSICompleteData read_rssi_complete; // 8 bytes + // Security events - we store the full security union + // Used by: ble_client (automation), bluetooth_proxy, esp32_ble_client + esp_ble_sec_t security; // Variable size, but fits within scan_result size }; } gap; // 80 bytes total @@ -180,6 +229,9 @@ 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; } + esp_bt_status_t adv_complete_status() const { return event_.gap.adv_complete.status; } + const RSSICompleteData &read_rssi_complete() const { return event_.gap.read_rssi_complete; } + const esp_ble_sec_t &security() const { return event_.gap.security; } private: // Initialize GAP event data @@ -215,8 +267,47 @@ class BLEEvent { this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status; break; + // Advertising complete events - all have same structure with just status + // Used by: esp32_ble_beacon, esp32_ble server components + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + this->event_.gap.adv_complete.status = p->adv_data_cmpl.status; + break; + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + this->event_.gap.adv_complete.status = p->scan_rsp_data_cmpl.status; + break; + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: // Used by: esp32_ble_beacon + this->event_.gap.adv_complete.status = p->adv_data_raw_cmpl.status; + break; + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: // Used by: esp32_ble_beacon + this->event_.gap.adv_complete.status = p->adv_start_cmpl.status; + break; + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: // Used by: esp32_ble_beacon + this->event_.gap.adv_complete.status = p->adv_stop_cmpl.status; + break; + + // RSSI complete event + // Used by: ble_client (ble_rssi_sensor) + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + this->event_.gap.read_rssi_complete.status = p->read_rssi_cmpl.status; + this->event_.gap.read_rssi_complete.rssi = p->read_rssi_cmpl.rssi; + memcpy(this->event_.gap.read_rssi_complete.remote_addr, p->read_rssi_cmpl.remote_addr, sizeof(esp_bd_addr_t)); + break; + + // Security events - copy the entire security union + // Used by: ble_client, bluetooth_proxy, esp32_ble_client + case ESP_GAP_BLE_AUTH_CMPL_EVT: // Used by: bluetooth_proxy, esp32_ble_client + case ESP_GAP_BLE_SEC_REQ_EVT: // Used by: esp32_ble_client + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: // Used by: ble_client automation + case ESP_GAP_BLE_PASSKEY_REQ_EVT: // Used by: ble_client automation + case ESP_GAP_BLE_NC_REQ_EVT: // Used by: ble_client automation + memcpy(&this->event_.gap.security, &p->ble_security, sizeof(esp_ble_sec_t)); + break; + default: - // We only handle 4 GAP event types, others are dropped + // We only store data for GAP events that components currently use + // Unknown events still get queued and logged in ble.cpp:375 as + // "Unhandled GAP event type in loop" - this helps identify new events + // that components might need in the future break; } } @@ -295,6 +386,13 @@ class BLEEvent { } }; +// Verify the gap_event struct hasn't grown beyond expected size +// The gap member in the union should be 80 bytes (including the gap_event enum) +static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes"); + +// Verify esp_ble_sec_t fits within our union +static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult"); + // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) } // namespace esp32_ble From 2f2ecadae7c2d6d18f8cb77da488f10461d8c073 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:41:06 +1200 Subject: [PATCH 029/115] [config validation] Add more ip address / network validators (#9181) --- esphome/config_validation.py | 45 +++++++++++++++++++++++++++++++++++- esphome/yaml_util.py | 3 ++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 964f533215..bf69b81bb5 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -3,7 +3,15 @@ from contextlib import contextmanager from dataclasses import dataclass from datetime import datetime -from ipaddress import AddressValueError, IPv4Address, ip_address +from ipaddress import ( + AddressValueError, + IPv4Address, + IPv4Network, + IPv6Address, + IPv6Network, + ip_address, + ip_network, +) import logging import os import re @@ -1176,6 +1184,14 @@ def ipv4address(value): return address +def ipv6address(value): + try: + address = IPv6Address(value) + except AddressValueError as exc: + raise Invalid(f"{value} is not a valid IPv6 address") from exc + return address + + def ipv4address_multi_broadcast(value): address = ipv4address(value) if not (address.is_multicast or (address == IPv4Address("255.255.255.255"))): @@ -1193,6 +1209,33 @@ def ipaddress(value): return address +def ipv4network(value): + """Validate that the value is a valid IPv4 network.""" + try: + network = IPv4Network(value, strict=False) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IPv4 network") from exc + return network + + +def ipv6network(value): + """Validate that the value is a valid IPv6 network.""" + try: + network = IPv6Network(value, strict=False) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IPv6 network") from exc + return network + + +def ipnetwork(value): + """Validate that the value is a valid IP network.""" + try: + network = ip_network(value, strict=False) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IP network") from exc + return network + + def _valid_topic(value): """Validate that this is a valid topic name/filter.""" if value is None: # Used to disable publishing and subscribing diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 78deec8e65..bd1806affc 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -5,7 +5,7 @@ import fnmatch import functools import inspect from io import BytesIO, TextIOBase, TextIOWrapper -from ipaddress import _BaseAddress +from ipaddress import _BaseAddress, _BaseNetwork import logging import math import os @@ -621,6 +621,7 @@ ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(int, ESPHomeDumper.represent_int) ESPHomeDumper.add_multi_representer(float, ESPHomeDumper.represent_float) ESPHomeDumper.add_multi_representer(_BaseAddress, ESPHomeDumper.represent_stringify) +ESPHomeDumper.add_multi_representer(_BaseNetwork, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda) From 5d6e690c12024ef7712a7bc24fec94755c8f72e0 Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Sun, 22 Jun 2025 23:41:29 -0400 Subject: [PATCH 030/115] Fixes for setup of OpenThread either using TLV or entering Credentials directly (#9157) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/openthread/__init__.py | 10 +++++----- esphome/components/openthread/openthread.cpp | 6 +++--- esphome/components/openthread/tlv.py | 7 +++++++ tests/components/openthread/test.esp32-c6-idf.yaml | 2 ++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 5b1ea491e3..393c47e720 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -46,7 +46,7 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", config[CONF_PAN_ID]) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", config[CONF_CHANNEL]) add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}" + "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}".lower() ) if network_name := config.get(CONF_NETWORK_NAME): @@ -54,14 +54,14 @@ def set_sdkconfig_options(config): if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None: add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}" + "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}".lower() ) if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None: add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix:X}" + "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix}".lower() ) if (pskc := config.get(CONF_PSKC)) is not None: - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}") + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower()) if CONF_FORCE_DATASET in config: if config[CONF_FORCE_DATASET]: @@ -98,7 +98,7 @@ _CONNECTION_SCHEMA = cv.Schema( cv.Optional(CONF_EXT_PAN_ID): cv.hex_int, cv.Optional(CONF_NETWORK_NAME): cv.string_strict, cv.Optional(CONF_PSKC): cv.hex_int, - cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.hex_int, + cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.ipv6network, } ) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index f40a56952a..24b3c23960 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -137,7 +137,7 @@ void OpenThreadSrpComponent::setup() { // Copy the mdns services to our local instance so that the c_str pointers remain valid for the lifetime of this // component this->mdns_services_ = this->mdns_->get_services(); - ESP_LOGW(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); + ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); for (const auto &service : this->mdns_services_) { otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance); if (!entry) { @@ -185,11 +185,11 @@ void OpenThreadSrpComponent::setup() { if (error != OT_ERROR_NONE) { ESP_LOGW(TAG, "Failed to add service: %s", otThreadErrorToString(error)); } - ESP_LOGW(TAG, "Added service: %s", full_service.c_str()); + ESP_LOGD(TAG, "Added service: %s", full_service.c_str()); } otSrpClientEnableAutoStartMode(instance, srp_start_callback, nullptr); - ESP_LOGW(TAG, "Finished SRP setup"); + ESP_LOGD(TAG, "Finished SRP setup"); } void *OpenThreadSrpComponent::pool_alloc_(size_t size) { diff --git a/esphome/components/openthread/tlv.py b/esphome/components/openthread/tlv.py index 45c8c47227..4a7d21c47d 100644 --- a/esphome/components/openthread/tlv.py +++ b/esphome/components/openthread/tlv.py @@ -1,5 +1,6 @@ # Sourced from https://gist.github.com/agners/0338576e0003318b63ec1ea75adc90f9 import binascii +import ipaddress from esphome.const import CONF_CHANNEL @@ -37,6 +38,12 @@ def parse_tlv(tlv) -> dict: if tag in TLV_TYPES: if tag == 3: output[TLV_TYPES[tag]] = val.decode("utf-8") + elif tag == 7: + mesh_local_prefix = binascii.hexlify(val).decode("utf-8") + mesh_local_prefix_str = f"{mesh_local_prefix}0000000000000000" + ipv6_bytes = bytes.fromhex(mesh_local_prefix_str) + ipv6_address = ipaddress.IPv6Address(ipv6_bytes) + output[TLV_TYPES[tag]] = f"{ipv6_address}/64" else: output[TLV_TYPES[tag]] = int.from_bytes(val) return output diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index 482fd1a453..f53b323bec 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -8,4 +8,6 @@ openthread: pan_id: 0x8f28 ext_pan_id: 0xd63e8e3e495ebbc3 pskc: 0xc23a76e98f1a6483639b1ac1271e2e27 + mesh_local_prefix: fd53:145f:ed22:ad81::/64 force_dataset: true + From 649936200e8390db5c91bb1450f5265d48d14852 Mon Sep 17 00:00:00 2001 From: myhomeiot <70070601+myhomeiot@users.noreply.github.com> Date: Mon, 23 Jun 2025 06:42:20 +0300 Subject: [PATCH 031/115] Restore access to BLEScanResult as get_scan_result (#9148) --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 1 + esphome/components/esp32_ble_tracker/esp32_ble_tracker.h | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index c5906779f1..a1fb727dd0 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -522,6 +522,7 @@ optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData } void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { + this->scan_result_ = &scan_result; for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) this->address_[i] = scan_result.bda[i]; this->address_type_ = static_cast(scan_result.ble_addr_type); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 16a100fb47..892f76f49c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -85,6 +85,9 @@ class ESPBTDevice { const std::vector &get_service_datas() const { return service_datas_; } + // Exposed through a function for use in lambdas + const BLEScanResult &get_scan_result() const { return *scan_result_; } + bool resolve_irk(const uint8_t *irk) const; optional get_ibeacon() const { @@ -111,6 +114,7 @@ class ESPBTDevice { std::vector service_uuids_{}; std::vector manufacturer_datas_{}; std::vector service_datas_{}; + const BLEScanResult *scan_result_{nullptr}; }; class ESP32BLETracker; From 22e360d4792b95449e2a93de1a7f956e9f9bb381 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 23 Jun 2025 23:32:22 +1200 Subject: [PATCH 032/115] Bump version to 2025.6.1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index d1e950c548..a4dd65bf45 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.6.0 +PROJECT_NUMBER = 2025.6.1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 95a5dbe218..3ef82aff5b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.6.0" +__version__ = "2025.6.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From ac942e0670e9afe9e8f8ca6903f3788320c674b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 19:58:32 +0200 Subject: [PATCH 033/115] Bump aioesphomeapi from 33.1.0 to 33.1.1 (#9187) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 01bbfa91c0..3f306fe4fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==33.1.0 +aioesphomeapi==33.1.1 zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.14 # dashboard_import From a35e476be5b0ec14ae04695c5906c583977df59c Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 23 Jun 2025 13:31:20 -0600 Subject: [PATCH 034/115] [opt3001] New component (#6625) Co-authored-by: Keith Burzinski --- CODEOWNERS | 1 + esphome/components/opt3001/__init__.py | 0 esphome/components/opt3001/opt3001.cpp | 122 ++++++++++++++++++ esphome/components/opt3001/opt3001.h | 27 ++++ esphome/components/opt3001/sensor.py | 35 +++++ tests/components/opt3001/common.yaml | 10 ++ tests/components/opt3001/test.esp32-ard.yaml | 5 + .../components/opt3001/test.esp32-c3-ard.yaml | 5 + .../components/opt3001/test.esp32-c3-idf.yaml | 5 + tests/components/opt3001/test.esp32-idf.yaml | 5 + .../components/opt3001/test.esp8266-ard.yaml | 5 + tests/components/opt3001/test.rp2040-ard.yaml | 5 + 12 files changed, 225 insertions(+) create mode 100644 esphome/components/opt3001/__init__.py create mode 100644 esphome/components/opt3001/opt3001.cpp create mode 100644 esphome/components/opt3001/opt3001.h create mode 100644 esphome/components/opt3001/sensor.py create mode 100644 tests/components/opt3001/common.yaml create mode 100644 tests/components/opt3001/test.esp32-ard.yaml create mode 100644 tests/components/opt3001/test.esp32-c3-ard.yaml create mode 100644 tests/components/opt3001/test.esp32-c3-idf.yaml create mode 100644 tests/components/opt3001/test.esp32-idf.yaml create mode 100644 tests/components/opt3001/test.esp8266-ard.yaml create mode 100644 tests/components/opt3001/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index ebbc8732ea..83d64a8850 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -323,6 +323,7 @@ esphome/components/one_wire/* @ssieb esphome/components/online_image/* @clydebarrow @guillempages esphome/components/opentherm/* @olegtarasov esphome/components/openthread/* @mrene +esphome/components/opt3001/* @ccutrer esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/packet_transport/* @clydebarrow diff --git a/esphome/components/opt3001/__init__.py b/esphome/components/opt3001/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/opt3001/opt3001.cpp b/esphome/components/opt3001/opt3001.cpp new file mode 100644 index 0000000000..2d65f1090d --- /dev/null +++ b/esphome/components/opt3001/opt3001.cpp @@ -0,0 +1,122 @@ +#include "opt3001.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace opt3001 { + +static const char *const TAG = "opt3001.sensor"; + +static const uint8_t OPT3001_REG_RESULT = 0x00; +static const uint8_t OPT3001_REG_CONFIGURATION = 0x01; +// See datasheet for full description of each bit. +static const uint16_t OPT3001_CONFIGURATION_RANGE_FULL = 0b1100000000000000; +static const uint16_t OPT3001_CONFIGURATION_CONVERSION_TIME_800 = 0b100000000000; +static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_MASK = 0b11000000000; +static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_SINGLE_SHOT = 0b01000000000; +static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN = 0b00000000000; +// tl;dr: Configure an automatic-ranged, 800ms single shot reading, +// with INT processing disabled +static const uint16_t OPT3001_CONFIGURATION_FULL_RANGE_ONE_SHOT = OPT3001_CONFIGURATION_RANGE_FULL | + OPT3001_CONFIGURATION_CONVERSION_TIME_800 | + OPT3001_CONFIGURATION_CONVERSION_MODE_SINGLE_SHOT; +static const uint16_t OPT3001_CONVERSION_TIME_800 = 825; // give it 25 extra ms; it seems to not be ready quite often + +/* +opt3001 properties: + +- e (exponent) = high 4 bits of result register +- m (mantissa) = low 12 bits of result register +- formula: (0.01 * 2^e) * m lx + +*/ + +void OPT3001Sensor::read_result_(const std::function &f) { + // ensure the single shot flag is clear, indicating it's done + uint16_t raw_value; + if (this->read(reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Reading configuration register failed"); + f(NAN); + return; + } + raw_value = i2c::i2ctohs(raw_value); + + if ((raw_value & OPT3001_CONFIGURATION_CONVERSION_MODE_MASK) != OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN) { + // not ready; wait 10ms and try again + ESP_LOGW(TAG, "Data not ready; waiting 10ms"); + this->set_timeout("opt3001_wait", 10, [this, f]() { read_result_(f); }); + return; + } + + if (this->read_register(OPT3001_REG_RESULT, reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Reading result register failed"); + f(NAN); + return; + } + raw_value = i2c::i2ctohs(raw_value); + + uint8_t exponent = raw_value >> 12; + uint16_t mantissa = raw_value & 0b111111111111; + + double lx = 0.01 * pow(2.0, double(exponent)) * double(mantissa); + f(float(lx)); +} + +void OPT3001Sensor::read_lx_(const std::function &f) { + // turn on (after one-shot sensor automatically powers down) + uint16_t start_measurement = i2c::htoi2cs(OPT3001_CONFIGURATION_FULL_RANGE_ONE_SHOT); + if (this->write_register(OPT3001_REG_CONFIGURATION, reinterpret_cast(&start_measurement), 2) != + i2c::ERROR_OK) { + ESP_LOGW(TAG, "Triggering one shot measurement failed"); + f(NAN); + return; + } + + this->set_timeout("read", OPT3001_CONVERSION_TIME_800, [this, f]() { + if (this->write(&OPT3001_REG_CONFIGURATION, 1, true) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Starting configuration register read failed"); + f(NAN); + return; + } + + this->read_result_(f); + }); +} + +void OPT3001Sensor::dump_config() { + LOG_SENSOR("", "OPT3001", this); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } + + LOG_UPDATE_INTERVAL(this); +} + +void OPT3001Sensor::update() { + // Set a flag and skip just in case the sensor isn't responding, + // and we just keep waiting for it in read_result_. + // This way we don't end up with potentially boundless "threads" + // using up memory and eventually crashing the device + if (this->updating_) { + return; + } + this->updating_ = true; + + this->read_lx_([this](float val) { + this->updating_ = false; + + if (std::isnan(val)) { + this->status_set_warning(); + this->publish_state(NAN); + return; + } + ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val); + this->status_clear_warning(); + this->publish_state(val); + }); +} + +float OPT3001Sensor::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace opt3001 +} // namespace esphome diff --git a/esphome/components/opt3001/opt3001.h b/esphome/components/opt3001/opt3001.h new file mode 100644 index 0000000000..ae3fde5c54 --- /dev/null +++ b/esphome/components/opt3001/opt3001.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace opt3001 { + +/// This class implements support for the i2c-based OPT3001 ambient light sensor. +class OPT3001Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void dump_config() override; + void update() override; + float get_setup_priority() const override; + + protected: + // checks if one-shot is complete before reading the result and returning it + void read_result_(const std::function &f); + // begins a one-shot measurement + void read_lx_(const std::function &f); + + bool updating_{false}; +}; + +} // namespace opt3001 +} // namespace esphome diff --git a/esphome/components/opt3001/sensor.py b/esphome/components/opt3001/sensor.py new file mode 100644 index 0000000000..a5bbf0e8dd --- /dev/null +++ b/esphome/components/opt3001/sensor.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@ccutrer"] + +opt3001_ns = cg.esphome_ns.namespace("opt3001") + +OPT3001Sensor = opt3001_ns.class_( + "OPT3001Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + OPT3001Sensor, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x44)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/tests/components/opt3001/common.yaml b/tests/components/opt3001/common.yaml new file mode 100644 index 0000000000..dab4f824f8 --- /dev/null +++ b/tests/components/opt3001/common.yaml @@ -0,0 +1,10 @@ +i2c: + - id: i2c_opt3001 + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: opt3001 + name: Living Room Brightness + address: 0x44 + update_interval: 30s diff --git a/tests/components/opt3001/test.esp32-ard.yaml b/tests/components/opt3001/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/opt3001/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp32-c3-ard.yaml b/tests/components/opt3001/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/opt3001/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp32-c3-idf.yaml b/tests/components/opt3001/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/opt3001/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp32-idf.yaml b/tests/components/opt3001/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/opt3001/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.esp8266-ard.yaml b/tests/components/opt3001/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/opt3001/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/opt3001/test.rp2040-ard.yaml b/tests/components/opt3001/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/opt3001/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml From 612c8d5841657e03d310ef37e0e4602f89221998 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:43:40 +1000 Subject: [PATCH 035/115] [lvgl] Fix dangling pointer issue with qrcode (#9190) --- esphome/components/lvgl/widgets/qrcode.py | 10 ++++------ tests/components/lvgl/lvgl-package.yaml | 4 +++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 742b538938..7d8d13d8c4 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.const import CONF_SIZE, CONF_TEXT from esphome.cpp_generator import MockObjClass -from ..defines import CONF_MAIN, literal +from ..defines import CONF_MAIN from ..lv_validation import color, color_retmapper, lv_text from ..lvcode import LocalVariable, lv, lv_expr from ..schemas import TEXT_SCHEMA @@ -34,7 +34,7 @@ class QrCodeType(WidgetType): ) def get_uses(self): - return ("canvas", "img") + return ("canvas", "img", "label") def obj_creator(self, parent: MockObjClass, config: dict): dark_color = color_retmapper(config[CONF_DARK_COLOR]) @@ -45,10 +45,8 @@ class QrCodeType(WidgetType): async def to_code(self, w: Widget, config): if (value := config.get(CONF_TEXT)) is not None: value = await lv_text.process(value) - with LocalVariable( - "qr_text", cg.const_char_ptr, value, modifier="" - ) as str_obj: - lv.qrcode_update(w.obj, str_obj, literal(f"strlen({str_obj})")) + with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj: + lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size()) qr_code_spec = QrCodeType() diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index f32b09d0e6..212e30c1eb 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -646,7 +646,9 @@ lvgl: on_click: lvgl.qrcode.update: id: lv_qr - text: homeassistant.io + text: + format: "A string with a number %d" + args: ['(int)(random_uint32() % 1000)'] - slider: min_value: 0 From 7ad6dab3839686ec64ba15de4d0a10c41e506346 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:31:38 +1200 Subject: [PATCH 036/115] [mqtt] Don't wait for connection unless configured to (#8933) --- esphome/components/mqtt/__init__.py | 4 ++++ esphome/components/mqtt/mqtt_client.cpp | 3 ++- esphome/components/mqtt/mqtt_client.h | 9 ++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 63d8da5788..f0d5a95d43 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -68,6 +68,7 @@ def AUTO_LOAD(): CONF_DISCOVER_IP = "discover_ip" CONF_IDF_SEND_ASYNC = "idf_send_async" +CONF_WAIT_FOR_CONNECTION = "wait_for_connection" def validate_message_just_topic(value): @@ -298,6 +299,7 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_PUBLISH_NAN_AS_NONE, default=False): cv.boolean, + cv.Optional(CONF_WAIT_FOR_CONNECTION, default=False): cv.boolean, } ), validate_config, @@ -453,6 +455,8 @@ async def to_code(config): cg.add(var.set_publish_nan_as_none(config[CONF_PUBLISH_NAN_AS_NONE])) + cg.add(var.set_wait_for_connection(config[CONF_WAIT_FOR_CONNECTION])) + MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema( { diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index ceb56bdfbe..3ba1ac6077 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -176,7 +176,8 @@ void MQTTClientComponent::dump_config() { } } bool MQTTClientComponent::can_proceed() { - return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected(); + return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected() || + !this->wait_for_connection_; } void MQTTClientComponent::start_dnslookup_() { diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index c68b3c62eb..a95b122383 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -4,11 +4,11 @@ #ifdef USE_MQTT -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/core/log.h" #include "esphome/components/json/json_util.h" #include "esphome/components/network/ip_address.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/log.h" #if defined(USE_ESP32) #include "mqtt_backend_esp32.h" #elif defined(USE_ESP8266) @@ -267,6 +267,8 @@ class MQTTClientComponent : public Component { void set_publish_nan_as_none(bool publish_nan_as_none); bool is_publish_nan_as_none() const; + void set_wait_for_connection(bool wait_for_connection) { this->wait_for_connection_ = wait_for_connection; } + protected: void send_device_info_(); @@ -334,6 +336,7 @@ class MQTTClientComponent : public Component { optional disconnect_reason_{}; bool publish_nan_as_none_{false}; + bool wait_for_connection_{false}; }; extern MQTTClientComponent *global_mqtt_client; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From 2df0ebd895220f8f8fd0b37bb929749f47f1a6ab Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Wed, 25 Jun 2025 01:31:23 +0200 Subject: [PATCH 037/115] [modbus_controller] Fix modbus read_lambda precision for non-floats or large integers (#9159) --- esphome/components/modbus/modbus.h | 8 +++ .../components/modbus_controller/__init__.py | 43 ++++++++++----- .../modbus_controller/modbus_controller.cpp | 21 ++++---- .../modbus_controller/modbus_controller.h | 53 ++++++++++++++++--- 4 files changed, 96 insertions(+), 29 deletions(-) diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index 4a78ed4aab..aebdbccc78 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -64,6 +64,14 @@ class ModbusDevice { this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload); } void send_raw(const std::vector &payload) { this->parent_->send_raw(payload); } + void send_error(uint8_t function_code, uint8_t exception_code) { + std::vector error_response; + error_response.reserve(3); + error_response.push_back(this->address_); + error_response.push_back(function_code | 0x80); + error_response.push_back(exception_code); + this->send_raw(error_response); + } // If more than one device is connected block sending a new command before a response is received bool waiting_for_response() { return parent_->waiting_for_response != 0; } diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 61b60498d0..8079b824b0 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -112,6 +112,22 @@ TYPE_REGISTER_MAP = { "FP32_R": 2, } +CPP_TYPE_REGISTER_MAP = { + "RAW": cg.uint16, + "U_WORD": cg.uint16, + "S_WORD": cg.int16, + "U_DWORD": cg.uint32, + "U_DWORD_R": cg.uint32, + "S_DWORD": cg.int32, + "S_DWORD_R": cg.int32, + "U_QWORD": cg.uint64, + "U_QWORD_R": cg.uint64, + "S_QWORD": cg.int64, + "S_QWORD_R": cg.int64, + "FP32": cg.float_, + "FP32_R": cg.float_, +} + ModbusCommandSentTrigger = modbus_controller_ns.class_( "ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_) ) @@ -285,21 +301,24 @@ async def to_code(config): cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) if CONF_SERVER_REGISTERS in config: for server_register in config[CONF_SERVER_REGISTERS]: + server_register_var = cg.new_Pvariable( + server_register[CONF_ID], + server_register[CONF_ADDRESS], + server_register[CONF_VALUE_TYPE], + TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], + ) + cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]] cg.add( - var.add_server_register( - cg.new_Pvariable( - server_register[CONF_ID], - server_register[CONF_ADDRESS], - server_register[CONF_VALUE_TYPE], - TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], - await cg.process_lambda( - server_register[CONF_READ_LAMBDA], - [], - return_type=cg.float_, - ), - ) + server_register_var.set_read_lambda( + cg.TemplateArguments(cpp_type), + await cg.process_lambda( + server_register[CONF_READ_LAMBDA], + [(cg.uint16, "address")], + return_type=cpp_type, + ), ) ) + cg.add(var.add_server_register(server_register_var)) await register_modbus_device(var, config) for conf in config.get(CONF_ON_COMMAND_SENT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 48ff868087..81e9ccf0a6 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -117,12 +117,17 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t bool found = false; for (auto *server_register : this->server_registers_) { if (server_register->address == current_address) { - float value = server_register->read_lambda(); + if (!server_register->read_lambda) { + break; + } + int64_t value = server_register->read_lambda(); + ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.", + server_register->address, static_cast(server_register->value_type), + server_register->register_count, server_register->format_value(value).c_str()); - ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %0.1f.", - server_register->address, static_cast(server_register->value_type), - server_register->register_count, value); - std::vector payload = float_to_payload(value, server_register->value_type); + std::vector payload; + payload.reserve(server_register->register_count * 2); + number_to_payload(payload, value, server_register->value_type); sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend()); current_address += server_register->register_count; found = true; @@ -132,11 +137,7 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t if (!found) { ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address); - std::vector error_response; - error_response.push_back(this->address_); - error_response.push_back(0x81); - error_response.push_back(0x02); - this->send_raw(error_response); + send_error(function_code, 0x02); return; } } diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index dfd52e44bc..11d27c4025 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -63,6 +63,10 @@ enum class SensorValueType : uint8_t { FP32_R = 0xD }; +inline bool value_type_is_float(SensorValueType v) { + return v == SensorValueType::FP32 || v == SensorValueType::FP32_R; +} + inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { switch (reg_type) { case ModbusRegisterType::COIL: @@ -253,18 +257,53 @@ class SensorItem { }; class ServerRegister { + using ReadLambda = std::function; + public: - ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count, - std::function read_lambda) { + ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) { this->address = address; this->value_type = value_type; this->register_count = register_count; - this->read_lambda = std::move(read_lambda); } + + template void set_read_lambda(const std::function &&user_read_lambda) { + this->read_lambda = [this, user_read_lambda]() -> int64_t { + T user_value = user_read_lambda(this->address); + if constexpr (std::is_same_v) { + return bit_cast(user_value); + } else { + return static_cast(user_value); + } + }; + } + + // Formats a raw value into a string representation based on the value type for debugging + std::string format_value(int64_t value) const { + switch (this->value_type) { + case SensorValueType::U_WORD: + case SensorValueType::U_DWORD: + case SensorValueType::U_DWORD_R: + case SensorValueType::U_QWORD: + case SensorValueType::U_QWORD_R: + return std::to_string(static_cast(value)); + case SensorValueType::S_WORD: + case SensorValueType::S_DWORD: + case SensorValueType::S_DWORD_R: + case SensorValueType::S_QWORD: + case SensorValueType::S_QWORD_R: + return std::to_string(value); + case SensorValueType::FP32_R: + case SensorValueType::FP32: + return str_sprintf("%.1f", bit_cast(static_cast(value))); + default: + return std::to_string(value); + } + } + uint16_t address{0}; SensorValueType value_type{SensorValueType::RAW}; uint8_t register_count{0}; - std::function read_lambda; + ReadLambda read_lambda; }; // ModbusController::create_register_ranges_ tries to optimize register range @@ -444,7 +483,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void on_modbus_data(const std::vector &data) override; /// called when a modbus error response was received void on_modbus_error(uint8_t function_code, uint8_t exception_code) override; - /// called when a modbus request (function code 3 or 4) was parsed without errors + /// called when a modbus request (function code 0x03 or 0x04) was parsed without errors void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final; /// default delegate called by process_modbus_data when a response has retrieved from the incoming queue void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data); @@ -529,7 +568,7 @@ inline float payload_to_float(const std::vector &data, const SensorItem int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask); float float_value; - if (item.sensor_value_type == SensorValueType::FP32 || item.sensor_value_type == SensorValueType::FP32_R) { + if (value_type_is_float(item.sensor_value_type)) { float_value = bit_cast(static_cast(number)); } else { float_value = static_cast(number); @@ -541,7 +580,7 @@ inline float payload_to_float(const std::vector &data, const SensorItem inline std::vector float_to_payload(float value, SensorValueType value_type) { int64_t val; - if (value_type == SensorValueType::FP32 || value_type == SensorValueType::FP32_R) { + if (value_type_is_float(value_type)) { val = bit_cast(value); } else { val = llroundf(value); From 9f831e91b326fee96fa187cee85e4b777f7d3031 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 24 Jun 2025 19:36:33 -0500 Subject: [PATCH 038/115] [helpers] Add ``format_mac_address_pretty`` function, migrate components (#9193) --- esphome/components/esp32_ble/ble.cpp | 10 ++++----- esphome/components/ld2410/ld2410.cpp | 21 +++---------------- esphome/components/ld2450/ld2450.cpp | 10 +++------ esphome/components/wifi/wifi_component.cpp | 5 ----- esphome/components/wifi/wifi_component.h | 2 -- .../wifi/wifi_component_esp32_arduino.cpp | 10 ++++----- .../wifi/wifi_component_esp8266.cpp | 13 ++++++------ .../wifi/wifi_component_esp_idf.cpp | 10 ++++----- .../wifi/wifi_component_libretiny.cpp | 10 ++++----- esphome/components/xiaomi_ble/xiaomi_ble.cpp | 2 +- esphome/core/helpers.cpp | 6 +++++- esphome/core/helpers.h | 2 ++ 12 files changed, 41 insertions(+), 60 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index cf63ad34d7..b10d1fe10a 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -4,6 +4,7 @@ #include "ble_event_pool.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include @@ -516,13 +517,12 @@ void ESP32BLE::dump_config() { break; } ESP_LOGCONFIG(TAG, - "ESP32 BLE:\n" - " MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n" + "BLE:\n" + " MAC address: %s\n" " IO Capability: %s", - mac_address[0], mac_address[1], mac_address[2], mac_address[3], mac_address[4], mac_address[5], - io_capability_s); + format_mac_address_pretty(mac_address).c_str(), io_capability_s); } else { - ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled"); + ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } } diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index d7007ae0bd..4272159da6 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -299,21 +299,6 @@ const char MAC_FMT[] = "%02X:%02X:%02X:%02X:%02X:%02X"; const std::string UNKNOWN_MAC("unknown"); const std::string NO_MAC("08:05:04:03:02:01"); -std::string format_mac(uint8_t *buffer) { - std::string::size_type mac_size = 256; - std::string mac; - do { - mac.resize(mac_size + 1); - mac_size = std::snprintf(&mac[0], mac.size(), MAC_FMT, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14], - buffer[15]); - } while (mac_size + 1 > mac.size()); - mac.resize(mac_size); - if (mac == NO_MAC) { - return UNKNOWN_MAC; - } - return mac; -} - #ifdef USE_NUMBER std::function set_number_value(number::Number *n, float value) { float normalized_value = value * 1.0; @@ -406,11 +391,11 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { if (len < 20) { return false; } - this->mac_ = format_mac(buffer); - ESP_LOGV(TAG, "MAC Address is: %s", const_cast(this->mac_.c_str())); + this->mac_ = format_mac_address_pretty(&buffer[10]); + ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str()); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { - this->mac_text_sensor_->publish_state(this->mac_); + this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_); } #endif #ifdef USE_SWITCH diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 519e4d89a3..4070c75fad 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -7,6 +7,7 @@ #include "esphome/components/sensor/sensor.h" #endif #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) @@ -96,11 +97,6 @@ static inline std::string get_direction(int16_t speed) { return STATIONARY; } -static inline std::string format_mac(uint8_t *buffer) { - return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14], - buffer[15]); -} - static inline std::string format_version(uint8_t *buffer) { return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); @@ -613,7 +609,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { if (len < 20) { return false; } - this->mac_ = ld2450::format_mac(buffer); + this->mac_ = format_mac_address_pretty(&buffer[10]); ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str()); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { @@ -622,7 +618,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif #ifdef USE_SWITCH if (this->bluetooth_switch_ != nullptr) { - this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); + this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); } #endif break; diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 51ae1c9f8e..d717b68340 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -741,11 +741,6 @@ void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->po void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; } -std::string WiFiComponent::format_mac_addr(const uint8_t *mac) { - char buf[20]; - sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - return buf; -} bool WiFiComponent::is_captive_portal_active_() { #ifdef USE_CAPTIVE_PORTAL return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 982007e47f..efd43077d1 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -321,8 +321,6 @@ class WiFiComponent : public Component { int32_t get_wifi_channel(); protected: - static std::string format_mac_addr(const uint8_t mac[6]); - #ifdef USE_WIFI_AP void setup_ap_config_(); #endif // USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 3fc2c009db..a7877eb90b 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -550,7 +550,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, - format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); + format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); #if USE_NETWORK_IPV6 this->set_timeout(100, [] { WiFi.enableIPv6(); }); #endif /* USE_NETWORK_IPV6 */ @@ -566,7 +566,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); } uint8_t reason = it.reason; @@ -636,13 +636,13 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { auto it = info.wifi_sta_connected; auto &mac = it.bssid; - ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { auto it = info.wifi_sta_disconnected; auto &mac = it.bssid; - ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: { @@ -651,7 +651,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { auto it = info.wifi_ap_probereqrecved; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); break; } default: diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 594bc79e54..ae1daed8b5 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -496,7 +496,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { char buf[33]; memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_addr(it.bssid).c_str(), it.channel); + ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(), + it.channel); s_sta_connected = true; break; } @@ -510,7 +511,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { s_sta_connect_not_found = true; } else { ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_addr(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); + format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); s_sta_connect_error = true; } s_sta_connected = false; @@ -545,17 +546,17 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } case EVENT_SOFTAPMODE_STACONNECTED: { auto it = event->event_info.sta_connected; - ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), it.aid); break; } case EVENT_SOFTAPMODE_STADISCONNECTED: { auto it = event->event_info.sta_disconnected; - ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), it.aid); break; } case EVENT_SOFTAPMODE_PROBEREQRECVED: { auto it = event->event_info.ap_probereqrecved; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); break; } #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) @@ -567,7 +568,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: { auto it = event->event_info.distribute_sta_ip; - ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_addr(it.mac).c_str(), + ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), format_ip_addr(it.ip).c_str(), it.aid); break; } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index e767e7ffc1..f0655a6d1d 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -691,7 +691,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, - format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); + format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); s_sta_connected = true; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { @@ -708,7 +708,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { return; } else { ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); s_sta_connect_error = true; } s_sta_connected = false; @@ -780,15 +780,15 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) { const auto &it = data->data.ap_probe_req_rx; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STACONNECTED) { const auto &it = data->data.ap_staconnected; - ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(it.mac).c_str()); + ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(it.mac).c_str()); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STADISCONNECTED) { const auto &it = data->data.ap_stadisconnected; - ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(it.mac).c_str()); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(it.mac).c_str()); } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) { const auto &it = data->data.ip_ap_staipassigned; diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 0f7b688290..b15f710150 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -281,7 +281,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ memcpy(buf, it.ssid, it.ssid_len); buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, - format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); + format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); break; } @@ -294,7 +294,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); } uint8_t reason = it.reason; @@ -349,13 +349,13 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { auto it = info.wifi_sta_connected; auto &mac = it.bssid; - ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { auto it = info.wifi_sta_disconnected; auto &mac = it.bssid; - ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(mac).c_str()); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str()); break; } case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: { @@ -364,7 +364,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { auto it = info.wifi_ap_probereqrecved; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); break; } default: diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index a80daa0b80..564870d74e 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -320,7 +320,7 @@ bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, c memcpy(mac_address + 4, mac_reverse + 1, 1); memcpy(mac_address + 5, mac_reverse, 1); ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed."); - ESP_LOGVV(TAG, " MAC address : %s", format_hex_pretty(mac_address, 6).c_str()); + ESP_LOGVV(TAG, " MAC address : %s", format_mac_address_pretty(mac_address).c_str()); ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str()); ESP_LOGVV(TAG, " Key : %s", format_hex_pretty(vector.key, vector.keysize).c_str()); ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty(vector.iv, vector.ivsize).c_str()); diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index ec79cb8bbb..79dbb314c8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -356,6 +356,10 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { return chars; } +std::string format_mac_address_pretty(const uint8_t *mac) { + return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +} + static char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; } std::string format_hex(const uint8_t *data, size_t length) { std::string ret; @@ -732,7 +736,7 @@ std::string get_mac_address() { std::string get_mac_address_pretty() { uint8_t mac[6]; get_mac_address_raw(mac); - return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return format_mac_address_pretty(mac); } #ifdef USE_ESP32 diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 477f260bf0..8bd5b813c7 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -402,6 +402,8 @@ template::value, int> = 0> optional< return parse_hex(str.c_str(), str.length()); } +/// Format the six-byte array \p mac into a MAC address. +std::string format_mac_address_pretty(const uint8_t mac[6]); /// Format the byte array \p data of length \p len in lowercased hex. std::string format_hex(const uint8_t *data, size_t length); /// Format the vector \p data in lowercased hex. From cf5197b68acb0dcc379fe2932cf44b3c179553ba Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 25 Jun 2025 03:15:50 -0500 Subject: [PATCH 039/115] [ld2450] Use ``App.get_loop_component_start_time()``, shorten log messages (#9192) --- esphome/components/ld2450/ld2450.cpp | 55 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 4070c75fad..718c853d22 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -6,6 +6,7 @@ #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif +#include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -116,7 +117,7 @@ void LD2450Component::setup() { } void LD2450Component::dump_config() { - ESP_LOGCONFIG(TAG, "HLK-LD2450 Human motion tracking radar module:"); + ESP_LOGCONFIG(TAG, "LD2450:"); #ifdef USE_BINARY_SENSOR LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_); LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); @@ -185,9 +186,9 @@ void LD2450Component::dump_config() { LOG_NUMBER(" ", "PresenceTimeoutNumber", this->presence_timeout_number_); #endif ESP_LOGCONFIG(TAG, - " Throttle : %ums\n" - " MAC Address : %s\n" - " Firmware version : %s", + " Throttle: %ums\n" + " MAC Address: %s\n" + " Firmware version: %s", this->throttle_, const_cast(this->mac_.c_str()), const_cast(this->version_.c_str())); } @@ -262,8 +263,7 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) { if (this->timeout_ == 0) { this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT); } - auto current_millis = millis(); - return current_millis - check_millis >= this->timeout_; + return App.get_loop_component_start_time() - check_millis >= this->timeout_; } // Extract, store and publish zone details LD2450 buffer @@ -350,25 +350,24 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu // Header Target 1 Target 2 Target 3 End void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { if (len < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes) - ESP_LOGE(TAG, "Periodic data: invalid message length"); + ESP_LOGE(TAG, "Invalid message length"); return; } if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) { // header - ESP_LOGE(TAG, "Periodic data: invalid message header"); + ESP_LOGE(TAG, "Invalid message header"); return; } if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) { // footer - ESP_LOGE(TAG, "Periodic data: invalid message footer"); + ESP_LOGE(TAG, "Invalid message footer"); return; } - auto current_millis = millis(); - if (current_millis - this->last_periodic_millis_ < this->throttle_) { + if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { ESP_LOGV(TAG, "Throttling: %d", this->throttle_); return; } - this->last_periodic_millis_ = current_millis; + this->last_periodic_millis_ = App.get_loop_component_start_time(); int16_t target_count = 0; int16_t still_target_count = 0; @@ -551,13 +550,13 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { #ifdef USE_SENSOR // For presence timeout check if (target_count > 0) { - this->presence_millis_ = millis(); + this->presence_millis_ = App.get_loop_component_start_time(); } if (moving_target_count > 0) { - this->moving_presence_millis_ = millis(); + this->moving_presence_millis_ = App.get_loop_component_start_time(); } if (still_target_count > 0) { - this->still_presence_millis_ = millis(); + this->still_presence_millis_ = App.get_loop_component_start_time(); } #endif } @@ -565,31 +564,31 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]); if (len < 10) { - ESP_LOGE(TAG, "Ack data: invalid length"); + ESP_LOGE(TAG, "Invalid ack length"); return true; } if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // frame header - ESP_LOGE(TAG, "Ack data: invalid header (command %02X)", buffer[COMMAND]); + ESP_LOGE(TAG, "Invalid ack header (command %02X)", buffer[COMMAND]); return true; } if (buffer[COMMAND_STATUS] != 0x01) { - ESP_LOGE(TAG, "Ack data: invalid status"); + ESP_LOGE(TAG, "Invalid ack status"); return true; } if (buffer[8] || buffer[9]) { - ESP_LOGE(TAG, "Ack data: last buffer was %u, %u", buffer[8], buffer[9]); + ESP_LOGE(TAG, "Last buffer was %u, %u", buffer[8], buffer[9]); return true; } switch (buffer[COMMAND]) { case lowbyte(CMD_ENABLE_CONF): - ESP_LOGV(TAG, "Got enable conf command"); + ESP_LOGV(TAG, "Enable conf command"); break; case lowbyte(CMD_DISABLE_CONF): - ESP_LOGV(TAG, "Got disable conf command"); + ESP_LOGV(TAG, "Disable conf command"); break; case lowbyte(CMD_SET_BAUD_RATE): - ESP_LOGV(TAG, "Got baud rate change command"); + ESP_LOGV(TAG, "Baud rate change command"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str()); @@ -623,10 +622,10 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif break; case lowbyte(CMD_BLUETOOTH): - ESP_LOGV(TAG, "Got Bluetooth command"); + ESP_LOGV(TAG, "Bluetooth command"); break; case lowbyte(CMD_SINGLE_TARGET_MODE): - ESP_LOGV(TAG, "Got single target conf command"); + ESP_LOGV(TAG, "Single target conf command"); #ifdef USE_SWITCH if (this->multi_target_switch_ != nullptr) { this->multi_target_switch_->publish_state(false); @@ -634,7 +633,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif break; case lowbyte(CMD_MULTI_TARGET_MODE): - ESP_LOGV(TAG, "Got multi target conf command"); + ESP_LOGV(TAG, "Multi target conf command"); #ifdef USE_SWITCH if (this->multi_target_switch_ != nullptr) { this->multi_target_switch_->publish_state(true); @@ -642,7 +641,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif break; case lowbyte(CMD_QUERY_TARGET_MODE): - ESP_LOGV(TAG, "Got query target tracking mode command"); + ESP_LOGV(TAG, "Query target tracking mode command"); #ifdef USE_SWITCH if (this->multi_target_switch_ != nullptr) { this->multi_target_switch_->publish_state(buffer[10] == 0x02); @@ -650,7 +649,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif break; case lowbyte(CMD_QUERY_ZONE): - ESP_LOGV(TAG, "Got query zone conf command"); + ESP_LOGV(TAG, "Query zone conf command"); this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16); this->publish_zone_type(); #ifdef USE_SELECT @@ -670,7 +669,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { this->process_zone_(buffer); break; case lowbyte(CMD_SET_ZONE): - ESP_LOGV(TAG, "Got set zone conf command"); + ESP_LOGV(TAG, "Set zone conf command"); this->query_zone_info(); break; default: From 47db5e26f3b2ff41c831ae3ad2d2b8f74652c9e9 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 25 Jun 2025 03:16:05 -0500 Subject: [PATCH 040/115] [ld2420] Shorten log messages + other clean-up (#9200) --- esphome/components/ld2420/ld2420.cpp | 121 +++++++++++++-------------- esphome/components/ld2420/ld2420.h | 4 +- 2 files changed, 61 insertions(+), 64 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 5b3206bf12..62f1685598 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -1,4 +1,5 @@ #include "ld2420.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" /* @@ -40,7 +41,7 @@ There are three documented parameters for modes: 00 04 = Energy output mode This mode outputs detailed signal energy values for each gate and the target distance. The data format consist of the following. - Header HH, Length LL, Persence PP, Distance DD, 16 Gate Energies EE, Footer FF + Header HH, Length LL, Presence PP, Distance DD, 16 Gate Energies EE, Footer FF HH HH HH HH LL LL PP DD DD EE EE .. 16x .. FF FF FF FF F4 F3 F2 F1 23 00 00 00 00 00 00 .. .. .. .. F8 F7 F6 F5 00 00 = debug output mode @@ -67,10 +68,10 @@ float LD2420Component::get_setup_priority() const { return setup_priority::BUS; void LD2420Component::dump_config() { ESP_LOGCONFIG(TAG, "LD2420:\n" - " Firmware Version : %7s\n" - "LD2420 Number:", + " Firmware version: %7s", this->ld2420_firmware_ver_); #ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, "Number:"); LOG_NUMBER(TAG, " Gate Timeout:", this->gate_timeout_number_); LOG_NUMBER(TAG, " Gate Max Distance:", this->max_gate_distance_number_); LOG_NUMBER(TAG, " Gate Min Distance:", this->min_gate_distance_number_); @@ -86,10 +87,10 @@ void LD2420Component::dump_config() { LOG_BUTTON(TAG, " Factory Reset:", this->factory_reset_button_); LOG_BUTTON(TAG, " Restart Module:", this->restart_module_button_); #endif - ESP_LOGCONFIG(TAG, "LD2420 Select:"); + ESP_LOGCONFIG(TAG, "Select:"); LOG_SELECT(TAG, " Operating Mode", this->operating_selector_); - if (this->get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { - ESP_LOGW(TAG, "LD2420 Firmware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_); + if (LD2420Component::get_firmware_int(this->ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { + ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->ld2420_firmware_ver_); } } @@ -102,7 +103,7 @@ uint8_t LD2420Component::calc_checksum(void *data, size_t size) { return checksum; } -int LD2420Component::get_firmware_int_(const char *version_string) { +int LD2420Component::get_firmware_int(const char *version_string) { std::string version_str = version_string; if (version_str[0] == 'v') { version_str = version_str.substr(1); @@ -115,7 +116,7 @@ int LD2420Component::get_firmware_int_(const char *version_string) { void LD2420Component::setup() { ESP_LOGCONFIG(TAG, "Running setup"); if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { - ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); this->mark_failed(); return; } @@ -127,7 +128,7 @@ void LD2420Component::setup() { const char *pfw = this->ld2420_firmware_ver_; std::string fw_str(pfw); - for (auto &listener : listeners_) { + for (auto &listener : this->listeners_) { listener->on_fw_version(fw_str); } @@ -137,11 +138,11 @@ void LD2420Component::setup() { } memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); - if (get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { + if (LD2420Component::get_firmware_int(this->ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { this->set_operating_mode(OP_SIMPLE_MODE_STRING); this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING); this->set_mode_(CMD_SYSTEM_MODE_SIMPLE); - ESP_LOGW(TAG, "LD2420 Frimware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_); + ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->ld2420_firmware_ver_); } else { this->set_mode_(CMD_SYSTEM_MODE_ENERGY); this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING); @@ -151,18 +152,17 @@ void LD2420Component::setup() { #endif this->set_system_mode(this->system_mode_); this->set_config_mode(false); - ESP_LOGCONFIG(TAG, "LD2420 setup complete."); } void LD2420Component::apply_config_action() { const uint8_t checksum = calc_checksum(&this->new_config, sizeof(this->new_config)); if (checksum == calc_checksum(&this->current_config, sizeof(this->current_config))) { - ESP_LOGCONFIG(TAG, "No configuration change detected"); + ESP_LOGD(TAG, "No configuration change detected"); return; } - ESP_LOGCONFIG(TAG, "Reconfiguring LD2420"); + ESP_LOGD(TAG, "Reconfiguring"); if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { - ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); this->mark_failed(); return; } @@ -178,13 +178,12 @@ void LD2420Component::apply_config_action() { this->set_system_mode(this->system_mode_); this->set_config_mode(false); // Disable config mode to save new values in LD2420 nvm this->set_operating_mode(OP_NORMAL_MODE_STRING); - ESP_LOGCONFIG(TAG, "LD2420 reconfig complete."); } void LD2420Component::factory_reset_action() { - ESP_LOGCONFIG(TAG, "Setting factory defaults"); + ESP_LOGD(TAG, "Setting factory defaults"); if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { - ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); this->mark_failed(); return; } @@ -207,18 +206,16 @@ void LD2420Component::factory_reset_action() { this->init_gate_config_numbers(); this->refresh_gate_config_numbers(); #endif - ESP_LOGCONFIG(TAG, "LD2420 factory reset complete."); } void LD2420Component::restart_module_action() { - ESP_LOGCONFIG(TAG, "Restarting LD2420 module"); + ESP_LOGD(TAG, "Restarting"); this->send_module_restart(); this->set_timeout(250, [this]() { this->set_config_mode(true); - this->set_system_mode(system_mode_); + this->set_system_mode(this->system_mode_); this->set_config_mode(false); }); - ESP_LOGCONFIG(TAG, "LD2420 Restarted."); } void LD2420Component::revert_config_action() { @@ -226,18 +223,18 @@ void LD2420Component::revert_config_action() { #ifdef USE_NUMBER this->init_gate_config_numbers(); #endif - ESP_LOGCONFIG(TAG, "Reverted config number edits."); + ESP_LOGD(TAG, "Reverted config number edits"); } void LD2420Component::loop() { // If there is a active send command do not process it here, the send command call will handle it. - if (!get_cmd_active_()) { - if (!available()) + if (!this->get_cmd_active_()) { + if (!this->available()) return; static uint8_t buffer[2048]; static uint8_t rx_data; - while (available()) { - rx_data = read(); + while (this->available()) { + rx_data = this->read(); this->readline_(rx_data, buffer, sizeof(buffer)); } } @@ -292,7 +289,7 @@ void LD2420Component::report_gate_data() { void LD2420Component::set_operating_mode(const std::string &state) { // If unsupported firmware ignore mode select - if (get_firmware_int_(ld2420_firmware_ver_) >= CALIBRATE_VERSION_MIN) { + if (LD2420Component::get_firmware_int(ld2420_firmware_ver_) >= CALIBRATE_VERSION_MIN) { this->current_operating_mode = OP_MODE_TO_UINT.at(state); // Entering Auto Calibrate we need to clear the privoiuos data collection this->operating_selector_->publish_state(state); @@ -365,13 +362,13 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { } // Resonable refresh rate for home assistant database size health - const int32_t current_millis = millis(); + const int32_t current_millis = App.get_loop_component_start_time(); if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) return; this->last_periodic_millis = current_millis; for (auto &listener : this->listeners_) { - listener->on_distance(get_distance_()); - listener->on_presence(get_presence_()); + listener->on_distance(this->get_distance_()); + listener->on_presence(this->get_presence_()); listener->on_energy(this->gate_energy_, sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0])); } @@ -392,9 +389,9 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { char outbuf[bufsize]{0}; while (true) { if (inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') { - set_presence_(false); + this->set_presence_(false); } else if (inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') { - set_presence_(true); + this->set_presence_(true); } if (inbuf[pos] >= '0' && inbuf[pos] <= '9') { if (index < bufsize - 1) { @@ -411,18 +408,18 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { } outbuf[index] = '\0'; if (index > 1) - set_distance_(strtol(outbuf, &endptr, 10)); + this->set_distance_(strtol(outbuf, &endptr, 10)); - if (get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { + if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { // Resonable refresh rate for home assistant database size health - const int32_t current_millis = millis(); + const int32_t current_millis = App.get_loop_component_start_time(); if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) return; this->last_normal_periodic_millis = current_millis; for (auto &listener : this->listeners_) - listener->on_distance(get_distance_()); + listener->on_distance(this->get_distance_()); for (auto &listener : this->listeners_) - listener->on_presence(get_presence_()); + listener->on_presence(this->get_presence_()); } } @@ -433,10 +430,10 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { uint8_t data_element = 0; uint16_t data_pos = 0; if (this->cmd_reply_.length > CMD_MAX_BYTES) { - ESP_LOGW(TAG, "LD2420 reply - received command reply frame is corrupt, length exceeds %d bytes.", CMD_MAX_BYTES); + ESP_LOGW(TAG, "Reply frame too long"); return; } else if (this->cmd_reply_.length < 2) { - ESP_LOGW(TAG, "LD2420 reply - received command frame is corrupt, length is less than 2 bytes."); + ESP_LOGW(TAG, "Command frame too short"); return; } memcpy(&this->cmd_reply_.error, &buffer[CMD_ERROR_WORD], sizeof(this->cmd_reply_.error)); @@ -447,13 +444,13 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { this->cmd_reply_.ack = true; switch ((uint16_t) this->cmd_reply_.command) { case (CMD_ENABLE_CONF): - ESP_LOGD(TAG, "LD2420 reply - set config enable: CMD = %2X %s", CMD_ENABLE_CONF, result); + ESP_LOGV(TAG, "Set config enable: CMD = %2X %s", CMD_ENABLE_CONF, result); break; case (CMD_DISABLE_CONF): - ESP_LOGD(TAG, "LD2420 reply - set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result); + ESP_LOGV(TAG, "Set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result); break; case (CMD_READ_REGISTER): - ESP_LOGD(TAG, "LD2420 reply - read register: CMD = %2X %s", CMD_READ_REGISTER, result); + ESP_LOGV(TAG, "Read register: CMD = %2X %s", CMD_READ_REGISTER, result); // TODO Read/Write register is not implemented yet, this will get flushed out to a proper header file data_pos = 0x0A; for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT @@ -465,13 +462,13 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { } break; case (CMD_WRITE_REGISTER): - ESP_LOGD(TAG, "LD2420 reply - write register: CMD = %2X %s", CMD_WRITE_REGISTER, result); + ESP_LOGV(TAG, "Write register: CMD = %2X %s", CMD_WRITE_REGISTER, result); break; case (CMD_WRITE_ABD_PARAM): - ESP_LOGD(TAG, "LD2420 reply - write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result); + ESP_LOGV(TAG, "Write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result); break; case (CMD_READ_ABD_PARAM): - ESP_LOGD(TAG, "LD2420 reply - read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result); + ESP_LOGV(TAG, "Read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result); data_pos = CMD_ABD_DATA_REPLY_START; for (uint16_t index = 0; index < (CMD_ABD_DATA_REPLY_SIZE * // NOLINT ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE)); @@ -483,11 +480,11 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { } break; case (CMD_WRITE_SYS_PARAM): - ESP_LOGD(TAG, "LD2420 reply - set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result); + ESP_LOGV(TAG, "Set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result); break; case (CMD_READ_VERSION): memcpy(this->ld2420_firmware_ver_, &buffer[12], buffer[10]); - ESP_LOGD(TAG, "LD2420 reply - module firmware version: %7s %s", this->ld2420_firmware_ver_, result); + ESP_LOGV(TAG, "Firmware version: %7s %s", this->ld2420_firmware_ver_, result); break; default: break; @@ -533,7 +530,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { } while (!this->cmd_reply_.ack) { - while (available()) { + while (this->available()) { this->readline_(read(), ack_buffer, sizeof(ack_buffer)); } delay_microseconds_safe(1450); @@ -548,7 +545,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { if (this->cmd_reply_.ack) retry = 0; if (this->cmd_reply_.error > 0) - handle_cmd_error(error); + this->handle_cmd_error(error); } return error; } @@ -563,7 +560,7 @@ uint8_t LD2420Component::set_config_mode(bool enable) { cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER); } cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command); + ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command); return this->send_cmd_from_array(cmd_frame); } @@ -576,7 +573,7 @@ void LD2420Component::ld2420_restart() { cmd_frame.header = CMD_FRAME_HEADER; cmd_frame.command = CMD_RESTART; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending restart command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending restart command: %2X", cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -588,7 +585,7 @@ void LD2420Component::get_reg_value_(uint16_t reg) { cmd_frame.data[1] = reg; cmd_frame.data_length += 2; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command); + ESP_LOGV(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -602,11 +599,11 @@ void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) { memcpy(&cmd_frame.data[cmd_frame.data_length], &value, sizeof(CMD_REG_DATA_REPLY_SIZE)); cmd_frame.data_length += 2; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value); + ESP_LOGV(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value); this->send_cmd_from_array(cmd_frame); } -void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGI(TAG, "Command failed: %s", ERR_MESSAGE[error]); } +void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGE(TAG, "Command failed: %s", ERR_MESSAGE[error]); } int LD2420Component::get_gate_threshold_(uint8_t gate) { uint8_t error; @@ -619,7 +616,7 @@ int LD2420Component::get_gate_threshold_(uint8_t gate) { memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_STILL_THRESH[gate], sizeof(CMD_GATE_STILL_THRESH[gate])); cmd_frame.data_length += 2; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending read gate %d high/low theshold command: %2X", gate, cmd_frame.command); + ESP_LOGV(TAG, "Sending read gate %d high/low threshold command: %2X", gate, cmd_frame.command); error = this->send_cmd_from_array(cmd_frame); if (error == 0) { this->current_config.move_thresh[gate] = cmd_reply_.data[0]; @@ -644,7 +641,7 @@ int LD2420Component::get_min_max_distances_timeout_() { sizeof(CMD_TIMEOUT_REG)); // Register: global delay time cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG); cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command); error = this->send_cmd_from_array(cmd_frame); if (error == 0) { this->current_config.min_gate = (uint16_t) cmd_reply_.data[0]; @@ -667,9 +664,9 @@ void LD2420Component::set_system_mode(uint16_t mode) { memcpy(&cmd_frame.data[cmd_frame.data_length], &unknown_parm, sizeof(unknown_parm)); cmd_frame.data_length += sizeof(unknown_parm); cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending write system mode command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command); if (this->send_cmd_from_array(cmd_frame) == 0) - set_mode_(mode); + this->set_mode_(mode); } void LD2420Component::get_firmware_version_() { @@ -679,7 +676,7 @@ void LD2420Component::get_firmware_version_() { cmd_frame.command = CMD_READ_VERSION; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending read firmware version command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending read firmware version command: %2X", cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -712,7 +709,7 @@ void LD2420Component::set_min_max_distances_timeout(uint32_t max_gate_distance, cmd_frame.data_length += sizeof(timeout); cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -738,7 +735,7 @@ void LD2420Component::set_gate_threshold(uint8_t gate) { sizeof(this->new_config.still_thresh[gate])); cmd_frame.data_length += sizeof(this->new_config.still_thresh[gate]); cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command); + ESP_LOGV(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command); this->send_cmd_from_array(cmd_frame); } diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 2b50c7a1d4..5e011100e6 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -179,7 +179,7 @@ class LD2420Component : public Component, public uart::UARTDevice { void set_operating_mode(const std::string &state); void auto_calibrate_sensitivity(); void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number); - uint8_t calc_checksum(void *data, size_t size); + static uint8_t calc_checksum(void *data, size_t size); RegConfigT current_config; RegConfigT new_config; @@ -222,7 +222,7 @@ class LD2420Component : public Component, public uart::UARTDevice { volatile bool ack; }; - int get_firmware_int_(const char *version_string); + static int get_firmware_int(const char *version_string); void get_firmware_version_(); int get_gate_threshold_(uint8_t gate); void get_reg_value_(uint16_t reg); From 5531296ee092ae33c20fd318c396711c892fd5fe Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 25 Jun 2025 04:48:32 -0500 Subject: [PATCH 041/115] [ld2410] Use ``App.get_loop_component_start_time()``, shorten log messages (#9194) Co-authored-by: J. Nick Koston --- esphome/components/ld2410/ld2410.cpp | 50 +++++++++++++++------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 4272159da6..b3c3649ceb 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -8,6 +8,8 @@ #include "esphome/components/sensor/sensor.h" #endif +#include "esphome/core/application.h" + #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) @@ -73,9 +75,9 @@ void LD2410Component::dump_config() { #endif this->read_all_info(); ESP_LOGCONFIG(TAG, - " Throttle_ : %ums\n" - " MAC Address : %s\n" - " Firmware Version : %s", + " Throttle: %ums\n" + " MAC address: %s\n" + " Firmware version: %s", this->throttle_, const_cast(this->mac_.c_str()), const_cast(this->version_.c_str())); } @@ -153,7 +155,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { /* Reduce data update rate to prevent home assistant database size grow fast */ - int32_t current_millis = millis(); + int32_t current_millis = App.get_loop_component_start_time(); if (current_millis - last_periodic_millis_ < this->throttle_) return; last_periodic_millis_ = current_millis; @@ -313,40 +315,40 @@ std::function set_number_value(number::Number *n, float value) { bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", buffer[COMMAND]); if (len < 10) { - ESP_LOGE(TAG, "Error with last command : incorrect length"); + ESP_LOGE(TAG, "Invalid length"); return true; } if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // check 4 frame start bytes - ESP_LOGE(TAG, "Error with last command : incorrect Header"); + ESP_LOGE(TAG, "Invalid header"); return true; } if (buffer[COMMAND_STATUS] != 0x01) { - ESP_LOGE(TAG, "Error with last command : status != 0x01"); + ESP_LOGE(TAG, "Invalid status"); return true; } if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) { - ESP_LOGE(TAG, "Error with last command , last buffer was: %u , %u", buffer[8], buffer[9]); + ESP_LOGE(TAG, "Invalid command: %u, %u", buffer[8], buffer[9]); return true; } switch (buffer[COMMAND]) { case lowbyte(CMD_ENABLE_CONF): - ESP_LOGV(TAG, "Handled Enable conf command"); + ESP_LOGV(TAG, "Enable conf"); break; case lowbyte(CMD_DISABLE_CONF): - ESP_LOGV(TAG, "Handled Disabled conf command"); + ESP_LOGV(TAG, "Disabled conf"); break; case lowbyte(CMD_SET_BAUD_RATE): - ESP_LOGV(TAG, "Handled baud rate change command"); + ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGE(TAG, "Change baud rate component config to %s and reinstall", this->baud_rate_select_->state.c_str()); + ESP_LOGE(TAG, "Configure baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); } #endif break; case lowbyte(CMD_VERSION): this->version_ = format_version(buffer); - ESP_LOGV(TAG, "FW Version is: %s", const_cast(this->version_.c_str())); + ESP_LOGV(TAG, "Firmware version: %s", const_cast(this->version_.c_str())); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { this->version_text_sensor_->publish_state(this->version_); @@ -356,7 +358,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): { std::string distance_resolution = DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11])); - ESP_LOGV(TAG, "Distance resolution is: %s", const_cast(distance_resolution.c_str())); + ESP_LOGV(TAG, "Distance resolution: %s", const_cast(distance_resolution.c_str())); #ifdef USE_SELECT if (this->distance_resolution_select_ != nullptr && this->distance_resolution_select_->state != distance_resolution) { @@ -368,9 +370,9 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { this->light_function_ = LIGHT_FUNCTION_INT_TO_ENUM.at(buffer[10]); this->light_threshold_ = buffer[11] * 1.0; this->out_pin_level_ = OUT_PIN_LEVEL_INT_TO_ENUM.at(buffer[12]); - ESP_LOGV(TAG, "Light function is: %s", const_cast(this->light_function_.c_str())); - ESP_LOGV(TAG, "Light threshold is: %f", this->light_threshold_); - ESP_LOGV(TAG, "Out pin level is: %s", const_cast(this->out_pin_level_.c_str())); + ESP_LOGV(TAG, "Light function: %s", const_cast(this->light_function_.c_str())); + ESP_LOGV(TAG, "Light threshold: %f", this->light_threshold_); + ESP_LOGV(TAG, "Out pin level: %s", const_cast(this->out_pin_level_.c_str())); #ifdef USE_SELECT if (this->light_function_select_ != nullptr && this->light_function_select_->state != this->light_function_) { this->light_function_select_->publish_state(this->light_function_); @@ -405,19 +407,19 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { #endif break; case lowbyte(CMD_GATE_SENS): - ESP_LOGV(TAG, "Handled sensitivity command"); + ESP_LOGV(TAG, "Sensitivity"); break; case lowbyte(CMD_BLUETOOTH): - ESP_LOGV(TAG, "Handled bluetooth command"); + ESP_LOGV(TAG, "Bluetooth"); break; case lowbyte(CMD_SET_DISTANCE_RESOLUTION): - ESP_LOGV(TAG, "Handled set distance resolution command"); + ESP_LOGV(TAG, "Set distance resolution"); break; case lowbyte(CMD_SET_LIGHT_CONTROL): - ESP_LOGV(TAG, "Handled set light control command"); + ESP_LOGV(TAG, "Set light control"); break; case lowbyte(CMD_BT_PASSWORD): - ESP_LOGV(TAG, "Handled set bluetooth password command"); + ESP_LOGV(TAG, "Set bluetooth password"); break; case lowbyte(CMD_QUERY): // Query parameters response { @@ -517,7 +519,7 @@ void LD2410Component::set_baud_rate(const std::string &state) { void LD2410Component::set_bluetooth_password(const std::string &password) { if (password.length() != 6) { - ESP_LOGE(TAG, "set_bluetooth_password(): invalid password length, must be exactly 6 chars '%s'", password.c_str()); + ESP_LOGE(TAG, "Password must be exactly 6 chars"); return; } this->set_config_mode_(true); @@ -529,7 +531,7 @@ void LD2410Component::set_bluetooth_password(const std::string &password) { void LD2410Component::set_engineering_mode(bool enable) { this->set_config_mode_(true); - last_engineering_mode_change_millis_ = millis(); + last_engineering_mode_change_millis_ = App.get_loop_component_start_time(); uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; this->send_command_(cmd, nullptr, 0); this->set_config_mode_(false); From 5362d1a89f19df22611413bb45a5d28cfe2e6abc Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Jun 2025 05:49:31 -0400 Subject: [PATCH 042/115] [esp32_hall] Add dummy component (#9125) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/esp32_hall/__init__.py | 0 esphome/components/esp32_hall/sensor.py | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 esphome/components/esp32_hall/__init__.py create mode 100644 esphome/components/esp32_hall/sensor.py diff --git a/esphome/components/esp32_hall/__init__.py b/esphome/components/esp32_hall/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/esp32_hall/sensor.py b/esphome/components/esp32_hall/sensor.py new file mode 100644 index 0000000000..b644389d3b --- /dev/null +++ b/esphome/components/esp32_hall/sensor.py @@ -0,0 +1,5 @@ +import esphome.config_validation as cv + +CONFIG_SCHEMA = cv.invalid( + "The esp32_hall component has been removed as of ESPHome 2025.7.0. See https://github.com/esphome/esphome/pull/9117 for details." +) From 16860e8a30fee6d4f986718f430f45533df48ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Wed, 25 Jun 2025 10:20:29 +0000 Subject: [PATCH 043/115] fix(MQTT): Call disconnect callback on DNS error (#9016) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/mqtt/mqtt_backend.h | 3 ++- esphome/components/mqtt/mqtt_client.cpp | 4 ++++ esphome/components/mqtt/mqtt_client.h | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_backend.h b/esphome/components/mqtt/mqtt_backend.h index 3962c40a42..0c1720ec34 100644 --- a/esphome/components/mqtt/mqtt_backend.h +++ b/esphome/components/mqtt/mqtt_backend.h @@ -17,7 +17,8 @@ enum class MQTTClientDisconnectReason : int8_t { MQTT_MALFORMED_CREDENTIALS = 4, MQTT_NOT_AUTHORIZED = 5, ESP8266_NOT_ENOUGH_SPACE = 6, - TLS_BAD_FINGERPRINT = 7 + TLS_BAD_FINGERPRINT = 7, + DNS_RESOLVE_ERROR = 8 }; /// internal struct for MQTT messages. diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 3ba1ac6077..20e0b4a499 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -229,6 +229,8 @@ void MQTTClientComponent::check_dnslookup_() { if (this->dns_resolve_error_) { ESP_LOGW(TAG, "Couldn't resolve IP address for '%s'", this->credentials_.address.c_str()); this->state_ = MQTT_CLIENT_DISCONNECTED; + this->disconnect_reason_ = MQTTClientDisconnectReason::DNS_RESOLVE_ERROR; + this->on_disconnect_.call(MQTTClientDisconnectReason::DNS_RESOLVE_ERROR); return; } @@ -698,7 +700,9 @@ void MQTTClientComponent::set_on_connect(mqtt_on_connect_callback_t &&callback) } void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&callback) { + auto callback_copy = callback; this->mqtt_backend_.set_on_disconnect(std::forward(callback)); + this->on_disconnect_.add(std::move(callback_copy)); } #if ASYNC_TCP_SSL_ENABLED diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index a95b122383..325ca56f4b 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -8,6 +8,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #if defined(USE_ESP32) #include "mqtt_backend_esp32.h" @@ -334,6 +335,7 @@ class MQTTClientComponent : public Component { uint32_t connect_begin_; uint32_t last_connected_{0}; optional disconnect_reason_{}; + CallbackManager on_disconnect_; bool publish_nan_as_none_{false}; bool wait_for_connection_{false}; From 7c2813421411e55c188b177df9ca105c383e43be Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Wed, 25 Jun 2025 13:36:24 +0300 Subject: [PATCH 044/115] Rename kVARh/VARh to kvarh/varh (#9191) --- esphome/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/const.py b/esphome/const.py index e61af6c5b5..5e551efc2c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1096,7 +1096,7 @@ UNIT_KILOMETER_PER_HOUR = "km/h" UNIT_KILOVOLT_AMPS = "kVA" UNIT_KILOVOLT_AMPS_HOURS = "kVAh" UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR" -UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" +UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" UNIT_LITRE = "L" @@ -1132,7 +1132,7 @@ UNIT_VOLT = "V" UNIT_VOLT_AMPS = "VA" UNIT_VOLT_AMPS_HOURS = "VAh" UNIT_VOLT_AMPS_REACTIVE = "var" -UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh" +UNIT_VOLT_AMPS_REACTIVE_HOURS = "varh" UNIT_WATT = "W" UNIT_WATT_HOURS = "Wh" From b18ff48b4ad8bd594fc8410c0f8e5b2cc2a2583c Mon Sep 17 00:00:00 2001 From: DanielV Date: Wed, 25 Jun 2025 14:03:41 +0200 Subject: [PATCH 045/115] [API] Sub devices and areas (#8544) Co-authored-by: J. Nick Koston Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .../alarm_control_panel/__init__.py | 7 +- esphome/components/api/api.proto | 41 ++ esphome/components/api/api_connection.cpp | 17 + esphome/components/api/api_connection.h | 3 + esphome/components/api/api_pb2.cpp | 388 ++++++++++++ esphome/components/api/api_pb2.h | 81 ++- esphome/components/binary_sensor/__init__.py | 7 +- esphome/components/button/__init__.py | 7 +- esphome/components/climate/__init__.py | 7 +- esphome/components/cover/__init__.py | 7 +- esphome/components/datetime/__init__.py | 6 +- esphome/components/demo/__init__.py | 4 +- esphome/components/esp32_camera/__init__.py | 4 +- esphome/components/event/__init__.py | 7 +- esphome/components/fan/__init__.py | 7 +- esphome/components/light/__init__.py | 6 +- esphome/components/lock/__init__.py | 7 +- esphome/components/media_player/__init__.py | 7 +- esphome/components/number/__init__.py | 7 +- esphome/components/select/__init__.py | 7 +- esphome/components/sensor/__init__.py | 6 +- esphome/components/switch/__init__.py | 7 +- esphome/components/text/__init__.py | 7 +- esphome/components/text_sensor/__init__.py | 7 +- esphome/components/update/__init__.py | 7 +- esphome/components/valve/__init__.py | 7 +- esphome/config_validation.py | 13 +- esphome/const.py | 3 + esphome/core/__init__.py | 4 + esphome/core/application.h | 46 +- esphome/core/area.h | 19 + esphome/core/config.py | 146 ++++- esphome/core/defines.h | 2 + esphome/core/device.h | 20 + esphome/core/entity_base.cpp | 23 +- esphome/core/entity_base.h | 18 + esphome/core/entity_helpers.py | 158 ++++- esphome/cpp_helpers.py | 22 - esphome/dashboard/util/text.py | 26 +- esphome/helpers.py | 47 ++ tests/components/ade7880/common.yaml | 38 +- .../alarm_control_panel/common.yaml | 2 +- .../components/binary_sensor_map/common.yaml | 6 +- tests/components/dallas_temp/common.yaml | 4 +- tests/components/esphome/common.yaml | 20 +- tests/components/heatpumpir/common.yaml | 6 +- tests/components/light/common.yaml | 2 +- tests/components/ltr390/common.yaml | 16 +- tests/components/lvgl/common.yaml | 14 +- tests/components/opentherm/common.yaml | 2 +- tests/components/packages/test.esp32-ard.yaml | 2 +- tests/components/packages/test.esp32-idf.yaml | 2 +- .../remote_transmitter/common-buttons.yaml | 2 +- tests/dummy_main.cpp | 2 +- tests/integration/conftest.py | 1 + .../fixtures/areas_and_devices.yaml | 57 ++ ...plicate_entities_on_different_devices.yaml | 154 +++++ tests/integration/fixtures/legacy_area.yaml | 15 + tests/integration/test_areas_and_devices.py | 121 ++++ tests/integration/test_duplicate_entities.py | 184 ++++++ tests/integration/test_legacy_area.py | 41 ++ tests/unit_tests/conftest.py | 9 + tests/unit_tests/core/__init__.py | 0 tests/unit_tests/core/common.py | 33 + tests/unit_tests/core/conftest.py | 18 + tests/unit_tests/core/test_config.py | 225 +++++++ tests/unit_tests/core/test_entity_helpers.py | 595 ++++++++++++++++++ .../core/config/area_id_collision.yaml | 10 + .../core/config/area_id_hash_collision.yaml | 10 + .../core/config/device_duplicate_id.yaml | 10 + .../core/config/device_id_collision.yaml | 10 + .../core/config/device_invalid_area.yaml | 12 + .../core/config/device_without_area.yaml | 7 + .../core/config/legacy_string_area.yaml | 5 + .../core/config/multiple_areas_devices.yaml | 22 + .../core/config/valid_area_device.yaml | 11 + .../core/entity_helpers/duplicate_entity.yaml | 13 + .../duplicate_entity_with_devices.yaml | 26 + .../entity_different_platforms.yaml | 20 + 79 files changed, 2756 insertions(+), 186 deletions(-) create mode 100644 esphome/core/area.h create mode 100644 esphome/core/device.h create mode 100644 tests/integration/fixtures/areas_and_devices.yaml create mode 100644 tests/integration/fixtures/duplicate_entities_on_different_devices.yaml create mode 100644 tests/integration/fixtures/legacy_area.yaml create mode 100644 tests/integration/test_areas_and_devices.py create mode 100644 tests/integration/test_duplicate_entities.py create mode 100644 tests/integration/test_legacy_area.py create mode 100644 tests/unit_tests/core/__init__.py create mode 100644 tests/unit_tests/core/common.py create mode 100644 tests/unit_tests/core/conftest.py create mode 100644 tests/unit_tests/core/test_config.py create mode 100644 tests/unit_tests/core/test_entity_helpers.py create mode 100644 tests/unit_tests/fixtures/core/config/area_id_collision.yaml create mode 100644 tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml create mode 100644 tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml create mode 100644 tests/unit_tests/fixtures/core/config/device_id_collision.yaml create mode 100644 tests/unit_tests/fixtures/core/config/device_invalid_area.yaml create mode 100644 tests/unit_tests/fixtures/core/config/device_without_area.yaml create mode 100644 tests/unit_tests/fixtures/core/config/legacy_string_area.yaml create mode 100644 tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml create mode 100644 tests/unit_tests/fixtures/core/config/valid_area_device.yaml create mode 100644 tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity.yaml create mode 100644 tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity_with_devices.yaml create mode 100644 tests/unit_tests/fixtures/core/entity_helpers/entity_different_platforms.yaml diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index e88050132a..6d37d53a4c 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -14,8 +14,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@grahambrown11", "@hwstar"] IS_PLATFORM_COMPONENT = True @@ -149,6 +149,9 @@ _ALARM_CONTROL_PANEL_SCHEMA = ( ) +_ALARM_CONTROL_PANEL_SCHEMA.add_extra(entity_duplicate_validator("alarm_control_panel")) + + def alarm_control_panel_schema( class_: MockObjClass, *, @@ -190,7 +193,7 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( async def setup_alarm_control_panel_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "alarm_control_panel") for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index b23652a982..58a0b52555 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -188,6 +188,17 @@ message DeviceInfoRequest { // Empty } +message AreaInfo { + uint32 area_id = 1; + string name = 2; +} + +message DeviceInfo { + uint32 device_id = 1; + string name = 2; + uint32 area_id = 3; +} + message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; @@ -236,6 +247,12 @@ message DeviceInfoResponse { // Supports receiving and saving api encryption key bool api_encryption_supported = 19; + + repeated DeviceInfo devices = 20; + repeated AreaInfo areas = 21; + + // Top-level area info to phase out suggested_area + AreaInfo area = 22; } message ListEntitiesRequest { @@ -280,6 +297,7 @@ message ListEntitiesBinarySensorResponse { bool disabled_by_default = 7; string icon = 8; EntityCategory entity_category = 9; + uint32 device_id = 10; } message BinarySensorStateResponse { option (id) = 21; @@ -315,6 +333,7 @@ message ListEntitiesCoverResponse { string icon = 10; EntityCategory entity_category = 11; bool supports_stop = 12; + uint32 device_id = 13; } enum LegacyCoverState { @@ -388,6 +407,7 @@ message ListEntitiesFanResponse { string icon = 10; EntityCategory entity_category = 11; repeated string supported_preset_modes = 12; + uint32 device_id = 13; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -471,6 +491,7 @@ message ListEntitiesLightResponse { bool disabled_by_default = 13; string icon = 14; EntityCategory entity_category = 15; + uint32 device_id = 16; } message LightStateResponse { option (id) = 24; @@ -563,6 +584,7 @@ message ListEntitiesSensorResponse { SensorLastResetType legacy_last_reset_type = 11; bool disabled_by_default = 12; EntityCategory entity_category = 13; + uint32 device_id = 14; } message SensorStateResponse { option (id) = 25; @@ -595,6 +617,7 @@ message ListEntitiesSwitchResponse { bool disabled_by_default = 7; EntityCategory entity_category = 8; string device_class = 9; + uint32 device_id = 10; } message SwitchStateResponse { option (id) = 26; @@ -632,6 +655,7 @@ message ListEntitiesTextSensorResponse { bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; + uint32 device_id = 9; } message TextSensorStateResponse { option (id) = 27; @@ -814,6 +838,7 @@ message ListEntitiesCameraResponse { bool disabled_by_default = 5; string icon = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message CameraImageResponse { @@ -916,6 +941,7 @@ message ListEntitiesClimateResponse { bool supports_target_humidity = 23; float visual_min_humidity = 24; float visual_max_humidity = 25; + uint32 device_id = 26; } message ClimateStateResponse { option (id) = 47; @@ -999,6 +1025,7 @@ message ListEntitiesNumberResponse { string unit_of_measurement = 11; NumberMode mode = 12; string device_class = 13; + uint32 device_id = 14; } message NumberStateResponse { option (id) = 50; @@ -1039,6 +1066,7 @@ message ListEntitiesSelectResponse { repeated string options = 6; bool disabled_by_default = 7; EntityCategory entity_category = 8; + uint32 device_id = 9; } message SelectStateResponse { option (id) = 53; @@ -1081,6 +1109,7 @@ message ListEntitiesSirenResponse { bool supports_duration = 8; bool supports_volume = 9; EntityCategory entity_category = 10; + uint32 device_id = 11; } message SirenStateResponse { option (id) = 56; @@ -1144,6 +1173,7 @@ message ListEntitiesLockResponse { // Not yet implemented: string code_format = 11; + uint32 device_id = 12; } message LockStateResponse { option (id) = 59; @@ -1183,6 +1213,7 @@ message ListEntitiesButtonResponse { bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; + uint32 device_id = 9; } message ButtonCommandRequest { option (id) = 62; @@ -1238,6 +1269,8 @@ message ListEntitiesMediaPlayerResponse { bool supports_pause = 8; repeated MediaPlayerSupportedFormat supported_formats = 9; + + uint32 device_id = 10; } message MediaPlayerStateResponse { option (id) = 64; @@ -1778,6 +1811,7 @@ message ListEntitiesAlarmControlPanelResponse { uint32 supported_features = 8; bool requires_code = 9; bool requires_code_to_arm = 10; + uint32 device_id = 11; } message AlarmControlPanelStateResponse { @@ -1823,6 +1857,7 @@ message ListEntitiesTextResponse { uint32 max_length = 9; string pattern = 10; TextMode mode = 11; + uint32 device_id = 12; } message TextStateResponse { option (id) = 98; @@ -1863,6 +1898,7 @@ message ListEntitiesDateResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message DateStateResponse { option (id) = 101; @@ -1906,6 +1942,7 @@ message ListEntitiesTimeResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message TimeStateResponse { option (id) = 104; @@ -1952,6 +1989,7 @@ message ListEntitiesEventResponse { string device_class = 8; repeated string event_types = 9; + uint32 device_id = 10; } message EventResponse { option (id) = 108; @@ -1983,6 +2021,7 @@ message ListEntitiesValveResponse { bool assumed_state = 9; bool supports_position = 10; bool supports_stop = 11; + uint32 device_id = 12; } enum ValveOperation { @@ -2029,6 +2068,7 @@ message ListEntitiesDateTimeResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message DateTimeStateResponse { option (id) = 113; @@ -2069,6 +2109,7 @@ message ListEntitiesUpdateResponse { bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; + uint32 device_id = 9; } message UpdateStateResponse { option (id) = 117; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ef791d462c..634174ce0a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1629,6 +1629,23 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { #endif #ifdef USE_API_NOISE resp.api_encryption_supported = true; +#endif +#ifdef USE_DEVICES + for (auto const &device : App.get_devices()) { + DeviceInfo device_info; + device_info.device_id = device->get_device_id(); + device_info.name = device->get_name(); + device_info.area_id = device->get_area_id(); + resp.devices.push_back(device_info); + } +#endif +#ifdef USE_AREAS + for (auto const &area : App.get_areas()) { + AreaInfo area_info; + area_info.area_id = area->get_area_id(); + area_info.name = area->get_name(); + resp.areas.push_back(area_info); + } #endif return resp; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 66b7ce38a7..da12a3e449 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -301,6 +301,9 @@ class APIConnection : public APIServerConnection { response.icon = entity->get_icon(); response.disabled_by_default = entity->is_disabled_by_default(); response.entity_category = static_cast(entity->get_entity_category()); +#ifdef USE_DEVICES + response.device_id = entity->get_device_id(); +#endif } // Helper function to fill common entity state fields diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 517b4d41b4..9793565ee5 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -812,6 +812,103 @@ void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {} #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } #endif +bool AreaInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->area_id = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool AreaInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->name = value.as_string(); + return true; + } + default: + return false; + } +} +void AreaInfo::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->area_id); + buffer.encode_string(2, this->name); +} +void AreaInfo::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint32_field(total_size, 1, this->area_id, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void AreaInfo::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("AreaInfo {\n"); + out.append(" area_id: "); + sprintf(buffer, "%" PRIu32, this->area_id); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool DeviceInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->device_id = value.as_uint32(); + return true; + } + case 3: { + this->area_id = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool DeviceInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->name = value.as_string(); + return true; + } + default: + return false; + } +} +void DeviceInfo::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->device_id); + buffer.encode_string(2, this->name); + buffer.encode_uint32(3, this->area_id); +} +void DeviceInfo::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_uint32_field(total_size, 1, this->area_id, false); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void DeviceInfo::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DeviceInfo {\n"); + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" area_id: "); + sprintf(buffer, "%" PRIu32, this->area_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -896,6 +993,18 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v this->bluetooth_mac_address = value.as_string(); return true; } + case 20: { + this->devices.push_back(value.as_message()); + return true; + } + case 21: { + this->areas.push_back(value.as_message()); + return true; + } + case 22: { + this->area = value.as_message(); + return true; + } default: return false; } @@ -920,6 +1029,13 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(16, this->suggested_area); buffer.encode_string(18, this->bluetooth_mac_address); buffer.encode_bool(19, this->api_encryption_supported); + for (auto &it : this->devices) { + buffer.encode_message(20, it, true); + } + for (auto &it : this->areas) { + buffer.encode_message(21, it, true); + } + buffer.encode_message(22, this->area); } void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->uses_password, false); @@ -941,6 +1057,9 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 2, this->suggested_area, false); ProtoSize::add_string_field(total_size, 2, this->bluetooth_mac_address, false); ProtoSize::add_bool_field(total_size, 2, this->api_encryption_supported, false); + ProtoSize::add_repeated_message(total_size, 2, this->devices); + ProtoSize::add_repeated_message(total_size, 2, this->areas); + ProtoSize::add_message_object(total_size, 2, this->area, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { @@ -1026,6 +1145,22 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append(" api_encryption_supported: "); out.append(YESNO(this->api_encryption_supported)); out.append("\n"); + + for (const auto &it : this->devices) { + out.append(" devices: "); + it.dump_to(out); + out.append("\n"); + } + + for (const auto &it : this->areas) { + out.append(" areas: "); + it.dump_to(out); + out.append("\n"); + } + + out.append(" area: "); + this->area.dump_to(out); + out.append("\n"); out.append("}"); } #endif @@ -1052,6 +1187,10 @@ bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVar this->entity_category = value.as_enum(); return true; } + case 10: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -1102,6 +1241,7 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(7, this->disabled_by_default); buffer.encode_string(8, this->icon); buffer.encode_enum(9, this->entity_category); + buffer.encode_uint32(10, this->device_id); } void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -1113,6 +1253,7 @@ void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) cons ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { @@ -1154,6 +1295,11 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -1236,6 +1382,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->supports_stop = value.as_bool(); return true; } + case 13: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -1289,6 +1439,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(10, this->icon); buffer.encode_enum(11, this->entity_category); buffer.encode_bool(12, this->supports_stop); + buffer.encode_uint32(13, this->device_id); } void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -1303,6 +1454,7 @@ void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCoverResponse::dump_to(std::string &out) const { @@ -1356,6 +1508,11 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append(" supports_stop: "); out.append(YESNO(this->supports_stop)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -1565,6 +1722,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->entity_category = value.as_enum(); return true; } + case 13: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -1620,6 +1781,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->supported_preset_modes) { buffer.encode_string(12, it, true); } + buffer.encode_uint32(13, this->device_id); } void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -1638,6 +1800,7 @@ void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, it, true); } } + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesFanResponse::dump_to(std::string &out) const { @@ -1694,6 +1857,11 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append("'").append(it).append("'"); out.append("\n"); } + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -1987,6 +2155,10 @@ bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->entity_category = value.as_enum(); return true; } + case 16: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -2055,6 +2227,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(13, this->disabled_by_default); buffer.encode_string(14, this->icon); buffer.encode_enum(15, this->entity_category); + buffer.encode_uint32(16, this->device_id); } void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -2080,6 +2253,7 @@ void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 2, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesLightResponse::dump_to(std::string &out) const { @@ -2151,6 +2325,11 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -2658,6 +2837,10 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 14: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -2716,6 +2899,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(11, this->legacy_last_reset_type); buffer.encode_bool(12, this->disabled_by_default); buffer.encode_enum(13, this->entity_category); + buffer.encode_uint32(14, this->device_id); } void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -2731,6 +2915,7 @@ void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_last_reset_type), false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSensorResponse::dump_to(std::string &out) const { @@ -2789,6 +2974,11 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -2860,6 +3050,10 @@ bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 10: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -2910,6 +3104,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(7, this->disabled_by_default); buffer.encode_enum(8, this->entity_category); buffer.encode_string(9, this->device_class); + buffer.encode_uint32(10, this->device_id); } void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -2921,6 +3116,7 @@ void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSwitchResponse::dump_to(std::string &out) const { @@ -2962,6 +3158,11 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -3061,6 +3262,10 @@ bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarIn this->entity_category = value.as_enum(); return true; } + case 9: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -3110,6 +3315,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); buffer.encode_string(8, this->device_class); + buffer.encode_uint32(9, this->device_id); } void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -3120,6 +3326,7 @@ void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { @@ -3157,6 +3364,11 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -3922,6 +4134,10 @@ bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -3966,6 +4182,7 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(5, this->disabled_by_default); buffer.encode_string(6, this->icon); buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->device_id); } void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -3975,6 +4192,7 @@ void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesCameraResponse::dump_to(std::string &out) const { @@ -4008,6 +4226,11 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -4156,6 +4379,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->supports_target_humidity = value.as_bool(); return true; } + case 26: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -4262,6 +4489,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(23, this->supports_target_humidity); buffer.encode_float(24, this->visual_min_humidity); buffer.encode_float(25, this->visual_max_humidity); + buffer.encode_uint32(26, this->device_id); } void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -4313,6 +4541,7 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 2, this->supports_target_humidity, false); ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_min_humidity != 0.0f, false); ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_max_humidity != 0.0f, false); + ProtoSize::add_uint32_field(total_size, 2, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { @@ -4436,6 +4665,11 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { sprintf(buffer, "%g", this->visual_max_humidity); out.append(buffer); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -4901,6 +5135,10 @@ bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->mode = value.as_enum(); return true; } + case 14: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -4971,6 +5209,7 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(11, this->unit_of_measurement); buffer.encode_enum(12, this->mode); buffer.encode_string(13, this->device_class); + buffer.encode_uint32(14, this->device_id); } void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -4986,6 +5225,7 @@ void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesNumberResponse::dump_to(std::string &out) const { @@ -5046,6 +5286,11 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -5151,6 +5396,10 @@ bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 9: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5202,6 +5451,7 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_bool(7, this->disabled_by_default); buffer.encode_enum(8, this->entity_category); + buffer.encode_uint32(9, this->device_id); } void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -5216,6 +5466,7 @@ void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { } ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSelectResponse::dump_to(std::string &out) const { @@ -5255,6 +5506,11 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -5378,6 +5634,10 @@ bool ListEntitiesSirenResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->entity_category = value.as_enum(); return true; } + case 11: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5431,6 +5691,7 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(8, this->supports_duration); buffer.encode_bool(9, this->supports_volume); buffer.encode_enum(10, this->entity_category); + buffer.encode_uint32(11, this->device_id); } void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -5447,6 +5708,7 @@ void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->supports_duration, false); ProtoSize::add_bool_field(total_size, 1, this->supports_volume, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSirenResponse::dump_to(std::string &out) const { @@ -5494,6 +5756,11 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -5683,6 +5950,10 @@ bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->requires_code = value.as_bool(); return true; } + case 12: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5735,6 +6006,7 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(9, this->supports_open); buffer.encode_bool(10, this->requires_code); buffer.encode_string(11, this->code_format); + buffer.encode_uint32(12, this->device_id); } void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -5748,6 +6020,7 @@ void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->supports_open, false); ProtoSize::add_bool_field(total_size, 1, this->requires_code, false); ProtoSize::add_string_field(total_size, 1, this->code_format, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesLockResponse::dump_to(std::string &out) const { @@ -5797,6 +6070,11 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { out.append(" code_format: "); out.append("'").append(this->code_format).append("'"); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -5922,6 +6200,10 @@ bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 9: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5971,6 +6253,7 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); buffer.encode_string(8, this->device_class); + buffer.encode_uint32(9, this->device_id); } void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -5981,6 +6264,7 @@ void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesButtonResponse::dump_to(std::string &out) const { @@ -6018,6 +6302,11 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -6135,6 +6424,10 @@ bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarI this->supports_pause = value.as_bool(); return true; } + case 10: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -6187,6 +6480,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->supported_formats) { buffer.encode_message(9, it, true); } + buffer.encode_uint32(10, this->device_id); } void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -6198,6 +6492,7 @@ void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_bool_field(total_size, 1, this->supports_pause, false); ProtoSize::add_repeated_message(total_size, 1, this->supported_formats); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { @@ -6241,6 +6536,11 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { it.dump_to(out); out.append("\n"); } + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -8551,6 +8851,10 @@ bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, Pro this->requires_code_to_arm = value.as_bool(); return true; } + case 11: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -8598,6 +8902,7 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) cons buffer.encode_uint32(8, this->supported_features); buffer.encode_bool(9, this->requires_code); buffer.encode_bool(10, this->requires_code_to_arm); + buffer.encode_uint32(11, this->device_id); } void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -8610,6 +8915,7 @@ void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) ProtoSize::add_uint32_field(total_size, 1, this->supported_features, false); ProtoSize::add_bool_field(total_size, 1, this->requires_code, false); ProtoSize::add_bool_field(total_size, 1, this->requires_code_to_arm, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { @@ -8656,6 +8962,11 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { out.append(" requires_code_to_arm: "); out.append(YESNO(this->requires_code_to_arm)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -8783,6 +9094,10 @@ bool ListEntitiesTextResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->mode = value.as_enum(); return true; } + case 12: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -8835,6 +9150,7 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(9, this->max_length); buffer.encode_string(10, this->pattern); buffer.encode_enum(11, this->mode); + buffer.encode_uint32(12, this->device_id); } void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -8848,6 +9164,7 @@ void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->max_length, false); ProtoSize::add_string_field(total_size, 1, this->pattern, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTextResponse::dump_to(std::string &out) const { @@ -8899,6 +9216,11 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { out.append(" mode: "); out.append(proto_enum_to_string(this->mode)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -9014,6 +9336,10 @@ bool ListEntitiesDateResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->entity_category = value.as_enum(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9058,6 +9384,7 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->device_id); } void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9067,6 +9394,7 @@ void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesDateResponse::dump_to(std::string &out) const { @@ -9100,6 +9428,11 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -9255,6 +9588,10 @@ bool ListEntitiesTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->entity_category = value.as_enum(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9299,6 +9636,7 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->device_id); } void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9308,6 +9646,7 @@ void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesTimeResponse::dump_to(std::string &out) const { @@ -9341,6 +9680,11 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -9496,6 +9840,10 @@ bool ListEntitiesEventResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->entity_category = value.as_enum(); return true; } + case 10: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9552,6 +9900,7 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->event_types) { buffer.encode_string(9, it, true); } + buffer.encode_uint32(10, this->device_id); } void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9567,6 +9916,7 @@ void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, it, true); } } + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesEventResponse::dump_to(std::string &out) const { @@ -9610,6 +9960,11 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { out.append("'").append(it).append("'"); out.append("\n"); } + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -9678,6 +10033,10 @@ bool ListEntitiesValveResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->supports_stop = value.as_bool(); return true; } + case 12: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9730,6 +10089,7 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(9, this->assumed_state); buffer.encode_bool(10, this->supports_position); buffer.encode_bool(11, this->supports_stop); + buffer.encode_uint32(12, this->device_id); } void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9743,6 +10103,7 @@ void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false); ProtoSize::add_bool_field(total_size, 1, this->supports_position, false); ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesValveResponse::dump_to(std::string &out) const { @@ -9792,6 +10153,11 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const { out.append(" supports_stop: "); out.append(YESNO(this->supports_stop)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -9923,6 +10289,10 @@ bool ListEntitiesDateTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt this->entity_category = value.as_enum(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9967,6 +10337,7 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->device_id); } void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9976,6 +10347,7 @@ void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { @@ -10009,6 +10381,11 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif @@ -10114,6 +10491,10 @@ bool ListEntitiesUpdateResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 9: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -10163,6 +10544,7 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); buffer.encode_string(8, this->device_class); + buffer.encode_uint32(9, this->device_id); } void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -10173,6 +10555,7 @@ void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesUpdateResponse::dump_to(std::string &out) const { @@ -10210,6 +10593,11 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const { out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); + + out.append(" device_id: "); + sprintf(buffer, "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 7d92125290..6a5b51d3a1 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -264,6 +264,7 @@ class InfoResponseProtoMessage : public ProtoMessage { bool disabled_by_default{false}; std::string icon{}; enums::EntityCategory entity_category{}; + uint32_t device_id{0}; protected: }; @@ -415,10 +416,39 @@ class DeviceInfoRequest : public ProtoMessage { protected: }; +class AreaInfo : public ProtoMessage { + public: + uint32_t area_id{0}; + std::string name{}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class DeviceInfo : public ProtoMessage { + public: + uint32_t device_id{0}; + std::string name{}; + uint32_t area_id{0}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; class DeviceInfoResponse : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 10; - static constexpr uint16_t ESTIMATED_SIZE = 129; + static constexpr uint16_t ESTIMATED_SIZE = 219; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "device_info_response"; } #endif @@ -441,6 +471,9 @@ class DeviceInfoResponse : public ProtoMessage { std::string suggested_area{}; std::string bluetooth_mac_address{}; bool api_encryption_supported{false}; + std::vector devices{}; + std::vector areas{}; + AreaInfo area{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -493,7 +526,7 @@ class SubscribeStatesRequest : public ProtoMessage { class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 12; - static constexpr uint16_t ESTIMATED_SIZE = 56; + static constexpr uint16_t ESTIMATED_SIZE = 60; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_binary_sensor_response"; } #endif @@ -532,7 +565,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage { class ListEntitiesCoverResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 13; - static constexpr uint16_t ESTIMATED_SIZE = 62; + static constexpr uint16_t ESTIMATED_SIZE = 66; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_cover_response"; } #endif @@ -601,7 +634,7 @@ class CoverCommandRequest : public ProtoMessage { class ListEntitiesFanResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 14; - static constexpr uint16_t ESTIMATED_SIZE = 73; + static constexpr uint16_t ESTIMATED_SIZE = 77; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_fan_response"; } #endif @@ -679,7 +712,7 @@ class FanCommandRequest : public ProtoMessage { class ListEntitiesLightResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 15; - static constexpr uint16_t ESTIMATED_SIZE = 85; + static constexpr uint16_t ESTIMATED_SIZE = 90; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_light_response"; } #endif @@ -780,7 +813,7 @@ class LightCommandRequest : public ProtoMessage { class ListEntitiesSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 16; - static constexpr uint16_t ESTIMATED_SIZE = 73; + static constexpr uint16_t ESTIMATED_SIZE = 77; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_sensor_response"; } #endif @@ -823,7 +856,7 @@ class SensorStateResponse : public StateResponseProtoMessage { class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 17; - static constexpr uint16_t ESTIMATED_SIZE = 56; + static constexpr uint16_t ESTIMATED_SIZE = 60; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_switch_response"; } #endif @@ -880,7 +913,7 @@ class SwitchCommandRequest : public ProtoMessage { class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 18; - static constexpr uint16_t ESTIMATED_SIZE = 54; + static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_text_sensor_response"; } #endif @@ -1196,7 +1229,7 @@ class ExecuteServiceRequest : public ProtoMessage { class ListEntitiesCameraResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 43; - static constexpr uint16_t ESTIMATED_SIZE = 45; + static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_camera_response"; } #endif @@ -1253,7 +1286,7 @@ class CameraImageRequest : public ProtoMessage { class ListEntitiesClimateResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 46; - static constexpr uint16_t ESTIMATED_SIZE = 151; + static constexpr uint16_t ESTIMATED_SIZE = 156; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_climate_response"; } #endif @@ -1362,7 +1395,7 @@ class ClimateCommandRequest : public ProtoMessage { class ListEntitiesNumberResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 49; - static constexpr uint16_t ESTIMATED_SIZE = 80; + static constexpr uint16_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_number_response"; } #endif @@ -1423,7 +1456,7 @@ class NumberCommandRequest : public ProtoMessage { class ListEntitiesSelectResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 52; - static constexpr uint16_t ESTIMATED_SIZE = 63; + static constexpr uint16_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_select_response"; } #endif @@ -1481,7 +1514,7 @@ class SelectCommandRequest : public ProtoMessage { class ListEntitiesSirenResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 55; - static constexpr uint16_t ESTIMATED_SIZE = 67; + static constexpr uint16_t ESTIMATED_SIZE = 71; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_siren_response"; } #endif @@ -1547,7 +1580,7 @@ class SirenCommandRequest : public ProtoMessage { class ListEntitiesLockResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 58; - static constexpr uint16_t ESTIMATED_SIZE = 60; + static constexpr uint16_t ESTIMATED_SIZE = 64; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_lock_response"; } #endif @@ -1609,7 +1642,7 @@ class LockCommandRequest : public ProtoMessage { class ListEntitiesButtonResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 61; - static constexpr uint16_t ESTIMATED_SIZE = 54; + static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_button_response"; } #endif @@ -1662,7 +1695,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage { class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 63; - static constexpr uint16_t ESTIMATED_SIZE = 81; + static constexpr uint16_t ESTIMATED_SIZE = 85; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_media_player_response"; } #endif @@ -2532,7 +2565,7 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 94; - static constexpr uint16_t ESTIMATED_SIZE = 53; + static constexpr uint16_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_alarm_control_panel_response"; } #endif @@ -2592,7 +2625,7 @@ class AlarmControlPanelCommandRequest : public ProtoMessage { class ListEntitiesTextResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 97; - static constexpr uint16_t ESTIMATED_SIZE = 64; + static constexpr uint16_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_text_response"; } #endif @@ -2653,7 +2686,7 @@ class TextCommandRequest : public ProtoMessage { class ListEntitiesDateResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 100; - static constexpr uint16_t ESTIMATED_SIZE = 45; + static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_date_response"; } #endif @@ -2713,7 +2746,7 @@ class DateCommandRequest : public ProtoMessage { class ListEntitiesTimeResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 103; - static constexpr uint16_t ESTIMATED_SIZE = 45; + static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_time_response"; } #endif @@ -2773,7 +2806,7 @@ class TimeCommandRequest : public ProtoMessage { class ListEntitiesEventResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 107; - static constexpr uint16_t ESTIMATED_SIZE = 72; + static constexpr uint16_t ESTIMATED_SIZE = 76; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_event_response"; } #endif @@ -2811,7 +2844,7 @@ class EventResponse : public StateResponseProtoMessage { class ListEntitiesValveResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 109; - static constexpr uint16_t ESTIMATED_SIZE = 60; + static constexpr uint16_t ESTIMATED_SIZE = 64; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_valve_response"; } #endif @@ -2873,7 +2906,7 @@ class ValveCommandRequest : public ProtoMessage { class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 112; - static constexpr uint16_t ESTIMATED_SIZE = 45; + static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_date_time_response"; } #endif @@ -2928,7 +2961,7 @@ class DateTimeCommandRequest : public ProtoMessage { class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 116; - static constexpr uint16_t ESTIMATED_SIZE = 54; + static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP static constexpr const char *message_name() { return "list_entities_update_response"; } #endif diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index bc26c09622..fd9551b850 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -60,8 +60,8 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from esphome.util import Registry CODEOWNERS = ["@esphome/core"] @@ -491,6 +491,9 @@ _BINARY_SENSOR_SCHEMA = ( ) +_BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor")) + + def binary_sensor_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -521,7 +524,7 @@ BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor")) async def setup_binary_sensor_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "binary_sensor") if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 892bf62f3a..ed2670a5c5 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -18,8 +18,8 @@ from esphome.const import ( DEVICE_CLASS_UPDATE, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -61,6 +61,9 @@ _BUTTON_SCHEMA = ( ) +_BUTTON_SCHEMA.add_extra(entity_duplicate_validator("button")) + + def button_schema( class_: MockObjClass, *, @@ -87,7 +90,7 @@ BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button")) async def setup_button_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "button") for conf in config.get(CONF_ON_PRESS, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 52938a17d0..9530ecdcca 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -48,8 +48,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -247,6 +247,9 @@ _CLIMATE_SCHEMA = ( ) +_CLIMATE_SCHEMA.add_extra(entity_duplicate_validator("climate")) + + def climate_schema( class_: MockObjClass, *, @@ -273,7 +276,7 @@ CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate")) async def setup_climate_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "climate") visual = config[CONF_VISUAL] if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 9fe7593eab..cd97a38ecc 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -33,8 +33,8 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -126,6 +126,9 @@ _COVER_SCHEMA = ( ) +_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover")) + + def cover_schema( class_: MockObjClass, *, @@ -154,7 +157,7 @@ COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover")) async def setup_cover_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "cover") if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 24fbf5a1ec..4788810965 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -22,8 +22,8 @@ from esphome.const import ( CONF_YEAR, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@rfdarter", "@jesserockz"] @@ -84,6 +84,8 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) ).add_extra(_validate_time_present) +_DATETIME_SCHEMA.add_extra(entity_duplicate_validator("datetime")) + def date_schema(class_: MockObjClass) -> cv.Schema: schema = cv.Schema( @@ -133,7 +135,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema: async def setup_datetime_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "datetime") if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py index 0a56073284..2af0c18c18 100644 --- a/esphome/components/demo/__init__.py +++ b/esphome/components/demo/__init__.py @@ -455,7 +455,7 @@ CONFIG_SCHEMA = cv.Schema( CONF_NAME: "Demo Plain Sensor", }, { - CONF_NAME: "Demo Temperature Sensor", + CONF_NAME: "Demo Temperature Sensor 1", CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, CONF_ICON: ICON_THERMOMETER, CONF_ACCURACY_DECIMALS: 1, @@ -463,7 +463,7 @@ CONFIG_SCHEMA = cv.Schema( CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { - CONF_NAME: "Demo Temperature Sensor", + CONF_NAME: "Demo Temperature Sensor 2", CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, CONF_ICON: ICON_THERMOMETER, CONF_ACCURACY_DECIMALS: 1, diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 05522265ae..cfca0ed6fc 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -19,7 +19,7 @@ from esphome.const import ( CONF_VSYNC_PIN, ) from esphome.core import CORE -from esphome.cpp_helpers import setup_entity +from esphome.core.entity_helpers import setup_entity DEPENDENCIES = ["esp32"] @@ -284,7 +284,7 @@ SETTERS = { async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await setup_entity(var, config) + await setup_entity(var, config, "camera") await cg.register_component(var, config) for key, setter in SETTERS.items(): diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index e7ab489a25..3aff96a48e 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -18,8 +18,8 @@ from esphome.const import ( DEVICE_CLASS_MOTION, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@nohat"] IS_PLATFORM_COMPONENT = True @@ -59,6 +59,9 @@ _EVENT_SCHEMA = ( ) +_EVENT_SCHEMA.add_extra(entity_duplicate_validator("event")) + + def event_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -88,7 +91,7 @@ EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event")) async def setup_event_core_(var, config, *, event_types: list[str]): - await setup_entity(var, config) + await setup_entity(var, config, "event") for conf in config.get(CONF_ON_EVENT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index c6ff938cd6..0b1d39575d 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -32,7 +32,7 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority -from esphome.cpp_helpers import setup_entity +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity IS_PLATFORM_COMPONENT = True @@ -161,6 +161,9 @@ _FAN_SCHEMA = ( ) +_FAN_SCHEMA.add_extra(entity_duplicate_validator("fan")) + + def fan_schema( class_: cg.Pvariable, *, @@ -225,7 +228,7 @@ def validate_preset_modes(value): async def setup_fan_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "fan") cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index a013029fc2..7ab899edb2 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -38,8 +38,8 @@ from esphome.const import ( CONF_WHITE, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from .automation import LIGHT_STATE_SCHEMA from .effects import ( @@ -110,6 +110,8 @@ LIGHT_SCHEMA = ( ) ) +LIGHT_SCHEMA.add_extra(entity_duplicate_validator("light")) + BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( { cv.Optional(CONF_EFFECTS): validate_effects(BINARY_EFFECTS), @@ -207,7 +209,7 @@ def validate_color_temperature_channels(value): async def setup_light_core_(light_var, output_var, config): - await setup_entity(light_var, config) + await setup_entity(light_var, config, "light") cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 0fb67e3948..e62d9f3e2b 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -14,8 +14,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -67,6 +67,9 @@ _LOCK_SCHEMA = ( ) +_LOCK_SCHEMA.add_extra(entity_duplicate_validator("lock")) + + def lock_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -94,7 +97,7 @@ LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock")) async def _setup_lock_core(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "lock") for conf in config.get(CONF_ON_LOCK, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index ef76419de3..ccded1deb2 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -11,9 +11,9 @@ from esphome.const import ( CONF_VOLUME, ) from esphome.core import CORE +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.coroutine import coroutine_with_priority from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@jesserockz"] @@ -81,7 +81,7 @@ IsAnnouncingCondition = media_player_ns.class_( async def setup_media_player_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "media_player") for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) @@ -143,6 +143,8 @@ _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( } ) +_MEDIA_PLAYER_SCHEMA.add_extra(entity_duplicate_validator("media_player")) + def media_player_schema( class_: MockObjClass, @@ -166,7 +168,6 @@ def media_player_schema( MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer) MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player")) - MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( cv.Schema( { diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 2567d9ffe1..4beed57188 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -76,8 +76,8 @@ from esphome.const import ( DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ @@ -207,6 +207,9 @@ _NUMBER_SCHEMA = ( ) +_NUMBER_SCHEMA.add_extra(entity_duplicate_validator("number")) + + def number_schema( class_: MockObjClass, *, @@ -237,7 +240,7 @@ NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number")) async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: float ): - await setup_entity(var, config) + await setup_entity(var, config, "number") cg.add(var.traits.set_min_value(min_value)) cg.add(var.traits.set_max_value(max_value)) diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index e14a9351a0..ed1f6c020d 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -17,8 +17,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -65,6 +65,9 @@ _SELECT_SCHEMA = ( ) +_SELECT_SCHEMA.add_extra(entity_duplicate_validator("select")) + + def select_schema( class_: MockObjClass, *, @@ -89,7 +92,7 @@ SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select")) async def setup_select_core_(var, config, *, options: list[str]): - await setup_entity(var, config) + await setup_entity(var, config, "select") cg.add(var.traits.set_options(options)) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 1ad3cfabee..ea74361d51 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -101,8 +101,8 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from esphome.util import Registry CODEOWNERS = ["@esphome/core"] @@ -318,6 +318,8 @@ _SENSOR_SCHEMA = ( ) ) +_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("sensor")) + def sensor_schema( class_: MockObjClass = cv.UNDEFINED, @@ -787,7 +789,7 @@ async def build_filters(config): async def setup_sensor_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "sensor") if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 0211c648fc..c09675069f 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -20,8 +20,8 @@ from esphome.const import ( DEVICE_CLASS_SWITCH, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -91,6 +91,9 @@ _SWITCH_SCHEMA = ( ) +_SWITCH_SCHEMA.add_extra(entity_duplicate_validator("switch")) + + def switch_schema( class_: MockObjClass, *, @@ -131,7 +134,7 @@ SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch")) async def setup_switch_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "switch") if (inverted := config.get(CONF_INVERTED)) is not None: cg.add(var.set_inverted(inverted)) diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 40b3a90d6b..8362e09ac0 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -14,8 +14,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@mauritskorse"] IS_PLATFORM_COMPONENT = True @@ -58,6 +58,9 @@ _TEXT_SCHEMA = ( ) +_TEXT_SCHEMA.add_extra(entity_duplicate_validator("text")) + + def text_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -94,7 +97,7 @@ async def setup_text_core_( max_length: int | None, pattern: str | None, ): - await setup_entity(var, config) + await setup_entity(var, config, "text") cg.add(var.traits.set_min_length(min_length)) cg.add(var.traits.set_max_length(max_length)) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index c7ac17c35a..abb2dcae6c 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -21,8 +21,8 @@ from esphome.const import ( DEVICE_CLASS_TIMESTAMP, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from esphome.util import Registry DEVICE_CLASSES = [ @@ -153,6 +153,9 @@ _TEXT_SENSOR_SCHEMA = ( ) +_TEXT_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("text_sensor")) + + def text_sensor_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -186,7 +189,7 @@ async def build_filters(config): async def setup_text_sensor_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "text_sensor") if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 09b0698903..758267f412 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -15,8 +15,8 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@jesserockz"] IS_PLATFORM_COMPONENT = True @@ -58,6 +58,9 @@ _UPDATE_SCHEMA = ( ) +_UPDATE_SCHEMA.add_extra(entity_duplicate_validator("update")) + + def update_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -87,7 +90,7 @@ UPDATE_SCHEMA.add_extra(cv.deprecated_schema_constant("update")) async def setup_update_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "update") if device_class_config := config.get(CONF_DEVICE_CLASS): cg.add(var.set_device_class(device_class_config)) diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index a6f1428cd2..cb27546120 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -22,8 +22,8 @@ from esphome.const import ( DEVICE_CLASS_WATER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -103,6 +103,9 @@ _VALVE_SCHEMA = ( ) +_VALVE_SCHEMA.add_extra(entity_duplicate_validator("valve")) + + def valve_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -132,7 +135,7 @@ VALVE_SCHEMA.add_extra(cv.deprecated_schema_constant("valve")) async def _setup_valve_core(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "valve") if device_class_config := config.get(CONF_DEVICE_CLASS): cg.add(var.set_device_class(device_class_config)) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index bf69b81bb5..09b132a458 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1,5 +1,7 @@ """Helpers for config validation using voluptuous.""" +from __future__ import annotations + from contextlib import contextmanager from dataclasses import dataclass from datetime import datetime @@ -29,6 +31,7 @@ from esphome.const import ( CONF_COMMAND_RETAIN, CONF_COMMAND_TOPIC, CONF_DAY, + CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_DISCOVERY, CONF_ENTITY_CATEGORY, @@ -355,6 +358,13 @@ def icon(value): ) +def sub_device_id(value: str | None) -> core.ID: + # Lazy import to avoid circular imports + from esphome.core.config import Device + + return use_id(Device)(value) + + def boolean(value): """Validate the given config option to be a boolean. @@ -1896,6 +1906,7 @@ ENTITY_BASE_SCHEMA = Schema( Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean, Optional(CONF_ICON): icon, Optional(CONF_ENTITY_CATEGORY): entity_category, + Optional(CONF_DEVICE_ID): sub_device_id, } ) @@ -1964,7 +1975,7 @@ class Version: return f"{self.major}.{self.minor}.{self.patch}" @classmethod - def parse(cls, value: str) -> "Version": + def parse(cls, value: str) -> Version: match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value) if match is None: raise ValueError(f"Not a valid version number {value}") diff --git a/esphome/const.py b/esphome/const.py index 5e551efc2c..ed6390d8c3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -56,6 +56,8 @@ CONF_AP = "ap" CONF_APPARENT_POWER = "apparent_power" CONF_ARDUINO_VERSION = "arduino_version" CONF_AREA = "area" +CONF_AREA_ID = "area_id" +CONF_AREAS = "areas" CONF_ARGS = "args" CONF_ASSUMED_STATE = "assumed_state" CONF_AT = "at" @@ -217,6 +219,7 @@ CONF_DEST = "dest" CONF_DEVICE = "device" CONF_DEVICE_CLASS = "device_class" CONF_DEVICE_FACTOR = "device_factor" +CONF_DEVICE_ID = "device_id" CONF_DEVICES = "devices" CONF_DIELECTRIC_CONSTANT = "dielectric_constant" CONF_DIMENSIONS = "dimensions" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index bc98ff54db..368e2affe9 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -522,6 +522,9 @@ class EsphomeCore: # Dict to track platform entity counts for pre-allocation # Key: platform name (e.g. "sensor", "binary_sensor"), Value: count self.platform_counts: defaultdict[str, int] = defaultdict(int) + # Track entity unique IDs to handle duplicates + # Set of (device_id, platform, sanitized_name) tuples + self.unique_ids: set[tuple[str, str, str]] = set() # Whether ESPHome was started in verbose mode self.verbose = False # Whether ESPHome was started in quiet mode @@ -553,6 +556,7 @@ class EsphomeCore: self.loaded_integrations = set() self.component_ids = set() self.platform_counts = defaultdict(int) + self.unique_ids = set() PIN_SCHEMA_REGISTRY.reset() @property diff --git a/esphome/core/application.h b/esphome/core/application.h index 93d5a78958..17270ca459 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -9,6 +9,13 @@ #include "esphome/core/preferences.h" #include "esphome/core/scheduler.h" +#ifdef USE_DEVICES +#include "esphome/core/device.h" +#endif +#ifdef USE_AREAS +#include "esphome/core/area.h" +#endif + #ifdef USE_SOCKET_SELECT_SUPPORT #include #endif @@ -87,7 +94,7 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, const char *area, const char *comment, + void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment, const char *compilation_time, bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; @@ -102,11 +109,17 @@ class Application { this->name_ = name; this->friendly_name_ = friendly_name; } - this->area_ = area; this->comment_ = comment; this->compilation_time_ = compilation_time; } +#ifdef USE_DEVICES + void register_device(Device *device) { this->devices_.push_back(device); } +#endif +#ifdef USE_AREAS + void register_area(Area *area) { this->areas_.push_back(area); } +#endif + void set_current_component(Component *component) { this->current_component_ = component; } Component *get_current_component() { return this->current_component_; } @@ -264,6 +277,12 @@ class Application { #ifdef USE_UPDATE void reserve_update(size_t count) { this->updates_.reserve(count); } #endif +#ifdef USE_AREAS + void reserve_area(size_t count) { this->areas_.reserve(count); } +#endif +#ifdef USE_DEVICES + void reserve_device(size_t count) { this->devices_.reserve(count); } +#endif /// Register the component in this Application instance. template C *register_component(C *c) { @@ -285,7 +304,15 @@ class Application { const std::string &get_friendly_name() const { return this->friendly_name_; } /// Get the area of this Application set by pre_setup(). - std::string get_area() const { return this->area_ == nullptr ? "" : this->area_; } + const char *get_area() const { +#ifdef USE_AREAS + // If we have areas registered, return the name of the first one (which is the top-level area) + if (!this->areas_.empty() && this->areas_[0] != nullptr) { + return this->areas_[0]->get_name(); + } +#endif + return ""; + } /// Get the comment of this Application set by pre_setup(). std::string get_comment() const { return this->comment_; } @@ -334,6 +361,12 @@ class Application { uint8_t get_app_state() const { return this->app_state_; } +#ifdef USE_DEVICES + const std::vector &get_devices() { return this->devices_; } +#endif +#ifdef USE_AREAS + const std::vector &get_areas() { return this->areas_; } +#endif #ifdef USE_BINARY_SENSOR const std::vector &get_binary_sensors() { return this->binary_sensors_; } binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) { @@ -610,6 +643,12 @@ class Application { uint16_t current_loop_index_{0}; bool in_loop_{false}; +#ifdef USE_DEVICES + std::vector devices_{}; +#endif +#ifdef USE_AREAS + std::vector areas_{}; +#endif #ifdef USE_BINARY_SENSOR std::vector binary_sensors_{}; #endif @@ -676,7 +715,6 @@ class Application { std::string name_; std::string friendly_name_; - const char *area_{nullptr}; const char *comment_{nullptr}; const char *compilation_time_{nullptr}; bool name_add_mac_suffix_; diff --git a/esphome/core/area.h b/esphome/core/area.h new file mode 100644 index 0000000000..f6d88fe703 --- /dev/null +++ b/esphome/core/area.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace esphome { + +class Area { + public: + void set_area_id(uint32_t area_id) { this->area_id_ = area_id; } + uint32_t get_area_id() { return this->area_id_; } + void set_name(const char *name) { this->name_ = name; } + const char *get_name() { return this->name_; } + + protected: + uint32_t area_id_{}; + const char *name_ = ""; +}; + +} // namespace esphome diff --git a/esphome/core/config.py b/esphome/core/config.py index c407e1c11a..641c73a292 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -1,18 +1,24 @@ +from __future__ import annotations + import logging import os from pathlib import Path -from esphome import automation +from esphome import automation, core import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( CONF_AREA, + CONF_AREA_ID, + CONF_AREAS, CONF_BUILD_PATH, CONF_COMMENT, CONF_COMPILE_PROCESS_LIMIT, CONF_DEBUG_SCHEDULER, + CONF_DEVICES, CONF_ESPHOME, CONF_FRIENDLY_NAME, + CONF_ID, CONF_INCLUDES, CONF_LIBRARIES, CONF_MIN_VERSION, @@ -32,7 +38,13 @@ from esphome.const import ( __version__ as ESPHOME_VERSION, ) from esphome.core import CORE, coroutine_with_priority -from esphome.helpers import copy_file_if_changed, get_str_env, walk_files +from esphome.helpers import ( + copy_file_if_changed, + fnv1a_32bit_hash, + get_str_env, + walk_files, +) +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -48,7 +60,8 @@ LoopTrigger = cg.esphome_ns.class_( ProjectUpdateTrigger = cg.esphome_ns.class_( "ProjectUpdateTrigger", cg.Component, automation.Trigger.template(cg.std_string) ) - +Device = cg.esphome_ns.class_("Device") +Area = cg.esphome_ns.class_("Area") VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"} @@ -71,6 +84,56 @@ def validate_hostname(config): return config +def validate_ids_and_references(config: ConfigType) -> ConfigType: + """Validate that there are no hash collisions between IDs and that area_id references are valid. + + This validation is critical because we use 32-bit hashes for performance on microcontrollers. + By detecting collisions at compile time, we prevent any runtime issues while maintaining + optimal performance on 32-bit platforms. In practice, with typical deployments having only + a handful of areas and devices, hash collisions are virtually impossible. + """ + + # Helper to check hash collisions + def check_hash_collision( + id_obj: core.ID, + hash_dict: dict[int, str], + item_type: str, + path: list[str | int], + ) -> None: + hash_val: int = fnv1a_32bit_hash(id_obj.id) + if hash_val in hash_dict and hash_dict[hash_val] != id_obj.id: + raise cv.Invalid( + f"{item_type} ID '{id_obj.id}' with hash {hash_val} collides with " + f"existing {item_type.lower()} ID '{hash_dict[hash_val]}'", + path=path, + ) + hash_dict[hash_val] = id_obj.id + + # Collect all areas + all_areas: list[dict[str, str | core.ID]] = [] + if CONF_AREA in config: + all_areas.append(config[CONF_AREA]) + all_areas.extend(config[CONF_AREAS]) + + # Validate area hash collisions and collect IDs + area_hashes: dict[int, str] = {} + area_ids: set[str] = set() + for area in all_areas: + area_id: core.ID = area[CONF_ID] + check_hash_collision(area_id, area_hashes, "Area", [CONF_AREAS, area_id.id]) + area_ids.add(area_id.id) + + # Validate device hash collisions and area references + device_hashes: dict[int, str] = {} + for device in config[CONF_DEVICES]: + device_id: core.ID = device[CONF_ID] + check_hash_collision( + device_id, device_hashes, "Device", [CONF_DEVICES, device_id.id] + ) + + return config + + def valid_include(value): # Look for "<...>" includes if value.startswith("<") and value.endswith(">"): @@ -111,13 +174,32 @@ if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ: else: _compile_process_limit_default = cv.UNDEFINED +AREA_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(Area), + cv.Required(CONF_NAME): cv.string, + } +) + +DEVICE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(Device), + cv.Required(CONF_NAME): cv.string, + cv.Optional(CONF_AREA_ID): cv.use_id(Area), + } +) + + +def validate_area_config(config: dict | str) -> dict[str, str | core.ID]: + return cv.maybe_simple_value(AREA_SCHEMA, key=CONF_NAME)(config) + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string, - cv.Optional(CONF_AREA, ""): cv.string, + cv.Optional(CONF_AREA): validate_area_config, cv.Optional(CONF_COMMENT): cv.string, cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( @@ -167,11 +249,17 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_COMPILE_PROCESS_LIMIT, default=_compile_process_limit_default ): cv.int_range(min=1, max=get_usable_cpu_count()), + cv.Optional(CONF_AREAS, default=[]): cv.ensure_list(AREA_SCHEMA), + cv.Optional(CONF_DEVICES, default=[]): cv.ensure_list(DEVICE_SCHEMA), } ), validate_hostname, ) + +FINAL_VALIDATE_SCHEMA = cv.All(validate_ids_and_references) + + PRELOAD_CONFIG_SCHEMA = cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, @@ -336,7 +424,7 @@ async def _add_platform_reserves() -> None: @coroutine_with_priority(100.0) -async def to_code(config): +async def to_code(config: ConfigType) -> None: cg.add_global(cg.global_ns.namespace("esphome").using) # These can be used by user lambdas, put them to default scope cg.add_global(cg.RawExpression("using std::isnan")) @@ -347,7 +435,6 @@ async def to_code(config): cg.App.pre_setup( config[CONF_NAME], config[CONF_FRIENDLY_NAME], - config[CONF_AREA], config.get(CONF_COMMENT, ""), cg.RawExpression('__DATE__ ", " __TIME__'), config[CONF_NAME_ADD_MAC_SUFFIX], @@ -417,3 +504,50 @@ async def to_code(config): if config[CONF_PLATFORMIO_OPTIONS]: CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS]) + + # Process areas + all_areas: list[dict[str, str | core.ID]] = [] + if CONF_AREA in config: + all_areas.append(config[CONF_AREA]) + all_areas.extend(config[CONF_AREAS]) + + if all_areas: + cg.add(cg.RawStatement(f"App.reserve_area({len(all_areas)});")) + cg.add_define("USE_AREAS") + + for area_conf in all_areas: + area_id: core.ID = area_conf[CONF_ID] + area_id_hash: int = fnv1a_32bit_hash(area_id.id) + area_name: str = area_conf[CONF_NAME] + + area_var = cg.new_Pvariable(area_id) + cg.add(area_var.set_area_id(area_id_hash)) + cg.add(area_var.set_name(area_name)) + cg.add(cg.App.register_area(area_var)) + + # Process devices + devices: list[dict[str, str | core.ID]] = config[CONF_DEVICES] + if not devices: + return + + # Reserve space for devices + cg.add(cg.RawStatement(f"App.reserve_device({len(devices)});")) + cg.add_define("USE_DEVICES") + + # Process each device + for dev_conf in devices: + device_id: core.ID = dev_conf[CONF_ID] + device_id_hash = fnv1a_32bit_hash(device_id.id) + device_name: str = dev_conf[CONF_NAME] + + dev = cg.new_Pvariable(device_id) + cg.add(dev.set_device_id(device_id_hash)) + cg.add(dev.set_name(device_name)) + + # Set area if specified + if CONF_AREA_ID in dev_conf: + area_id: core.ID = dev_conf[CONF_AREA_ID] + area_id_hash = fnv1a_32bit_hash(area_id.id) + cg.add(dev.set_area_id(area_id_hash)) + + cg.add(cg.App.register_device(dev)) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 657827c364..c9fea90386 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -20,6 +20,7 @@ // Feature flags #define USE_ALARM_CONTROL_PANEL +#define USE_AREAS #define USE_BINARY_SENSOR #define USE_BUTTON #define USE_CLIMATE @@ -29,6 +30,7 @@ #define USE_DATETIME_DATETIME #define USE_DATETIME_TIME #define USE_DEEP_SLEEP +#define USE_DEVICES #define USE_DISPLAY #define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_EVENT diff --git a/esphome/core/device.h b/esphome/core/device.h new file mode 100644 index 0000000000..3d0d1e7c23 --- /dev/null +++ b/esphome/core/device.h @@ -0,0 +1,20 @@ +#pragma once + +namespace esphome { + +class Device { + public: + void set_device_id(uint32_t device_id) { this->device_id_ = device_id; } + uint32_t get_device_id() { return this->device_id_; } + void set_name(const char *name) { this->name_ = name; } + const char *get_name() { return this->name_; } + void set_area_id(uint32_t area_id) { this->area_id_ = area_id; } + uint32_t get_area_id() { return this->area_id_; } + + protected: + uint32_t device_id_{}; + uint32_t area_id_{}; + const char *name_ = ""; +}; + +} // namespace esphome diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 791b6615a1..6afd02ff65 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -11,7 +11,14 @@ const StringRef &EntityBase::get_name() const { return this->name_; } void EntityBase::set_name(const char *name) { this->name_ = StringRef(name); if (this->name_.empty()) { - this->name_ = StringRef(App.get_friendly_name()); +#ifdef USE_DEVICES + if (this->device_ != nullptr) { + this->name_ = StringRef(this->device_->get_name()); + } else +#endif + { + this->name_ = StringRef(App.get_friendly_name()); + } this->flags_.has_own_name = false; } else { this->flags_.has_own_name = true; @@ -47,19 +54,7 @@ void EntityBase::set_object_id(const char *object_id) { } // Calculate Object ID Hash from Entity Name -void EntityBase::calc_object_id_() { - // Check if `App.get_friendly_name()` is constant or dynamic. - if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { - // `App.get_friendly_name()` is dynamic. - const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name())); - // FNV-1 hash - this->object_id_hash_ = fnv1_hash(object_id); - } else { - // `App.get_friendly_name()` is constant. - // FNV-1 hash - this->object_id_hash_ = fnv1_hash(this->object_id_c_str_); - } -} +void EntityBase::calc_object_id_() { this->object_id_hash_ = fnv1_hash(this->get_object_id()); } uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 0f0d635962..4819b66108 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -6,6 +6,10 @@ #include "helpers.h" #include "log.h" +#ifdef USE_DEVICES +#include "device.h" +#endif + namespace esphome { enum EntityCategory : uint8_t { @@ -51,6 +55,17 @@ class EntityBase { std::string get_icon() const; void set_icon(const char *icon); +#ifdef USE_DEVICES + // Get/set this entity's device id + uint32_t get_device_id() const { + if (this->device_ == nullptr) { + return 0; // No device set, return 0 + } + return this->device_->get_device_id(); + } + void set_device(Device *device) { this->device_ = device; } +#endif + // Check if this entity has state bool has_state() const { return this->flags_.has_state; } @@ -67,6 +82,9 @@ class EntityBase { const char *object_id_c_str_{nullptr}; const char *icon_c_str_{nullptr}; uint32_t object_id_hash_{}; +#ifdef USE_DEVICES + Device *device_{}; +#endif // Bit-packed flags to save memory (1 byte instead of 5) struct EntityFlags { diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 7f6a9b48ab..c95acebbf9 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -1,5 +1,116 @@ -from esphome.const import CONF_ID +from collections.abc import Callable +import logging + +import esphome.config_validation as cv +from esphome.const import ( + CONF_DEVICE_ID, + CONF_DISABLED_BY_DEFAULT, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_INTERNAL, + CONF_NAME, +) +from esphome.core import CORE, ID +from esphome.cpp_generator import MockObj, add, get_variable import esphome.final_validate as fv +from esphome.helpers import sanitize, snake_case +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) + + +def get_base_entity_object_id( + name: str, friendly_name: str | None, device_name: str | None = None +) -> str: + """Calculate the base object ID for an entity that will be set via set_object_id(). + + This function calculates what object_id_c_str_ should be set to in C++. + + The C++ EntityBase::get_object_id() (entity_base.cpp lines 38-49) works as: + - If !has_own_name && is_name_add_mac_suffix_enabled(): + return str_sanitize(str_snake_case(App.get_friendly_name())) // Dynamic + - Else: + return object_id_c_str_ ?? "" // What we set via set_object_id() + + Since we're calculating what to pass to set_object_id(), we always need to + generate the object_id the same way, regardless of name_add_mac_suffix setting. + + Args: + name: The entity name (empty string if no name) + friendly_name: The friendly name from CORE.friendly_name + device_name: The device name if entity is on a sub-device + + Returns: + The base object ID to use for duplicate checking and to pass to set_object_id() + """ + + if name: + # Entity has its own name (has_own_name will be true) + base_str = name + elif device_name: + # Entity has empty name and is on a sub-device + # C++ EntityBase::set_name() uses device->get_name() when device is set + base_str = device_name + elif friendly_name: + # Entity has empty name (has_own_name will be false) + # C++ uses App.get_friendly_name() which returns friendly_name or device name + base_str = friendly_name + else: + # Fallback to device name + base_str = CORE.name + + return sanitize(snake_case(base_str)) + + +async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: + """Set up generic properties of an Entity. + + This function sets up the common entity properties like name, icon, + entity category, etc. + + Args: + var: The entity variable to set up + config: Configuration dictionary containing entity settings + platform: The platform name (e.g., "sensor", "binary_sensor") + """ + # Get device info + device_name: str | None = None + if CONF_DEVICE_ID in config: + device_id_obj: ID = config[CONF_DEVICE_ID] + device: MockObj = await get_variable(device_id_obj) + add(var.set_device(device)) + # Get device name for object ID calculation + device_name = device_id_obj.id + + add(var.set_name(config[CONF_NAME])) + + # Calculate base object_id using the same logic as C++ + # This must match the C++ behavior in esphome/core/entity_base.cpp + base_object_id = get_base_entity_object_id( + config[CONF_NAME], CORE.friendly_name, device_name + ) + + if not config[CONF_NAME]: + _LOGGER.debug( + "Entity has empty name, using '%s' as object_id base", base_object_id + ) + + # Set the object ID + add(var.set_object_id(base_object_id)) + _LOGGER.debug( + "Setting object_id '%s' for entity '%s' on platform '%s'", + base_object_id, + config[CONF_NAME], + platform, + ) + add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) + if CONF_INTERNAL in config: + add(var.set_internal(config[CONF_INTERNAL])) + if CONF_ICON in config: + add(var.set_icon(config[CONF_ICON])) + if CONF_ENTITY_CATEGORY in config: + add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) def inherit_property_from(property_to_inherit, parent_id_property, transform=None): @@ -54,3 +165,48 @@ def inherit_property_from(property_to_inherit, parent_id_property, transform=Non return config return inherit_property + + +def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigType]: + """Create a validator function to check for duplicate entity names. + + This validator is meant to be used with schema.add_extra() for entity base schemas. + + Args: + platform: The platform name (e.g., "sensor", "binary_sensor") + + Returns: + A validator function that checks for duplicate names + """ + + def validator(config: ConfigType) -> ConfigType: + if CONF_NAME not in config: + # No name to validate + return config + + # Get the entity name and device info + entity_name = config[CONF_NAME] + device_id = "" # Empty string for main device + + if CONF_DEVICE_ID in config: + device_id_obj = config[CONF_DEVICE_ID] + # Use the device ID string directly for uniqueness + device_id = device_id_obj.id + + # For duplicate detection, just use the sanitized name + name_key = sanitize(snake_case(entity_name)) + + # Check for duplicates + unique_key = (device_id, platform, name_key) + if unique_key in CORE.unique_ids: + device_prefix = f" on device '{device_id}'" if device_id else "" + raise cv.Invalid( + f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. " + f"Each entity on a device must have a unique name within its platform." + ) + + # Add to tracking set + CORE.unique_ids.add(unique_key) + return config + + return validator diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 9a775bad33..3f64be6154 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,11 +1,6 @@ import logging from esphome.const import ( - CONF_DISABLED_BY_DEFAULT, - CONF_ENTITY_CATEGORY, - CONF_ICON, - CONF_INTERNAL, - CONF_NAME, CONF_SAFE_MODE, CONF_SETUP_PRIORITY, CONF_TYPE_ID, @@ -16,7 +11,6 @@ from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable from esphome.cpp_generator import add, get_variable from esphome.cpp_types import App -from esphome.helpers import sanitize, snake_case from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -96,22 +90,6 @@ async def register_parented(var, value): add(var.set_parent(paren)) -async def setup_entity(var, config): - """Set up generic properties of an Entity""" - add(var.set_name(config[CONF_NAME])) - if not config[CONF_NAME]: - add(var.set_object_id(sanitize(snake_case(CORE.friendly_name)))) - else: - add(var.set_object_id(sanitize(snake_case(config[CONF_NAME])))) - add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) - if CONF_INTERNAL in config: - add(var.set_internal(config[CONF_INTERNAL])) - if CONF_ICON in config: - add(var.set_icon(config[CONF_ICON])) - if CONF_ENTITY_CATEGORY in config: - add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) - - def extract_registry_entry_config( registry: Registry, full_config: ConfigType, diff --git a/esphome/dashboard/util/text.py b/esphome/dashboard/util/text.py index 08d2df6abf..2a3b9042e6 100644 --- a/esphome/dashboard/util/text.py +++ b/esphome/dashboard/util/text.py @@ -1,25 +1,9 @@ from __future__ import annotations -import unicodedata - -from esphome.const import ALLOWED_NAME_CHARS +from esphome.helpers import slugify -def strip_accents(value): - return "".join( - c - for c in unicodedata.normalize("NFD", str(value)) - if unicodedata.category(c) != "Mn" - ) - - -def friendly_name_slugify(value): - value = ( - strip_accents(value) - .lower() - .replace(" ", "-") - .replace("_", "-") - .replace("--", "-") - .strip("-") - ) - return "".join(c for c in value if c in ALLOWED_NAME_CHARS) +def friendly_name_slugify(value: str) -> str: + """Convert a friendly name to a slug with dashes instead of underscores.""" + # First use the standard slugify, then convert underscores to dashes + return slugify(value).replace("_", "-") diff --git a/esphome/helpers.py b/esphome/helpers.py index d95546ac94..bf0e3b5cf7 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -29,6 +29,53 @@ def ensure_unique_string(preferred_string, current_strings): return test_string +def fnv1a_32bit_hash(string: str) -> int: + """FNV-1a 32-bit hash function. + + Note: This uses 32-bit hash instead of 64-bit for several reasons: + 1. ESPHome targets 32-bit microcontrollers with limited RAM (often <320KB) + 2. Using 64-bit hashes would double the RAM usage for storing IDs + 3. 64-bit operations are slower on 32-bit processors + + While there's a ~50% collision probability at ~77,000 unique IDs, + ESPHome validates for collisions at compile time, preventing any + runtime issues. In practice, most ESPHome installations only have + a handful of area_ids and device_ids (typically <10 areas and <100 + devices), making collisions virtually impossible. + """ + hash_value = 2166136261 + for char in string: + hash_value ^= ord(char) + hash_value = (hash_value * 16777619) & 0xFFFFFFFF + return hash_value + + +def strip_accents(value: str) -> str: + """Remove accents from a string.""" + import unicodedata + + return "".join( + c + for c in unicodedata.normalize("NFD", str(value)) + if unicodedata.category(c) != "Mn" + ) + + +def slugify(value: str) -> str: + """Convert a string to a valid C++ identifier slug.""" + from esphome.const import ALLOWED_NAME_CHARS + + value = ( + strip_accents(value) + .lower() + .replace(" ", "_") + .replace("-", "_") + .replace("__", "_") + .strip("_") + ) + return "".join(c for c in value if c in ALLOWED_NAME_CHARS) + + def indent_all_but_first_and_last(text, padding=" "): lines = text.splitlines(True) if len(lines) <= 2: diff --git a/tests/components/ade7880/common.yaml b/tests/components/ade7880/common.yaml index 0aa388a325..48c22c8485 100644 --- a/tests/components/ade7880/common.yaml +++ b/tests/components/ade7880/common.yaml @@ -12,12 +12,12 @@ sensor: frequency: 60Hz phase_a: name: Channel A - voltage: Voltage - current: Current - active_power: Active Power - power_factor: Power Factor - forward_active_energy: Forward Active Energy - reverse_active_energy: Reverse Active Energy + voltage: Channel A Voltage + current: Channel A Current + active_power: Channel A Active Power + power_factor: Channel A Power Factor + forward_active_energy: Channel A Forward Active Energy + reverse_active_energy: Channel A Reverse Active Energy calibration: current_gain: 3116628 voltage_gain: -757178 @@ -25,12 +25,12 @@ sensor: phase_angle: 188 phase_b: name: Channel B - voltage: Voltage - current: Current - active_power: Active Power - power_factor: Power Factor - forward_active_energy: Forward Active Energy - reverse_active_energy: Reverse Active Energy + voltage: Channel B Voltage + current: Channel B Current + active_power: Channel B Active Power + power_factor: Channel B Power Factor + forward_active_energy: Channel B Forward Active Energy + reverse_active_energy: Channel B Reverse Active Energy calibration: current_gain: 3133655 voltage_gain: -755235 @@ -38,12 +38,12 @@ sensor: phase_angle: 188 phase_c: name: Channel C - voltage: Voltage - current: Current - active_power: Active Power - power_factor: Power Factor - forward_active_energy: Forward Active Energy - reverse_active_energy: Reverse Active Energy + voltage: Channel C Voltage + current: Channel C Current + active_power: Channel C Active Power + power_factor: Channel C Power Factor + forward_active_energy: Channel C Forward Active Energy + reverse_active_energy: Channel C Reverse Active Energy calibration: current_gain: 3111158 voltage_gain: -743813 @@ -51,6 +51,6 @@ sensor: phase_angle: 180 neutral: name: Neutral - current: Current + current: Neutral Current calibration: current_gain: 3189 diff --git a/tests/components/alarm_control_panel/common.yaml b/tests/components/alarm_control_panel/common.yaml index 5b8ae5a282..142bf3c7e6 100644 --- a/tests/components/alarm_control_panel/common.yaml +++ b/tests/components/alarm_control_panel/common.yaml @@ -26,7 +26,7 @@ alarm_control_panel: ESP_LOGD("TEST", "State change %s", LOG_STR_ARG(alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state()))); - platform: template id: alarmcontrolpanel2 - name: Alarm Panel + name: Alarm Panel 2 codes: - "1234" requires_code_to_arm: true diff --git a/tests/components/binary_sensor_map/common.yaml b/tests/components/binary_sensor_map/common.yaml index 8ffdd1f379..2fed5ae515 100644 --- a/tests/components/binary_sensor_map/common.yaml +++ b/tests/components/binary_sensor_map/common.yaml @@ -26,7 +26,7 @@ binary_sensor: sensor: - platform: binary_sensor_map - name: Binary Sensor Map + name: Binary Sensor Map Group type: group channels: - binary_sensor: bin1 @@ -36,7 +36,7 @@ sensor: - binary_sensor: bin3 value: 100.0 - platform: binary_sensor_map - name: Binary Sensor Map + name: Binary Sensor Map Sum type: sum channels: - binary_sensor: bin1 @@ -46,7 +46,7 @@ sensor: - binary_sensor: bin3 value: 100.0 - platform: binary_sensor_map - name: Binary Sensor Map + name: Binary Sensor Map Bayesian type: bayesian prior: 0.4 observations: diff --git a/tests/components/dallas_temp/common.yaml b/tests/components/dallas_temp/common.yaml index 2f846ca278..fb51f4818e 100644 --- a/tests/components/dallas_temp/common.yaml +++ b/tests/components/dallas_temp/common.yaml @@ -5,7 +5,7 @@ one_wire: sensor: - platform: dallas_temp address: 0x1C0000031EDD2A28 - name: Dallas Temperature + name: Dallas Temperature 1 resolution: 9 - platform: dallas_temp - name: Dallas Temperature + name: Dallas Temperature 2 diff --git a/tests/components/esphome/common.yaml b/tests/components/esphome/common.yaml index 05954e37d7..a4b309b69d 100644 --- a/tests/components/esphome/common.yaml +++ b/tests/components/esphome/common.yaml @@ -2,7 +2,9 @@ esphome: debug_scheduler: true platformio_options: board_build.flash_mode: dio - area: testing + area: + id: testing_area + name: Testing Area on_boot: logger.log: on_boot on_shutdown: @@ -17,4 +19,20 @@ esphome: version: "1.1" on_update: logger.log: on_update + areas: + - id: another_area + name: Another area + devices: + - id: other_device + name: Another device + area_id: another_area + - id: test_device + name: Test device in main area + area_id: testing_area # Reference the main area (not in areas) + - id: no_area_device + name: Device without area # This device has no area_id +binary_sensor: + - platform: template + name: Other device sensor + device_id: other_device diff --git a/tests/components/heatpumpir/common.yaml b/tests/components/heatpumpir/common.yaml index 2df195c5de..d740f31518 100644 --- a/tests/components/heatpumpir/common.yaml +++ b/tests/components/heatpumpir/common.yaml @@ -7,20 +7,20 @@ climate: protocol: mitsubishi_heavy_zm horizontal_default: left vertical_default: up - name: HeatpumpIR Climate + name: HeatpumpIR Climate Mitsubishi min_temperature: 18 max_temperature: 30 - platform: heatpumpir protocol: daikin horizontal_default: mleft vertical_default: mup - name: HeatpumpIR Climate + name: HeatpumpIR Climate Daikin min_temperature: 18 max_temperature: 30 - platform: heatpumpir protocol: panasonic_altdke horizontal_default: mright vertical_default: mdown - name: HeatpumpIR Climate + name: HeatpumpIR Climate Panasonic min_temperature: 18 max_temperature: 30 diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml index a224dbe8bc..d4f64dcdea 100644 --- a/tests/components/light/common.yaml +++ b/tests/components/light/common.yaml @@ -114,7 +114,7 @@ light: warm_white_color_temperature: 500 mireds - platform: rgb id: test_rgb_light_initial_state - name: RGB Light + name: RGB Light Initial State red: test_ledc_1 green: test_ledc_2 blue: test_ledc_3 diff --git a/tests/components/ltr390/common.yaml b/tests/components/ltr390/common.yaml index 2eebe9d1c3..e5e331e7ba 100644 --- a/tests/components/ltr390/common.yaml +++ b/tests/components/ltr390/common.yaml @@ -6,13 +6,13 @@ i2c: sensor: - platform: ltr390 uv: - name: LTR390 UV + name: LTR390 UV 1 uv_index: - name: LTR390 UVI + name: LTR390 UVI 1 light: - name: LTR390 Light + name: LTR390 Light 1 ambient_light: - name: LTR390 ALS + name: LTR390 ALS 1 gain: X3 resolution: 18 window_correction_factor: 1.0 @@ -20,13 +20,13 @@ sensor: update_interval: 60s - platform: ltr390 uv: - name: LTR390 UV + name: LTR390 UV 2 uv_index: - name: LTR390 UVI + name: LTR390 UVI 2 light: - name: LTR390 Light + name: LTR390 Light 2 ambient_light: - name: LTR390 ALS + name: LTR390 ALS 2 gain: ambient_light: X9 uv: X3 diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 59602414a7..a035900386 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -24,33 +24,33 @@ sensor: widget: lv_arc - platform: lvgl widget: slider_id - name: LVGL Slider + name: LVGL Slider Sensor - platform: lvgl widget: bar_id id: lvgl_bar_sensor - name: LVGL Bar + name: LVGL Bar Sensor - platform: lvgl widget: spinbox_id - name: LVGL Spinbox + name: LVGL Spinbox Sensor number: - platform: lvgl widget: slider_id - name: LVGL Slider + name: LVGL Slider Number update_on_release: true restore_value: true - platform: lvgl widget: lv_arc id: lvgl_arc_number - name: LVGL Arc + name: LVGL Arc Number - platform: lvgl widget: bar_id id: lvgl_bar_number - name: LVGL Bar + name: LVGL Bar Number - platform: lvgl widget: spinbox_id id: lvgl_spinbox_number - name: LVGL Spinbox + name: LVGL Spinbox Number light: - platform: lvgl diff --git a/tests/components/opentherm/common.yaml b/tests/components/opentherm/common.yaml index 5edacc6f17..1e58a04bf0 100644 --- a/tests/components/opentherm/common.yaml +++ b/tests/components/opentherm/common.yaml @@ -170,4 +170,4 @@ switch: otc_active: name: "Boiler Outside temperature compensation active" ch2_active: - name: "Boiler Central Heating 2 active" + name: "Boiler Central Heating 2 active status" diff --git a/tests/components/packages/test.esp32-ard.yaml b/tests/components/packages/test.esp32-ard.yaml index d35c27d997..d882116c10 100644 --- a/tests/components/packages/test.esp32-ard.yaml +++ b/tests/components/packages/test.esp32-ard.yaml @@ -5,7 +5,7 @@ packages: - !include package.yaml - github://esphome/esphome/tests/components/template/common.yaml@dev - url: https://github.com/esphome/esphome - file: tests/components/binary_sensor_map/common.yaml + file: tests/components/absolute_humidity/common.yaml ref: dev refresh: 1d diff --git a/tests/components/packages/test.esp32-idf.yaml b/tests/components/packages/test.esp32-idf.yaml index 9f1484d1fd..720a5777c2 100644 --- a/tests/components/packages/test.esp32-idf.yaml +++ b/tests/components/packages/test.esp32-idf.yaml @@ -7,7 +7,7 @@ packages: shorthand: github://esphome/esphome/tests/components/template/common.yaml@dev github: url: https://github.com/esphome/esphome - file: tests/components/binary_sensor_map/common.yaml + file: tests/components/absolute_humidity/common.yaml ref: dev refresh: 1d diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index 1fb7ef6dbe..29f48d995d 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -115,7 +115,7 @@ button: address: 0x00 command: 0x0B - platform: template - name: RC5 + name: RC5 Raw on_press: remote_transmitter.transmit_raw: code: [1000, -1000] diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 3ba4c8bd07..afd393c095 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -12,7 +12,7 @@ using namespace esphome; void setup() { - App.pre_setup("livingroom", "LivingRoom", "LivingRoomArea", "comment", __DATE__ ", " __TIME__, false); + App.pre_setup("livingroom", "LivingRoom", "comment", __DATE__ ", " __TIME__, false); auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 525e3541b3..8f5f77ca52 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -203,6 +203,7 @@ async def compile_esphome( loop = asyncio.get_running_loop() def _read_config_and_get_binary(): + CORE.reset() # Reset CORE state between test runs CORE.config_path = str(config_path) config = esphome.config.read_config( {"command": "compile", "config": str(config_path)} diff --git a/tests/integration/fixtures/areas_and_devices.yaml b/tests/integration/fixtures/areas_and_devices.yaml new file mode 100644 index 0000000000..4a327b73a1 --- /dev/null +++ b/tests/integration/fixtures/areas_and_devices.yaml @@ -0,0 +1,57 @@ +esphome: + name: areas-devices-test + # Define top-level area + area: + id: living_room_area + name: Living Room + # Define additional areas + areas: + - id: bedroom_area + name: Bedroom + - id: kitchen_area + name: Kitchen + # Define devices with area assignments + devices: + - id: light_controller_device + name: Light Controller + area_id: living_room_area # Uses top-level area + - id: temp_sensor_device + name: Temperature Sensor + area_id: bedroom_area + - id: motion_detector_device + name: Motion Detector + area_id: living_room_area # Reuses top-level area + - id: smart_switch_device + name: Smart Switch + area_id: kitchen_area + +host: +api: +logger: + +# Sensors assigned to different devices +sensor: + - platform: template + name: Light Controller Sensor + device_id: light_controller_device + lambda: return 1.0; + update_interval: 0.1s + + - platform: template + name: Temperature Sensor Reading + device_id: temp_sensor_device + lambda: return 2.0; + update_interval: 0.1s + + - platform: template + name: Motion Detector Status + device_id: motion_detector_device + lambda: return 3.0; + update_interval: 0.1s + + - platform: template + name: Smart Switch Power + device_id: smart_switch_device + lambda: return 4.0; + update_interval: 0.1s + diff --git a/tests/integration/fixtures/duplicate_entities_on_different_devices.yaml b/tests/integration/fixtures/duplicate_entities_on_different_devices.yaml new file mode 100644 index 0000000000..ecc502ad28 --- /dev/null +++ b/tests/integration/fixtures/duplicate_entities_on_different_devices.yaml @@ -0,0 +1,154 @@ +esphome: + name: duplicate-entities-test + # Define devices to test multi-device duplicate handling + devices: + - id: controller_1 + name: Controller 1 + - id: controller_2 + name: Controller 2 + - id: controller_3 + name: Controller 3 + +host: +api: # Port will be automatically injected +logger: + +# Test that duplicate entity names are allowed on different devices + +# Scenario 1: Same sensor name on different devices (allowed) +sensor: + - platform: template + name: Temperature + device_id: controller_1 + lambda: return 21.0; + update_interval: 0.1s + + - platform: template + name: Temperature + device_id: controller_2 + lambda: return 22.0; + update_interval: 0.1s + + - platform: template + name: Temperature + device_id: controller_3 + lambda: return 23.0; + update_interval: 0.1s + + # Main device sensor (no device_id) + - platform: template + name: Temperature + lambda: return 20.0; + update_interval: 0.1s + + # Different sensor with unique name + - platform: template + name: Humidity + lambda: return 60.0; + update_interval: 0.1s + +# Scenario 2: Same binary sensor name on different devices (allowed) +binary_sensor: + - platform: template + name: Status + device_id: controller_1 + lambda: return true; + + - platform: template + name: Status + device_id: controller_2 + lambda: return false; + + - platform: template + name: Status + lambda: return true; # Main device + + # Different platform can have same name as sensor + - platform: template + name: Temperature + lambda: return true; + +# Scenario 3: Same text sensor name on different devices +text_sensor: + - platform: template + name: Device Info + device_id: controller_1 + lambda: return {"Controller 1 Active"}; + update_interval: 0.1s + + - platform: template + name: Device Info + device_id: controller_2 + lambda: return {"Controller 2 Active"}; + update_interval: 0.1s + + - platform: template + name: Device Info + lambda: return {"Main Device Active"}; + update_interval: 0.1s + +# Scenario 4: Same switch name on different devices +switch: + - platform: template + name: Power + device_id: controller_1 + lambda: return false; + turn_on_action: [] + turn_off_action: [] + + - platform: template + name: Power + device_id: controller_2 + lambda: return true; + turn_on_action: [] + turn_off_action: [] + + - platform: template + name: Power + device_id: controller_3 + lambda: return false; + turn_on_action: [] + turn_off_action: [] + + # Unique switch on main device + - platform: template + name: Main Power + lambda: return true; + turn_on_action: [] + turn_off_action: [] + +# Scenario 5: Empty names on different devices (should use device name) +button: + - platform: template + name: "" + device_id: controller_1 + on_press: [] + + - platform: template + name: "" + device_id: controller_2 + on_press: [] + + - platform: template + name: "" + on_press: [] # Main device + +# Scenario 6: Special characters in names +number: + - platform: template + name: "Temperature Setpoint!" + device_id: controller_1 + min_value: 10.0 + max_value: 30.0 + step: 0.1 + lambda: return 21.0; + set_action: [] + + - platform: template + name: "Temperature Setpoint!" + device_id: controller_2 + min_value: 10.0 + max_value: 30.0 + step: 0.1 + lambda: return 22.0; + set_action: [] diff --git a/tests/integration/fixtures/legacy_area.yaml b/tests/integration/fixtures/legacy_area.yaml new file mode 100644 index 0000000000..4d1617c395 --- /dev/null +++ b/tests/integration/fixtures/legacy_area.yaml @@ -0,0 +1,15 @@ +esphome: + name: legacy-area-test + # Using legacy string-based area configuration + area: Master Bedroom + +host: +api: +logger: + +# Simple sensor to ensure the device compiles and runs +sensor: + - platform: template + name: Test Sensor + lambda: return 42.0; + update_interval: 1s diff --git a/tests/integration/test_areas_and_devices.py b/tests/integration/test_areas_and_devices.py new file mode 100644 index 0000000000..4ce55a30a7 --- /dev/null +++ b/tests/integration/test_areas_and_devices.py @@ -0,0 +1,121 @@ +"""Integration test for areas and devices feature.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_areas_and_devices( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test areas and devices configuration with entity mapping.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get device info which includes areas and devices + device_info = await client.device_info() + assert device_info is not None + + # Verify areas are reported + areas = device_info.areas + assert len(areas) >= 2, f"Expected at least 2 areas, got {len(areas)}" + + # Find our specific areas + main_area = next((a for a in areas if a.name == "Living Room"), None) + bedroom_area = next((a for a in areas if a.name == "Bedroom"), None) + kitchen_area = next((a for a in areas if a.name == "Kitchen"), None) + + assert main_area is not None, "Living Room area not found" + assert bedroom_area is not None, "Bedroom area not found" + assert kitchen_area is not None, "Kitchen area not found" + + # Verify devices are reported + devices = device_info.devices + assert len(devices) >= 4, f"Expected at least 4 devices, got {len(devices)}" + + # Find our specific devices + light_controller = next( + (d for d in devices if d.name == "Light Controller"), None + ) + temp_sensor = next((d for d in devices if d.name == "Temperature Sensor"), None) + motion_detector = next( + (d for d in devices if d.name == "Motion Detector"), None + ) + smart_switch = next((d for d in devices if d.name == "Smart Switch"), None) + + assert light_controller is not None, "Light Controller device not found" + assert temp_sensor is not None, "Temperature Sensor device not found" + assert motion_detector is not None, "Motion Detector device not found" + assert smart_switch is not None, "Smart Switch device not found" + + # Verify device area assignments + assert light_controller.area_id == main_area.area_id, ( + "Light Controller should be in Living Room" + ) + assert temp_sensor.area_id == bedroom_area.area_id, ( + "Temperature Sensor should be in Bedroom" + ) + assert motion_detector.area_id == main_area.area_id, ( + "Motion Detector should be in Living Room" + ) + assert smart_switch.area_id == kitchen_area.area_id, ( + "Smart Switch should be in Kitchen" + ) + + # Verify suggested_area is set to the top-level area name + assert device_info.suggested_area == "Living Room", ( + f"Expected suggested_area to be 'Living Room', got '{device_info.suggested_area}'" + ) + + # Get entity list to verify device_id mapping + entities = await client.list_entities_services() + + # Collect sensor entities + sensor_entities = [e for e in entities[0] if hasattr(e, "device_id")] + assert len(sensor_entities) >= 4, ( + f"Expected at least 4 sensor entities, got {len(sensor_entities)}" + ) + + # Subscribe to states to get sensor values + loop = asyncio.get_running_loop() + states: dict[int, EntityState] = {} + states_future: asyncio.Future[bool] = loop.create_future() + + def on_state(state: EntityState) -> None: + states[state.key] = state + # Check if we have all expected sensor states + if len(states) >= 4 and not states_future.done(): + states_future.set_result(True) + + client.subscribe_states(on_state) + + # Wait for sensor states + try: + await asyncio.wait_for(states_future, timeout=10.0) + except asyncio.TimeoutError: + pytest.fail( + f"Did not receive all sensor states within 10 seconds. " + f"Received {len(states)} states" + ) + + # Verify we have sensor entities with proper device_id assignments + device_id_mapping = { + "Light Controller Sensor": light_controller.device_id, + "Temperature Sensor Reading": temp_sensor.device_id, + "Motion Detector Status": motion_detector.device_id, + "Smart Switch Power": smart_switch.device_id, + } + + for entity in sensor_entities: + if entity.name in device_id_mapping: + expected_device_id = device_id_mapping[entity.name] + assert entity.device_id == expected_device_id, ( + f"{entity.name} has device_id {entity.device_id}, " + f"expected {expected_device_id}" + ) diff --git a/tests/integration/test_duplicate_entities.py b/tests/integration/test_duplicate_entities.py new file mode 100644 index 0000000000..99968204d4 --- /dev/null +++ b/tests/integration/test_duplicate_entities.py @@ -0,0 +1,184 @@ +"""Integration test for duplicate entity handling with new validation.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityInfo +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_duplicate_entities_on_different_devices( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that duplicate entity names are allowed on different devices.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get device info + device_info = await client.device_info() + assert device_info is not None + + # Get devices + devices = device_info.devices + assert len(devices) >= 3, f"Expected at least 3 devices, got {len(devices)}" + + # Find our test devices + controller_1 = next((d for d in devices if d.name == "Controller 1"), None) + controller_2 = next((d for d in devices if d.name == "Controller 2"), None) + controller_3 = next((d for d in devices if d.name == "Controller 3"), None) + + assert controller_1 is not None, "Controller 1 device not found" + assert controller_2 is not None, "Controller 2 device not found" + assert controller_3 is not None, "Controller 3 device not found" + + # Get entity list + entities = await client.list_entities_services() + all_entities: list[EntityInfo] = [] + for entity_list in entities[0]: + all_entities.append(entity_list) + + # Group entities by type for easier testing + sensors = [e for e in all_entities if e.__class__.__name__ == "SensorInfo"] + binary_sensors = [ + e for e in all_entities if e.__class__.__name__ == "BinarySensorInfo" + ] + text_sensors = [ + e for e in all_entities if e.__class__.__name__ == "TextSensorInfo" + ] + switches = [e for e in all_entities if e.__class__.__name__ == "SwitchInfo"] + buttons = [e for e in all_entities if e.__class__.__name__ == "ButtonInfo"] + numbers = [e for e in all_entities if e.__class__.__name__ == "NumberInfo"] + + # Scenario 1: Check sensors with same "Temperature" name on different devices + temp_sensors = [s for s in sensors if s.name == "Temperature"] + assert len(temp_sensors) == 4, ( + f"Expected exactly 4 temperature sensors, got {len(temp_sensors)}" + ) + + # Verify each sensor is on a different device + temp_device_ids = set() + temp_object_ids = set() + + for sensor in temp_sensors: + temp_device_ids.add(sensor.device_id) + temp_object_ids.add(sensor.object_id) + + # All should have object_id "temperature" (no suffix) + assert sensor.object_id == "temperature", ( + f"Expected object_id 'temperature', got '{sensor.object_id}'" + ) + + # Should have 4 different device IDs (including None for main device) + assert len(temp_device_ids) == 4, ( + f"Temperature sensors should be on different devices, got {temp_device_ids}" + ) + + # Scenario 2: Check binary sensors "Status" on different devices + status_binary = [b for b in binary_sensors if b.name == "Status"] + assert len(status_binary) == 3, ( + f"Expected exactly 3 status binary sensors, got {len(status_binary)}" + ) + + # All should have object_id "status" + for binary in status_binary: + assert binary.object_id == "status", ( + f"Expected object_id 'status', got '{binary.object_id}'" + ) + + # Scenario 3: Check that sensor and binary_sensor can have same name + temp_binary = [b for b in binary_sensors if b.name == "Temperature"] + assert len(temp_binary) == 1, ( + f"Expected exactly 1 temperature binary sensor, got {len(temp_binary)}" + ) + assert temp_binary[0].object_id == "temperature" + + # Scenario 4: Check text sensors "Device Info" on different devices + info_text = [t for t in text_sensors if t.name == "Device Info"] + assert len(info_text) == 3, ( + f"Expected exactly 3 device info text sensors, got {len(info_text)}" + ) + + # All should have object_id "device_info" + for text in info_text: + assert text.object_id == "device_info", ( + f"Expected object_id 'device_info', got '{text.object_id}'" + ) + + # Scenario 5: Check switches "Power" on different devices + power_switches = [s for s in switches if s.name == "Power"] + assert len(power_switches) == 3, ( + f"Expected exactly 3 power switches, got {len(power_switches)}" + ) + + # All should have object_id "power" + for switch in power_switches: + assert switch.object_id == "power", ( + f"Expected object_id 'power', got '{switch.object_id}'" + ) + + # Scenario 6: Check empty name buttons (should use device name) + empty_buttons = [b for b in buttons if b.name == ""] + assert len(empty_buttons) == 3, ( + f"Expected exactly 3 empty name buttons, got {len(empty_buttons)}" + ) + + # Group by device + c1_buttons = [b for b in empty_buttons if b.device_id == controller_1.device_id] + c2_buttons = [b for b in empty_buttons if b.device_id == controller_2.device_id] + + # For main device, device_id is 0 + main_buttons = [b for b in empty_buttons if b.device_id == 0] + + # Check object IDs for empty name entities + assert len(c1_buttons) == 1 and c1_buttons[0].object_id == "controller_1" + assert len(c2_buttons) == 1 and c2_buttons[0].object_id == "controller_2" + assert ( + len(main_buttons) == 1 + and main_buttons[0].object_id == "duplicate-entities-test" + ) + + # Scenario 7: Check special characters in number names + temp_numbers = [n for n in numbers if n.name == "Temperature Setpoint!"] + assert len(temp_numbers) == 2, ( + f"Expected exactly 2 temperature setpoint numbers, got {len(temp_numbers)}" + ) + + # Special characters should be sanitized to _ in object_id + for number in temp_numbers: + assert number.object_id == "temperature_setpoint_", ( + f"Expected object_id 'temperature_setpoint_', got '{number.object_id}'" + ) + + # Verify we can get states for all entities (ensures they're functional) + loop = asyncio.get_running_loop() + states_future: asyncio.Future[None] = loop.create_future() + state_count = 0 + expected_count = ( + len(sensors) + + len(binary_sensors) + + len(text_sensors) + + len(switches) + + len(buttons) + + len(numbers) + ) + + def on_state(state) -> None: + nonlocal state_count + state_count += 1 + if state_count >= expected_count and not states_future.done(): + states_future.set_result(None) + + client.subscribe_states(on_state) + + # Wait for all entity states + try: + await asyncio.wait_for(states_future, timeout=10.0) + except asyncio.TimeoutError: + pytest.fail( + f"Did not receive all entity states within 10 seconds. " + f"Expected {expected_count}, received {state_count}" + ) diff --git a/tests/integration/test_legacy_area.py b/tests/integration/test_legacy_area.py new file mode 100644 index 0000000000..d10a01ec6a --- /dev/null +++ b/tests/integration/test_legacy_area.py @@ -0,0 +1,41 @@ +"""Integration test for legacy string-based area configuration.""" + +from __future__ import annotations + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_legacy_area( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test legacy string-based area configuration.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get device info which includes areas + device_info = await client.device_info() + assert device_info is not None + + # Verify the area is reported (should be converted to structured format) + areas = device_info.areas + assert len(areas) == 1, f"Expected exactly 1 area, got {len(areas)}" + + # Find the area - should be slugified from "Master Bedroom" + area = areas[0] + assert area.name == "Master Bedroom", ( + f"Expected area name 'Master Bedroom', got '{area.name}'" + ) + + # Verify area.id is set (it should be a hash) + assert area.area_id > 0, "Area ID should be a positive hash value" + + # The suggested_area field should be set for backward compatibility + assert device_info.suggested_area == "Master Bedroom", ( + f"Expected suggested_area to be 'Master Bedroom', got '{device_info.suggested_area}'" + ) + + # Verify deprecated warning would have been logged during compilation + # (We can't check logs directly in integration tests, but the code should work) diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 955869b799..aac5a642f6 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -14,6 +14,8 @@ import sys import pytest +from esphome.core import CORE + here = Path(__file__).parent # Configure location of package root @@ -21,6 +23,13 @@ package_root = here.parent.parent sys.path.insert(0, package_root.as_posix()) +@pytest.fixture(autouse=True) +def reset_core(): + """Reset CORE after each test.""" + yield + CORE.reset() + + @pytest.fixture def fixture_path() -> Path: """ diff --git a/tests/unit_tests/core/__init__.py b/tests/unit_tests/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/core/common.py b/tests/unit_tests/core/common.py new file mode 100644 index 0000000000..1848d5397b --- /dev/null +++ b/tests/unit_tests/core/common.py @@ -0,0 +1,33 @@ +"""Common test utilities for core unit tests.""" + +from collections.abc import Callable +from pathlib import Path +from unittest.mock import patch + +from esphome import config, yaml_util +from esphome.config import Config +from esphome.core import CORE + + +def load_config_from_yaml( + yaml_file: Callable[[str], str], yaml_content: str +) -> Config | None: + """Load configuration from YAML content.""" + yaml_path = yaml_file(yaml_content) + parsed_yaml = yaml_util.load_yaml(yaml_path) + + # Mock yaml_util.load_yaml to return our parsed content + with ( + patch.object(yaml_util, "load_yaml", return_value=parsed_yaml), + patch.object(CORE, "config_path", yaml_path), + ): + return config.read_config({}) + + +def load_config_from_fixture( + yaml_file: Callable[[str], str], fixture_name: str, fixtures_dir: Path +) -> Config | None: + """Load configuration from a fixture file.""" + fixture_path = fixtures_dir / fixture_name + yaml_content = fixture_path.read_text() + return load_config_from_yaml(yaml_file, yaml_content) diff --git a/tests/unit_tests/core/conftest.py b/tests/unit_tests/core/conftest.py new file mode 100644 index 0000000000..60d6738ce9 --- /dev/null +++ b/tests/unit_tests/core/conftest.py @@ -0,0 +1,18 @@ +"""Shared fixtures for core unit tests.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + + +@pytest.fixture +def yaml_file(tmp_path: Path) -> Callable[[str], str]: + """Create a temporary YAML file for testing.""" + + def _yaml_file(content: str) -> str: + yaml_path = tmp_path / "test.yaml" + yaml_path.write_text(content) + return str(yaml_path) + + return _yaml_file diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py new file mode 100644 index 0000000000..46e3b513d7 --- /dev/null +++ b/tests/unit_tests/core/test_config.py @@ -0,0 +1,225 @@ +"""Unit tests for core config functionality including areas and devices.""" + +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import pytest + +from esphome import config_validation as cv, core +from esphome.const import CONF_AREA, CONF_AREAS, CONF_DEVICES +from esphome.core.config import Area, validate_area_config + +from .common import load_config_from_fixture + +FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "config" + + +def test_validate_area_config_with_string() -> None: + """Test that string area config is converted to structured format.""" + result = validate_area_config("Living Room") + + assert isinstance(result, dict) + assert "id" in result + assert "name" in result + assert result["name"] == "Living Room" + assert isinstance(result["id"], core.ID) + assert result["id"].is_declaration + assert not result["id"].is_manual + + +def test_validate_area_config_with_dict() -> None: + """Test that structured area config passes through unchanged.""" + area_id = cv.declare_id(Area)("test_area") + input_config: dict[str, Any] = { + "id": area_id, + "name": "Test Area", + } + + result = validate_area_config(input_config) + + assert result == input_config + assert result["id"] == area_id + assert result["name"] == "Test Area" + + +def test_device_with_valid_area_id(yaml_file: Callable[[str], str]) -> None: + """Test that device with valid area_id works correctly.""" + result = load_config_from_fixture(yaml_file, "valid_area_device.yaml", FIXTURES_DIR) + assert result is not None + + esphome_config = result["esphome"] + + # Verify areas were parsed correctly + assert CONF_AREAS in esphome_config + areas = esphome_config[CONF_AREAS] + assert len(areas) == 1 + assert areas[0]["id"].id == "bedroom_area" + assert areas[0]["name"] == "Bedroom" + + # Verify devices were parsed correctly + assert CONF_DEVICES in esphome_config + devices = esphome_config[CONF_DEVICES] + assert len(devices) == 1 + assert devices[0]["id"].id == "test_device" + assert devices[0]["name"] == "Test Device" + assert devices[0]["area_id"].id == "bedroom_area" + + +def test_multiple_areas_and_devices(yaml_file: Callable[[str], str]) -> None: + """Test multiple areas and devices configuration.""" + result = load_config_from_fixture( + yaml_file, "multiple_areas_devices.yaml", FIXTURES_DIR + ) + assert result is not None + + esphome_config = result["esphome"] + + # Verify main area + assert CONF_AREA in esphome_config + main_area = esphome_config[CONF_AREA] + assert main_area["id"].id == "main_area" + assert main_area["name"] == "Main Area" + + # Verify additional areas + assert CONF_AREAS in esphome_config + areas = esphome_config[CONF_AREAS] + assert len(areas) == 2 + area_ids = {area["id"].id for area in areas} + assert area_ids == {"area1", "area2"} + + # Verify devices + assert CONF_DEVICES in esphome_config + devices = esphome_config[CONF_DEVICES] + assert len(devices) == 3 + + # Check device-area associations + device_area_map = {dev["id"].id: dev["area_id"].id for dev in devices} + assert device_area_map == { + "device1": "main_area", + "device2": "area1", + "device3": "area2", + } + + +def test_legacy_string_area( + yaml_file: Callable[[str], str], caplog: pytest.LogCaptureFixture +) -> None: + """Test legacy string area configuration with deprecation warning.""" + result = load_config_from_fixture( + yaml_file, "legacy_string_area.yaml", FIXTURES_DIR + ) + assert result is not None + + esphome_config = result["esphome"] + + # Verify the string was converted to structured format + assert CONF_AREA in esphome_config + area = esphome_config[CONF_AREA] + assert isinstance(area, dict) + assert area["name"] == "Living Room" + assert isinstance(area["id"], core.ID) + assert area["id"].is_declaration + assert not area["id"].is_manual + + +def test_area_id_collision( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that duplicate area IDs are detected.""" + result = load_config_from_fixture(yaml_file, "area_id_collision.yaml", FIXTURES_DIR) + assert result is None + + # Check for the specific error message in stdout + captured = capsys.readouterr() + # Exact duplicates are now caught by IDPassValidationStep + assert "ID duplicate_id redefined! Check esphome->area->id." in captured.out + + +def test_device_without_area(yaml_file: Callable[[str], str]) -> None: + """Test that devices without area_id work correctly.""" + result = load_config_from_fixture( + yaml_file, "device_without_area.yaml", FIXTURES_DIR + ) + assert result is not None + + esphome_config = result["esphome"] + + # Verify device was parsed + assert CONF_DEVICES in esphome_config + devices = esphome_config[CONF_DEVICES] + assert len(devices) == 1 + + device = devices[0] + assert device["id"].id == "test_device" + assert device["name"] == "Test Device" + + # Verify no area_id is present + assert "area_id" not in device + + +def test_device_with_invalid_area_id( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that device with non-existent area_id fails validation.""" + result = load_config_from_fixture( + yaml_file, "device_invalid_area.yaml", FIXTURES_DIR + ) + assert result is None + + # Check for the specific error message in stdout + captured = capsys.readouterr() + assert ( + "Couldn't find ID 'nonexistent_area'. Please check you have defined an ID with that name in your configuration." + in captured.out + ) + + +def test_device_id_hash_collision( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that device IDs with hash collisions are detected.""" + result = load_config_from_fixture( + yaml_file, "device_id_collision.yaml", FIXTURES_DIR + ) + assert result is None + + # Check for the specific error message about hash collision + captured = capsys.readouterr() + # The error message shows the ID that collides and includes the hash value + assert ( + "Device ID 'd6ka' with hash 3082558663 collides with existing device ID 'test_2258'" + in captured.out + ) + + +def test_area_id_hash_collision( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that area IDs with hash collisions are detected.""" + result = load_config_from_fixture( + yaml_file, "area_id_hash_collision.yaml", FIXTURES_DIR + ) + assert result is None + + # Check for the specific error message about hash collision + captured = capsys.readouterr() + # The error message shows the ID that collides and includes the hash value + assert ( + "Area ID 'd6ka' with hash 3082558663 collides with existing area ID 'test_2258'" + in captured.out + ) + + +def test_device_duplicate_id( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that duplicate device IDs are detected by IDPassValidationStep.""" + result = load_config_from_fixture( + yaml_file, "device_duplicate_id.yaml", FIXTURES_DIR + ) + assert result is None + + # Check for the specific error message from IDPassValidationStep + captured = capsys.readouterr() + assert "ID duplicate_device redefined!" in captured.out diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py new file mode 100644 index 0000000000..e166eeedee --- /dev/null +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -0,0 +1,595 @@ +"""Test get_base_entity_object_id function matches C++ behavior.""" + +from collections.abc import Callable, Generator +from pathlib import Path +import re +from typing import Any + +import pytest + +from esphome.config_validation import Invalid +from esphome.const import CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ICON, CONF_NAME +from esphome.core import CORE, ID, entity_helpers +from esphome.core.entity_helpers import get_base_entity_object_id, setup_entity +from esphome.cpp_generator import MockObj +from esphome.helpers import sanitize, snake_case + +from .common import load_config_from_fixture + +# Pre-compiled regex pattern for extracting object IDs from expressions +OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)') + +FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers" + + +@pytest.fixture(autouse=True) +def restore_core_state() -> Generator[None, None, None]: + """Save and restore CORE state for tests.""" + original_name = CORE.name + original_friendly_name = CORE.friendly_name + yield + CORE.name = original_name + CORE.friendly_name = original_friendly_name + + +def test_with_entity_name() -> None: + """Test when entity has its own name - should use entity name.""" + # Simple name + assert get_base_entity_object_id("Temperature Sensor", None) == "temperature_sensor" + assert ( + get_base_entity_object_id("Temperature Sensor", "Device Name") + == "temperature_sensor" + ) + # Even with device name, entity name takes precedence + assert ( + get_base_entity_object_id("Temperature Sensor", "Device Name", "Sub Device") + == "temperature_sensor" + ) + + # Name with special characters + assert ( + get_base_entity_object_id("Temp!@#$%^&*()Sensor", None) + == "temp__________sensor" + ) + assert get_base_entity_object_id("Temp-Sensor_123", None) == "temp-sensor_123" + + # Already snake_case + assert get_base_entity_object_id("temperature_sensor", None) == "temperature_sensor" + + # Mixed case + assert get_base_entity_object_id("TemperatureSensor", None) == "temperaturesensor" + assert get_base_entity_object_id("TEMPERATURE SENSOR", None) == "temperature_sensor" + + +def test_empty_name_with_device_name() -> None: + """Test when entity has empty name and is on a sub-device - should use device name.""" + # C++ behavior: when has_own_name is false and device is set, uses device->get_name() + assert ( + get_base_entity_object_id("", "Friendly Device", "Sub Device 1") + == "sub_device_1" + ) + assert ( + get_base_entity_object_id("", "Kitchen Controller", "controller_1") + == "controller_1" + ) + assert get_base_entity_object_id("", None, "Test-Device_123") == "test-device_123" + + +def test_empty_name_with_friendly_name() -> None: + """Test when entity has empty name and no device - should use friendly name.""" + # C++ behavior: when has_own_name is false, uses App.get_friendly_name() + assert get_base_entity_object_id("", "Friendly Device") == "friendly_device" + assert get_base_entity_object_id("", "Kitchen Controller") == "kitchen_controller" + assert get_base_entity_object_id("", "Test-Device_123") == "test-device_123" + + # Special characters in friendly name + assert get_base_entity_object_id("", "Device!@#$%") == "device_____" + + +def test_empty_name_no_friendly_name() -> None: + """Test when entity has empty name and no friendly name - should use device name.""" + # Test with CORE.name set + CORE.name = "device-name" + assert get_base_entity_object_id("", None) == "device-name" + + CORE.name = "Test Device" + assert get_base_entity_object_id("", None) == "test_device" + + +def test_edge_cases() -> None: + """Test edge cases.""" + # Only spaces + assert get_base_entity_object_id(" ", None) == "___" + + # Unicode characters (should be replaced) + assert get_base_entity_object_id("Température", None) == "temp_rature" + assert get_base_entity_object_id("测试", None) == "__" + + # Empty string with empty friendly name (empty friendly name is treated as None) + # Falls back to CORE.name + CORE.name = "device" + assert get_base_entity_object_id("", "") == "device" + + # Very long name (should work fine) + long_name = "a" * 100 + " " + "b" * 100 + expected = "a" * 100 + "_" + "b" * 100 + assert get_base_entity_object_id(long_name, None) == expected + + +@pytest.mark.parametrize( + ("name", "expected"), + [ + ("Temperature Sensor", "temperature_sensor"), + ("Living Room Light", "living_room_light"), + ("Test-Device_123", "test-device_123"), + ("Special!@#Chars", "special___chars"), + ("UPPERCASE NAME", "uppercase_name"), + ("lowercase name", "lowercase_name"), + ("Mixed Case Name", "mixed_case_name"), + (" Spaces ", "___spaces___"), + ], +) +def test_matches_cpp_helpers(name: str, expected: str) -> None: + """Test that the logic matches using snake_case and sanitize directly.""" + # For non-empty names, verify our function produces same result as direct snake_case + sanitize + assert get_base_entity_object_id(name, None) == sanitize(snake_case(name)) + assert get_base_entity_object_id(name, None) == expected + + +def test_empty_name_fallback() -> None: + """Test empty name handling which falls back to friendly_name or CORE.name.""" + # Empty name is handled specially - it doesn't just use sanitize(snake_case("")) + # Instead it falls back to friendly_name or CORE.name + assert sanitize(snake_case("")) == "" # Direct conversion gives empty string + # But our function returns a fallback + CORE.name = "device" + assert get_base_entity_object_id("", None) == "device" # Uses device name + + +def test_name_add_mac_suffix_behavior() -> None: + """Test behavior related to name_add_mac_suffix. + + In C++, when name_add_mac_suffix is enabled and entity has no name, + get_object_id() returns str_sanitize(str_snake_case(App.get_friendly_name())) + dynamically. Our function always returns the same result since we're + calculating the base for duplicate tracking. + """ + # The function should always return the same result regardless of + # name_add_mac_suffix setting, as we're calculating the base object_id + assert get_base_entity_object_id("", "Test Device") == "test_device" + assert get_base_entity_object_id("Entity Name", "Test Device") == "entity_name" + + +def test_priority_order() -> None: + """Test the priority order: entity name > device name > friendly name > CORE.name.""" + CORE.name = "core-device" + + # 1. Entity name has highest priority + assert ( + get_base_entity_object_id("Entity Name", "Friendly Name", "Device Name") + == "entity_name" + ) + + # 2. Device name is next priority (when entity name is empty) + assert ( + get_base_entity_object_id("", "Friendly Name", "Device Name") == "device_name" + ) + + # 3. Friendly name is next (when entity and device names are empty) + assert get_base_entity_object_id("", "Friendly Name", None) == "friendly_name" + + # 4. CORE.name is last resort + assert get_base_entity_object_id("", None, None) == "core-device" + + +@pytest.mark.parametrize( + ("name", "friendly_name", "device_name", "expected"), + [ + # name, friendly_name, device_name, expected + ("Living Room Light", None, None, "living_room_light"), + ("", "Kitchen Controller", None, "kitchen_controller"), + ( + "", + "ESP32 Device", + "controller_1", + "controller_1", + ), # Device name takes precedence + ("GPIO2 Button", None, None, "gpio2_button"), + ("WiFi Signal", "My Device", None, "wifi_signal"), + ("", None, "esp32_node", "esp32_node"), + ("Front Door Sensor", "Home Assistant", "door_controller", "front_door_sensor"), + ], +) +def test_real_world_examples( + name: str, friendly_name: str | None, device_name: str | None, expected: str +) -> None: + """Test real-world entity naming scenarios.""" + result = get_base_entity_object_id(name, friendly_name, device_name) + assert result == expected + + +def test_issue_6953_scenarios() -> None: + """Test specific scenarios from issue #6953.""" + # Scenario 1: Multiple empty names on main device with name_add_mac_suffix + # The Python code calculates the base, C++ might append MAC suffix dynamically + CORE.name = "device-name" + CORE.friendly_name = "Friendly Device" + + # All empty names should resolve to same base + assert get_base_entity_object_id("", CORE.friendly_name) == "friendly_device" + assert get_base_entity_object_id("", CORE.friendly_name) == "friendly_device" + assert get_base_entity_object_id("", CORE.friendly_name) == "friendly_device" + + # Scenario 2: Empty names on sub-devices + assert ( + get_base_entity_object_id("", "Main Device", "controller_1") == "controller_1" + ) + assert ( + get_base_entity_object_id("", "Main Device", "controller_2") == "controller_2" + ) + + # Scenario 3: xyz duplicates + assert get_base_entity_object_id("xyz", None) == "xyz" + assert get_base_entity_object_id("xyz", "Device") == "xyz" + + +# Tests for setup_entity function + + +@pytest.fixture +def setup_test_environment() -> Generator[list[str], None, None]: + """Set up test environment for setup_entity tests.""" + # Set CORE state for tests + CORE.name = "test-device" + CORE.friendly_name = "Test Device" + # Store original add function + + original_add = entity_helpers.add + # Track what gets added + added_expressions: list[str] = [] + + def mock_add(expression: Any) -> Any: + added_expressions.append(str(expression)) + return original_add(expression) + + # Patch add function in entity_helpers module + entity_helpers.add = mock_add + yield added_expressions + # Clean up + entity_helpers.add = original_add + + +def extract_object_id_from_expressions(expressions: list[str]) -> str | None: + """Extract the object ID that was set from the generated expressions.""" + for expr in expressions: + # Look for set_object_id calls with regex to handle various formats + # Matches: var.set_object_id("temperature_2") or var.set_object_id('temperature_2') + if match := OBJECT_ID_PATTERN.search(expr): + return match.group(1) + return None + + +@pytest.mark.asyncio +async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> None: + """Test setup_entity with unique names.""" + + added_expressions = setup_test_environment + + # Create mock entities + var1 = MockObj("sensor1") + var2 = MockObj("sensor2") + + # Set up first entity + config1 = { + CONF_NAME: "Temperature", + CONF_DISABLED_BY_DEFAULT: False, + } + await setup_entity(var1, config1, "sensor") + + # Get object ID from first entity + object_id1 = extract_object_id_from_expressions(added_expressions) + assert object_id1 == "temperature" + + # Clear for next entity + added_expressions.clear() + + # Set up second entity with different name + config2 = { + CONF_NAME: "Humidity", + CONF_DISABLED_BY_DEFAULT: False, + } + await setup_entity(var2, config2, "sensor") + + # Get object ID from second entity + object_id2 = extract_object_id_from_expressions(added_expressions) + assert object_id2 == "humidity" + + +@pytest.mark.asyncio +async def test_setup_entity_different_platforms( + setup_test_environment: list[str], +) -> None: + """Test that same name on different platforms doesn't conflict.""" + + added_expressions = setup_test_environment + + # Create mock entities + sensor = MockObj("sensor1") + binary_sensor = MockObj("binary_sensor1") + text_sensor = MockObj("text_sensor1") + + config = { + CONF_NAME: "Status", + CONF_DISABLED_BY_DEFAULT: False, + } + + # Set up entities on different platforms + platforms = [ + (sensor, "sensor"), + (binary_sensor, "binary_sensor"), + (text_sensor, "text_sensor"), + ] + + object_ids: list[str] = [] + for var, platform in platforms: + added_expressions.clear() + await setup_entity(var, config, platform) + object_id = extract_object_id_from_expressions(added_expressions) + object_ids.append(object_id) + + # All should get base object ID without suffix + assert all(obj_id == "status" for obj_id in object_ids) + + +@pytest.fixture +def mock_get_variable() -> Generator[dict[ID, MockObj], None, None]: + """Mock get_variable to return test devices.""" + devices = {} + original_get_variable = entity_helpers.get_variable + + async def _mock_get_variable(device_id: ID) -> MockObj: + if device_id in devices: + return devices[device_id] + return await original_get_variable(device_id) + + entity_helpers.get_variable = _mock_get_variable + yield devices + # Clean up + entity_helpers.get_variable = original_get_variable + + +@pytest.mark.asyncio +async def test_setup_entity_with_devices( + setup_test_environment: list[str], mock_get_variable: dict[ID, MockObj] +) -> None: + """Test that same name on different devices doesn't conflict.""" + added_expressions = setup_test_environment + + # Create mock devices + device1_id = ID("device1", type="Device") + device2_id = ID("device2", type="Device") + device1 = MockObj("device1_obj") + device2 = MockObj("device2_obj") + + # Register devices with the mock + mock_get_variable[device1_id] = device1 + mock_get_variable[device2_id] = device2 + + # Create sensors with same name on different devices + sensor1 = MockObj("sensor1") + sensor2 = MockObj("sensor2") + + config1 = { + CONF_NAME: "Temperature", + CONF_DEVICE_ID: device1_id, + CONF_DISABLED_BY_DEFAULT: False, + } + + config2 = { + CONF_NAME: "Temperature", + CONF_DEVICE_ID: device2_id, + CONF_DISABLED_BY_DEFAULT: False, + } + + # Get object IDs + object_ids: list[str] = [] + for var, config in [(sensor1, config1), (sensor2, config2)]: + added_expressions.clear() + await setup_entity(var, config, "sensor") + object_id = extract_object_id_from_expressions(added_expressions) + object_ids.append(object_id) + + # Both should get base object ID without suffix (different devices) + assert object_ids[0] == "temperature" + assert object_ids[1] == "temperature" + + +@pytest.mark.asyncio +async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> None: + """Test setup_entity with empty entity name.""" + + added_expressions = setup_test_environment + + var = MockObj("sensor1") + + config = { + CONF_NAME: "", + CONF_DISABLED_BY_DEFAULT: False, + } + + await setup_entity(var, config, "sensor") + + object_id = extract_object_id_from_expressions(added_expressions) + # Should use friendly name + assert object_id == "test_device" + + +@pytest.mark.asyncio +async def test_setup_entity_special_characters( + setup_test_environment: list[str], +) -> None: + """Test setup_entity with names containing special characters.""" + + added_expressions = setup_test_environment + + var = MockObj("sensor1") + + config = { + CONF_NAME: "Temperature Sensor!", + CONF_DISABLED_BY_DEFAULT: False, + } + + await setup_entity(var, config, "sensor") + object_id = extract_object_id_from_expressions(added_expressions) + + # Special characters should be sanitized + assert object_id == "temperature_sensor_" + + +@pytest.mark.asyncio +async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None: + """Test setup_entity sets icon correctly.""" + + added_expressions = setup_test_environment + + var = MockObj("sensor1") + + config = { + CONF_NAME: "Temperature", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ICON: "mdi:thermometer", + } + + await setup_entity(var, config, "sensor") + + # Check icon was set + assert any( + 'sensor1.set_icon("mdi:thermometer")' in expr for expr in added_expressions + ) + + +@pytest.mark.asyncio +async def test_setup_entity_disabled_by_default( + setup_test_environment: list[str], +) -> None: + """Test setup_entity sets disabled_by_default correctly.""" + + added_expressions = setup_test_environment + + var = MockObj("sensor1") + + config = { + CONF_NAME: "Temperature", + CONF_DISABLED_BY_DEFAULT: True, + } + + await setup_entity(var, config, "sensor") + + # Check disabled_by_default was set + assert any( + "sensor1.set_disabled_by_default(true)" in expr for expr in added_expressions + ) + + +def test_entity_duplicate_validator() -> None: + """Test the entity_duplicate_validator function.""" + from esphome.core.entity_helpers import entity_duplicate_validator + + # Reset CORE unique_ids for clean test + CORE.unique_ids.clear() + + # Create validator for sensor platform + validator = entity_duplicate_validator("sensor") + + # First entity should pass + config1 = {CONF_NAME: "Temperature"} + validated1 = validator(config1) + assert validated1 == config1 + assert ("", "sensor", "temperature") in CORE.unique_ids + + # Second entity with different name should pass + config2 = {CONF_NAME: "Humidity"} + validated2 = validator(config2) + assert validated2 == config2 + assert ("", "sensor", "humidity") in CORE.unique_ids + + # Duplicate entity should fail + config3 = {CONF_NAME: "Temperature"} + with pytest.raises( + Invalid, match=r"Duplicate sensor entity with name 'Temperature' found" + ): + validator(config3) + + +def test_entity_duplicate_validator_with_devices() -> None: + """Test entity_duplicate_validator with devices.""" + from esphome.core.entity_helpers import entity_duplicate_validator + + # Reset CORE unique_ids for clean test + CORE.unique_ids.clear() + + # Create validator for sensor platform + validator = entity_duplicate_validator("sensor") + + # Create mock device IDs + device1 = ID("device1", type="Device") + device2 = ID("device2", type="Device") + + # Same name on different devices should pass + config1 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} + validated1 = validator(config1) + assert validated1 == config1 + assert ("device1", "sensor", "temperature") in CORE.unique_ids + + config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device2} + validated2 = validator(config2) + assert validated2 == config2 + assert ("device2", "sensor", "temperature") in CORE.unique_ids + + # Duplicate on same device should fail + config3 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} + with pytest.raises( + Invalid, + match=r"Duplicate sensor entity with name 'Temperature' found on device 'device1'", + ): + validator(config3) + + +def test_duplicate_entity_yaml_validation( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that duplicate entity names are caught during YAML config validation.""" + result = load_config_from_fixture(yaml_file, "duplicate_entity.yaml", FIXTURES_DIR) + assert result is None + + # Check for the duplicate entity error message + captured = capsys.readouterr() + assert "Duplicate sensor entity with name 'Temperature' found" in captured.out + + +def test_duplicate_entity_with_devices_yaml_validation( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test duplicate entity validation with devices.""" + result = load_config_from_fixture( + yaml_file, "duplicate_entity_with_devices.yaml", FIXTURES_DIR + ) + assert result is None + + # Check for the duplicate entity error message with device + captured = capsys.readouterr() + assert ( + "Duplicate sensor entity with name 'Temperature' found on device 'device1'" + in captured.out + ) + + +def test_entity_different_platforms_yaml_validation( + yaml_file: Callable[[str], str], +) -> None: + """Test that same entity name on different platforms is allowed.""" + result = load_config_from_fixture( + yaml_file, "entity_different_platforms.yaml", FIXTURES_DIR + ) + # This should succeed + assert result is not None diff --git a/tests/unit_tests/fixtures/core/config/area_id_collision.yaml b/tests/unit_tests/fixtures/core/config/area_id_collision.yaml new file mode 100644 index 0000000000..fb2e930e61 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/area_id_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test-collision + area: + id: duplicate_id + name: Area 1 + areas: + - id: duplicate_id + name: Area 2 + +host: diff --git a/tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml b/tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml new file mode 100644 index 0000000000..3a2e8ab8a9 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + areas: + - id: test_2258 + name: "Area 1" + - id: d6ka + name: "Area 2" + +esp32: + board: esp32dev diff --git a/tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml b/tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml new file mode 100644 index 0000000000..2aa3055686 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + devices: + - id: duplicate_device + name: "Device 1" + - id: duplicate_device + name: "Device 2" + +esp32: + board: esp32dev diff --git a/tests/unit_tests/fixtures/core/config/device_id_collision.yaml b/tests/unit_tests/fixtures/core/config/device_id_collision.yaml new file mode 100644 index 0000000000..9cf04e0595 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/device_id_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + devices: + - id: test_2258 + name: "Device 1" + - id: d6ka + name: "Device 2" + +esp32: + board: esp32dev diff --git a/tests/unit_tests/fixtures/core/config/device_invalid_area.yaml b/tests/unit_tests/fixtures/core/config/device_invalid_area.yaml new file mode 100644 index 0000000000..9a8ec0a1eb --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/device_invalid_area.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + areas: + - id: valid_area + name: "Valid Area" + devices: + - id: test_device + name: "Test Device" + area_id: nonexistent_area + +esp32: + board: esp32dev diff --git a/tests/unit_tests/fixtures/core/config/device_without_area.yaml b/tests/unit_tests/fixtures/core/config/device_without_area.yaml new file mode 100644 index 0000000000..8464cf37df --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/device_without_area.yaml @@ -0,0 +1,7 @@ +esphome: + name: test-device-no-area + devices: + - id: test_device + name: Test Device + +host: diff --git a/tests/unit_tests/fixtures/core/config/legacy_string_area.yaml b/tests/unit_tests/fixtures/core/config/legacy_string_area.yaml new file mode 100644 index 0000000000..fe2dc3db17 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/legacy_string_area.yaml @@ -0,0 +1,5 @@ +esphome: + name: test-legacy-area + area: Living Room + +host: diff --git a/tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml b/tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml new file mode 100644 index 0000000000..ef3b4f6e67 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml @@ -0,0 +1,22 @@ +esphome: + name: test-multiple + area: + id: main_area + name: Main Area + areas: + - id: area1 + name: Area 1 + - id: area2 + name: Area 2 + devices: + - id: device1 + name: Device 1 + area_id: main_area + - id: device2 + name: Device 2 + area_id: area1 + - id: device3 + name: Device 3 + area_id: area2 + +host: diff --git a/tests/unit_tests/fixtures/core/config/valid_area_device.yaml b/tests/unit_tests/fixtures/core/config/valid_area_device.yaml new file mode 100644 index 0000000000..fc97894586 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/valid_area_device.yaml @@ -0,0 +1,11 @@ +esphome: + name: test-valid-area + areas: + - id: bedroom_area + name: Bedroom + devices: + - id: test_device + name: Test Device + area_id: bedroom_area + +host: diff --git a/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity.yaml b/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity.yaml new file mode 100644 index 0000000000..2a8dad66c9 --- /dev/null +++ b/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity.yaml @@ -0,0 +1,13 @@ +esphome: + name: test-duplicate + +esp32: + board: esp32dev + +sensor: + - platform: template + name: "Temperature" + lambda: return 21.0; + - platform: template + name: "Temperature" # Duplicate - should fail + lambda: return 22.0; diff --git a/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity_with_devices.yaml b/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity_with_devices.yaml new file mode 100644 index 0000000000..42e16231a5 --- /dev/null +++ b/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity_with_devices.yaml @@ -0,0 +1,26 @@ +esphome: + name: test-duplicate-devices + devices: + - id: device1 + name: "Device 1" + - id: device2 + name: "Device 2" + +esp32: + board: esp32dev + +sensor: + # Same name on different devices - should pass + - platform: template + device_id: device1 + name: "Temperature" + lambda: return 21.0; + - platform: template + device_id: device2 + name: "Temperature" + lambda: return 22.0; + # Duplicate on same device - should fail + - platform: template + device_id: device1 + name: "Temperature" + lambda: return 23.0; diff --git a/tests/unit_tests/fixtures/core/entity_helpers/entity_different_platforms.yaml b/tests/unit_tests/fixtures/core/entity_helpers/entity_different_platforms.yaml new file mode 100644 index 0000000000..00181c52c4 --- /dev/null +++ b/tests/unit_tests/fixtures/core/entity_helpers/entity_different_platforms.yaml @@ -0,0 +1,20 @@ +esphome: + name: test-different-platforms + +esp32: + board: esp32dev + +sensor: + - platform: template + name: "Status" + lambda: return 1.0; + +binary_sensor: + - platform: template + name: "Status" # Same name, different platform - should pass + lambda: return true; + +text_sensor: + - platform: template + name: "Status" # Same name, different platform - should pass + lambda: return {"OK"}; From f35be6b5ccae002e1b839cbc7d1bf386fe24dada Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 25 Jun 2025 22:09:43 +1000 Subject: [PATCH 046/115] [binary_sensor] Add timeout filter (#9198) --- esphome/components/binary_sensor/__init__.py | 14 +++++++++++ esphome/components/binary_sensor/filter.cpp | 6 +++++ esphome/components/binary_sensor/filter.h | 12 +++++++++- tests/components/binary_sensor/common.yaml | 25 ++++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index fd9551b850..c97de6d5e5 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -148,6 +148,7 @@ BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Conditi # Filters Filter = binary_sensor_ns.class_("Filter") +TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter, cg.Component) DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component) DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component) DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component) @@ -171,6 +172,19 @@ async def invert_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id) +@register_filter( + "timeout", + TimeoutFilter, + cv.templatable(cv.positive_time_period_milliseconds), +) +async def timeout_filter_to_code(config, filter_id): + var = cg.new_Pvariable(filter_id) + await cg.register_component(var, {}) + template_ = await cg.templatable(config, [], cg.uint32) + cg.add(var.set_timeout_value(template_)) + return var + + @register_filter( "delayed_on_off", DelayedOnOffFilter, diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 41d0553b35..3567e9c72b 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -25,6 +25,12 @@ void Filter::input(bool value) { } } +void TimeoutFilter::input(bool value) { + this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); }); + // we do not de-dup here otherwise changes from invalid to valid state will not be output + this->output(value); +} + optional DelayedOnOffFilter::new_value(bool value) { if (value) { this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 65838da49d..16f44aa5fe 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -16,7 +16,7 @@ class Filter { public: virtual optional new_value(bool value) = 0; - void input(bool value); + virtual void input(bool value); void output(bool value); @@ -28,6 +28,16 @@ class Filter { Deduplicator dedup_; }; +class TimeoutFilter : public Filter, public Component { + public: + optional new_value(bool value) override { return value; } + void input(bool value) override; + template void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; } + + protected: + TemplatableValue timeout_delay_{}; +}; + class DelayedOnOffFilter : public Filter, public Component { public: optional new_value(bool value) override; diff --git a/tests/components/binary_sensor/common.yaml b/tests/components/binary_sensor/common.yaml index 148b7d2405..2b4a006352 100644 --- a/tests/components/binary_sensor/common.yaml +++ b/tests/components/binary_sensor/common.yaml @@ -4,6 +4,31 @@ binary_sensor: id: some_binary_sensor name: "Random binary" lambda: return (random_uint32() & 1) == 0; + filters: + - invert: + - delayed_on: 100ms + - delayed_off: 100ms + # Templated, delays for 1s (1000ms) only if a reed switch is active + - delayed_on_off: !lambda "return 1000;" + - delayed_on_off: + time_on: 10s + time_off: !lambda "return 1000;" + - autorepeat: + - delay: 1s + time_off: 100ms + time_on: 900ms + - delay: 5s + time_off: 100ms + time_on: 400ms + - lambda: |- + if (id(some_binary_sensor).state) { + return x; + } else { + return {}; + } + - settle: 100ms + - timeout: 10s + on_state_change: then: - logger.log: From 6d0c6329ad426f2e86d8fe2e8602e255ce1a59c2 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 26 Jun 2025 08:45:14 +1000 Subject: [PATCH 047/115] [lvgl] Allow linear positioning of grid cells (#9196) --- esphome/components/lvgl/__init__.py | 2 +- esphome/components/lvgl/schemas.py | 64 +++++++++++++++++++++++-- tests/components/lvgl/lvgl-package.yaml | 4 +- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index dd49efd447..4a450375c4 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -466,7 +466,7 @@ LVGL_SCHEMA = cv.All( ): lvalid.lv_color, cv.Optional(df.CONF_THEME): cv.Schema( { - cv.Optional(name): obj_schema(w) + cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) for name, w in WIDGET_TYPES.items() } ), diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index a0be65c928..959d203c41 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -21,7 +21,7 @@ from esphome.core.config import StartupTrigger from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid -from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR +from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR, TYPE_GRID from .helpers import add_lv_use, requires_component, validate_printf from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity from .lvcode import LvglComponent, lv_event_t_ptr @@ -349,7 +349,60 @@ def obj_schema(widget_type: WidgetType): ) +def _validate_grid_layout(config): + layout = config[df.CONF_LAYOUT] + rows = len(layout[df.CONF_GRID_ROWS]) + columns = len(layout[df.CONF_GRID_COLUMNS]) + used_cells = [[None] * columns for _ in range(rows)] + for index, widget in enumerate(config[df.CONF_WIDGETS]): + _, w = next(iter(widget.items())) + if (df.CONF_GRID_CELL_COLUMN_POS in w) != (df.CONF_GRID_CELL_ROW_POS in w): + # pylint: disable=raise-missing-from + raise cv.Invalid( + "Both row and column positions must be specified, or both omitted", + [df.CONF_WIDGETS, index], + ) + if df.CONF_GRID_CELL_ROW_POS in w: + row = w[df.CONF_GRID_CELL_ROW_POS] + column = w[df.CONF_GRID_CELL_COLUMN_POS] + else: + try: + row, column = next( + (r_idx, c_idx) + for r_idx, row in enumerate(used_cells) + for c_idx, value in enumerate(row) + if value is None + ) + except StopIteration: + # pylint: disable=raise-missing-from + raise cv.Invalid( + "No free cells available in grid layout", [df.CONF_WIDGETS, index] + ) + w[df.CONF_GRID_CELL_ROW_POS] = row + w[df.CONF_GRID_CELL_COLUMN_POS] = column + + for i in range(w[df.CONF_GRID_CELL_ROW_SPAN]): + for j in range(w[df.CONF_GRID_CELL_COLUMN_SPAN]): + if row + i >= rows or column + j >= columns: + # pylint: disable=raise-missing-from + raise cv.Invalid( + f"Cell at {row}/{column} span {w[df.CONF_GRID_CELL_ROW_SPAN]}x{w[df.CONF_GRID_CELL_COLUMN_SPAN]} " + f"exceeds grid size {rows}x{columns}", + [df.CONF_WIDGETS, index], + ) + if used_cells[row + i][column + j] is not None: + # pylint: disable=raise-missing-from + raise cv.Invalid( + f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", + [df.CONF_WIDGETS, index], + ) + used_cells[row + i][column + j] = index + + return config + + LAYOUT_SCHEMAS = {} +LAYOUT_VALIDATORS = {TYPE_GRID: _validate_grid_layout} ALIGN_TO_SCHEMA = { cv.Optional(df.CONF_ALIGN_TO): cv.Schema( @@ -402,8 +455,8 @@ LAYOUT_SCHEMA = { } GRID_CELL_SCHEMA = { - cv.Required(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, - cv.Required(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, @@ -474,7 +527,10 @@ def container_validator(schema, widget_type: WidgetType): result = result.extend( LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE]) ) - return result(value) + value = result(value) + if layout_validator := LAYOUT_VALIDATORS.get(ltype): + value = layout_validator(value) + return value return validator diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 212e30c1eb..2edc62b6a1 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -839,9 +839,7 @@ lvgl: styles: bdr_style grid_cell_x_align: center grid_cell_y_align: stretch - grid_cell_row_pos: 0 - grid_cell_column_pos: 1 - grid_cell_column_span: 1 + grid_cell_column_span: 2 text: "Grid cell 0/1" - label: grid_cell_x_align: end From 17497eec43819c989c5d77a34576d1c90a2c87cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 01:15:59 +0200 Subject: [PATCH 048/115] Reduce memory required for sensor entities (#9201) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/sensor/sensor.cpp | 18 ++++++++++------ esphome/components/sensor/sensor.h | 18 +++++++++++----- .../fixtures/host_mode_with_sensor.yaml | 3 +++ tests/integration/test_host_mode_sensor.py | 21 +++++++++++++++++++ 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 6d6cff0400..7dab63b026 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -23,16 +23,22 @@ std::string state_class_to_string(StateClass state_class) { Sensor::Sensor() : state(NAN), raw_state(NAN) {} int8_t Sensor::get_accuracy_decimals() { - if (this->accuracy_decimals_.has_value()) - return *this->accuracy_decimals_; + if (this->sensor_flags_.has_accuracy_override) + return this->accuracy_decimals_; return 0; } -void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } +void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { + this->accuracy_decimals_ = accuracy_decimals; + this->sensor_flags_.has_accuracy_override = true; +} -void Sensor::set_state_class(StateClass state_class) { this->state_class_ = state_class; } +void Sensor::set_state_class(StateClass state_class) { + this->state_class_ = state_class; + this->sensor_flags_.has_state_class_override = true; +} StateClass Sensor::get_state_class() { - if (this->state_class_.has_value()) - return *this->state_class_; + if (this->sensor_flags_.has_state_class_override) + return this->state_class_; return StateClass::STATE_CLASS_NONE; } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 456e876497..3fb6e5522b 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -80,9 +80,9 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa * state changes to the database when they are published, even if the state is the * same as before. */ - bool get_force_update() const { return force_update_; } + bool get_force_update() const { return sensor_flags_.force_update; } /// Set force update mode. - void set_force_update(bool force_update) { force_update_ = force_update; } + void set_force_update(bool force_update) { sensor_flags_.force_update = force_update; } /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -155,9 +155,17 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa Filter *filter_list_{nullptr}; ///< Store all active filters. - optional accuracy_decimals_; ///< Accuracy in decimals override - optional state_class_{STATE_CLASS_NONE}; ///< State class override - bool force_update_{false}; ///< Force update mode + // Group small members together to avoid padding + int8_t accuracy_decimals_{-1}; ///< Accuracy in decimals (-1 = not set) + StateClass state_class_{STATE_CLASS_NONE}; ///< State class (STATE_CLASS_NONE = not set) + + // Bit-packed flags for sensor-specific settings + struct SensorFlags { + uint8_t has_accuracy_override : 1; + uint8_t has_state_class_override : 1; + uint8_t force_update : 1; + uint8_t reserved : 5; // Reserved for future use + } sensor_flags_{}; }; } // namespace sensor diff --git a/tests/integration/fixtures/host_mode_with_sensor.yaml b/tests/integration/fixtures/host_mode_with_sensor.yaml index fecd0b435b..0ac495f3b1 100644 --- a/tests/integration/fixtures/host_mode_with_sensor.yaml +++ b/tests/integration/fixtures/host_mode_with_sensor.yaml @@ -8,5 +8,8 @@ sensor: name: Test Sensor id: test_sensor unit_of_measurement: °C + accuracy_decimals: 2 + state_class: measurement + force_update: true lambda: return 42.0; update_interval: 0.1s diff --git a/tests/integration/test_host_mode_sensor.py b/tests/integration/test_host_mode_sensor.py index f0c938da1c..049f7db619 100644 --- a/tests/integration/test_host_mode_sensor.py +++ b/tests/integration/test_host_mode_sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio +import aioesphomeapi from aioesphomeapi import EntityState import pytest @@ -47,3 +48,23 @@ async def test_host_mode_with_sensor( # Verify the sensor state assert test_sensor_state.state == 42.0 assert len(states) > 0, "No states received" + + # Verify the optimized fields are working correctly + # Get entity info to check accuracy_decimals, state_class, etc. + entities, _ = await client.list_entities_services() + sensor_info: aioesphomeapi.SensorInfo | None = None + for entity in entities: + if isinstance(entity, aioesphomeapi.SensorInfo): + sensor_info = entity + break + + assert sensor_info is not None, "Sensor entity info not found" + assert sensor_info.accuracy_decimals == 2, ( + f"Expected accuracy_decimals=2, got {sensor_info.accuracy_decimals}" + ) + assert sensor_info.state_class == 1, ( + f"Expected state_class=1 (measurement), got {sensor_info.state_class}" + ) + assert sensor_info.force_update is True, ( + f"Expected force_update=True, got {sensor_info.force_update}" + ) From e0172504455de27b43833c0900fc7c1d33f8f3b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 03:44:07 +0200 Subject: [PATCH 049/115] Reduce logger CPU usage by disabling loop when buffer is empty (#9160) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/logger/logger.cpp | 13 +++++++++++++ esphome/components/logger/logger.h | 20 ++++++++++++++++++++ esphome/core/defines.h | 2 ++ 3 files changed, 35 insertions(+) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index b42496af66..a2c2aa0320 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -48,6 +48,11 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), current_task, format, args); + if (message_sent) { + // Enable logger loop to process the buffered message + // This is safe to call from any context including ISRs + this->enable_loop_soon_any_context(); + } #endif // USE_ESPHOME_TASK_LOG_BUFFER // Emergency console logging for non-main tasks when ring buffer is full or disabled @@ -139,6 +144,10 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate #ifdef USE_ESPHOME_TASK_LOG_BUFFER void Logger::init_log_buffer(size_t total_buffer_size) { this->log_buffer_ = esphome::make_unique(total_buffer_size); + + // Start with loop disabled when using task buffer (unless using USB CDC) + // The loop will be enabled automatically when messages arrive + this->disable_loop_when_buffer_empty_(); } #endif @@ -189,6 +198,10 @@ void Logger::loop() { this->write_msg_(this->tx_buffer_); } } + } else { + // No messages to process, disable loop if appropriate + // This reduces overhead when there's no async logging activity + this->disable_loop_when_buffer_empty_(); } #endif } diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index ea82764393..38faf73d84 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -358,6 +358,26 @@ class Logger : public Component { static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); } + +#ifdef USE_ESP32 + // Disable loop when task buffer is empty (with USB CDC check) + inline void disable_loop_when_buffer_empty_() { + // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() + // concurrently. If that happens between our check and disable_loop(), the enable request + // will be processed on the next main loop iteration since: + // - disable_loop() takes effect immediately + // - enable_loop_soon_any_context() sets a pending flag that's checked at loop start +#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) + // Only disable if not using USB CDC (which needs loop for connection detection) + if (this->uart_ != UART_SELECTION_USB_CDC) { + this->disable_loop(); + } +#else + // No USB CDC support, always safe to disable + this->disable_loop(); +#endif + } +#endif }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c9fea90386..8abd6598f7 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -132,6 +132,8 @@ // ESP32-specific feature flags #ifdef USE_ESP32 +#define USE_ESPHOME_TASK_LOG_BUFFER + #define USE_BLUETOOTH_PROXY #define USE_CAPTIVE_PORTAL #define USE_ESP32_BLE From 15ef93ccc9e7e3a93ad77e11e14b0a16561801f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 03:47:41 +0200 Subject: [PATCH 050/115] Optimize API connection loop performance (#9184) --- esphome/components/api/api_connection.cpp | 54 +++++------ esphome/components/api/api_frame_helper.cpp | 46 ++++----- esphome/components/api/api_frame_helper.h | 4 +- esphome/components/api/api_server.cpp | 93 ++++++++++++------- esphome/components/api/api_server.h | 2 +- .../fixtures/api_reboot_timeout.yaml | 7 ++ tests/integration/test_api_reboot_timeout.py | 35 +++++++ 7 files changed, 146 insertions(+), 95 deletions(-) create mode 100644 tests/integration/fixtures/api_reboot_timeout.yaml create mode 100644 tests/integration/test_api_reboot_timeout.py diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 634174ce0a..e9b46853f4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -33,9 +33,14 @@ namespace api { // Since each message could contain multiple protobuf messages when using packet batching, // this limits the number of messages processed, not the number of TCP packets. static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; +static constexpr uint8_t MAX_PING_RETRIES = 60; +static constexpr uint16_t PING_RETRY_INTERVAL = 1000; +static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; static const char *const TAG = "api.connection"; +#ifdef USE_ESP32_CAMERA static const int ESP32_CAMERA_STOP_STREAM = 5000; +#endif APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { @@ -90,16 +95,6 @@ APIConnection::~APIConnection() { } void APIConnection::loop() { - if (this->remove_) - return; - - if (!network::is_connected()) { - // when network is disconnected force disconnect immediately - // don't wait for timeout - this->on_fatal_error(); - ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->get_client_combined_info().c_str()); - return; - } if (this->next_close_) { // requested a disconnect this->helper_->close(); @@ -152,20 +147,19 @@ void APIConnection::loop() { // Process deferred batch if scheduled if (this->deferred_batch_.batch_scheduled && - App.get_loop_component_start_time() - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { + now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { this->process_batch_(); } - if (!this->list_entities_iterator_.completed()) + if (!this->list_entities_iterator_.completed()) { this->list_entities_iterator_.advance(); - if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed()) + } else if (!this->initial_state_iterator_.completed()) { this->initial_state_iterator_.advance(); + } - static uint8_t max_ping_retries = 60; - static uint16_t ping_retry_interval = 1000; if (this->sent_ping_) { // Disconnect if not responded within 2.5*keepalive - if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) { + if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { on_fatal_error(); ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); } @@ -173,17 +167,15 @@ void APIConnection::loop() { ESP_LOGVV(TAG, "Sending keepalive PING"); this->sent_ping_ = this->send_message(PingRequest()); if (!this->sent_ping_) { - this->next_ping_retry_ = now + ping_retry_interval; + this->next_ping_retry_ = now + PING_RETRY_INTERVAL; this->ping_retries_++; - std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);", - this->get_client_combined_info().c_str(), this->ping_retries_); - if (this->ping_retries_ >= max_ping_retries) { + if (this->ping_retries_ >= MAX_PING_RETRIES) { on_fatal_error(); - ESP_LOGE(TAG, "%s disconnecting", warn_str.c_str()); + ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_); } else if (this->ping_retries_ >= 10) { - ESP_LOGW(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval); + ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); } else { - ESP_LOGD(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval); + ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); } } } @@ -207,22 +199,20 @@ void APIConnection::loop() { // bool done = 3; buffer.encode_bool(3, done); - bool success = this->send_buffer(buffer, 44); + bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE); if (success) { this->image_reader_.consume_data(to_send); - } - if (success && done) { - this->image_reader_.return_image(); + if (done) { + this->image_reader_.return_image(); + } } } #endif - if (state_subs_at_ != -1) { + if (state_subs_at_ >= 0) { const auto &subs = this->parent_->get_state_subs(); - if (state_subs_at_ >= (int) subs.size()) { - state_subs_at_ = -1; - } else { + if (state_subs_at_ < static_cast(subs.size())) { auto &it = subs[state_subs_at_]; SubscribeHomeAssistantStateResponse resp; resp.entity_id = it.entity_id; @@ -231,6 +221,8 @@ void APIConnection::loop() { if (this->send_message(resp)) { state_subs_at_++; } + } else { + state_subs_at_ = -1; } } } diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index ff660f439e..af6dd0220d 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -66,6 +66,17 @@ const char *api_error_to_str(APIError err) { return "UNKNOWN"; } +// Default implementation for loop - handles sending buffered data +APIError APIFrameHelper::loop() { + if (!this->tx_buf_.empty()) { + APIError err = try_send_tx_buf_(); + if (err != APIError::OK && err != APIError::WOULD_BLOCK) { + return err; + } + } + return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination +} + // Helper method to buffer data from IOVs void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { SendBuffer buffer; @@ -287,13 +298,8 @@ APIError APINoiseFrameHelper::loop() { } } - if (!this->tx_buf_.empty()) { - APIError err = try_send_tx_buf_(); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - return err; - } - } - return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination + // Use base class implementation for buffer sending + return APIFrameHelper::loop(); } /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter @@ -339,17 +345,15 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { return APIError::WOULD_BLOCK; } + if (rx_header_buf_[0] != 0x01) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); + return APIError::BAD_INDICATOR; + } // header reading done } // read body - uint8_t indicator = rx_header_buf_[0]; - if (indicator != 0x01) { - state_ = State::FAILED; - HELPER_LOG("Bad indicator byte %u", indicator); - return APIError::BAD_INDICATOR; - } - uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; if (state_ != State::DATA && msg_size > 128) { @@ -595,10 +599,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::BAD_DATA_PACKET; } - // uint16_t type; - // uint16_t data_len; - // uint8_t *data; - // uint8_t *padding; zero or more bytes to fill up the rest of the packet uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; if (data_len > msg_size - 4) { @@ -831,18 +831,12 @@ APIError APIPlaintextFrameHelper::init() { state_ = State::DATA; return APIError::OK; } -/// Not used for plaintext APIError APIPlaintextFrameHelper::loop() { if (state_ != State::DATA) { return APIError::BAD_STATE; } - if (!this->tx_buf_.empty()) { - APIError err = try_send_tx_buf_(); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - return err; - } - } - return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination + // Use base class implementation for buffer sending + return APIFrameHelper::loop(); } /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 7e90153091..1e157278a1 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -38,7 +38,7 @@ struct PacketInfo { : message_type(type), offset(off), payload_size(size), padding(0) {} }; -enum class APIError : int { +enum class APIError : uint16_t { OK = 0, WOULD_BLOCK = 1001, BAD_HANDSHAKE_PACKET_LEN = 1002, @@ -74,7 +74,7 @@ class APIFrameHelper { } virtual ~APIFrameHelper() = default; virtual APIError init() = 0; - virtual APIError loop() = 0; + virtual APIError loop(); virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } std::string getpeername() { return socket_->getpeername(); } diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 740e4259b1..583837af82 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -47,6 +47,11 @@ void APIServer::setup() { } #endif + // Schedule reboot if no clients connect within timeout + if (this->reboot_timeout_ != 0) { + this->schedule_reboot_timeout_(); + } + this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections if (this->socket_ == nullptr) { ESP_LOGW(TAG, "Could not create socket"); @@ -106,8 +111,6 @@ void APIServer::setup() { } #endif - this->last_connected_ = App.get_loop_component_start_time(); - #ifdef USE_ESP32_CAMERA if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { esp32_camera::global_esp32_camera->add_image_callback( @@ -121,6 +124,16 @@ void APIServer::setup() { #endif } +void APIServer::schedule_reboot_timeout_() { + this->status_set_warning(); + this->set_timeout("api_reboot", this->reboot_timeout_, []() { + if (!global_api_server->is_connected()) { + ESP_LOGE(TAG, "No clients; rebooting"); + App.reboot(); + } + }); +} + void APIServer::loop() { // Accept new clients only if the socket exists and has incoming connections if (this->socket_ && this->socket_->ready()) { @@ -130,51 +143,61 @@ void APIServer::loop() { auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); if (!sock) break; - ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str()); + ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str()); auto *conn = new APIConnection(std::move(sock), this); this->clients_.emplace_back(conn); conn->start(); + + // Clear warning status and cancel reboot when first client connects + if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { + this->status_clear_warning(); + this->cancel_timeout("api_reboot"); + } } } + if (this->clients_.empty()) { + return; + } + // Process clients and remove disconnected ones in a single pass - if (!this->clients_.empty()) { - size_t client_index = 0; - while (client_index < this->clients_.size()) { - auto &client = this->clients_[client_index]; - - if (client->remove_) { - // Handle disconnection - this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); - ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str()); - - // Swap with the last element and pop (avoids expensive vector shifts) - if (client_index < this->clients_.size() - 1) { - std::swap(this->clients_[client_index], this->clients_.back()); - } - this->clients_.pop_back(); - // Don't increment client_index since we need to process the swapped element - } else { - // Process active client - client->loop(); - client_index++; // Move to next client - } + // Check network connectivity once for all clients + if (!network::is_connected()) { + // Network is down - disconnect all clients + for (auto &client : this->clients_) { + client->on_fatal_error(); + ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str()); } + // Continue to process and clean up the clients below } - if (this->reboot_timeout_ != 0) { - const uint32_t now = App.get_loop_component_start_time(); - if (!this->is_connected()) { - if (now - this->last_connected_ > this->reboot_timeout_) { - ESP_LOGE(TAG, "No client connected; rebooting"); - App.reboot(); - } - this->status_set_warning(); - } else { - this->last_connected_ = now; - this->status_clear_warning(); + size_t client_index = 0; + while (client_index < this->clients_.size()) { + auto &client = this->clients_[client_index]; + + if (!client->remove_) { + // Common case: process active client + client->loop(); + client_index++; + continue; } + + // Rare case: handle disconnection + this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); + ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); + + // Swap with the last element and pop (avoids expensive vector shifts) + if (client_index < this->clients_.size() - 1) { + std::swap(this->clients_[client_index], this->clients_.back()); + } + this->clients_.pop_back(); + + // Schedule reboot when last client disconnects + if (this->clients_.empty() && this->reboot_timeout_ != 0) { + this->schedule_reboot_timeout_(); + } + // Don't increment client_index since we need to process the swapped element } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 33412d8a68..27341dc596 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -142,6 +142,7 @@ class APIServer : public Component, public Controller { } protected: + void schedule_reboot_timeout_(); // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; Trigger *client_connected_trigger_ = new Trigger(); @@ -150,7 +151,6 @@ class APIServer : public Component, public Controller { // 4-byte aligned types uint32_t reboot_timeout_{300000}; uint32_t batch_delay_{100}; - uint32_t last_connected_{0}; // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; diff --git a/tests/integration/fixtures/api_reboot_timeout.yaml b/tests/integration/fixtures/api_reboot_timeout.yaml new file mode 100644 index 0000000000..881bb5b2fc --- /dev/null +++ b/tests/integration/fixtures/api_reboot_timeout.yaml @@ -0,0 +1,7 @@ +esphome: + name: api-reboot-test +host: +api: + reboot_timeout: 0.5s # Very short timeout for fast testing +logger: + level: DEBUG diff --git a/tests/integration/test_api_reboot_timeout.py b/tests/integration/test_api_reboot_timeout.py new file mode 100644 index 0000000000..dd9f5fbd1e --- /dev/null +++ b/tests/integration/test_api_reboot_timeout.py @@ -0,0 +1,35 @@ +"""Test API server reboot timeout functionality.""" + +import asyncio +import re + +import pytest + +from .types import RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_reboot_timeout( + yaml_config: str, + run_compiled: RunCompiledFunction, +) -> None: + """Test that the device reboots when no API clients connect within the timeout.""" + loop = asyncio.get_running_loop() + reboot_future = loop.create_future() + reboot_pattern = re.compile(r"No clients; rebooting") + + def check_output(line: str) -> None: + """Check output for reboot message.""" + if not reboot_future.done() and reboot_pattern.search(line): + reboot_future.set_result(True) + + # Run the device without connecting any API client + async with run_compiled(yaml_config, line_callback=check_output): + # Wait for reboot with timeout + # (0.5s reboot timeout + some margin for processing) + try: + await asyncio.wait_for(reboot_future, timeout=2.0) + except asyncio.TimeoutError: + pytest.fail("Device did not reboot within expected timeout") + + # Test passes if we get here - reboot was detected From c74e5e0f04da6ccb9d9fcbd24569f403588efe45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 03:51:51 +0200 Subject: [PATCH 051/115] Optimize TemplatableValue memory (#9202) --- esphome/core/automation.h | 67 +++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 02c9d44f16..e156818312 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -27,20 +27,67 @@ template class TemplatableValue { public: TemplatableValue() : type_(NONE) {} - template::value, int> = 0> - TemplatableValue(F value) : type_(VALUE), value_(std::move(value)) {} + template::value, int> = 0> TemplatableValue(F value) : type_(VALUE) { + new (&this->value_) T(std::move(value)); + } - template::value, int> = 0> - TemplatableValue(F f) : type_(LAMBDA), f_(f) {} + template::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) { + this->f_ = new std::function(std::move(f)); + } + + // Copy constructor + TemplatableValue(const TemplatableValue &other) : type_(other.type_) { + if (type_ == VALUE) { + new (&this->value_) T(other.value_); + } else if (type_ == LAMBDA) { + this->f_ = new std::function(*other.f_); + } + } + + // Move constructor + TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { + if (type_ == VALUE) { + new (&this->value_) T(std::move(other.value_)); + } else if (type_ == LAMBDA) { + this->f_ = other.f_; + other.f_ = nullptr; + } + other.type_ = NONE; + } + + // Assignment operators + TemplatableValue &operator=(const TemplatableValue &other) { + if (this != &other) { + this->~TemplatableValue(); + new (this) TemplatableValue(other); + } + return *this; + } + + TemplatableValue &operator=(TemplatableValue &&other) noexcept { + if (this != &other) { + this->~TemplatableValue(); + new (this) TemplatableValue(std::move(other)); + } + return *this; + } + + ~TemplatableValue() { + if (type_ == VALUE) { + this->value_.~T(); + } else if (type_ == LAMBDA) { + delete this->f_; + } + } bool has_value() { return this->type_ != NONE; } T value(X... x) { if (this->type_ == LAMBDA) { - return this->f_(x...); + return (*this->f_)(x...); } // return value also when none - return this->value_; + return this->type_ == VALUE ? this->value_ : T{}; } optional optional_value(X... x) { @@ -58,14 +105,16 @@ template class TemplatableValue { } protected: - enum { + enum : uint8_t { NONE, VALUE, LAMBDA, } type_; - T value_{}; - std::function f_{}; + union { + T value_; + std::function *f_; + }; }; /** Base class for all automation conditions. From 79e3d2b2d78c52e4a00f0e2a177736c1f489bf4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 03:55:12 +0200 Subject: [PATCH 052/115] Optimize API connection memory with tagged pointers (#9203) --- esphome/components/api/api_connection.cpp | 35 +++++----- esphome/components/api/api_connection.h | 83 ++++++++++++----------- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e9b46853f4..b7f6f04be0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1432,7 +1432,7 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #ifdef USE_EVENT void APIConnection::send_event(event::Event *event, const std::string &event_type) { - this->schedule_message_(event, MessageCreator(event_type, EventResponse::MESSAGE_TYPE), EventResponse::MESSAGE_TYPE); + this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE); } void APIConnection::send_event_info(event::Event *event) { this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); @@ -1787,7 +1787,8 @@ void APIConnection::process_batch_() { const auto &item = this->deferred_batch_.items[0]; // Let the creator calculate size and encode if it fits - uint16_t payload_size = item.creator(item.entity, this, std::numeric_limits::max(), true); + uint16_t payload_size = + item.creator(item.entity, this, std::numeric_limits::max(), true, item.message_type); if (payload_size > 0 && this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { @@ -1837,7 +1838,7 @@ void APIConnection::process_batch_() { for (const auto &item : this->deferred_batch_.items) { // Try to encode message // The creator will calculate overhead to determine if the message fits - uint16_t payload_size = item.creator(item.entity, this, remaining_size, false); + uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type); if (payload_size == 0) { // Message won't fit, stop processing @@ -1900,21 +1901,23 @@ void APIConnection::process_batch_() { } uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) const { - switch (message_type_) { - case 0: // Function pointer - return data_.ptr(entity, conn, remaining_size, is_single); - + bool is_single, uint16_t message_type) const { + if (has_tagged_string_ptr_()) { + // Handle string-based messages + switch (message_type) { #ifdef USE_EVENT - case EventResponse::MESSAGE_TYPE: { - auto *e = static_cast(entity); - return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); - } + case EventResponse::MESSAGE_TYPE: { + auto *e = static_cast(entity); + return APIConnection::try_send_event_response(e, *get_string_ptr_(), conn, remaining_size, is_single); + } #endif - - default: - // Should not happen, return 0 to indicate no message - return 0; + default: + // Should not happen, return 0 to indicate no message + return 0; + } + } else { + // Function pointer case + return data_.ptr(entity, conn, remaining_size, is_single); } } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index da12a3e449..e872711e95 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -483,55 +483,57 @@ class APIConnection : public APIServerConnection { // Function pointer type for message encoding using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); - // Optimized MessageCreator class using union dispatch + // Optimized MessageCreator class using tagged pointer class MessageCreator { + // Ensure pointer alignment allows LSB tagging + static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging"); + public: - // Constructor for function pointer (message_type = 0) - MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; } + // Constructor for function pointer + MessageCreator(MessageCreatorPtr ptr) { + // Function pointers are always aligned, so LSB is 0 + data_.ptr = ptr; + } // Constructor for string state capture - MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) { - data_.string_ptr = new std::string(value); + explicit MessageCreator(const std::string &str_value) { + // Allocate string and tag the pointer + auto *str = new std::string(str_value); + // Set LSB to 1 to indicate string pointer + data_.tagged = reinterpret_cast(str) | 1; } // Destructor ~MessageCreator() { - // Clean up string data for string-based message types - if (uses_string_data_()) { - delete data_.string_ptr; + if (has_tagged_string_ptr_()) { + delete get_string_ptr_(); } } // Copy constructor - MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) { - if (message_type_ == 0) { - data_.ptr = other.data_.ptr; - } else if (uses_string_data_()) { - data_.string_ptr = new std::string(*other.data_.string_ptr); + MessageCreator(const MessageCreator &other) { + if (other.has_tagged_string_ptr_()) { + auto *str = new std::string(*other.get_string_ptr_()); + data_.tagged = reinterpret_cast(str) | 1; } else { - data_ = other.data_; // For POD types + data_ = other.data_; } } // Move constructor - MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) { - other.message_type_ = 0; // Reset other to function pointer type - other.data_.ptr = nullptr; - } + MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; } // Assignment operators (needed for batch deduplication) MessageCreator &operator=(const MessageCreator &other) { if (this != &other) { // Clean up current string data if needed - if (uses_string_data_()) { - delete data_.string_ptr; + if (has_tagged_string_ptr_()) { + delete get_string_ptr_(); } // Copy new data - message_type_ = other.message_type_; - if (other.message_type_ == 0) { - data_.ptr = other.data_.ptr; - } else if (other.uses_string_data_()) { - data_.string_ptr = new std::string(*other.data_.string_ptr); + if (other.has_tagged_string_ptr_()) { + auto *str = new std::string(*other.get_string_ptr_()); + data_.tagged = reinterpret_cast(str) | 1; } else { data_ = other.data_; } @@ -542,30 +544,35 @@ class APIConnection : public APIServerConnection { MessageCreator &operator=(MessageCreator &&other) noexcept { if (this != &other) { // Clean up current string data if needed - if (uses_string_data_()) { - delete data_.string_ptr; + if (has_tagged_string_ptr_()) { + delete get_string_ptr_(); } // Move data - message_type_ = other.message_type_; data_ = other.data_; // Reset other to safe state - other.message_type_ = 0; other.data_.ptr = nullptr; } return *this; } - // Call operator - uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const; + // Call operator - now accepts message_type as parameter + uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, + uint16_t message_type) const; private: - // Helper to check if this message type uses heap-allocated strings - bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; } - union CreatorData { - MessageCreatorPtr ptr; // 8 bytes - std::string *string_ptr; // 8 bytes - } data_; // 8 bytes - uint16_t message_type_; // 2 bytes (0 = function ptr, >0 = state capture) + // Check if this contains a string pointer + bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; } + + // Get the actual string pointer (clears the tag bit) + std::string *get_string_ptr_() const { + // NOLINTNEXTLINE(performance-no-int-to-ptr) + return reinterpret_cast(data_.tagged & ~uintptr_t(1)); + } + + union { + MessageCreatorPtr ptr; + uintptr_t tagged; + } data_; // 4 bytes on 32-bit }; // Generic batching mechanism for both state updates and entity info From f029f4f20e8074a057fba889720fdc410c30764b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 03:57:41 +0200 Subject: [PATCH 053/115] Fix missing protobuf message dump for batched messages with very verbose logging (#9206) --- esphome/components/api/api_connection.cpp | 5 + esphome/components/api/api_pb2.h | 254 +++++++++++----------- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/proto.h | 1 + script/api_protobuf/api_protobuf.py | 4 +- 5 files changed, 136 insertions(+), 130 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b7f6f04be0..fdcce6088c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -276,6 +276,11 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes // Encode directly into buffer msg.encode(buffer); +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log the message for VV debugging + conn->log_send_message_(msg.message_name(), msg.dump()); +#endif + // Calculate actual encoded size (not including header that was already added) size_t actual_payload_size = shared_buf.size() - size_before_encode; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 6a5b51d3a1..2f0444c2cd 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -281,7 +281,7 @@ class HelloRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 1; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "hello_request"; } + const char *message_name() const override { return "hello_request"; } #endif std::string client_info{}; uint32_t api_version_major{0}; @@ -301,7 +301,7 @@ class HelloResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 2; static constexpr uint16_t ESTIMATED_SIZE = 26; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "hello_response"; } + const char *message_name() const override { return "hello_response"; } #endif uint32_t api_version_major{0}; uint32_t api_version_minor{0}; @@ -322,7 +322,7 @@ class ConnectRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 3; static constexpr uint16_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "connect_request"; } + const char *message_name() const override { return "connect_request"; } #endif std::string password{}; void encode(ProtoWriteBuffer buffer) const override; @@ -339,7 +339,7 @@ class ConnectResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 4; static constexpr uint16_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "connect_response"; } + const char *message_name() const override { return "connect_response"; } #endif bool invalid_password{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -356,7 +356,7 @@ class DisconnectRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 5; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "disconnect_request"; } + const char *message_name() const override { return "disconnect_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -369,7 +369,7 @@ class DisconnectResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 6; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "disconnect_response"; } + const char *message_name() const override { return "disconnect_response"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -382,7 +382,7 @@ class PingRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 7; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "ping_request"; } + const char *message_name() const override { return "ping_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -395,7 +395,7 @@ class PingResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 8; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "ping_response"; } + const char *message_name() const override { return "ping_response"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -408,7 +408,7 @@ class DeviceInfoRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 9; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "device_info_request"; } + const char *message_name() const override { return "device_info_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -450,7 +450,7 @@ class DeviceInfoResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 10; static constexpr uint16_t ESTIMATED_SIZE = 219; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "device_info_response"; } + const char *message_name() const override { return "device_info_response"; } #endif bool uses_password{false}; std::string name{}; @@ -489,7 +489,7 @@ class ListEntitiesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 11; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_request"; } + const char *message_name() const override { return "list_entities_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -502,7 +502,7 @@ class ListEntitiesDoneResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 19; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_done_response"; } + const char *message_name() const override { return "list_entities_done_response"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -515,7 +515,7 @@ class SubscribeStatesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 20; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_states_request"; } + const char *message_name() const override { return "subscribe_states_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -528,7 +528,7 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 12; static constexpr uint16_t ESTIMATED_SIZE = 60; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_binary_sensor_response"; } + const char *message_name() const override { return "list_entities_binary_sensor_response"; } #endif std::string device_class{}; bool is_status_binary_sensor{false}; @@ -548,7 +548,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 21; static constexpr uint16_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "binary_sensor_state_response"; } + const char *message_name() const override { return "binary_sensor_state_response"; } #endif bool state{false}; bool missing_state{false}; @@ -567,7 +567,7 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 13; static constexpr uint16_t ESTIMATED_SIZE = 66; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_cover_response"; } + const char *message_name() const override { return "list_entities_cover_response"; } #endif bool assumed_state{false}; bool supports_position{false}; @@ -590,7 +590,7 @@ class CoverStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 22; static constexpr uint16_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "cover_state_response"; } + const char *message_name() const override { return "cover_state_response"; } #endif enums::LegacyCoverState legacy_state{}; float position{0.0f}; @@ -611,7 +611,7 @@ class CoverCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 30; static constexpr uint16_t ESTIMATED_SIZE = 25; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "cover_command_request"; } + const char *message_name() const override { return "cover_command_request"; } #endif uint32_t key{0}; bool has_legacy_command{false}; @@ -636,7 +636,7 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 14; static constexpr uint16_t ESTIMATED_SIZE = 77; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_fan_response"; } + const char *message_name() const override { return "list_entities_fan_response"; } #endif bool supports_oscillation{false}; bool supports_speed{false}; @@ -659,7 +659,7 @@ class FanStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 23; static constexpr uint16_t ESTIMATED_SIZE = 26; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "fan_state_response"; } + const char *message_name() const override { return "fan_state_response"; } #endif bool state{false}; bool oscillating{false}; @@ -683,7 +683,7 @@ class FanCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 31; static constexpr uint16_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "fan_command_request"; } + const char *message_name() const override { return "fan_command_request"; } #endif uint32_t key{0}; bool has_state{false}; @@ -714,7 +714,7 @@ class ListEntitiesLightResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 15; static constexpr uint16_t ESTIMATED_SIZE = 90; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_light_response"; } + const char *message_name() const override { return "list_entities_light_response"; } #endif std::vector supported_color_modes{}; bool legacy_supports_brightness{false}; @@ -740,7 +740,7 @@ class LightStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 24; static constexpr uint16_t ESTIMATED_SIZE = 63; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "light_state_response"; } + const char *message_name() const override { return "light_state_response"; } #endif bool state{false}; float brightness{0.0f}; @@ -770,7 +770,7 @@ class LightCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 32; static constexpr uint16_t ESTIMATED_SIZE = 107; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "light_command_request"; } + const char *message_name() const override { return "light_command_request"; } #endif uint32_t key{0}; bool has_state{false}; @@ -815,7 +815,7 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 16; static constexpr uint16_t ESTIMATED_SIZE = 77; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_sensor_response"; } + const char *message_name() const override { return "list_entities_sensor_response"; } #endif std::string unit_of_measurement{}; int32_t accuracy_decimals{0}; @@ -839,7 +839,7 @@ class SensorStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 25; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "sensor_state_response"; } + const char *message_name() const override { return "sensor_state_response"; } #endif float state{0.0f}; bool missing_state{false}; @@ -858,7 +858,7 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 17; static constexpr uint16_t ESTIMATED_SIZE = 60; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_switch_response"; } + const char *message_name() const override { return "list_entities_switch_response"; } #endif bool assumed_state{false}; std::string device_class{}; @@ -878,7 +878,7 @@ class SwitchStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 26; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "switch_state_response"; } + const char *message_name() const override { return "switch_state_response"; } #endif bool state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -896,7 +896,7 @@ class SwitchCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 33; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "switch_command_request"; } + const char *message_name() const override { return "switch_command_request"; } #endif uint32_t key{0}; bool state{false}; @@ -915,7 +915,7 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 18; static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_text_sensor_response"; } + const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; @@ -934,7 +934,7 @@ class TextSensorStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 27; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "text_sensor_state_response"; } + const char *message_name() const override { return "text_sensor_state_response"; } #endif std::string state{}; bool missing_state{false}; @@ -954,7 +954,7 @@ class SubscribeLogsRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 28; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_logs_request"; } + const char *message_name() const override { return "subscribe_logs_request"; } #endif enums::LogLevel level{}; bool dump_config{false}; @@ -972,7 +972,7 @@ class SubscribeLogsResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 29; static constexpr uint16_t ESTIMATED_SIZE = 13; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_logs_response"; } + const char *message_name() const override { return "subscribe_logs_response"; } #endif enums::LogLevel level{}; std::string message{}; @@ -992,7 +992,7 @@ class NoiseEncryptionSetKeyRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 124; static constexpr uint16_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "noise_encryption_set_key_request"; } + const char *message_name() const override { return "noise_encryption_set_key_request"; } #endif std::string key{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1009,7 +1009,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 125; static constexpr uint16_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "noise_encryption_set_key_response"; } + const char *message_name() const override { return "noise_encryption_set_key_response"; } #endif bool success{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1026,7 +1026,7 @@ class SubscribeHomeassistantServicesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 34; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_homeassistant_services_request"; } + const char *message_name() const override { return "subscribe_homeassistant_services_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1052,7 +1052,7 @@ class HomeassistantServiceResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 35; static constexpr uint16_t ESTIMATED_SIZE = 113; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "homeassistant_service_response"; } + const char *message_name() const override { return "homeassistant_service_response"; } #endif std::string service{}; std::vector data{}; @@ -1074,7 +1074,7 @@ class SubscribeHomeAssistantStatesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 38; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_home_assistant_states_request"; } + const char *message_name() const override { return "subscribe_home_assistant_states_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1087,7 +1087,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 39; static constexpr uint16_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_home_assistant_state_response"; } + const char *message_name() const override { return "subscribe_home_assistant_state_response"; } #endif std::string entity_id{}; std::string attribute{}; @@ -1107,7 +1107,7 @@ class HomeAssistantStateResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 40; static constexpr uint16_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "home_assistant_state_response"; } + const char *message_name() const override { return "home_assistant_state_response"; } #endif std::string entity_id{}; std::string state{}; @@ -1126,7 +1126,7 @@ class GetTimeRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 36; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "get_time_request"; } + const char *message_name() const override { return "get_time_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1139,7 +1139,7 @@ class GetTimeResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 37; static constexpr uint16_t ESTIMATED_SIZE = 5; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "get_time_response"; } + const char *message_name() const override { return "get_time_response"; } #endif uint32_t epoch_seconds{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1170,7 +1170,7 @@ class ListEntitiesServicesResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 41; static constexpr uint16_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_services_response"; } + const char *message_name() const override { return "list_entities_services_response"; } #endif std::string name{}; uint32_t key{0}; @@ -1212,7 +1212,7 @@ class ExecuteServiceRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 42; static constexpr uint16_t ESTIMATED_SIZE = 39; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "execute_service_request"; } + const char *message_name() const override { return "execute_service_request"; } #endif uint32_t key{0}; std::vector args{}; @@ -1231,7 +1231,7 @@ class ListEntitiesCameraResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 43; static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_camera_response"; } + const char *message_name() const override { return "list_entities_camera_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1249,7 +1249,7 @@ class CameraImageResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 44; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "camera_image_response"; } + const char *message_name() const override { return "camera_image_response"; } #endif uint32_t key{0}; std::string data{}; @@ -1270,7 +1270,7 @@ class CameraImageRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 45; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "camera_image_request"; } + const char *message_name() const override { return "camera_image_request"; } #endif bool single{false}; bool stream{false}; @@ -1288,7 +1288,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 46; static constexpr uint16_t ESTIMATED_SIZE = 156; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_climate_response"; } + const char *message_name() const override { return "list_entities_climate_response"; } #endif bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; @@ -1324,7 +1324,7 @@ class ClimateStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 47; static constexpr uint16_t ESTIMATED_SIZE = 65; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "climate_state_response"; } + const char *message_name() const override { return "climate_state_response"; } #endif enums::ClimateMode mode{}; float current_temperature{0.0f}; @@ -1356,7 +1356,7 @@ class ClimateCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 48; static constexpr uint16_t ESTIMATED_SIZE = 83; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "climate_command_request"; } + const char *message_name() const override { return "climate_command_request"; } #endif uint32_t key{0}; bool has_mode{false}; @@ -1397,7 +1397,7 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 49; static constexpr uint16_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_number_response"; } + const char *message_name() const override { return "list_entities_number_response"; } #endif float min_value{0.0f}; float max_value{0.0f}; @@ -1421,7 +1421,7 @@ class NumberStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 50; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "number_state_response"; } + const char *message_name() const override { return "number_state_response"; } #endif float state{0.0f}; bool missing_state{false}; @@ -1440,7 +1440,7 @@ class NumberCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 51; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "number_command_request"; } + const char *message_name() const override { return "number_command_request"; } #endif uint32_t key{0}; float state{0.0f}; @@ -1458,7 +1458,7 @@ class ListEntitiesSelectResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 52; static constexpr uint16_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_select_response"; } + const char *message_name() const override { return "list_entities_select_response"; } #endif std::vector options{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1477,7 +1477,7 @@ class SelectStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 53; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "select_state_response"; } + const char *message_name() const override { return "select_state_response"; } #endif std::string state{}; bool missing_state{false}; @@ -1497,7 +1497,7 @@ class SelectCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 54; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "select_command_request"; } + const char *message_name() const override { return "select_command_request"; } #endif uint32_t key{0}; std::string state{}; @@ -1516,7 +1516,7 @@ class ListEntitiesSirenResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 55; static constexpr uint16_t ESTIMATED_SIZE = 71; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_siren_response"; } + const char *message_name() const override { return "list_entities_siren_response"; } #endif std::vector tones{}; bool supports_duration{false}; @@ -1537,7 +1537,7 @@ class SirenStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 56; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "siren_state_response"; } + const char *message_name() const override { return "siren_state_response"; } #endif bool state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1555,7 +1555,7 @@ class SirenCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 57; static constexpr uint16_t ESTIMATED_SIZE = 33; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "siren_command_request"; } + const char *message_name() const override { return "siren_command_request"; } #endif uint32_t key{0}; bool has_state{false}; @@ -1582,7 +1582,7 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 58; static constexpr uint16_t ESTIMATED_SIZE = 64; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_lock_response"; } + const char *message_name() const override { return "list_entities_lock_response"; } #endif bool assumed_state{false}; bool supports_open{false}; @@ -1604,7 +1604,7 @@ class LockStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 59; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "lock_state_response"; } + const char *message_name() const override { return "lock_state_response"; } #endif enums::LockState state{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1622,7 +1622,7 @@ class LockCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 60; static constexpr uint16_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "lock_command_request"; } + const char *message_name() const override { return "lock_command_request"; } #endif uint32_t key{0}; enums::LockCommand command{}; @@ -1644,7 +1644,7 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 61; static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_button_response"; } + const char *message_name() const override { return "list_entities_button_response"; } #endif std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1663,7 +1663,7 @@ class ButtonCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 62; static constexpr uint16_t ESTIMATED_SIZE = 5; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "button_command_request"; } + const char *message_name() const override { return "button_command_request"; } #endif uint32_t key{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1697,7 +1697,7 @@ class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 63; static constexpr uint16_t ESTIMATED_SIZE = 85; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_media_player_response"; } + const char *message_name() const override { return "list_entities_media_player_response"; } #endif bool supports_pause{false}; std::vector supported_formats{}; @@ -1717,7 +1717,7 @@ class MediaPlayerStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 64; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "media_player_state_response"; } + const char *message_name() const override { return "media_player_state_response"; } #endif enums::MediaPlayerState state{}; float volume{0.0f}; @@ -1737,7 +1737,7 @@ class MediaPlayerCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 65; static constexpr uint16_t ESTIMATED_SIZE = 31; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "media_player_command_request"; } + const char *message_name() const override { return "media_player_command_request"; } #endif uint32_t key{0}; bool has_command{false}; @@ -1764,7 +1764,7 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 66; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_bluetooth_le_advertisements_request"; } + const char *message_name() const override { return "subscribe_bluetooth_le_advertisements_request"; } #endif uint32_t flags{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1796,7 +1796,7 @@ class BluetoothLEAdvertisementResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 67; static constexpr uint16_t ESTIMATED_SIZE = 107; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_le_advertisement_response"; } + const char *message_name() const override { return "bluetooth_le_advertisement_response"; } #endif uint64_t address{0}; std::string name{}; @@ -1836,7 +1836,7 @@ class BluetoothLERawAdvertisementsResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 93; static constexpr uint16_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_le_raw_advertisements_response"; } + const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } #endif std::vector advertisements{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1853,7 +1853,7 @@ class BluetoothDeviceRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 68; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_request"; } + const char *message_name() const override { return "bluetooth_device_request"; } #endif uint64_t address{0}; enums::BluetoothDeviceRequestType request_type{}; @@ -1873,7 +1873,7 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 69; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_connection_response"; } + const char *message_name() const override { return "bluetooth_device_connection_response"; } #endif uint64_t address{0}; bool connected{false}; @@ -1893,7 +1893,7 @@ class BluetoothGATTGetServicesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 70; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_get_services_request"; } + const char *message_name() const override { return "bluetooth_gatt_get_services_request"; } #endif uint64_t address{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1954,7 +1954,7 @@ class BluetoothGATTGetServicesResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 71; static constexpr uint16_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_get_services_response"; } + const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } #endif uint64_t address{0}; std::vector services{}; @@ -1973,7 +1973,7 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 72; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_get_services_done_response"; } + const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } #endif uint64_t address{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1990,7 +1990,7 @@ class BluetoothGATTReadRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 73; static constexpr uint16_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_read_request"; } + const char *message_name() const override { return "bluetooth_gatt_read_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2008,7 +2008,7 @@ class BluetoothGATTReadResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 74; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_read_response"; } + const char *message_name() const override { return "bluetooth_gatt_read_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2028,7 +2028,7 @@ class BluetoothGATTWriteRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 75; static constexpr uint16_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_write_request"; } + const char *message_name() const override { return "bluetooth_gatt_write_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2049,7 +2049,7 @@ class BluetoothGATTReadDescriptorRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 76; static constexpr uint16_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_read_descriptor_request"; } + const char *message_name() const override { return "bluetooth_gatt_read_descriptor_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2067,7 +2067,7 @@ class BluetoothGATTWriteDescriptorRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 77; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_write_descriptor_request"; } + const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2087,7 +2087,7 @@ class BluetoothGATTNotifyRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 78; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_notify_request"; } + const char *message_name() const override { return "bluetooth_gatt_notify_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2106,7 +2106,7 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 79; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_notify_data_response"; } + const char *message_name() const override { return "bluetooth_gatt_notify_data_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2126,7 +2126,7 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 80; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_bluetooth_connections_free_request"; } + const char *message_name() const override { return "subscribe_bluetooth_connections_free_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -2139,7 +2139,7 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 81; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_connections_free_response"; } + const char *message_name() const override { return "bluetooth_connections_free_response"; } #endif uint32_t free{0}; uint32_t limit{0}; @@ -2158,7 +2158,7 @@ class BluetoothGATTErrorResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 82; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_error_response"; } + const char *message_name() const override { return "bluetooth_gatt_error_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2177,7 +2177,7 @@ class BluetoothGATTWriteResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 83; static constexpr uint16_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_write_response"; } + const char *message_name() const override { return "bluetooth_gatt_write_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2195,7 +2195,7 @@ class BluetoothGATTNotifyResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 84; static constexpr uint16_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_notify_response"; } + const char *message_name() const override { return "bluetooth_gatt_notify_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2213,7 +2213,7 @@ class BluetoothDevicePairingResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 85; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_pairing_response"; } + const char *message_name() const override { return "bluetooth_device_pairing_response"; } #endif uint64_t address{0}; bool paired{false}; @@ -2232,7 +2232,7 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 86; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_unpairing_response"; } + const char *message_name() const override { return "bluetooth_device_unpairing_response"; } #endif uint64_t address{0}; bool success{false}; @@ -2251,7 +2251,7 @@ class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 87; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "unsubscribe_bluetooth_le_advertisements_request"; } + const char *message_name() const override { return "unsubscribe_bluetooth_le_advertisements_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -2264,7 +2264,7 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 88; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_clear_cache_response"; } + const char *message_name() const override { return "bluetooth_device_clear_cache_response"; } #endif uint64_t address{0}; bool success{false}; @@ -2283,7 +2283,7 @@ class BluetoothScannerStateResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 126; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_scanner_state_response"; } + const char *message_name() const override { return "bluetooth_scanner_state_response"; } #endif enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; @@ -2301,7 +2301,7 @@ class BluetoothScannerSetModeRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 127; static constexpr uint16_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_scanner_set_mode_request"; } + const char *message_name() const override { return "bluetooth_scanner_set_mode_request"; } #endif enums::BluetoothScannerMode mode{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2318,7 +2318,7 @@ class SubscribeVoiceAssistantRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 89; static constexpr uint16_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_voice_assistant_request"; } + const char *message_name() const override { return "subscribe_voice_assistant_request"; } #endif bool subscribe{false}; uint32_t flags{0}; @@ -2351,7 +2351,7 @@ class VoiceAssistantRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 90; static constexpr uint16_t ESTIMATED_SIZE = 41; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_request"; } + const char *message_name() const override { return "voice_assistant_request"; } #endif bool start{false}; std::string conversation_id{}; @@ -2373,7 +2373,7 @@ class VoiceAssistantResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 91; static constexpr uint16_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_response"; } + const char *message_name() const override { return "voice_assistant_response"; } #endif uint32_t port{0}; bool error{false}; @@ -2404,7 +2404,7 @@ class VoiceAssistantEventResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 92; static constexpr uint16_t ESTIMATED_SIZE = 36; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_event_response"; } + const char *message_name() const override { return "voice_assistant_event_response"; } #endif enums::VoiceAssistantEvent event_type{}; std::vector data{}; @@ -2423,7 +2423,7 @@ class VoiceAssistantAudio : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 106; static constexpr uint16_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_audio"; } + const char *message_name() const override { return "voice_assistant_audio"; } #endif std::string data{}; bool end{false}; @@ -2442,7 +2442,7 @@ class VoiceAssistantTimerEventResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 115; static constexpr uint16_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_timer_event_response"; } + const char *message_name() const override { return "voice_assistant_timer_event_response"; } #endif enums::VoiceAssistantTimerEvent event_type{}; std::string timer_id{}; @@ -2465,7 +2465,7 @@ class VoiceAssistantAnnounceRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 119; static constexpr uint16_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_announce_request"; } + const char *message_name() const override { return "voice_assistant_announce_request"; } #endif std::string media_id{}; std::string text{}; @@ -2486,7 +2486,7 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 120; static constexpr uint16_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_announce_finished"; } + const char *message_name() const override { return "voice_assistant_announce_finished"; } #endif bool success{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -2517,7 +2517,7 @@ class VoiceAssistantConfigurationRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 121; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_configuration_request"; } + const char *message_name() const override { return "voice_assistant_configuration_request"; } #endif #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -2530,7 +2530,7 @@ class VoiceAssistantConfigurationResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 122; static constexpr uint16_t ESTIMATED_SIZE = 56; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_configuration_response"; } + const char *message_name() const override { return "voice_assistant_configuration_response"; } #endif std::vector available_wake_words{}; std::vector active_wake_words{}; @@ -2550,7 +2550,7 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 123; static constexpr uint16_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_set_configuration"; } + const char *message_name() const override { return "voice_assistant_set_configuration"; } #endif std::vector active_wake_words{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2567,7 +2567,7 @@ class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 94; static constexpr uint16_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_alarm_control_panel_response"; } + const char *message_name() const override { return "list_entities_alarm_control_panel_response"; } #endif uint32_t supported_features{0}; bool requires_code{false}; @@ -2588,7 +2588,7 @@ class AlarmControlPanelStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 95; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "alarm_control_panel_state_response"; } + const char *message_name() const override { return "alarm_control_panel_state_response"; } #endif enums::AlarmControlPanelState state{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2606,7 +2606,7 @@ class AlarmControlPanelCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 96; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "alarm_control_panel_command_request"; } + const char *message_name() const override { return "alarm_control_panel_command_request"; } #endif uint32_t key{0}; enums::AlarmControlPanelStateCommand command{}; @@ -2627,7 +2627,7 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 97; static constexpr uint16_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_text_response"; } + const char *message_name() const override { return "list_entities_text_response"; } #endif uint32_t min_length{0}; uint32_t max_length{0}; @@ -2649,7 +2649,7 @@ class TextStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 98; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "text_state_response"; } + const char *message_name() const override { return "text_state_response"; } #endif std::string state{}; bool missing_state{false}; @@ -2669,7 +2669,7 @@ class TextCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 99; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "text_command_request"; } + const char *message_name() const override { return "text_command_request"; } #endif uint32_t key{0}; std::string state{}; @@ -2688,7 +2688,7 @@ class ListEntitiesDateResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 100; static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_date_response"; } + const char *message_name() const override { return "list_entities_date_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2706,7 +2706,7 @@ class DateStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 101; static constexpr uint16_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "date_state_response"; } + const char *message_name() const override { return "date_state_response"; } #endif bool missing_state{false}; uint32_t year{0}; @@ -2727,7 +2727,7 @@ class DateCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 102; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "date_command_request"; } + const char *message_name() const override { return "date_command_request"; } #endif uint32_t key{0}; uint32_t year{0}; @@ -2748,7 +2748,7 @@ class ListEntitiesTimeResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 103; static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_time_response"; } + const char *message_name() const override { return "list_entities_time_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2766,7 +2766,7 @@ class TimeStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 104; static constexpr uint16_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "time_state_response"; } + const char *message_name() const override { return "time_state_response"; } #endif bool missing_state{false}; uint32_t hour{0}; @@ -2787,7 +2787,7 @@ class TimeCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 105; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "time_command_request"; } + const char *message_name() const override { return "time_command_request"; } #endif uint32_t key{0}; uint32_t hour{0}; @@ -2808,7 +2808,7 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 107; static constexpr uint16_t ESTIMATED_SIZE = 76; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_event_response"; } + const char *message_name() const override { return "list_entities_event_response"; } #endif std::string device_class{}; std::vector event_types{}; @@ -2828,7 +2828,7 @@ class EventResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 108; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "event_response"; } + const char *message_name() const override { return "event_response"; } #endif std::string event_type{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2846,7 +2846,7 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 109; static constexpr uint16_t ESTIMATED_SIZE = 64; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_valve_response"; } + const char *message_name() const override { return "list_entities_valve_response"; } #endif std::string device_class{}; bool assumed_state{false}; @@ -2868,7 +2868,7 @@ class ValveStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 110; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "valve_state_response"; } + const char *message_name() const override { return "valve_state_response"; } #endif float position{0.0f}; enums::ValveOperation current_operation{}; @@ -2887,7 +2887,7 @@ class ValveCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 111; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "valve_command_request"; } + const char *message_name() const override { return "valve_command_request"; } #endif uint32_t key{0}; bool has_position{false}; @@ -2908,7 +2908,7 @@ class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 112; static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_date_time_response"; } + const char *message_name() const override { return "list_entities_date_time_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2926,7 +2926,7 @@ class DateTimeStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 113; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "date_time_state_response"; } + const char *message_name() const override { return "date_time_state_response"; } #endif bool missing_state{false}; uint32_t epoch_seconds{0}; @@ -2945,7 +2945,7 @@ class DateTimeCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 114; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "date_time_command_request"; } + const char *message_name() const override { return "date_time_command_request"; } #endif uint32_t key{0}; uint32_t epoch_seconds{0}; @@ -2963,7 +2963,7 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 116; static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_update_response"; } + const char *message_name() const override { return "list_entities_update_response"; } #endif std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2982,7 +2982,7 @@ class UpdateStateResponse : public StateResponseProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 117; static constexpr uint16_t ESTIMATED_SIZE = 61; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "update_state_response"; } + const char *message_name() const override { return "update_state_response"; } #endif bool missing_state{false}; bool in_progress{false}; @@ -3009,7 +3009,7 @@ class UpdateCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 118; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "update_command_request"; } + const char *message_name() const override { return "update_command_request"; } #endif uint32_t key{0}; enums::UpdateCommand command{}; diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index b2be314aaf..047c56198a 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -19,7 +19,7 @@ class APIServerConnectionBase : public ProtoService { template bool send_message(const T &msg) { #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_send_message_(T::message_name(), msg.dump()); + this->log_send_message_(msg.message_name(), msg.dump()); #endif return this->send_message_(msg, T::MESSAGE_TYPE); } diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index e850236db6..d9c9e3c85d 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -335,6 +335,7 @@ class ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP std::string dump() const; virtual void dump_to(std::string &out) const = 0; + virtual const char *message_name() const { return "unknown"; } #endif protected: diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index bd1be66649..419b5aa97d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -886,7 +886,7 @@ def build_message_type( public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP") snake_name = camel_to_snake(desc.name) public_content.append( - f'static constexpr const char *message_name() {{ return "{snake_name}"; }}' + f'const char *message_name() const override {{ return "{snake_name}"; }}' ) public_content.append("#endif") @@ -1356,7 +1356,7 @@ def main() -> None: hpp += " template\n" hpp += " bool send_message(const T &msg) {\n" hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" - hpp += " this->log_send_message_(T::message_name(), msg.dump());\n" + hpp += " this->log_send_message_(msg.message_name(), msg.dump());\n" hpp += "#endif\n" hpp += " return this->send_message_(msg, T::MESSAGE_TYPE);\n" hpp += " }\n\n" From 23b1e428dec69e4743b79d604200c17b1608fbe5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 05:35:01 +0200 Subject: [PATCH 054/115] Optimize Application class memory layout and reduce loop_interval size (#9208) --- esphome/core/application.h | 66 ++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 17270ca459..6ee05309ca 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include "esphome/core/component.h" @@ -335,11 +337,16 @@ class Application { * Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester * helper in helpers.h * + * Note: This method is not called by ESPHome core code. It is only used by lambda functions + * in YAML configurations or by external components. + * * @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds. */ - void set_loop_interval(uint32_t loop_interval) { this->loop_interval_ = loop_interval; } + void set_loop_interval(uint32_t loop_interval) { + this->loop_interval_ = std::min(loop_interval, static_cast(std::numeric_limits::max())); + } - uint32_t get_loop_interval() const { return this->loop_interval_; } + uint32_t get_loop_interval() const { return static_cast(this->loop_interval_); } void schedule_dump_config() { this->dump_config_at_ = 0; } @@ -618,6 +625,17 @@ class Application { /// Perform a delay while also monitoring socket file descriptors for readiness void yield_with_select_(uint32_t delay_ms); + // === Member variables ordered by size to minimize padding === + + // Pointer-sized members first + Component *current_component_{nullptr}; + const char *comment_{nullptr}; + const char *compilation_time_{nullptr}; + + // size_t members + size_t dump_config_at_{SIZE_MAX}; + + // Vectors (largest members) std::vector components_{}; // Partitioned vector design for looping components @@ -637,11 +655,6 @@ class Application { // and active_end_ is incremented // - This eliminates branch mispredictions from flag checking in the hot loop std::vector 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_DEVICES std::vector devices_{}; @@ -713,26 +726,39 @@ class Application { std::vector updates_{}; #endif +#ifdef USE_SOCKET_SELECT_SUPPORT + std::vector socket_fds_; // Vector of all monitored socket file descriptors +#endif + + // String members std::string name_; std::string friendly_name_; - const char *comment_{nullptr}; - const char *compilation_time_{nullptr}; - bool name_add_mac_suffix_; + + // 4-byte members uint32_t last_loop_{0}; - uint32_t loop_interval_{16}; - size_t dump_config_at_{SIZE_MAX}; - uint8_t app_state_{0}; - volatile bool has_pending_enable_loop_requests_{false}; - Component *current_component_{nullptr}; uint32_t loop_component_start_time_{0}; #ifdef USE_SOCKET_SELECT_SUPPORT - // Socket select management - std::vector socket_fds_; // Vector of all monitored socket file descriptors + int max_fd_{-1}; // Highest file descriptor number for select() +#endif + + // 2-byte members (grouped together for alignment) + uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) + uint16_t looping_components_active_end_{0}; + uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration + + // 1-byte members (grouped together to minimize padding) + uint8_t app_state_{0}; + bool name_add_mac_suffix_; + bool in_loop_{false}; + volatile bool has_pending_enable_loop_requests_{false}; + +#ifdef USE_SOCKET_SELECT_SUPPORT bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes - int max_fd_{-1}; // Highest file descriptor number for select() - fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes - fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_ + + // Variable-sized members at end + fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes + fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_ #endif }; From 9daa9a6de847f9b7fd7ee0c95eabe2be28ff5517 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 26 Jun 2025 16:21:51 +1200 Subject: [PATCH 055/115] Use shared workflow for locking (#9211) --- .github/workflows/lock.yml | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index ee10f49f61..4d7c86deee 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -1,28 +1,11 @@ --- -name: Lock +name: Lock closed issues and PRs on: schedule: - - cron: "30 0 * * *" + - cron: "30 0 * * *" # Run daily at 00:30 UTC workflow_dispatch: -permissions: - issues: write - pull-requests: write - -concurrency: - group: lock - jobs: lock: - runs-on: ubuntu-latest - steps: - - uses: dessant/lock-threads@v5.0.1 - with: - pr-inactive-days: "1" - pr-lock-reason: "" - exclude-any-pr-labels: keep-open - - issue-inactive-days: "7" - issue-lock-reason: "" - exclude-any-issue-labels: keep-open + uses: esphome/workflows/.github/workflows/lock.yml@main From 92365f133d384ec1b8781bd1917286a7b228b140 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Jun 2025 01:29:42 -0400 Subject: [PATCH 056/115] [esp32] Improve and simplify IDF component support (#9163) --- esphome/components/esp32/__init__.py | 166 +++++++++++---------------- 1 file changed, 70 insertions(+), 96 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f179c315f9..4e2a6ab852 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -4,7 +4,7 @@ import logging import os from pathlib import Path -from esphome import git +from esphome import yaml_util import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( @@ -23,7 +23,6 @@ from esphome.const import ( CONF_REFRESH, CONF_SOURCE, CONF_TYPE, - CONF_URL, CONF_VARIANT, CONF_VERSION, KEY_CORE, @@ -32,14 +31,13 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_ESP32, - TYPE_GIT, - TYPE_LOCAL, __version__, ) from esphome.core import CORE, HexInt, TimePeriod from esphome.cpp_generator import RawExpression import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed +from esphome.types import ConfigType from .boards import BOARDS from .const import ( # noqa @@ -49,10 +47,8 @@ from .const import ( # noqa KEY_EXTRA_BUILD_FILES, KEY_PATH, KEY_REF, - KEY_REFRESH, KEY_REPO, KEY_SDKCONFIG_OPTIONS, - KEY_SUBMODULES, KEY_VARIANT, VARIANT_ESP32, VARIANT_ESP32C2, @@ -235,7 +231,7 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): def add_idf_component( *, name: str, - repo: str, + repo: str = None, ref: str = None, path: str = None, refresh: TimePeriod = None, @@ -245,30 +241,27 @@ def add_idf_component( """Add an esp-idf component to the project.""" if not CORE.using_esp_idf: raise ValueError("Not an esp-idf project") - if components is None: - components = [] - if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: + if not repo and not ref and not path: + raise ValueError("Requires at least one of repo, ref or path") + if refresh or submodules or components: + _LOGGER.warning( + "The refresh, components and submodules parameters in add_idf_component() are " + "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " + "an issue to the external_component author and ask them to update it." + ) + if components: + for comp in components: + CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = { + KEY_REPO: repo, + KEY_REF: ref, + KEY_PATH: f"{path}/{comp}" if path else comp, + } + else: CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { KEY_REPO: repo, KEY_REF: ref, KEY_PATH: path, - KEY_REFRESH: refresh, - KEY_COMPONENTS: components, - KEY_SUBMODULES: submodules, } - else: - component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name] - if components is not None: - component_config[KEY_COMPONENTS] = list( - set(component_config[KEY_COMPONENTS] + components) - ) - if submodules is not None: - if component_config[KEY_SUBMODULES] is None: - component_config[KEY_SUBMODULES] = submodules - else: - component_config[KEY_SUBMODULES] = list( - set(component_config[KEY_SUBMODULES] + submodules) - ) def add_extra_script(stage: str, filename: str, path: str): @@ -575,6 +568,17 @@ CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server" CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" + +def _validate_idf_component(config: ConfigType) -> ConfigType: + """Validate IDF component config and warn about deprecated options.""" + if CONF_REFRESH in config: + _LOGGER.warning( + "The 'refresh' option for IDF components is deprecated and has no effect. " + "It will be removed in ESPHome 2026.1. Please remove it from your configuration." + ) + return config + + ESP_IDF_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { @@ -614,15 +618,19 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( } ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( - cv.Schema( - { - cv.Required(CONF_NAME): cv.string_strict, - cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA, - cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH, default="1d"): cv.All( - cv.string, cv.source_refresh - ), - } + cv.All( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.git_ref, + cv.Optional(CONF_REF): cv.string, + cv.Optional(CONF_PATH): cv.string, + cv.Optional(CONF_REFRESH): cv.All( + cv.string, cv.source_refresh + ), + } + ), + _validate_idf_component, ) ), } @@ -814,18 +822,12 @@ async def to_code(config): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) for component in conf[CONF_COMPONENTS]: - source = component[CONF_SOURCE] - if source[CONF_TYPE] == TYPE_GIT: - add_idf_component( - name=component[CONF_NAME], - repo=source[CONF_URL], - ref=source.get(CONF_REF), - path=component.get(CONF_PATH), - refresh=component[CONF_REFRESH], - ) - elif source[CONF_TYPE] == TYPE_LOCAL: - _LOGGER.warning("Local components are not implemented yet.") - + add_idf_component( + name=component[CONF_NAME], + repo=component.get(CONF_SOURCE), + ref=component.get(CONF_REF), + path=component.get(CONF_PATH), + ) elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") @@ -924,6 +926,26 @@ def _write_sdkconfig(): write_file_if_changed(sdk_path, contents) +def _write_idf_component_yml(): + yml_path = Path(CORE.relative_build_path("src/idf_component.yml")) + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: + components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] + dependencies = {} + for name, component in components.items(): + dependency = {} + if component[KEY_REF]: + dependency["version"] = component[KEY_REF] + if component[KEY_REPO]: + dependency["git"] = component[KEY_REPO] + if component[KEY_PATH]: + dependency["path"] = component[KEY_PATH] + dependencies[name] = dependency + contents = yaml_util.dump({"dependencies": dependencies}) + else: + contents = "" + write_file_if_changed(yml_path, contents) + + # Called by writer.py def copy_files(): if CORE.using_arduino: @@ -936,6 +958,7 @@ def copy_files(): ) if CORE.using_esp_idf: _write_sdkconfig() + _write_idf_component_yml() if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: write_file_if_changed( CORE.relative_build_path("partitions.csv"), @@ -952,55 +975,6 @@ def copy_files(): __version__, ) - import shutil - - shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True) - - if CORE.data[KEY_ESP32][KEY_COMPONENTS]: - components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] - - for name, component in components.items(): - repo_dir, _ = git.clone_or_update( - url=component[KEY_REPO], - ref=component[KEY_REF], - refresh=component[KEY_REFRESH], - domain="idf_components", - submodules=component[KEY_SUBMODULES], - ) - mkdir_p(CORE.relative_build_path("components")) - component_dir = repo_dir - if component[KEY_PATH] is not None: - component_dir = component_dir / component[KEY_PATH] - - if component[KEY_COMPONENTS] == ["*"]: - shutil.copytree( - component_dir, - CORE.relative_build_path("components"), - dirs_exist_ok=True, - ignore=shutil.ignore_patterns(".git*"), - symlinks=True, - ignore_dangling_symlinks=True, - ) - elif len(component[KEY_COMPONENTS]) > 0: - for comp in component[KEY_COMPONENTS]: - shutil.copytree( - component_dir / comp, - CORE.relative_build_path(f"components/{comp}"), - dirs_exist_ok=True, - ignore=shutil.ignore_patterns(".git*"), - symlinks=True, - ignore_dangling_symlinks=True, - ) - else: - shutil.copytree( - component_dir, - CORE.relative_build_path(f"components/{name}"), - dirs_exist_ok=True, - ignore=shutil.ignore_patterns(".git*"), - symlinks=True, - ignore_dangling_symlinks=True, - ) - for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): if file[KEY_PATH].startswith("http"): import requests From f7ac32cedaa604941d1deaaed4025403b17299cf Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 26 Jun 2025 00:35:30 -0500 Subject: [PATCH 057/115] [ld2450] More optimizing, fix copypasta (#9210) --- esphome/components/ld2450/ld2450.cpp | 18 ++++++------------ esphome/components/ld2450/ld2450.h | 9 ++++----- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 718c853d22..e78b79bead 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -17,8 +17,9 @@ namespace esphome { namespace ld2450 { static const char *const TAG = "ld2450"; -static const char *const NO_MAC("08:05:04:03:02:01"); -static const char *const UNKNOWN_MAC("unknown"); +static const char *const NO_MAC = "08:05:04:03:02:01"; +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; // LD2450 UART Serial Commands static const uint8_t CMD_ENABLE_CONF = 0x00FF; @@ -98,13 +99,6 @@ static inline std::string get_direction(int16_t speed) { return STATIONARY; } -static inline std::string format_version(uint8_t *buffer) { - return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], - buffer[14]); -} - -LD2450Component::LD2450Component() {} - void LD2450Component::setup() { ESP_LOGCONFIG(TAG, "Running setup"); #ifdef USE_NUMBER @@ -189,7 +183,7 @@ void LD2450Component::dump_config() { " Throttle: %ums\n" " MAC Address: %s\n" " Firmware version: %s", - this->throttle_, const_cast(this->mac_.c_str()), const_cast(this->version_.c_str())); + this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str()); } void LD2450Component::loop() { @@ -596,7 +590,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif break; case lowbyte(CMD_VERSION): - this->version_ = ld2450::format_version(buffer); + this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { @@ -617,7 +611,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { #endif #ifdef USE_SWITCH if (this->bluetooth_switch_ != nullptr) { - this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); + this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); } #endif break; diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index e0927e5d7d..cd3cb52a62 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -141,7 +141,6 @@ class LD2450Component : public Component, public uart::UARTDevice { #endif public: - LD2450Component(); void setup() override; void dump_config() override; void loop() override; @@ -197,17 +196,17 @@ class LD2450Component : public Component, public uart::UARTDevice { bool get_timeout_status_(uint32_t check_millis); uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); - Target target_info_[MAX_TARGETS]; - Zone zone_config_[MAX_ZONES]; - uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer - uint8_t buffer_data_[MAX_LINE_LENGTH]; uint32_t last_periodic_millis_ = 0; uint32_t presence_millis_ = 0; uint32_t still_presence_millis_ = 0; uint32_t moving_presence_millis_ = 0; uint16_t throttle_ = 0; uint16_t timeout_ = 5; + uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer + uint8_t buffer_data_[MAX_LINE_LENGTH]; uint8_t zone_type_ = 0; + Target target_info_[MAX_TARGETS]; + Zone zone_config_[MAX_ZONES]; std::string version_{}; std::string mac_{}; #ifdef USE_NUMBER From 95493040071e6666a2fd8b03b0cf6f2bfdb3e4f3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:44:02 +1200 Subject: [PATCH 058/115] [ci] Lint lock.yml (#9214) --- .github/workflows/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 4d7c86deee..8806a89748 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -3,7 +3,7 @@ name: Lock closed issues and PRs on: schedule: - - cron: "30 0 * * *" # Run daily at 00:30 UTC + - cron: "30 0 * * *" # Run daily at 00:30 UTC workflow_dispatch: jobs: From 09e5aa60110143553fc423ae6e5162e7dff7fe8b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:59:16 +1200 Subject: [PATCH 059/115] [script] Add exec bit to run-in-env (#9212) --- script/run-in-env.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 script/run-in-env.py diff --git a/script/run-in-env.py b/script/run-in-env.py old mode 100644 new mode 100755 From b12b9b97f4a23704b7f4793352a4bed982d4af8b Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 26 Jun 2025 04:04:38 -0500 Subject: [PATCH 060/115] [ld2410] More optimizations (#9209) Co-authored-by: J. Nick Koston --- esphome/components/ld2410/ld2410.cpp | 210 ++++++++++++++++++++++----- esphome/components/ld2410/ld2410.h | 113 +------------- 2 files changed, 176 insertions(+), 147 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index b3c3649ceb..a34f99ee33 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -10,6 +10,7 @@ #include "esphome/core/application.h" +#define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) @@ -17,8 +18,162 @@ namespace esphome { namespace ld2410 { static const char *const TAG = "ld2410"; +static const char *const NO_MAC = "08:05:04:03:02:01"; +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; -LD2410Component::LD2410Component() {} +enum BaudRateStructure : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8, +}; + +enum DistanceResolutionStructure : uint8_t { + DISTANCE_RESOLUTION_0_2 = 0x01, + DISTANCE_RESOLUTION_0_75 = 0x00, +}; + +enum LightFunctionStructure : uint8_t { + LIGHT_FUNCTION_OFF = 0x00, + LIGHT_FUNCTION_BELOW = 0x01, + LIGHT_FUNCTION_ABOVE = 0x02, +}; + +enum OutPinLevelStructure : uint8_t { + OUT_PIN_LEVEL_LOW = 0x00, + OUT_PIN_LEVEL_HIGH = 0x01, +}; + +enum PeriodicDataStructure : uint8_t { + DATA_TYPES = 6, + TARGET_STATES = 8, + MOVING_TARGET_LOW = 9, + MOVING_TARGET_HIGH = 10, + MOVING_ENERGY = 11, + STILL_TARGET_LOW = 12, + STILL_TARGET_HIGH = 13, + STILL_ENERGY = 14, + DETECT_DISTANCE_LOW = 15, + DETECT_DISTANCE_HIGH = 16, + MOVING_SENSOR_START = 19, + STILL_SENSOR_START = 28, + LIGHT_SENSOR = 37, + OUT_PIN_SENSOR = 38, +}; + +enum PeriodicDataValue : uint8_t { + HEAD = 0xAA, + END = 0x55, + CHECK = 0x00, +}; + +enum AckDataStructure : uint8_t { + COMMAND = 6, + COMMAND_STATUS = 7, +}; + +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + uint8_t value; +}; + +struct Uint8ToString { + uint8_t value; + const char *str; +}; + +constexpr StringToUint8 BAUD_RATES_BY_STR[] = { + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, +}; + +constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = { + {"0.2m", DISTANCE_RESOLUTION_0_2}, + {"0.75m", DISTANCE_RESOLUTION_0_75}, +}; + +constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = { + {DISTANCE_RESOLUTION_0_2, "0.2m"}, + {DISTANCE_RESOLUTION_0_75, "0.75m"}, +}; + +constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = { + {"off", LIGHT_FUNCTION_OFF}, + {"below", LIGHT_FUNCTION_BELOW}, + {"above", LIGHT_FUNCTION_ABOVE}, +}; + +constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = { + {LIGHT_FUNCTION_OFF, "off"}, + {LIGHT_FUNCTION_BELOW, "below"}, + {LIGHT_FUNCTION_ABOVE, "above"}, +}; + +constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = { + {"low", OUT_PIN_LEVEL_LOW}, + {"high", OUT_PIN_LEVEL_HIGH}, +}; + +constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { + {OUT_PIN_LEVEL_LOW, "low"}, + {OUT_PIN_LEVEL_HIGH, "high"}, +}; + +// Helper functions for lookups +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { + for (const auto &entry : arr) { + if (str == entry.str) + return entry.value; + } + return 0xFF; // Not found +} + +template const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { + for (const auto &entry : arr) { + if (value == entry.value) + return entry.str; + } + return ""; // Not found +} + +// Commands +static const uint8_t CMD_ENABLE_CONF = 0xFF; +static const uint8_t CMD_DISABLE_CONF = 0xFE; +static const uint8_t CMD_ENABLE_ENG = 0x62; +static const uint8_t CMD_DISABLE_ENG = 0x63; +static const uint8_t CMD_MAXDIST_DURATION = 0x60; +static const uint8_t CMD_QUERY = 0x61; +static const uint8_t CMD_GATE_SENS = 0x64; +static const uint8_t CMD_VERSION = 0xA0; +static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0xAB; +static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0xAA; +static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0xAE; +static const uint8_t CMD_SET_LIGHT_CONTROL = 0xAD; +static const uint8_t CMD_SET_BAUD_RATE = 0xA1; +static const uint8_t CMD_BT_PASSWORD = 0xA9; +static const uint8_t CMD_MAC = 0xA5; +static const uint8_t CMD_RESET = 0xA2; +static const uint8_t CMD_RESTART = 0xA3; +static const uint8_t CMD_BLUETOOTH = 0xA4; +// Commands values +static const uint8_t CMD_MAX_MOVE_VALUE = 0x00; +static const uint8_t CMD_MAX_STILL_VALUE = 0x01; +static const uint8_t CMD_DURATION_VALUE = 0x02; +// Command Header & Footer +static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; +static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; +// Data Header & Footer +static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1}; +static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5}; + +static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } void LD2410Component::dump_config() { ESP_LOGCONFIG(TAG, "LD2410:"); @@ -78,7 +233,7 @@ void LD2410Component::dump_config() { " Throttle: %ums\n" " MAC address: %s\n" " Firmware version: %s", - this->throttle_, const_cast(this->mac_.c_str()), const_cast(this->version_.c_str())); + this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str()); } void LD2410Component::setup() { @@ -200,7 +355,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { */ #ifdef USE_SENSOR if (this->moving_target_distance_sensor_ != nullptr) { - int new_moving_target_distance = this->two_byte_to_int_(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]); + int new_moving_target_distance = ld2410::two_byte_to_int(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]); if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance) this->moving_target_distance_sensor_->publish_state(new_moving_target_distance); } @@ -210,7 +365,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { this->moving_target_energy_sensor_->publish_state(new_moving_target_energy); } if (this->still_target_distance_sensor_ != nullptr) { - int new_still_target_distance = this->two_byte_to_int_(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]); + int new_still_target_distance = ld2410::two_byte_to_int(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]); if (this->still_target_distance_sensor_->get_state() != new_still_target_distance) this->still_target_distance_sensor_->publish_state(new_still_target_distance); } @@ -220,7 +375,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { this->still_target_energy_sensor_->publish_state(new_still_target_energy); } if (this->detection_distance_sensor_ != nullptr) { - int new_detect_distance = this->two_byte_to_int_(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]); + int new_detect_distance = ld2410::two_byte_to_int(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]); if (this->detection_distance_sensor_->get_state() != new_detect_distance) this->detection_distance_sensor_->publish_state(new_detect_distance); } @@ -282,25 +437,6 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { #endif } -const char VERSION_FMT[] = "%u.%02X.%02X%02X%02X%02X"; - -std::string format_version(uint8_t *buffer) { - std::string::size_type version_size = 256; - std::string version; - do { - version.resize(version_size + 1); - version_size = std::snprintf(&version[0], version.size(), VERSION_FMT, buffer[13], buffer[12], buffer[17], - buffer[16], buffer[15], buffer[14]); - } while (version_size + 1 > version.size()); - version.resize(version_size); - return version; -} - -const char MAC_FMT[] = "%02X:%02X:%02X:%02X:%02X:%02X"; - -const std::string UNKNOWN_MAC("unknown"); -const std::string NO_MAC("08:05:04:03:02:01"); - #ifdef USE_NUMBER std::function set_number_value(number::Number *n, float value) { float normalized_value = value * 1.0; @@ -326,7 +462,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { ESP_LOGE(TAG, "Invalid status"); return true; } - if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) { + if (ld2410::two_byte_to_int(buffer[8], buffer[9]) != 0x00) { ESP_LOGE(TAG, "Invalid command: %u, %u", buffer[8], buffer[9]); return true; } @@ -347,8 +483,8 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { #endif break; case lowbyte(CMD_VERSION): - this->version_ = format_version(buffer); - ESP_LOGV(TAG, "Firmware version: %s", const_cast(this->version_.c_str())); + this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); + ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { this->version_text_sensor_->publish_state(this->version_); @@ -357,8 +493,8 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { break; case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): { std::string distance_resolution = - DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11])); - ESP_LOGV(TAG, "Distance resolution: %s", const_cast(distance_resolution.c_str())); + find_str(DISTANCE_RESOLUTIONS_BY_UINT, ld2410::two_byte_to_int(buffer[10], buffer[11])); + ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution.c_str()); #ifdef USE_SELECT if (this->distance_resolution_select_ != nullptr && this->distance_resolution_select_->state != distance_resolution) { @@ -367,9 +503,9 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { #endif } break; case lowbyte(CMD_QUERY_LIGHT_CONTROL): { - this->light_function_ = LIGHT_FUNCTION_INT_TO_ENUM.at(buffer[10]); + this->light_function_ = find_str(LIGHT_FUNCTIONS_BY_UINT, buffer[10]); this->light_threshold_ = buffer[11] * 1.0; - this->out_pin_level_ = OUT_PIN_LEVEL_INT_TO_ENUM.at(buffer[12]); + this->out_pin_level_ = find_str(OUT_PIN_LEVELS_BY_UINT, buffer[12]); ESP_LOGV(TAG, "Light function: %s", const_cast(this->light_function_.c_str())); ESP_LOGV(TAG, "Light threshold: %f", this->light_threshold_); ESP_LOGV(TAG, "Out pin level: %s", const_cast(this->out_pin_level_.c_str())); @@ -402,7 +538,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { #endif #ifdef USE_SWITCH if (this->bluetooth_switch_ != nullptr) { - this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); + this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); } #endif break; @@ -448,7 +584,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { /* None Duration: 33~34th bytes */ - updates.push_back(set_number_value(this->timeout_number_, this->two_byte_to_int_(buffer[32], buffer[33]))); + updates.push_back(set_number_value(this->timeout_number_, ld2410::two_byte_to_int(buffer[32], buffer[33]))); for (auto &update : updates) { update(); } @@ -505,14 +641,14 @@ void LD2410Component::set_bluetooth(bool enable) { void LD2410Component::set_distance_resolution(const std::string &state) { this->set_config_mode_(true); - uint8_t cmd_value[2] = {DISTANCE_RESOLUTION_ENUM_TO_INT.at(state), 0x00}; + uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00}; this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, 2); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } void LD2410Component::set_baud_rate(const std::string &state) { this->set_config_mode_(true); - uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; + uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); this->set_timeout(200, [this]() { this->restart_(); }); } @@ -646,9 +782,9 @@ void LD2410Component::set_light_out_control() { return; } this->set_config_mode_(true); - uint8_t light_function = LIGHT_FUNCTION_ENUM_TO_INT.at(this->light_function_); + uint8_t light_function = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_); uint8_t light_threshold = static_cast(this->light_threshold_); - uint8_t out_pin_level = OUT_PIN_LEVEL_ENUM_TO_INT.at(this->out_pin_level_); + uint8_t out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_); uint8_t value[4] = {light_function, light_threshold, out_pin_level, 0x00}; this->send_command_(CMD_SET_LIGHT_CONTROL, value, 4); delay(50); // NOLINT diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 1bbaa8987a..1b5f6e3057 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -26,114 +26,9 @@ #include "esphome/core/automation.h" #include "esphome/core/helpers.h" -#include - namespace esphome { namespace ld2410 { -#define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) - -// Commands -static const uint8_t CMD_ENABLE_CONF = 0x00FF; -static const uint8_t CMD_DISABLE_CONF = 0x00FE; -static const uint8_t CMD_ENABLE_ENG = 0x0062; -static const uint8_t CMD_DISABLE_ENG = 0x0063; -static const uint8_t CMD_MAXDIST_DURATION = 0x0060; -static const uint8_t CMD_QUERY = 0x0061; -static const uint8_t CMD_GATE_SENS = 0x0064; -static const uint8_t CMD_VERSION = 0x00A0; -static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x00AB; -static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x00AA; -static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0x00AE; -static const uint8_t CMD_SET_LIGHT_CONTROL = 0x00AD; -static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; -static const uint8_t CMD_BT_PASSWORD = 0x00A9; -static const uint8_t CMD_MAC = 0x00A5; -static const uint8_t CMD_RESET = 0x00A2; -static const uint8_t CMD_RESTART = 0x00A3; -static const uint8_t CMD_BLUETOOTH = 0x00A4; - -enum BaudRateStructure : uint8_t { - BAUD_RATE_9600 = 1, - BAUD_RATE_19200 = 2, - BAUD_RATE_38400 = 3, - BAUD_RATE_57600 = 4, - BAUD_RATE_115200 = 5, - BAUD_RATE_230400 = 6, - BAUD_RATE_256000 = 7, - BAUD_RATE_460800 = 8 -}; - -static const std::map BAUD_RATE_ENUM_TO_INT{ - {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, - {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, - {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; - -enum DistanceResolutionStructure : uint8_t { DISTANCE_RESOLUTION_0_2 = 0x01, DISTANCE_RESOLUTION_0_75 = 0x00 }; - -static const std::map DISTANCE_RESOLUTION_ENUM_TO_INT{{"0.2m", DISTANCE_RESOLUTION_0_2}, - {"0.75m", DISTANCE_RESOLUTION_0_75}}; -static const std::map DISTANCE_RESOLUTION_INT_TO_ENUM{{DISTANCE_RESOLUTION_0_2, "0.2m"}, - {DISTANCE_RESOLUTION_0_75, "0.75m"}}; - -enum LightFunctionStructure : uint8_t { - LIGHT_FUNCTION_OFF = 0x00, - LIGHT_FUNCTION_BELOW = 0x01, - LIGHT_FUNCTION_ABOVE = 0x02 -}; - -static const std::map LIGHT_FUNCTION_ENUM_TO_INT{ - {"off", LIGHT_FUNCTION_OFF}, {"below", LIGHT_FUNCTION_BELOW}, {"above", LIGHT_FUNCTION_ABOVE}}; -static const std::map LIGHT_FUNCTION_INT_TO_ENUM{ - {LIGHT_FUNCTION_OFF, "off"}, {LIGHT_FUNCTION_BELOW, "below"}, {LIGHT_FUNCTION_ABOVE, "above"}}; - -enum OutPinLevelStructure : uint8_t { OUT_PIN_LEVEL_LOW = 0x00, OUT_PIN_LEVEL_HIGH = 0x01 }; - -static const std::map OUT_PIN_LEVEL_ENUM_TO_INT{{"low", OUT_PIN_LEVEL_LOW}, - {"high", OUT_PIN_LEVEL_HIGH}}; -static const std::map OUT_PIN_LEVEL_INT_TO_ENUM{{OUT_PIN_LEVEL_LOW, "low"}, - {OUT_PIN_LEVEL_HIGH, "high"}}; - -// Commands values -static const uint8_t CMD_MAX_MOVE_VALUE = 0x0000; -static const uint8_t CMD_MAX_STILL_VALUE = 0x0001; -static const uint8_t CMD_DURATION_VALUE = 0x0002; -// Command Header & Footer -static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; -static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; -// Data Header & Footer -static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1}; -static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5}; -/* -Data Type: 6th byte -Target states: 9th byte - Moving target distance: 10~11th bytes - Moving target energy: 12th byte - Still target distance: 13~14th bytes - Still target energy: 15th byte - Detect distance: 16~17th bytes -*/ -enum PeriodicDataStructure : uint8_t { - DATA_TYPES = 6, - TARGET_STATES = 8, - MOVING_TARGET_LOW = 9, - MOVING_TARGET_HIGH = 10, - MOVING_ENERGY = 11, - STILL_TARGET_LOW = 12, - STILL_TARGET_HIGH = 13, - STILL_ENERGY = 14, - DETECT_DISTANCE_LOW = 15, - DETECT_DISTANCE_HIGH = 16, - MOVING_SENSOR_START = 19, - STILL_SENSOR_START = 28, - LIGHT_SENSOR = 37, - OUT_PIN_SENSOR = 38, -}; -enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 }; - -enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; - -// char cmd[2] = {enable ? 0xFF : 0xFE, 0x00}; class LD2410Component : public Component, public uart::UARTDevice { #ifdef USE_SENSOR SUB_SENSOR(moving_target_distance) @@ -176,7 +71,6 @@ class LD2410Component : public Component, public uart::UARTDevice { #endif public: - LD2410Component(); void setup() override; void dump_config() override; void loop() override; @@ -202,7 +96,6 @@ class LD2410Component : public Component, public uart::UARTDevice { void factory_reset(); protected: - int two_byte_to_int_(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } void send_command_(uint8_t command_str, const uint8_t *command_value, int command_value_len); void set_config_mode_(bool enable); void handle_periodic_data_(uint8_t *buffer, int len); @@ -215,14 +108,14 @@ class LD2410Component : public Component, public uart::UARTDevice { void get_light_control_(); void restart_(); - int32_t last_periodic_millis_ = millis(); - int32_t last_engineering_mode_change_millis_ = millis(); + int32_t last_periodic_millis_ = 0; + int32_t last_engineering_mode_change_millis_ = 0; uint16_t throttle_; + float light_threshold_ = -1; std::string version_; std::string mac_; std::string out_pin_level_; std::string light_function_; - float light_threshold_ = -1; #ifdef USE_NUMBER std::vector gate_still_threshold_numbers_ = std::vector(9); std::vector gate_move_threshold_numbers_ = std::vector(9); From 2930c8e9a8a12d87afc0ebd67041dad90eaf4f04 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 26 Jun 2025 04:37:27 -0500 Subject: [PATCH 061/115] [ld2450] Move consts to cpp file, optimize memory use (#9215) --- esphome/components/ld2450/ld2450.cpp | 117 +++++++++++++++++++++++---- esphome/components/ld2450/ld2450.h | 45 ----------- 2 files changed, 101 insertions(+), 61 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index e78b79bead..0e1123db1a 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -21,20 +21,105 @@ static const char *const NO_MAC = "08:05:04:03:02:01"; static const char *const UNKNOWN_MAC = "unknown"; static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; +enum BaudRateStructure : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8 +}; + +// Zone type struct +enum ZoneTypeStructure : uint8_t { + ZONE_DISABLED = 0, + ZONE_DETECTION = 1, + ZONE_FILTER = 2, +}; + +enum PeriodicDataStructure : uint8_t { + TARGET_X = 4, + TARGET_Y = 6, + TARGET_SPEED = 8, + TARGET_RESOLUTION = 10, +}; + +enum PeriodicDataValue : uint8_t { + HEAD = 0xAA, + END = 0x55, + CHECK = 0x00, +}; + +enum AckDataStructure : uint8_t { + COMMAND = 6, + COMMAND_STATUS = 7, +}; + +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + uint8_t value; +}; + +struct Uint8ToString { + uint8_t value; + const char *str; +}; + +constexpr StringToUint8 BAUD_RATES_BY_STR[] = { + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, +}; + +constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = { + {ZONE_DISABLED, "Disabled"}, + {ZONE_DETECTION, "Detection"}, + {ZONE_FILTER, "Filter"}, +}; + +constexpr StringToUint8 ZONE_TYPE_BY_STR[] = { + {"Disabled", ZONE_DISABLED}, + {"Detection", ZONE_DETECTION}, + {"Filter", ZONE_FILTER}, +}; + +// Helper functions for lookups +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { + for (const auto &entry : arr) { + if (str == entry.str) + return entry.value; + } + return 0xFF; // Not found +} + +template const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { + for (const auto &entry : arr) { + if (value == entry.value) + return entry.str; + } + return ""; // Not found +} + +// LD2450 serial command header & footer +static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; +static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; // LD2450 UART Serial Commands -static const uint8_t CMD_ENABLE_CONF = 0x00FF; -static const uint8_t CMD_DISABLE_CONF = 0x00FE; -static const uint8_t CMD_VERSION = 0x00A0; -static const uint8_t CMD_MAC = 0x00A5; -static const uint8_t CMD_RESET = 0x00A2; -static const uint8_t CMD_RESTART = 0x00A3; -static const uint8_t CMD_BLUETOOTH = 0x00A4; -static const uint8_t CMD_SINGLE_TARGET_MODE = 0x0080; -static const uint8_t CMD_MULTI_TARGET_MODE = 0x0090; -static const uint8_t CMD_QUERY_TARGET_MODE = 0x0091; -static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; -static const uint8_t CMD_QUERY_ZONE = 0x00C1; -static const uint8_t CMD_SET_ZONE = 0x00C2; +static const uint8_t CMD_ENABLE_CONF = 0xFF; +static const uint8_t CMD_DISABLE_CONF = 0xFE; +static const uint8_t CMD_VERSION = 0xA0; +static const uint8_t CMD_MAC = 0xA5; +static const uint8_t CMD_RESET = 0xA2; +static const uint8_t CMD_RESTART = 0xA3; +static const uint8_t CMD_BLUETOOTH = 0xA4; +static const uint8_t CMD_SINGLE_TARGET_MODE = 0x80; +static const uint8_t CMD_MULTI_TARGET_MODE = 0x90; +static const uint8_t CMD_QUERY_TARGET_MODE = 0x91; +static const uint8_t CMD_SET_BAUD_RATE = 0xA1; +static const uint8_t CMD_QUERY_ZONE = 0xC1; +static const uint8_t CMD_SET_ZONE = 0xC2; static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; @@ -720,7 +805,7 @@ void LD2450Component::set_bluetooth(bool enable) { // Set Baud rate void LD2450Component::set_baud_rate(const std::string &state) { this->set_config_mode_(true); - uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; + uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); this->set_timeout(200, [this]() { this->restart_(); }); } @@ -728,7 +813,7 @@ void LD2450Component::set_baud_rate(const std::string &state) { // Set Zone Type - one of: Disabled, Detection, Filter void LD2450Component::set_zone_type(const std::string &state) { ESP_LOGV(TAG, "Set zone type: %s", state.c_str()); - uint8_t zone_type = ZONE_TYPE_ENUM_TO_INT.at(state); + uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state); this->zone_type_ = zone_type; this->send_set_zone_command_(); } @@ -736,7 +821,7 @@ void LD2450Component::set_zone_type(const std::string &state) { // Publish Zone Type to Select component void LD2450Component::publish_zone_type() { #ifdef USE_SELECT - std::string zone_type = ZONE_TYPE_INT_TO_ENUM.at(static_cast(this->zone_type_)); + std::string zone_type = find_str(ZONE_TYPE_BY_UINT, this->zone_type_); if (this->zone_type_select_ != nullptr) { this->zone_type_select_->publish_state(zone_type); } diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index cd3cb52a62..b0c19dc96c 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,7 +1,5 @@ #pragma once -#include -#include #include "esphome/components/uart/uart.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" @@ -66,49 +64,6 @@ struct ZoneOfNumbers { }; #endif -enum BaudRateStructure : uint8_t { - BAUD_RATE_9600 = 1, - BAUD_RATE_19200 = 2, - BAUD_RATE_38400 = 3, - BAUD_RATE_57600 = 4, - BAUD_RATE_115200 = 5, - BAUD_RATE_230400 = 6, - BAUD_RATE_256000 = 7, - BAUD_RATE_460800 = 8 -}; - -// Convert baud rate enum to int -static const std::map BAUD_RATE_ENUM_TO_INT{ - {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, - {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, - {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; - -// Zone type struct -enum ZoneTypeStructure : uint8_t { ZONE_DISABLED = 0, ZONE_DETECTION = 1, ZONE_FILTER = 2 }; - -// Convert zone type int to enum -static const std::map ZONE_TYPE_INT_TO_ENUM{ - {ZONE_DISABLED, "Disabled"}, {ZONE_DETECTION, "Detection"}, {ZONE_FILTER, "Filter"}}; - -// Convert zone type enum to int -static const std::map ZONE_TYPE_ENUM_TO_INT{ - {"Disabled", ZONE_DISABLED}, {"Detection", ZONE_DETECTION}, {"Filter", ZONE_FILTER}}; - -// LD2450 serial command header & footer -static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; -static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; - -enum PeriodicDataStructure : uint8_t { - TARGET_X = 4, - TARGET_Y = 6, - TARGET_SPEED = 8, - TARGET_RESOLUTION = 10, -}; - -enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 }; - -enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; - class LD2450Component : public Component, public uart::UARTDevice { #ifdef USE_SENSOR SUB_SENSOR(target_count) From 15c5dd222fcfcf4236e248cef3bfc0b29ad8bfc3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 26 Jun 2025 23:21:19 +1200 Subject: [PATCH 062/115] [tests] Remove extra newline (#9213) --- .../build_components_base.esp32-p4-idf.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_build_components/build_components_base.esp32-p4-idf.yaml b/tests/test_build_components/build_components_base.esp32-p4-idf.yaml index e2b975f643..9e4f0ddd61 100644 --- a/tests/test_build_components/build_components_base.esp32-p4-idf.yaml +++ b/tests/test_build_components/build_components_base.esp32-p4-idf.yaml @@ -15,4 +15,3 @@ packages: file: $component_test_file vars: component_test_file: $component_test_file - From d94896c0fbfd5428ecd870380ad17988f773d0b4 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 26 Jun 2025 20:11:50 +0100 Subject: [PATCH 063/115] [audio] Bugfix: improve timeout handling (#9221) --- esphome/components/audio/audio_reader.cpp | 62 +++++++++++++++-------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index b82c6db9ee..6966c95db7 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -5,6 +5,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE #include "esp_crt_bundle.h" @@ -16,13 +17,13 @@ namespace audio { static const uint32_t READ_WRITE_TIMEOUT_MS = 20; static const uint32_t CONNECTION_TIMEOUT_MS = 5000; - -// The number of times the http read times out with no data before throwing an error -static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100; +static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6; static const size_t HTTP_STREAM_BUFFER_SIZE = 2048; -static const uint8_t MAX_REDIRECTION = 5; +static const uint8_t MAX_REDIRECTIONS = 5; + +static const char *const TAG = "audio_reader"; // Some common HTTP status codes - borrowed from http_request component accessed 20241224 enum HttpStatus { @@ -94,7 +95,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { client_config.url = uri.c_str(); client_config.cert_pem = nullptr; client_config.disable_auto_redirect = false; - client_config.max_redirection_count = 10; + client_config.max_redirection_count = MAX_REDIRECTIONS; client_config.event_handler = http_event_handler; client_config.user_data = this; client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE; @@ -116,12 +117,29 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { esp_err_t err = esp_http_client_open(this->client_, 0); if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open URL"); this->cleanup_connection_(); return err; } int64_t header_length = esp_http_client_fetch_headers(this->client_); + uint8_t reattempt_count = 0; + while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) { + this->cleanup_connection_(); + if (header_length != -ESP_ERR_HTTP_EAGAIN) { + // Serious error, no recovery + return ESP_FAIL; + } else { + // Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available + this->client_ = esp_http_client_init(&client_config); + esp_http_client_open(this->client_, 0); + header_length = esp_http_client_fetch_headers(this->client_); + ++reattempt_count; + } + } + if (header_length < 0) { + ESP_LOGE(TAG, "Failed to fetch headers"); this->cleanup_connection_(); return ESP_FAIL; } @@ -135,7 +153,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { ssize_t redirect_count = 0; - while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) { + while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) { err = esp_http_client_open(this->client_, 0); if (err != ESP_OK) { this->cleanup_connection_(); @@ -267,27 +285,29 @@ AudioReaderState AudioReader::http_read_() { return AudioReaderState::FINISHED; } } else if (this->output_transfer_buffer_->free() > 0) { - size_t bytes_to_read = this->output_transfer_buffer_->free(); - int received_len = - esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read); + int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free()); if (received_len > 0) { this->output_transfer_buffer_->increase_buffer_length(received_len); this->last_data_read_ms_ = millis(); - } else if (received_len < 0) { + return AudioReaderState::READING; + } else if (received_len <= 0) { // HTTP read error - this->cleanup_connection_(); - return AudioReaderState::FAILED; - } else { - if (bytes_to_read > 0) { - // Read timed out - if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) { - this->cleanup_connection_(); - return AudioReaderState::FAILED; - } - - delay(READ_WRITE_TIMEOUT_MS); + if (received_len == -1) { + // A true connection error occured, no chance at recovery + this->cleanup_connection_(); + return AudioReaderState::FAILED; } + + // Read timed out, manually verify if it has been too long since the last successful read + if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) { + ESP_LOGE(TAG, "Timed out"); + this->cleanup_connection_(); + return AudioReaderState::FAILED; + } + + delay(READ_WRITE_TIMEOUT_MS); } } From 4fac8e9cd589a1172663c5aa5cd41f0bf77b25eb Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 26 Jun 2025 20:12:58 +0100 Subject: [PATCH 064/115] [speaker] bugfix: continue to block tasks if stop flag is set (#9222) --- .../speaker/media_player/audio_pipeline.cpp | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index ac122b6e0c..333a076bec 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -343,13 +343,12 @@ void AudioPipeline::read_task(void *params) { xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); // Wait until the pipeline notifies us the source of the media file - EventBits_t event_bits = - xEventGroupWaitBits(this_pipeline->event_group_, - EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP | - EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read - pdFALSE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set + EventBits_t event_bits = xEventGroupWaitBits( + this_pipeline->event_group_, + EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP, // Bit message to read + pdFALSE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) { xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED | @@ -434,12 +433,12 @@ void AudioPipeline::decode_task(void *params) { xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED); // Wait until the reader notifies us that the media type is available - EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_, - EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE | - EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read - pdFALSE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set + EventBits_t event_bits = + xEventGroupWaitBits(this_pipeline->event_group_, + EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE, // Bit message to read + pdFALSE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE); From b182f2d544c1b20f04579d380533ced0fc646983 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 26 Jun 2025 20:18:51 +0100 Subject: [PATCH 065/115] [voice_assistant] Support streaming TTS responses and fixes crash for long responses (#9224) --- CODEOWNERS | 2 +- .../components/voice_assistant/__init__.py | 13 ++++++- .../voice_assistant/voice_assistant.cpp | 38 ++++++++++++++++++- .../voice_assistant/voice_assistant.h | 4 ++ 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 83d64a8850..4eb4c42a6d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -491,7 +491,7 @@ esphome/components/vbus/* @ssieb esphome/components/veml3235/* @kbx81 esphome/components/veml7700/* @latonita esphome/components/version/* @esphome/core -esphome/components/voice_assistant/* @jesserockz +esphome/components/voice_assistant/* @jesserockz @kahrendt esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/watchdog/* @oarcher esphome/components/waveshare_epaper/* @clydebarrow diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index b9309ab422..59c7ec8383 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -17,10 +17,11 @@ from esphome.const import ( AUTO_LOAD = ["socket"] DEPENDENCIES = ["api", "microphone"] -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kahrendt"] CONF_ON_END = "on_end" CONF_ON_INTENT_END = "on_intent_end" +CONF_ON_INTENT_PROGRESS = "on_intent_progress" CONF_ON_INTENT_START = "on_intent_start" CONF_ON_LISTENING = "on_listening" CONF_ON_START = "on_start" @@ -136,6 +137,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_INTENT_PROGRESS): automation.validate_automation( + single=True + ), cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( single=True ), @@ -282,6 +286,13 @@ async def to_code(config): config[CONF_ON_INTENT_START], ) + if CONF_ON_INTENT_PROGRESS in config: + await automation.build_automation( + var.get_intent_progress_trigger(), + [(cg.std_string, "x")], + config[CONF_ON_INTENT_PROGRESS], + ) + if CONF_ON_INTENT_END in config: await automation.build_automation( var.get_intent_end_trigger(), diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 366a020d1c..9cf7d10936 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -555,7 +555,7 @@ void VoiceAssistant::request_stop() { break; case State::AWAITING_RESPONSE: this->signal_stop_(); - break; + // Fallthrough intended to stop a streaming TTS announcement that has potentially started case State::STREAMING_RESPONSE: #ifdef USE_MEDIA_PLAYER // Stop any ongoing media player announcement @@ -599,6 +599,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_RUN_START: ESP_LOGD(TAG, "Assist Pipeline running"); +#ifdef USE_MEDIA_PLAYER + this->started_streaming_tts_ = false; + for (auto arg : msg.data) { + if (arg.name == "url") { + this->tts_response_url_ = std::move(arg.value); + } + } +#endif this->defer([this]() { this->start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: @@ -622,6 +630,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { if (text.empty()) { ESP_LOGW(TAG, "No text in STT_END event"); return; + } else if (text.length() > 500) { + text = text.substr(0, 497) + "..."; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); @@ -631,6 +641,27 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Intent started"); this->defer([this]() { this->intent_start_trigger_->trigger(); }); break; + case api::enums::VOICE_ASSISTANT_INTENT_PROGRESS: { + ESP_LOGD(TAG, "Intent progress"); + std::string tts_url_for_trigger = ""; +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + for (const auto &arg : msg.data) { + if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) { + this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform(); + + this->media_player_wait_for_announcement_start_ = true; + this->media_player_wait_for_announcement_end_ = false; + this->started_streaming_tts_ = true; + tts_url_for_trigger = this->tts_response_url_; + this->tts_response_url_.clear(); // Reset streaming URL + } + } + } +#endif + this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_->trigger(tts_url_for_trigger); }); + break; + } case api::enums::VOICE_ASSISTANT_INTENT_END: { for (auto arg : msg.data) { if (arg.name == "conversation_id") { @@ -653,6 +684,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGW(TAG, "No text in TTS_START event"); return; } + if (text.length() > 500) { + text = text.substr(0, 497) + "..."; + } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); this->defer([this, text]() { this->tts_start_trigger_->trigger(text); @@ -678,7 +712,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); this->defer([this, url]() { #ifdef USE_MEDIA_PLAYER - if (this->media_player_ != nullptr) { + if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) { this->media_player_->make_call().set_media_url(url).set_announcement(true).perform(); this->media_player_wait_for_announcement_start_ = true; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 865731522f..2424ea6052 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -177,6 +177,7 @@ class VoiceAssistant : public Component { Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } + Trigger *get_intent_progress_trigger() const { return this->intent_progress_trigger_; } Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } Trigger<> *get_end_trigger() const { return this->end_trigger_; } Trigger<> *get_start_trigger() const { return this->start_trigger_; } @@ -233,6 +234,7 @@ class VoiceAssistant : public Component { Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); #endif + Trigger *intent_progress_trigger_ = new Trigger(); Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); @@ -268,6 +270,8 @@ class VoiceAssistant : public Component { #endif #ifdef USE_MEDIA_PLAYER media_player::MediaPlayer *media_player_{nullptr}; + std::string tts_response_url_{""}; + bool started_streaming_tts_{false}; bool media_player_wait_for_announcement_start_{false}; bool media_player_wait_for_announcement_end_{false}; #endif From 4f5aacdb3a113819294e2fcb8c9700d6dc44a12d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 01:25:26 +0200 Subject: [PATCH 066/115] Optimize SafeModeComponent memory layout to reduce padding (#9228) --- esphome/components/safe_mode/safe_mode.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 37e2c3a3d6..028b7b11cb 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -33,12 +33,15 @@ class SafeModeComponent : public Component { void write_rtc_(uint32_t val); uint32_t read_rtc_(); - bool boot_successful_{false}; ///< set to true after boot is considered successful + // Group all 4-byte aligned members together to avoid padding uint32_t safe_mode_boot_is_good_after_{60000}; ///< The amount of time after which the boot is considered successful uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should remain active for uint32_t safe_mode_rtc_value_{0}; uint32_t safe_mode_start_time_{0}; ///< stores when safe mode was enabled + // Group 1-byte members together to minimize padding + bool boot_successful_{false}; ///< set to true after boot is considered successful uint8_t safe_mode_num_attempts_{0}; + // Larger objects at the end ESPPreferenceObject rtc_; CallbackManager safe_mode_callback_{}; From 87321ce10b8611a5f8ea14787e497dcc8a278051 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:51:13 -0400 Subject: [PATCH 067/115] [esp32_hosted] Add support for remote wifi (#8833) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/esp32_hosted/__init__.py | 101 ++++++++++++++++++ .../esp32_hosted/esp32_hosted.py.script | 12 +++ esphome/idf_component.yml | 12 +++ tests/components/esp32_hosted/common.yaml | 15 +++ .../esp32_hosted/test.esp32-p4-idf.yaml | 1 + 6 files changed, 142 insertions(+) create mode 100644 esphome/components/esp32_hosted/__init__.py create mode 100644 esphome/components/esp32_hosted/esp32_hosted.py.script create mode 100644 tests/components/esp32_hosted/common.yaml create mode 100644 tests/components/esp32_hosted/test.esp32-p4-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 4eb4c42a6d..832c571ae4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -146,6 +146,7 @@ esphome/components/esp32_ble_client/* @jesserockz esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron +esphome/components/esp32_hosted/* @swoboda1337 esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py new file mode 100644 index 0000000000..330800df12 --- /dev/null +++ b/esphome/components/esp32_hosted/__init__.py @@ -0,0 +1,101 @@ +import os + +from esphome import pins +from esphome.components import esp32 +import esphome.config_validation as cv +from esphome.const import ( + CONF_CLK_PIN, + CONF_RESET_PIN, + CONF_VARIANT, + KEY_CORE, + KEY_FRAMEWORK_VERSION, +) +from esphome.core import CORE + +CODEOWNERS = ["@swoboda1337"] + +CONF_ACTIVE_HIGH = "active_high" +CONF_CMD_PIN = "cmd_pin" +CONF_D0_PIN = "d0_pin" +CONF_D1_PIN = "d1_pin" +CONF_D2_PIN = "d2_pin" +CONF_D3_PIN = "d3_pin" +CONF_SLOT = "slot" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True), + cv.Required(CONF_ACTIVE_HIGH): cv.boolean, + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D1_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D2_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), + } + ), +) + + +async def to_code(config): + if config[CONF_ACTIVE_HIGH]: + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH", + True, + ) + else: + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_LOW", + True, + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE", # NOLINT + config[CONF_RESET_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}", # NOLINT + True, + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_SDIO_SLOT_{config[CONF_SLOT]}", + True, + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{config[CONF_SLOT]}", + config[CONF_CLK_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{config[CONF_SLOT]}", + config[CONF_CMD_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{config[CONF_SLOT]}", + config[CONF_D0_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{config[CONF_SLOT]}", + config[CONF_D1_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{config[CONF_SLOT]}", + config[CONF_D2_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{config[CONF_SLOT]}", + config[CONF_D3_PIN], + ) + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True) + + framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" + esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.10.2") + esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11") + esp32.add_extra_script( + "post", + "esp32_hosted.py", + os.path.join(os.path.dirname(__file__), "esp32_hosted.py.script"), + ) diff --git a/esphome/components/esp32_hosted/esp32_hosted.py.script b/esphome/components/esp32_hosted/esp32_hosted.py.script new file mode 100644 index 0000000000..4be297c500 --- /dev/null +++ b/esphome/components/esp32_hosted/esp32_hosted.py.script @@ -0,0 +1,12 @@ +# pylint: disable=E0602 +Import("env") # noqa + +# Workaround whole archive issue +if "__LIB_DEPS" in env and "libespressif__esp_hosted.a" in env["__LIB_DEPS"]: + env.Append( + LINKFLAGS=[ + "-Wl,--whole-archive", + env["BUILD_DIR"] + "/esp-idf/espressif__esp_hosted/libespressif__esp_hosted.a", + "-Wl,--no-whole-archive", + ] + ) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 8460de5638..7dcfe918eb 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -11,3 +11,15 @@ dependencies: path: components/mdns rules: - if: "idf_version >=5.0" + espressif/esp_wifi_remote: + version: 0.10.2 + rules: + - if: "target in [esp32h2, esp32p4]" + espressif/eppp_link: + version: 0.2.0 + rules: + - if: "target in [esp32h2, esp32p4]" + espressif/esp_hosted: + version: 2.0.11 + rules: + - if: "target in [esp32h2, esp32p4]" diff --git a/tests/components/esp32_hosted/common.yaml b/tests/components/esp32_hosted/common.yaml new file mode 100644 index 0000000000..ab029e5064 --- /dev/null +++ b/tests/components/esp32_hosted/common.yaml @@ -0,0 +1,15 @@ +esp32_hosted: + variant: ESP32C6 + slot: 1 + active_high: true + reset_pin: GPIO15 + cmd_pin: GPIO13 + clk_pin: GPIO12 + d0_pin: GPIO11 + d1_pin: GPIO10 + d2_pin: GPIO9 + d3_pin: GPIO8 + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/esp32_hosted/test.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 61dfd5541f491d3306fc38106e5a8ba08513185f Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Fri, 27 Jun 2025 04:40:42 +0200 Subject: [PATCH 068/115] use c++17 `[[fallthrough]];` (#9149) --- esphome/components/audio/audio_decoder.cpp | 2 +- .../esp32_ble_client/ble_client_base.cpp | 10 +++++----- esphome/components/pn7150/pn7150.cpp | 14 +++++++------- esphome/components/pn7160/pn7160.cpp | 14 +++++++------- esphome/components/shelly_dimmer/stm32flash.cpp | 3 +-- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index c74b028c4b..90ba1aec1e 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -312,7 +312,7 @@ FileDecoderState AudioDecoder::decode_mp3_() { if (err) { switch (err) { case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY: - // Intentional fallthrough + [[fallthrough]]; case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER: return FileDecoderState::FAILED; break; diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 8ae1eb1bac..7d0a3bbfd5 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -496,17 +496,17 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { if (length > 2) { return (float) encode_uint16(value[1], value[2]); } - // fall through + [[fallthrough]]; case 0x7: // uint24. if (length > 3) { return (float) encode_uint24(value[1], value[2], value[3]); } - // fall through + [[fallthrough]]; case 0x8: // uint32. if (length > 4) { return (float) encode_uint32(value[1], value[2], value[3], value[4]); } - // fall through + [[fallthrough]]; case 0xC: // int8. return (float) ((int8_t) value[1]); case 0xD: // int12. @@ -514,12 +514,12 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { if (length > 2) { return (float) ((int16_t) (value[1] << 8) + (int16_t) value[2]); } - // fall through + [[fallthrough]]; case 0xF: // int24. if (length > 3) { return (float) ((int32_t) (value[1] << 16) + (int32_t) (value[2] << 8) + (int32_t) (value[3])); } - // fall through + [[fallthrough]]; case 0x10: // int32. if (length > 4) { return (float) ((int32_t) (value[1] << 24) + (int32_t) (value[2] << 16) + (int32_t) (value[3] << 8) + diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index 971ddd23cb..f827bd151a 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -584,7 +584,7 @@ void PN7150::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_INIT); } - // fall through + [[fallthrough]]; case NCIState::NFCC_INIT: if (this->init_core_() != nfc::STATUS_OK) { @@ -594,7 +594,7 @@ void PN7150::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); } - // fall through + [[fallthrough]]; case NCIState::NFCC_CONFIG: if (this->send_init_config_() != nfc::STATUS_OK) { @@ -605,7 +605,7 @@ void PN7150::nci_fsm_transition_() { this->config_refresh_pending_ = false; this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); } - // fall through + [[fallthrough]]; case NCIState::NFCC_SET_DISCOVER_MAP: if (this->set_discover_map_() != nfc::STATUS_OK) { @@ -615,7 +615,7 @@ void PN7150::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); } - // fall through + [[fallthrough]]; case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { @@ -625,7 +625,7 @@ void PN7150::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::RFST_IDLE); } - // fall through + [[fallthrough]]; case NCIState::RFST_IDLE: if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { @@ -650,14 +650,14 @@ void PN7150::nci_fsm_transition_() { case NCIState::RFST_W4_HOST_SELECT: select_endpoint_(); - // fall through + [[fallthrough]]; // All cases below are waiting for NOTIFICATION messages case NCIState::RFST_DISCOVERY: if (this->config_refresh_pending_) { this->refresh_core_config_(); } - // fall through + [[fallthrough]]; case NCIState::RFST_LISTEN_ACTIVE: case NCIState::RFST_LISTEN_SLEEP: diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 2a1de20657..a8edfadd8e 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -609,7 +609,7 @@ void PN7160::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_INIT); } - // fall through + [[fallthrough]]; case NCIState::NFCC_INIT: if (this->init_core_() != nfc::STATUS_OK) { @@ -619,7 +619,7 @@ void PN7160::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); } - // fall through + [[fallthrough]]; case NCIState::NFCC_CONFIG: if (this->send_init_config_() != nfc::STATUS_OK) { @@ -630,7 +630,7 @@ void PN7160::nci_fsm_transition_() { this->config_refresh_pending_ = false; this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); } - // fall through + [[fallthrough]]; case NCIState::NFCC_SET_DISCOVER_MAP: if (this->set_discover_map_() != nfc::STATUS_OK) { @@ -640,7 +640,7 @@ void PN7160::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); } - // fall through + [[fallthrough]]; case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { @@ -650,7 +650,7 @@ void PN7160::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::RFST_IDLE); } - // fall through + [[fallthrough]]; case NCIState::RFST_IDLE: if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { @@ -675,14 +675,14 @@ void PN7160::nci_fsm_transition_() { case NCIState::RFST_W4_HOST_SELECT: select_endpoint_(); - // fall through + [[fallthrough]]; // All cases below are waiting for NOTIFICATION messages case NCIState::RFST_DISCOVERY: if (this->config_refresh_pending_) { this->refresh_core_config_(); } - // fall through + [[fallthrough]]; case NCIState::RFST_LISTEN_ACTIVE: case NCIState::RFST_LISTEN_SLEEP: diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp index 3871d89a2f..b052c0cee9 100644 --- a/esphome/components/shelly_dimmer/stm32flash.cpp +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -445,8 +445,7 @@ template stm32_err_t stm32_check_ack_timeout(const stm32_err_t s_err return STM32_ERR_OK; case STM32_ERR_NACK: log(); - // TODO: c++17 [[fallthrough]] - /* fallthrough */ + [[fallthrough]]; default: return STM32_ERR_UNKNOWN; } From 1f94e4cc14f5225270875f62d83d3b7b45d6f80e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Jun 2025 23:37:30 -0400 Subject: [PATCH 069/115] [esp32] Update IDF components to use the registry (#9223) --- esphome/components/esp32_camera/__init__.py | 6 +----- esphome/components/mdns/__init__.py | 7 +------ esphome/components/micro_wake_word/__init__.py | 6 +----- esphome/idf_component.yml | 18 ++++++------------ 4 files changed, 9 insertions(+), 28 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index cfca0ed6fc..8dc2ede372 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -310,11 +310,7 @@ async def to_code(config): cg.add_define("USE_ESP32_CAMERA") if CORE.using_esp_idf: - add_idf_component( - name="esp32-camera", - repo="https://github.com/espressif/esp32-camera.git", - ref="v2.0.15", - ) + add_idf_component(name="espressif/esp32-camera", ref="2.0.15") for conf in config.get(CONF_ON_STREAM_START, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 4b5e40dfea..2f81068e8a 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -88,12 +88,7 @@ async def to_code(config): if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version( 5, 0, 0 ): - add_idf_component( - name="mdns", - repo="https://github.com/espressif/esp-protocols.git", - ref="mdns-v1.8.2", - path="components/mdns", - ) + add_idf_component(name="espressif/mdns", ref="1.8.2") cg.add_define("USE_MDNS") diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 0efe2ac288..cde8752157 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -449,11 +449,7 @@ async def to_code(config): cg.add_define("USE_MICRO_WAKE_WORD") cg.add_define("USE_OTA_STATE_CALLBACK") - esp32.add_idf_component( - name="esp-tflite-micro", - repo="https://github.com/espressif/esp-tflite-micro", - ref="v1.3.3.1", - ) + esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 7dcfe918eb..6299909033 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -1,16 +1,10 @@ dependencies: - esp-tflite-micro: - git: https://github.com/espressif/esp-tflite-micro.git - version: v1.3.1 - esp32_camera: - git: https://github.com/espressif/esp32-camera.git - version: v2.0.15 - mdns: - git: https://github.com/espressif/esp-protocols.git - version: mdns-v1.8.2 - path: components/mdns - rules: - - if: "idf_version >=5.0" + espressif/esp-tflite-micro: + version: 1.3.3~1 + espressif/esp32-camera: + version: 2.0.15 + espressif/mdns: + version: 1.8.2 espressif/esp_wifi_remote: version: 0.10.2 rules: From 62f28902c5d54793efeded201eb4059074bdf9e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 08:50:26 +0200 Subject: [PATCH 070/115] [wifi] Reduce memory usage (#9232) --- esphome/components/wifi/wifi_component.h | 48 +++++++++++++----------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index efd43077d1..64797a5801 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -62,7 +62,7 @@ struct SavedWifiFastConnectSettings { uint8_t channel; } PACKED; // NOLINT -enum WiFiComponentState { +enum WiFiComponentState : uint8_t { /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ WIFI_COMPONENT_STATE_OFF = 0, /** WiFi is disabled. */ @@ -146,14 +146,14 @@ class WiFiAP { protected: std::string ssid_; - optional bssid_; std::string password_; + optional bssid_; #ifdef USE_WIFI_WPA2_EAP optional eap_; #endif // USE_WIFI_WPA2_EAP - optional channel_; - float priority_{0}; optional manual_ip_; + float priority_{0}; + optional channel_; bool hidden_{false}; }; @@ -177,14 +177,14 @@ class WiFiScanResult { bool operator==(const WiFiScanResult &rhs) const; protected: - bool matches_{false}; bssid_t bssid_; std::string ssid_; + float priority_{0.0f}; uint8_t channel_; int8_t rssi_; + bool matches_{false}; bool with_auth_; bool is_hidden_; - float priority_{0.0f}; }; struct WiFiSTAPriority { @@ -192,7 +192,7 @@ struct WiFiSTAPriority { float priority; }; -enum WiFiPowerSaveMode { +enum WiFiPowerSaveMode : uint8_t { WIFI_POWER_SAVE_NONE = 0, WIFI_POWER_SAVE_LIGHT, WIFI_POWER_SAVE_HIGH, @@ -383,28 +383,36 @@ class WiFiComponent : public Component { std::string use_address_; std::vector sta_; std::vector sta_priorities_; + std::vector scan_result_; WiFiAP selected_ap_; - bool fast_connect_{false}; - bool retry_hidden_{false}; - - bool has_ap_{false}; WiFiAP ap_; - WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; - bool handled_connected_state_{false}; + optional output_power_; + ESPPreferenceObject pref_; + ESPPreferenceObject fast_connect_pref_; + + // Group all 32-bit integers together uint32_t action_started_; - uint8_t num_retried_{0}; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; uint32_t ap_timeout_{}; + + // Group all 8-bit values together + WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; + uint8_t num_retried_{0}; +#if USE_NETWORK_IPV6 + uint8_t num_ipv6_addresses_{0}; +#endif /* USE_NETWORK_IPV6 */ + + // Group all boolean values together + bool fast_connect_{false}; + bool retry_hidden_{false}; + bool has_ap_{false}; + bool handled_connected_state_{false}; bool error_from_callback_{false}; - std::vector scan_result_; bool scan_done_{false}; bool ap_setup_{false}; - optional output_power_; bool passive_scan_{false}; - ESPPreferenceObject pref_; - ESPPreferenceObject fast_connect_pref_; bool has_saved_wifi_settings_{false}; #ifdef USE_WIFI_11KV_SUPPORT bool btm_{false}; @@ -412,10 +420,8 @@ class WiFiComponent : public Component { #endif bool enable_on_boot_; bool got_ipv4_address_{false}; -#if USE_NETWORK_IPV6 - uint8_t num_ipv6_addresses_{0}; -#endif /* USE_NETWORK_IPV6 */ + // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; Trigger<> *disconnect_trigger_{new Trigger<>()}; }; From 7931423e8c99cd0a22d16f2f84d75c0d4f79aed6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 08:52:12 +0200 Subject: [PATCH 071/115] Reduce ethernet component memory usage by 8 bytes (#9231) --- .../components/ethernet/ethernet_component.h | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 7a205d89f0..0f0eff5ded 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -15,7 +15,7 @@ namespace esphome { namespace ethernet { -enum EthernetType { +enum EthernetType : uint8_t { ETHERNET_TYPE_UNKNOWN = 0, ETHERNET_TYPE_LAN8720, ETHERNET_TYPE_RTL8201, @@ -42,7 +42,7 @@ struct PHYRegister { uint32_t page; }; -enum class EthernetComponentState { +enum class EthernetComponentState : uint8_t { STOPPED, CONNECTING, CONNECTED, @@ -119,25 +119,31 @@ class EthernetComponent : public Component { uint32_t polling_interval_{0}; #endif #else - uint8_t phy_addr_{0}; + // Group all 32-bit members first int power_pin_{-1}; - uint8_t mdc_pin_{23}; - uint8_t mdio_pin_{18}; emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; std::vector phy_registers_{}; -#endif - EthernetType type_{ETHERNET_TYPE_UNKNOWN}; - optional manual_ip_{}; + // Group all 8-bit members together + uint8_t phy_addr_{0}; + uint8_t mdc_pin_{23}; + uint8_t mdio_pin_{18}; +#endif + optional manual_ip_{}; + uint32_t connect_begin_; + + // Group all uint8_t types together (enums and bools) + EthernetType type_{ETHERNET_TYPE_UNKNOWN}; + EthernetComponentState state_{EthernetComponentState::STOPPED}; bool started_{false}; bool connected_{false}; bool got_ipv4_address_{false}; #if LWIP_IPV6 uint8_t ipv6_count_{0}; #endif /* LWIP_IPV6 */ - EthernetComponentState state_{EthernetComponentState::STOPPED}; - uint32_t connect_begin_; + + // Pointers at the end (naturally aligned) esp_netif_t *eth_netif_{nullptr}; esp_eth_handle_t eth_handle_; esp_eth_phy_t *phy_{nullptr}; From 13512440ac57954c0246933a9f80e9455a1ab1d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 08:53:40 +0200 Subject: [PATCH 072/115] [gpio] Reduce ESP32 memory usage by optimizing struct padding (#9230) --- esphome/components/esp32/gpio.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index d69ac1c493..0fefc1c058 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -29,9 +29,9 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; gpio_num_t pin_; - bool inverted_; gpio_drive_cap_t drive_strength_; gpio::Flags flags_; + bool inverted_; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static bool isr_service_installed; }; From 837dd46adffe2f6dbb4184f54b5a34b79acb8bec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 08:56:54 +0200 Subject: [PATCH 073/115] Reduce component_iterator memory usage (#9205) --- esphome/core/component_iterator.cpp | 2 +- esphome/core/component_iterator.h | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index da593340c1..b06c964b7c 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -375,7 +375,7 @@ void ComponentIterator::advance() { } if (advance_platform) { - this->state_ = static_cast(static_cast(this->state_) + 1); + this->state_ = static_cast(static_cast(this->state_) + 1); this->at_ = 0; } else if (success) { this->at_++; diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 9e187f6c57..4b41872db7 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -93,7 +93,9 @@ class ComponentIterator { virtual bool on_end(); protected: - enum class IteratorState { + // Iterates over all ESPHome entities (sensors, switches, lights, etc.) + // Supports up to 256 entity types and up to 65,535 entities of each type + enum class IteratorState : uint8_t { NONE = 0, BEGIN, #ifdef USE_BINARY_SENSOR @@ -167,7 +169,7 @@ class ComponentIterator { #endif MAX, } state_{IteratorState::NONE}; - size_t at_{0}; + uint16_t at_{0}; // Supports up to 65,535 entities per type bool include_internal_{false}; }; From c0b1f32889c6e240aa8b52b0e19d708a2c2342a1 Mon Sep 17 00:00:00 2001 From: scaiper Date: Fri, 27 Jun 2025 13:43:18 +0300 Subject: [PATCH 074/115] [esp32] Change ``enable_lwip_mdns_queries`` default to ``True`` (#9188) --- esphome/components/esp32/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 4e2a6ab852..8319ed5e74 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -610,7 +610,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False ): cv.boolean, cv.Optional( - CONF_ENABLE_LWIP_MDNS_QUERIES, default=False + CONF_ENABLE_LWIP_MDNS_QUERIES, default=True ): cv.boolean, cv.Optional( CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False @@ -770,7 +770,7 @@ async def to_code(config): and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] ): add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) - if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, False): + if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True): add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) From cceab26bfb40a32858b2b31b7b6383c32f8e0b49 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 24 Jun 2025 09:43:40 +1000 Subject: [PATCH 075/115] [lvgl] Fix dangling pointer issue with qrcode (#9190) --- esphome/components/lvgl/widgets/qrcode.py | 10 ++++------ tests/components/lvgl/lvgl-package.yaml | 4 +++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 742b538938..7d8d13d8c4 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.const import CONF_SIZE, CONF_TEXT from esphome.cpp_generator import MockObjClass -from ..defines import CONF_MAIN, literal +from ..defines import CONF_MAIN from ..lv_validation import color, color_retmapper, lv_text from ..lvcode import LocalVariable, lv, lv_expr from ..schemas import TEXT_SCHEMA @@ -34,7 +34,7 @@ class QrCodeType(WidgetType): ) def get_uses(self): - return ("canvas", "img") + return ("canvas", "img", "label") def obj_creator(self, parent: MockObjClass, config: dict): dark_color = color_retmapper(config[CONF_DARK_COLOR]) @@ -45,10 +45,8 @@ class QrCodeType(WidgetType): async def to_code(self, w: Widget, config): if (value := config.get(CONF_TEXT)) is not None: value = await lv_text.process(value) - with LocalVariable( - "qr_text", cg.const_char_ptr, value, modifier="" - ) as str_obj: - lv.qrcode_update(w.obj, str_obj, literal(f"strlen({str_obj})")) + with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj: + lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size()) qr_code_spec = QrCodeType() diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index d8452bdd2a..e3391a5bac 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -646,7 +646,9 @@ lvgl: on_click: lvgl.qrcode.update: id: lv_qr - text: homeassistant.io + text: + format: "A string with a number %d" + args: ['(int)(random_uint32() % 1000)'] - slider: min_value: 0 From e146c0796abb32ff605ec68f23be74248c579039 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 26 Jun 2025 20:11:50 +0100 Subject: [PATCH 076/115] [audio] Bugfix: improve timeout handling (#9221) --- esphome/components/audio/audio_reader.cpp | 62 +++++++++++++++-------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index b82c6db9ee..6966c95db7 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -5,6 +5,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE #include "esp_crt_bundle.h" @@ -16,13 +17,13 @@ namespace audio { static const uint32_t READ_WRITE_TIMEOUT_MS = 20; static const uint32_t CONNECTION_TIMEOUT_MS = 5000; - -// The number of times the http read times out with no data before throwing an error -static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100; +static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6; static const size_t HTTP_STREAM_BUFFER_SIZE = 2048; -static const uint8_t MAX_REDIRECTION = 5; +static const uint8_t MAX_REDIRECTIONS = 5; + +static const char *const TAG = "audio_reader"; // Some common HTTP status codes - borrowed from http_request component accessed 20241224 enum HttpStatus { @@ -94,7 +95,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { client_config.url = uri.c_str(); client_config.cert_pem = nullptr; client_config.disable_auto_redirect = false; - client_config.max_redirection_count = 10; + client_config.max_redirection_count = MAX_REDIRECTIONS; client_config.event_handler = http_event_handler; client_config.user_data = this; client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE; @@ -116,12 +117,29 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { esp_err_t err = esp_http_client_open(this->client_, 0); if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open URL"); this->cleanup_connection_(); return err; } int64_t header_length = esp_http_client_fetch_headers(this->client_); + uint8_t reattempt_count = 0; + while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) { + this->cleanup_connection_(); + if (header_length != -ESP_ERR_HTTP_EAGAIN) { + // Serious error, no recovery + return ESP_FAIL; + } else { + // Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available + this->client_ = esp_http_client_init(&client_config); + esp_http_client_open(this->client_, 0); + header_length = esp_http_client_fetch_headers(this->client_); + ++reattempt_count; + } + } + if (header_length < 0) { + ESP_LOGE(TAG, "Failed to fetch headers"); this->cleanup_connection_(); return ESP_FAIL; } @@ -135,7 +153,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { ssize_t redirect_count = 0; - while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) { + while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) { err = esp_http_client_open(this->client_, 0); if (err != ESP_OK) { this->cleanup_connection_(); @@ -267,27 +285,29 @@ AudioReaderState AudioReader::http_read_() { return AudioReaderState::FINISHED; } } else if (this->output_transfer_buffer_->free() > 0) { - size_t bytes_to_read = this->output_transfer_buffer_->free(); - int received_len = - esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read); + int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free()); if (received_len > 0) { this->output_transfer_buffer_->increase_buffer_length(received_len); this->last_data_read_ms_ = millis(); - } else if (received_len < 0) { + return AudioReaderState::READING; + } else if (received_len <= 0) { // HTTP read error - this->cleanup_connection_(); - return AudioReaderState::FAILED; - } else { - if (bytes_to_read > 0) { - // Read timed out - if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) { - this->cleanup_connection_(); - return AudioReaderState::FAILED; - } - - delay(READ_WRITE_TIMEOUT_MS); + if (received_len == -1) { + // A true connection error occured, no chance at recovery + this->cleanup_connection_(); + return AudioReaderState::FAILED; } + + // Read timed out, manually verify if it has been too long since the last successful read + if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) { + ESP_LOGE(TAG, "Timed out"); + this->cleanup_connection_(); + return AudioReaderState::FAILED; + } + + delay(READ_WRITE_TIMEOUT_MS); } } From 068aa0ff1eadedf6895ffe31ba1b817e320fcbb0 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 26 Jun 2025 20:12:58 +0100 Subject: [PATCH 077/115] [speaker] bugfix: continue to block tasks if stop flag is set (#9222) --- .../speaker/media_player/audio_pipeline.cpp | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index ac122b6e0c..333a076bec 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -343,13 +343,12 @@ void AudioPipeline::read_task(void *params) { xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); // Wait until the pipeline notifies us the source of the media file - EventBits_t event_bits = - xEventGroupWaitBits(this_pipeline->event_group_, - EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP | - EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read - pdFALSE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set + EventBits_t event_bits = xEventGroupWaitBits( + this_pipeline->event_group_, + EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP, // Bit message to read + pdFALSE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) { xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED | @@ -434,12 +433,12 @@ void AudioPipeline::decode_task(void *params) { xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED); // Wait until the reader notifies us that the media type is available - EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_, - EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE | - EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read - pdFALSE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set + EventBits_t event_bits = + xEventGroupWaitBits(this_pipeline->event_group_, + EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE, // Bit message to read + pdFALSE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE); From 9f3f4ead4f71a5af9860a7527ac5d96bd3d3d24e Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 26 Jun 2025 20:18:51 +0100 Subject: [PATCH 078/115] [voice_assistant] Support streaming TTS responses and fixes crash for long responses (#9224) --- CODEOWNERS | 2 +- .../components/voice_assistant/__init__.py | 13 ++++++- .../voice_assistant/voice_assistant.cpp | 38 ++++++++++++++++++- .../voice_assistant/voice_assistant.h | 4 ++ 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 66ea80f8d6..a0812c9cd6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -490,7 +490,7 @@ esphome/components/vbus/* @ssieb esphome/components/veml3235/* @kbx81 esphome/components/veml7700/* @latonita esphome/components/version/* @esphome/core -esphome/components/voice_assistant/* @jesserockz +esphome/components/voice_assistant/* @jesserockz @kahrendt esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/watchdog/* @oarcher esphome/components/waveshare_epaper/* @clydebarrow diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index b9309ab422..59c7ec8383 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -17,10 +17,11 @@ from esphome.const import ( AUTO_LOAD = ["socket"] DEPENDENCIES = ["api", "microphone"] -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kahrendt"] CONF_ON_END = "on_end" CONF_ON_INTENT_END = "on_intent_end" +CONF_ON_INTENT_PROGRESS = "on_intent_progress" CONF_ON_INTENT_START = "on_intent_start" CONF_ON_LISTENING = "on_listening" CONF_ON_START = "on_start" @@ -136,6 +137,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_INTENT_PROGRESS): automation.validate_automation( + single=True + ), cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( single=True ), @@ -282,6 +286,13 @@ async def to_code(config): config[CONF_ON_INTENT_START], ) + if CONF_ON_INTENT_PROGRESS in config: + await automation.build_automation( + var.get_intent_progress_trigger(), + [(cg.std_string, "x")], + config[CONF_ON_INTENT_PROGRESS], + ) + if CONF_ON_INTENT_END in config: await automation.build_automation( var.get_intent_end_trigger(), diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index a692a7556e..879d9492f0 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -555,7 +555,7 @@ void VoiceAssistant::request_stop() { break; case State::AWAITING_RESPONSE: this->signal_stop_(); - break; + // Fallthrough intended to stop a streaming TTS announcement that has potentially started case State::STREAMING_RESPONSE: #ifdef USE_MEDIA_PLAYER // Stop any ongoing media player announcement @@ -599,6 +599,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_RUN_START: ESP_LOGD(TAG, "Assist Pipeline running"); +#ifdef USE_MEDIA_PLAYER + this->started_streaming_tts_ = false; + for (auto arg : msg.data) { + if (arg.name == "url") { + this->tts_response_url_ = std::move(arg.value); + } + } +#endif this->defer([this]() { this->start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: @@ -622,6 +630,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { if (text.empty()) { ESP_LOGW(TAG, "No text in STT_END event"); return; + } else if (text.length() > 500) { + text = text.substr(0, 497) + "..."; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); @@ -631,6 +641,27 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Intent started"); this->defer([this]() { this->intent_start_trigger_->trigger(); }); break; + case api::enums::VOICE_ASSISTANT_INTENT_PROGRESS: { + ESP_LOGD(TAG, "Intent progress"); + std::string tts_url_for_trigger = ""; +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + for (const auto &arg : msg.data) { + if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) { + this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform(); + + this->media_player_wait_for_announcement_start_ = true; + this->media_player_wait_for_announcement_end_ = false; + this->started_streaming_tts_ = true; + tts_url_for_trigger = this->tts_response_url_; + this->tts_response_url_.clear(); // Reset streaming URL + } + } + } +#endif + this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_->trigger(tts_url_for_trigger); }); + break; + } case api::enums::VOICE_ASSISTANT_INTENT_END: { for (auto arg : msg.data) { if (arg.name == "conversation_id") { @@ -653,6 +684,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGW(TAG, "No text in TTS_START event"); return; } + if (text.length() > 500) { + text = text.substr(0, 497) + "..."; + } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); this->defer([this, text]() { this->tts_start_trigger_->trigger(text); @@ -678,7 +712,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); this->defer([this, url]() { #ifdef USE_MEDIA_PLAYER - if (this->media_player_ != nullptr) { + if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) { this->media_player_->make_call().set_media_url(url).set_announcement(true).perform(); this->media_player_wait_for_announcement_start_ = true; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 865731522f..2424ea6052 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -177,6 +177,7 @@ class VoiceAssistant : public Component { Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } + Trigger *get_intent_progress_trigger() const { return this->intent_progress_trigger_; } Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } Trigger<> *get_end_trigger() const { return this->end_trigger_; } Trigger<> *get_start_trigger() const { return this->start_trigger_; } @@ -233,6 +234,7 @@ class VoiceAssistant : public Component { Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); #endif + Trigger *intent_progress_trigger_ = new Trigger(); Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); @@ -268,6 +270,8 @@ class VoiceAssistant : public Component { #endif #ifdef USE_MEDIA_PLAYER media_player::MediaPlayer *media_player_{nullptr}; + std::string tts_response_url_{""}; + bool started_streaming_tts_{false}; bool media_player_wait_for_announcement_start_{false}; bool media_player_wait_for_announcement_end_{false}; #endif From 9e993ac603a64b2f48c92e8910ef77eeee9aafc3 Mon Sep 17 00:00:00 2001 From: scaiper Date: Fri, 27 Jun 2025 13:43:18 +0300 Subject: [PATCH 079/115] [esp32] Change ``enable_lwip_mdns_queries`` default to ``True`` (#9188) --- esphome/components/esp32/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 157fd9db11..ba9a4894c2 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -605,7 +605,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False ): cv.boolean, cv.Optional( - CONF_ENABLE_LWIP_MDNS_QUERIES, default=False + CONF_ENABLE_LWIP_MDNS_QUERIES, default=True ): cv.boolean, cv.Optional( CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False @@ -760,7 +760,7 @@ async def to_code(config): and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] ): add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) - if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, False): + if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True): add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) From 948aa13fb9b0752132eabdcf0adb670585e57cac Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 27 Jun 2025 23:16:13 +1200 Subject: [PATCH 080/115] Bump version to 2025.6.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index a4dd65bf45..edbb5b1206 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.6.1 +PROJECT_NUMBER = 2025.6.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 3ef82aff5b..956edfd6be 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.6.1" +__version__ = "2025.6.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 68d66c873e62be045c55d7dcada54bee3cc3636d Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Fri, 27 Jun 2025 19:31:50 +0200 Subject: [PATCH 081/115] Upgrade to use C++20 (#9135) Co-authored-by: J. Nick Koston --- esphome/components/ac_dimmer/ac_dimmer.cpp | 3 ++- esphome/components/atm90e32/atm90e32.cpp | 3 ++- esphome/components/display/display.cpp | 7 ++++--- esphome/components/display/display.h | 2 -- esphome/components/esp32/__init__.py | 2 +- esphome/components/esp8266/__init__.py | 2 +- esphome/components/esp8266/gpio.cpp | 20 +++++++++---------- esphome/components/host/__init__.py | 2 +- .../http_request/ota/ota_http_request.cpp | 2 +- .../components/hydreon_rgxx/hydreon_rgxx.cpp | 12 +++-------- esphome/components/libretiny/__init__.py | 2 +- .../components/online_image/online_image.cpp | 2 +- .../pulse_meter/pulse_meter_sensor.cpp | 8 ++++---- .../radon_eye_ble/radon_eye_listener.cpp | 2 +- .../remote_receiver_esp8266.cpp | 2 +- esphome/components/rp2040/__init__.py | 2 +- esphome/components/sn74hc595/sn74hc595.cpp | 9 +++++---- esphome/components/sun/sun.cpp | 7 +++---- esphome/components/tx20/tx20.cpp | 4 ++-- esphome/components/wiegand/wiegand.cpp | 4 ++-- esphome/core/application.cpp | 17 ++++++++-------- esphome/cpp_generator.py | 2 +- platformio.ini | 6 +++--- 23 files changed, 59 insertions(+), 63 deletions(-) diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index 276adeebb0..e6f7a1214a 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -4,6 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include +#include #ifdef USE_ESP8266 #include @@ -203,7 +204,7 @@ void AcDimmer::setup() { #endif } void AcDimmer::write_state(float state) { - state = std::acos(1 - (2 * state)) / 3.14159; // RMS power compensation + state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation auto new_value = static_cast(roundf(state * 65535)); if (new_value != 0 && this->store_.value == 0) this->store_.init_cycle = this->init_with_half_cycle_; diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index f05d462845..4669a59e39 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -1,6 +1,7 @@ #include "atm90e32.h" #include #include +#include #include "esphome/core/log.h" namespace esphome { @@ -848,7 +849,7 @@ uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f; float target_voltage = nominal_voltage * multiplier; - float peak_01v = target_voltage * 100.0f * std::sqrt(2.0f); // convert RMS → peak, scale to 0.01V + float peak_01v = target_voltage * 100.0f * std::numbers::sqrt2_v; // convert RMS → peak, scale to 0.01V float divider = (2.0f * ugain) / 32768.0f; float threshold = peak_01v / divider; diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index a13464ce1b..c666eee298 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -1,5 +1,6 @@ #include "display.h" #include +#include #include "display_color_utils.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -424,15 +425,15 @@ void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int * // hence we rotate the shape by 270° to orient the polygon up. rotation_degrees += ROTATION_270_DEGREES; // Convert the rotation to radians, easier to use in trigonometrical calculations - float rotation_radians = rotation_degrees * PI / 180; + float rotation_radians = rotation_degrees * std::numbers::pi / 180; // A pointy top variation means the first vertex of the polygon is at the top center of the shape, this requires no // additional rotation of the shape. // A flat top variation means the first point of the polygon has to be rotated so that the first edge is horizontal, // this requires to rotate the shape by π/edges radians counter-clockwise so that the first point is located on the // left side of the first horizontal edge. - rotation_radians -= (variation == VARIATION_FLAT_TOP) ? PI / edges : 0.0; + rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi / edges : 0.0; - float vertex_angle = ((float) vertex_id) / edges * 2 * PI + rotation_radians; + float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi + rotation_radians; *vertex_x = (int) round(cos(vertex_angle) * radius) + center_x; *vertex_y = (int) round(sin(vertex_angle) * radius) + center_y; } diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 68c1184721..f2d79d12d9 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -138,8 +138,6 @@ enum DisplayRotation { DISPLAY_ROTATION_270_DEGREES = 270, }; -#define PI 3.1415926535897932384626433832795 - const int EDGES_TRIGON = 3; const int EDGES_TRIANGLE = 3; const int EDGES_TETRAGON = 4; diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8319ed5e74..7e1c71a7de 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -704,7 +704,7 @@ FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) - cg.set_cpp_standard("gnu++17") + cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 4b4862a1d0..81daad8c56 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -183,7 +183,7 @@ async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_ESP8266") - cg.set_cpp_standard("gnu++17") + cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "ESP8266") diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index 4a997a790c..ee3683c67d 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -129,9 +129,9 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { } } else { if (value != arg->inverted) { - *arg->out_set_reg |= 1; + *arg->out_set_reg = *arg->out_set_reg | 1; } else { - *arg->out_set_reg &= ~1; + *arg->out_set_reg = *arg->out_set_reg & ~1; } } } @@ -147,7 +147,7 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { if (flags & gpio::FLAG_OUTPUT) { *arg->mode_set_reg = arg->mask; if (flags & gpio::FLAG_OPEN_DRAIN) { - *arg->control_reg |= 1 << GPCD; + *arg->control_reg = *arg->control_reg | (1 << GPCD); } else { *arg->control_reg &= ~(1 << GPCD); } @@ -155,21 +155,21 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { *arg->mode_clr_reg = arg->mask; } if (flags & gpio::FLAG_PULLUP) { - *arg->func_reg |= 1 << GPFPU; - *arg->control_reg |= 1 << GPCD; + *arg->func_reg = *arg->func_reg | (1 << GPFPU); + *arg->control_reg = *arg->control_reg | (1 << GPCD); } else { - *arg->func_reg &= ~(1 << GPFPU); + *arg->func_reg = *arg->func_reg & ~(1 << GPFPU); } } else { if (flags & gpio::FLAG_OUTPUT) { - *arg->mode_set_reg |= 1; + *arg->mode_set_reg = *arg->mode_set_reg | 1; } else { - *arg->mode_set_reg &= ~1; + *arg->mode_set_reg = *arg->mode_set_reg & ~1; } if (flags & gpio::FLAG_PULLDOWN) { - *arg->func_reg |= 1 << GP16FPD; + *arg->func_reg = *arg->func_reg | (1 << GP16FPD); } else { - *arg->func_reg &= ~(1 << GP16FPD); + *arg->func_reg = *arg->func_reg & ~(1 << GP16FPD); } } } diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index b59d8ebd03..da75873eaf 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -41,6 +41,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add_build_flag("-DUSE_HOST") cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) - cg.add_build_flag("-std=gnu++17") + cg.add_build_flag("-std=gnu++20") cg.add_define("ESPHOME_BOARD", "host") cg.add_platformio_option("platform", "platformio/native") diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 4c8d49dad5..4d9e868c74 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -258,7 +258,7 @@ bool OtaHttpRequestComponent::http_get_md5_() { } bool OtaHttpRequestComponent::validate_url_(const std::string &url) { - if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { + if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) { ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); return false; } diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 2d8381b60c..9d4680fdf4 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -159,12 +159,6 @@ void HydreonRGxxComponent::schedule_reboot_() { }); } -bool HydreonRGxxComponent::buffer_starts_with_(const std::string &prefix) { - return this->buffer_starts_with_(prefix.c_str()); -} - -bool HydreonRGxxComponent::buffer_starts_with_(const char *prefix) { return buffer_.rfind(prefix, 0) == 0; } - void HydreonRGxxComponent::process_line_() { ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); @@ -191,7 +185,7 @@ void HydreonRGxxComponent::process_line_() { ESP_LOGW(TAG, "Received EmSat!"); this->em_sat_ = true; } - if (this->buffer_starts_with_("PwrDays")) { + if (buffer_.starts_with("PwrDays")) { if (this->boot_count_ <= 0) { this->boot_count_ = 1; } else { @@ -220,7 +214,7 @@ void HydreonRGxxComponent::process_line_() { } return; } - if (this->buffer_starts_with_("SW")) { + if (buffer_.starts_with("SW")) { std::string::size_type majend = this->buffer_.find('.'); std::string::size_type endversion = this->buffer_.find(' ', 3); if (majend == std::string::npos || endversion == std::string::npos || majend > endversion) { @@ -282,7 +276,7 @@ void HydreonRGxxComponent::process_line_() { } } else { for (const auto *ignore : IGNORE_STRINGS) { - if (this->buffer_starts_with_(ignore)) { + if (buffer_.starts_with(ignore)) { ESP_LOGI(TAG, "Ignoring %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); return; } diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 28ee1e702f..149e5d1179 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -264,7 +264,7 @@ async def component_to_code(config): # force using arduino framework cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") - cg.set_cpp_standard("gnu++17") + cg.set_cpp_standard("gnu++20") # disable library compatibility checks cg.add_platformio_option("lib_ldf_mode", "off") diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index e21b2528d5..d0c743ef93 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -344,7 +344,7 @@ void OnlineImage::end_connection_() { } bool OnlineImage::validate_url_(const std::string &url) { - if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { + if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) { ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); return false; } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index b82cb7a15c..81ecf22c71 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -63,7 +63,7 @@ void PulseMeterSensor::loop() { // If an edge was peeked, repay the debt if (this->peeked_edge_ && this->get_->count_ > 0) { this->peeked_edge_ = false; - this->get_->count_--; + this->get_->count_--; // NOLINT(clang-diagnostic-deprecated-volatile) } // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early @@ -71,7 +71,7 @@ void PulseMeterSensor::loop() { now - this->get_->last_rising_edge_us_ >= this->filter_us_) { this->peeked_edge_ = true; this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_; - this->get_->count_++; + this->get_->count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // Check if we detected a pulse this loop @@ -146,7 +146,7 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { state.last_sent_edge_us_ = now; set.last_detected_edge_us_ = now; set.last_rising_edge_us_ = now; - set.count_++; + set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // This ISR is bound to rising edges, so the pin is high @@ -169,7 +169,7 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { } else if (length && !state.latched_ && sensor->last_pin_val_) { // Long enough high edge state.latched_ = true; set.last_detected_edge_us_ = state.last_intr_; - set.count_++; + set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // Due to order of operations this includes diff --git a/esphome/components/radon_eye_ble/radon_eye_listener.cpp b/esphome/components/radon_eye_ble/radon_eye_listener.cpp index a4c79db753..0c6165c691 100644 --- a/esphome/components/radon_eye_ble/radon_eye_listener.cpp +++ b/esphome/components/radon_eye_ble/radon_eye_listener.cpp @@ -17,7 +17,7 @@ bool RadonEyeListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device // Check if the device name starts with any of the prefixes if (std::any_of(prefixes.begin(), prefixes.end(), - [&](const std::string &prefix) { return device.get_name().rfind(prefix, 0) == 0; })) { + [&](const std::string &prefix) { return device.get_name().starts_with(prefix); })) { // Device found ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().c_str(), device.address_str().c_str()); diff --git a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp b/esphome/components/remote_receiver/remote_receiver_esp8266.cpp index a0fd56bcf4..fe935ba227 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp8266.cpp @@ -27,7 +27,7 @@ void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverCompone if (time_since_change <= arg->filter_us) return; - arg->buffer[arg->buffer_write_at = next] = now; + arg->buffer[arg->buffer_write_at = next] = now; // NOLINT(clang-diagnostic-deprecated-volatile) } void RemoteReceiverComponent::setup() { diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 2718e3050f..ecbeb83bb4 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -167,7 +167,7 @@ async def to_code(config): cg.add_platformio_option("lib_ldf_mode", "chain+") cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_RP2040") - cg.set_cpp_standard("gnu++17") + cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "RP2040") diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index f8d24b898f..d8e33eec22 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -1,5 +1,6 @@ #include "sn74hc595.h" #include "esphome/core/log.h" +#include namespace esphome { namespace sn74hc595 { @@ -55,9 +56,9 @@ void SN74HC595Component::digital_write_(uint16_t pin, bool value) { } void SN74HC595GPIOComponent::write_gpio() { - for (auto byte = this->output_bytes_.rbegin(); byte != this->output_bytes_.rend(); byte++) { + for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) { for (int8_t i = 7; i >= 0; i--) { - bool bit = (*byte >> i) & 1; + bool bit = (output_byte >> i) & 1; this->data_pin_->digital_write(bit); this->clock_pin_->digital_write(true); this->clock_pin_->digital_write(false); @@ -68,9 +69,9 @@ void SN74HC595GPIOComponent::write_gpio() { #ifdef USE_SPI void SN74HC595SPIComponent::write_gpio() { - for (auto byte = this->output_bytes_.rbegin(); byte != this->output_bytes_.rend(); byte++) { + for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) { this->enable(); - this->transfer_byte(*byte); + this->transfer_byte(output_byte); this->disable(); } SN74HC595Component::write_gpio(); diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index 2fd9394a5e..df7030461b 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -1,5 +1,6 @@ #include "sun.h" #include "esphome/core/log.h" +#include /* The formulas/algorithms in this module are based on the book @@ -18,14 +19,12 @@ using namespace esphome::sun::internal; static const char *const TAG = "sun"; -#undef PI #undef degrees #undef radians #undef sq -static const num_t PI = 3.141592653589793; -inline num_t degrees(num_t rad) { return rad * 180 / PI; } -inline num_t radians(num_t deg) { return deg * PI / 180; } +inline num_t degrees(num_t rad) { return rad * 180 / std::numbers::pi; } +inline num_t radians(num_t deg) { return deg * std::numbers::pi / 180; } inline num_t arcdeg(num_t deg, num_t minutes, num_t seconds) { return deg + minutes / 60 + seconds / 3600; } inline num_t sq(num_t x) { return x * x; } inline num_t cb(num_t x) { return x * x * x; } diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index 73b865e8b8..42e3955fc2 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -152,7 +152,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { } arg->buffer[arg->buffer_index] = 1; arg->start_time = now; - arg->buffer_index++; + arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile) return; } const uint32_t delay = now - arg->start_time; @@ -183,7 +183,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { } arg->spent_time += delay; arg->start_time = now; - arg->buffer_index++; + arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile) } void IRAM_ATTR Tx20ComponentStore::reset() { tx20_available = false; diff --git a/esphome/components/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp index 5a2bb8deee..dd1443d10c 100644 --- a/esphome/components/wiegand/wiegand.cpp +++ b/esphome/components/wiegand/wiegand.cpp @@ -11,7 +11,7 @@ static const char *const KEYS = "0123456789*#"; void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { if (arg->d0.digital_read()) return; - arg->count++; + arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile) arg->value <<= 1; arg->last_bit_time = millis(); arg->done = false; @@ -20,7 +20,7 @@ void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) { if (arg->d1.digital_read()) return; - arg->count++; + arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile) arg->value = (arg->value << 1) | 1; arg->last_bit_time = millis(); arg->done = false; diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index f64070fa3d..328de00640 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -3,6 +3,7 @@ #include "esphome/core/version.h" #include "esphome/core/hal.h" #include +#include #ifdef USE_STATUS_LED #include "esphome/components/status_led/status_led.h" @@ -184,8 +185,8 @@ void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) { } void Application::reboot() { ESP_LOGI(TAG, "Forcing a reboot"); - for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { - (*it)->on_shutdown(); + for (auto &component : std::ranges::reverse_view(this->components_)) { + component->on_shutdown(); } arch_restart(); } @@ -198,17 +199,17 @@ void Application::safe_reboot() { } void Application::run_safe_shutdown_hooks() { - for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { - (*it)->on_safe_shutdown(); + for (auto &component : std::ranges::reverse_view(this->components_)) { + component->on_safe_shutdown(); } - for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { - (*it)->on_shutdown(); + for (auto &component : std::ranges::reverse_view(this->components_)) { + component->on_shutdown(); } } void Application::run_powerdown_hooks() { - for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { - (*it)->on_powerdown(); + for (auto &component : std::ranges::reverse_view(this->components_)) { + component->on_powerdown(); } } diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 2a7b7fe057..060dd36f8f 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -617,7 +617,7 @@ def set_cpp_standard(standard: str) -> None: """Set C++ standard with compiler flag `-std={standard}`.""" CORE.add_build_unflag("-std=gnu++11") CORE.add_build_unflag("-std=gnu++14") - CORE.add_build_unflag("-std=gnu++20") + CORE.add_build_unflag("-std=gnu++17") CORE.add_build_unflag("-std=gnu++23") CORE.add_build_unflag("-std=gnu++2a") CORE.add_build_unflag("-std=gnu++2b") diff --git a/platformio.ini b/platformio.ini index 6da9fc1338..be9d7587c2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -47,11 +47,11 @@ lib_deps = lvgl/lvgl@8.4.0 ; lvgl build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE - -std=gnu++17 + -std=gnu++20 build_unflags = -std=gnu++11 -std=gnu++14 - -std=gnu++20 + -std=gnu++17 -std=gnu++23 -std=gnu++2a -std=gnu++2b @@ -560,7 +560,7 @@ lib_deps = build_flags = ${common.build_flags} -DUSE_HOST - -std=c++17 + -std=c++20 build_unflags = ${common.build_unflags} From 156a9160ba9ac191543ae7c6c844ba259d0cd59b Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 27 Jun 2025 18:31:23 -0700 Subject: [PATCH 082/115] [mcp23xxx_base] fix pin interrupts (#9244) Co-authored-by: Samuel Sieb --- esphome/components/mcp23xxx_base/mcp23xxx_base.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 14a703fb9f..fc49f216ee 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -6,7 +6,11 @@ namespace mcp23xxx_base { float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; } -void MCP23XXXGPIOPin::setup() { pin_mode(flags_); } +void MCP23XXXGPIOPin::setup() { + pin_mode(flags_); + this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); +} + void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } From 52ca8deb1099eca9be1ef9a4fa5c30f2edf67343 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Jun 2025 21:32:18 -0400 Subject: [PATCH 083/115] [i2c] Disable i2c scan on certain idf versions (#9237) --- esphome/components/i2c/__init__.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index d56bb2d07c..fae1fa1d22 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -1,5 +1,8 @@ +import logging + from esphome import pins import esphome.codegen as cg +from esphome.components import esp32 import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -12,6 +15,8 @@ from esphome.const import ( CONF_SCL, CONF_SDA, CONF_TIMEOUT, + KEY_CORE, + KEY_FRAMEWORK_VERSION, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -19,6 +24,7 @@ from esphome.const import ( from esphome.core import CORE, coroutine_with_priority import esphome.final_validate as fv +LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] i2c_ns = cg.esphome_ns.namespace("i2c") I2CBus = i2c_ns.class_("I2CBus") @@ -41,6 +47,32 @@ def _bus_declare_type(value): raise NotImplementedError +def validate_config(config): + if ( + config[CONF_SCAN] + and CORE.is_esp32 + and CORE.using_esp_idf + and esp32.get_esp32_variant() + in [ + esp32.const.VARIANT_ESP32C5, + esp32.const.VARIANT_ESP32C6, + esp32.const.VARIANT_ESP32P4, + ] + ): + version: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + if version.major == 5 and ( + (version.minor == 3 and version.patch <= 3) + or (version.minor == 4 and version.patch <= 1) + ): + LOGGER.warning( + "There is a bug in esp-idf version %s that breaks I2C scan, I2C scan " + "has been disabled, see https://github.com/esphome/issues/issues/7128", + str(version), + ) + config[CONF_SCAN] = False + return config + + pin_with_input_and_output_support = pins.internal_gpio_pin_number( {CONF_OUTPUT: True, CONF_INPUT: True} ) @@ -66,6 +98,7 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + validate_config, ) From 5010a0f5e706d1067e13bb6a76661d1b78ce29c4 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 27 Jun 2025 18:31:23 -0700 Subject: [PATCH 084/115] [mcp23xxx_base] fix pin interrupts (#9244) Co-authored-by: Samuel Sieb --- esphome/components/mcp23xxx_base/mcp23xxx_base.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 14a703fb9f..fc49f216ee 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -6,7 +6,11 @@ namespace mcp23xxx_base { float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; } -void MCP23XXXGPIOPin::setup() { pin_mode(flags_); } +void MCP23XXXGPIOPin::setup() { + pin_mode(flags_); + this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); +} + void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } From 61b3379f48b2e4895da96ab0c72239e9516a3299 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Jun 2025 21:32:18 -0400 Subject: [PATCH 085/115] [i2c] Disable i2c scan on certain idf versions (#9237) --- esphome/components/i2c/__init__.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index e47dec650d..e20b7febeb 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -1,5 +1,8 @@ +import logging + from esphome import pins import esphome.codegen as cg +from esphome.components import esp32 import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -12,6 +15,8 @@ from esphome.const import ( CONF_SCL, CONF_SDA, CONF_TIMEOUT, + KEY_CORE, + KEY_FRAMEWORK_VERSION, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -19,6 +24,7 @@ from esphome.const import ( from esphome.core import CORE, coroutine_with_priority import esphome.final_validate as fv +LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] i2c_ns = cg.esphome_ns.namespace("i2c") I2CBus = i2c_ns.class_("I2CBus") @@ -40,6 +46,32 @@ def _bus_declare_type(value): raise NotImplementedError +def validate_config(config): + if ( + config[CONF_SCAN] + and CORE.is_esp32 + and CORE.using_esp_idf + and esp32.get_esp32_variant() + in [ + esp32.const.VARIANT_ESP32C5, + esp32.const.VARIANT_ESP32C6, + esp32.const.VARIANT_ESP32P4, + ] + ): + version: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + if version.major == 5 and ( + (version.minor == 3 and version.patch <= 3) + or (version.minor == 4 and version.patch <= 1) + ): + LOGGER.warning( + "There is a bug in esp-idf version %s that breaks I2C scan, I2C scan " + "has been disabled, see https://github.com/esphome/issues/issues/7128", + str(version), + ) + config[CONF_SCAN] = False + return config + + pin_with_input_and_output_support = pins.internal_gpio_pin_number( {CONF_OUTPUT: True, CONF_INPUT: True} ) @@ -65,6 +97,7 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + validate_config, ) From f8d59b5aeb1268d52a47fe674b115e7245ceb143 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 22:53:40 -0500 Subject: [PATCH 086/115] Reduce libretiny logconfig messages (#9239) --- esphome/components/libretiny/lt_component.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/libretiny/lt_component.cpp b/esphome/components/libretiny/lt_component.cpp index ec4b60eaeb..ffccd0ad7a 100644 --- a/esphome/components/libretiny/lt_component.cpp +++ b/esphome/components/libretiny/lt_component.cpp @@ -10,9 +10,11 @@ namespace libretiny { static const char *const TAG = "lt.component"; void LTComponent::dump_config() { - ESP_LOGCONFIG(TAG, "LibreTiny:"); - ESP_LOGCONFIG(TAG, " Version: %s", LT_BANNER_STR + 10); - ESP_LOGCONFIG(TAG, " Loglevel: %u", LT_LOGLEVEL); + ESP_LOGCONFIG(TAG, + "LibreTiny:\n" + " Version: %s\n" + " Loglevel: %u", + LT_BANNER_STR + 10, LT_LOGLEVEL); #ifdef USE_TEXT_SENSOR if (this->version_ != nullptr) { From 094bf19ec4d86de89d57cc60a3870bdcc6cc4567 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 23:58:53 -0500 Subject: [PATCH 087/115] Disable dynamic log level control for ESP32 ESP-IDF builds (#9233) --- esphome/components/esp32/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 7e1c71a7de..6425e57b13 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -758,6 +758,9 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) + # Disable dynamic log level control to save memory + add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) + # Set default CPU frequency add_idf_sdkconfig_option(f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{freq}", True) From 3f65cee17cb135895921f0309d01af404d96d61a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 23:59:52 -0500 Subject: [PATCH 088/115] Silence protobuf compatibility warnings when importing aioesphomeapi (#9236) --- esphome/components/api/client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 20136ef7b8..7f8e2529a5 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -4,9 +4,15 @@ import asyncio from datetime import datetime import logging from typing import TYPE_CHECKING, Any +import warnings -from aioesphomeapi import APIClient, parse_log_message -from aioesphomeapi.log_runner import async_run +# Suppress protobuf version warnings +with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=UserWarning, message=".*Protobuf gencode version.*" + ) + from aioesphomeapi import APIClient, parse_log_message + from aioesphomeapi.log_runner import async_run from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.core import CORE From d37f5b87bd024a14eb4828076b0a32f166d6f903 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 28 Jun 2025 02:30:59 -0400 Subject: [PATCH 089/115] [esp32] Allow 5.4.2 (#9243) --- esphome/components/esp32/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6425e57b13..32323b7504 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -341,6 +341,7 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ # List based on https://github.com/pioarduino/esp-idf/releases SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ cv.Version(5, 5, 0), + cv.Version(5, 4, 2), cv.Version(5, 4, 1), cv.Version(5, 4, 0), cv.Version(5, 3, 3), From 58b7d0b412187aa4fb0c8ec03444b5e23454d39b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 10:21:53 +0000 Subject: [PATCH 090/115] Bump ruff from 0.12.0 to 0.12.1 (#9241) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 96efee7020..1a0289a3ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.0 + rev: v0.12.1 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 89aba702b9..66b71c2225 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.7 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.0 # also change in .pre-commit-config.yaml when updating +ruff==0.12.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit From 30f61b26ff150e97b29adead8dae13e04d165510 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sat, 28 Jun 2025 21:56:12 +0200 Subject: [PATCH 091/115] Remove backports of `std` (#9246) --- esphome/core/datatypes.h | 4 +- esphome/core/helpers.cpp | 15 ------ esphome/core/helpers.h | 109 ++++++--------------------------------- 3 files changed, 18 insertions(+), 110 deletions(-) diff --git a/esphome/core/datatypes.h b/esphome/core/datatypes.h index 5356be6b52..4929518387 100644 --- a/esphome/core/datatypes.h +++ b/esphome/core/datatypes.h @@ -11,7 +11,7 @@ namespace internal { /// Wrapper class for memory using big endian data layout, transparently converting it to native order. template class BigEndianLayout { public: - constexpr14 operator T() { return convert_big_endian(val_); } + constexpr operator T() { return convert_big_endian(val_); } private: T val_; @@ -20,7 +20,7 @@ template class BigEndianLayout { /// Wrapper class for memory using big endian data layout, transparently converting it to native order. template class LittleEndianLayout { public: - constexpr14 operator T() { return convert_little_endian(val_); } + constexpr operator T() { return convert_little_endian(val_); } private: T val_; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 79dbb314c8..fc91d83972 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -76,23 +76,8 @@ static const uint16_t CRC16_1021_BE_LUT_H[] = {0x0000, 0x1231, 0x2462, 0x3653, 0 0x9188, 0x83b9, 0xb5ea, 0xa7db, 0xd94c, 0xcb7d, 0xfd2e, 0xef1f}; #endif -// STL backports - -#if _GLIBCXX_RELEASE < 8 -std::string to_string(int value) { return str_snprintf("%d", 32, value); } // NOLINT -std::string to_string(long value) { return str_snprintf("%ld", 32, value); } // NOLINT -std::string to_string(long long value) { return str_snprintf("%lld", 32, value); } // NOLINT -std::string to_string(unsigned value) { return str_snprintf("%u", 32, value); } // NOLINT -std::string to_string(unsigned long value) { return str_snprintf("%lu", 32, value); } // NOLINT -std::string to_string(unsigned long long value) { return str_snprintf("%llu", 32, value); } // NOLINT -std::string to_string(float value) { return str_snprintf("%f", 32, value); } -std::string to_string(double value) { return str_snprintf("%f", 32, value); } -std::string to_string(long double value) { return str_snprintf("%Lf", 32, value); } -#endif - // Mathematics -float lerp(float completion, float start, float end) { return start + (end - start) * completion; } uint8_t crc8(const uint8_t *data, uint8_t len) { uint8_t crc = 0; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 8bd5b813c7..7d5366f323 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -37,89 +37,18 @@ #define ESPHOME_ALWAYS_INLINE __attribute__((always_inline)) #define PACKED __attribute__((packed)) -// Various functions can be constexpr in C++14, but not in C++11 (because their body isn't just a return statement). -// Define a substitute constexpr keyword for those functions, until we can drop C++11 support. -#if __cplusplus >= 201402L -#define constexpr14 constexpr -#else -#define constexpr14 inline // constexpr implies inline -#endif - namespace esphome { /// @name STL backports ///@{ -// Backports for various STL features we like to use. Pull in the STL implementation wherever available, to avoid -// ambiguity and to provide a uniform API. - -// std::to_string() from C++11, available from libstdc++/g++ 8 -// See https://github.com/espressif/esp-idf/issues/1445 -#if _GLIBCXX_RELEASE >= 8 +// Keep "using" even after the removal of our backports, to avoid breaking existing code. using std::to_string; -#else -std::string to_string(int value); // NOLINT -std::string to_string(long value); // NOLINT -std::string to_string(long long value); // NOLINT -std::string to_string(unsigned value); // NOLINT -std::string to_string(unsigned long value); // NOLINT -std::string to_string(unsigned long long value); // NOLINT -std::string to_string(float value); -std::string to_string(double value); -std::string to_string(long double value); -#endif - -// std::is_trivially_copyable from C++11, implemented in libstdc++/g++ 5.1 (but minor releases can't be detected) -#if _GLIBCXX_RELEASE >= 6 using std::is_trivially_copyable; -#else -// Implementing this is impossible without compiler intrinsics, so don't bother. Invalid usage will be detected on -// other variants that use a newer compiler anyway. -// NOLINTNEXTLINE(readability-identifier-naming) -template struct is_trivially_copyable : public std::integral_constant {}; -#endif - -// std::make_unique() from C++14 -#if __cpp_lib_make_unique >= 201304 using std::make_unique; -#else -template std::unique_ptr make_unique(Args &&...args) { - return std::unique_ptr(new T(std::forward(args)...)); -} -#endif - -// std::enable_if_t from C++14 -#if __cplusplus >= 201402L using std::enable_if_t; -#else -template using enable_if_t = typename std::enable_if::type; -#endif - -// std::clamp from C++17 -#if __cpp_lib_clamp >= 201603 using std::clamp; -#else -template constexpr const T &clamp(const T &v, const T &lo, const T &hi, Compare comp) { - return comp(v, lo) ? lo : comp(hi, v) ? hi : v; -} -template constexpr const T &clamp(const T &v, const T &lo, const T &hi) { - return clamp(v, lo, hi, std::less{}); -} -#endif - -// std::is_invocable from C++17 -#if __cpp_lib_is_invocable >= 201703 using std::is_invocable; -#else -// https://stackoverflow.com/a/37161919/8924614 -template struct is_invocable { // NOLINT(readability-identifier-naming) - template static auto test(U *p) -> decltype((*p)(std::declval()...), void(), std::true_type()); - template static auto test(...) -> decltype(std::false_type()); - static constexpr auto value = decltype(test(nullptr))::value; // NOLINT -}; -#endif - -// std::bit_cast from C++20 #if __cpp_lib_bit_cast >= 201806 using std::bit_cast; #else @@ -134,31 +63,29 @@ To bit_cast(const From &src) { return dst; } #endif +using std::lerp; // std::byteswap from C++23 -template constexpr14 T byteswap(T n) { +template constexpr T byteswap(T n) { T m; for (size_t i = 0; i < sizeof(T); i++) reinterpret_cast(&m)[i] = reinterpret_cast(&n)[sizeof(T) - 1 - i]; return m; } -template<> constexpr14 uint8_t byteswap(uint8_t n) { return n; } -template<> constexpr14 uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); } -template<> constexpr14 uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); } -template<> constexpr14 uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); } -template<> constexpr14 int8_t byteswap(int8_t n) { return n; } -template<> constexpr14 int16_t byteswap(int16_t n) { return __builtin_bswap16(n); } -template<> constexpr14 int32_t byteswap(int32_t n) { return __builtin_bswap32(n); } -template<> constexpr14 int64_t byteswap(int64_t n) { return __builtin_bswap64(n); } +template<> constexpr uint8_t byteswap(uint8_t n) { return n; } +template<> constexpr uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); } +template<> constexpr uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); } +template<> constexpr uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); } +template<> constexpr int8_t byteswap(int8_t n) { return n; } +template<> constexpr int16_t byteswap(int16_t n) { return __builtin_bswap16(n); } +template<> constexpr int32_t byteswap(int32_t n) { return __builtin_bswap32(n); } +template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n); } ///@} /// @name Mathematics ///@{ -/// Linearly interpolate between \p start and \p end by \p completion (between 0 and 1). -float lerp(float completion, float start, float end); - /// Remap \p value from the range (\p min, \p max) to (\p min_out, \p max_out). template T remap(U value, U min, U max, T min_out, T max_out) { return (value - min) * (max_out - min_out) / (max - min) + min_out; @@ -203,8 +130,7 @@ constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, ui } /// Encode a value from its constituent bytes (from most to least significant) in an array with length sizeof(T). -template::value, int> = 0> -constexpr14 T encode_value(const uint8_t *bytes) { +template::value, int> = 0> constexpr T encode_value(const uint8_t *bytes) { T val = 0; for (size_t i = 0; i < sizeof(T); i++) { val <<= 8; @@ -214,12 +140,12 @@ constexpr14 T encode_value(const uint8_t *bytes) { } /// Encode a value from its constituent bytes (from most to least significant) in an std::array with length sizeof(T). template::value, int> = 0> -constexpr14 T encode_value(const std::array bytes) { +constexpr T encode_value(const std::array bytes) { return encode_value(bytes.data()); } /// Decode a value into its constituent bytes (from most to least significant). template::value, int> = 0> -constexpr14 std::array decode_value(T val) { +constexpr std::array decode_value(T val) { std::array ret{}; for (size_t i = sizeof(T); i > 0; i--) { ret[i - 1] = val & 0xFF; @@ -246,7 +172,7 @@ inline uint32_t reverse_bits(uint32_t x) { } /// Convert a value between host byte order and big endian (most significant byte first) order. -template constexpr14 T convert_big_endian(T val) { +template constexpr T convert_big_endian(T val) { #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ return byteswap(val); #else @@ -255,7 +181,7 @@ template constexpr14 T convert_big_endian(T val) { } /// Convert a value between host byte order and little endian (least significant byte first) order. -template constexpr14 T convert_little_endian(T val) { +template constexpr T convert_little_endian(T val) { #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ return val; #else @@ -276,9 +202,6 @@ bool str_startswith(const std::string &str, const std::string &start); /// Check whether a string ends with a value. bool str_endswith(const std::string &str, const std::string &end); -/// Convert the value to a string (added as extra overload so that to_string() can be used on all stringifiable types). -inline std::string to_string(const std::string &val) { return val; } - /// Truncate a string to a specific length. std::string str_truncate(const std::string &str, size_t length); From 13d4823db6a29fe2ad09a0a08c3f20c18b6eff78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jun 2025 15:04:42 -0500 Subject: [PATCH 092/115] Fix buffer corruption in API message encoding with very verbose logging (#9249) --- esphome/components/api/api_connection.cpp | 35 ++++++-- esphome/components/api/api_connection.h | 8 ++ .../integration/fixtures/api_vv_logging.yaml | 89 +++++++++++++++++++ tests/integration/test_api_vv_logging.py | 83 +++++++++++++++++ 4 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 tests/integration/fixtures/api_vv_logging.yaml create mode 100644 tests/integration/test_api_vv_logging.py diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index fdcce6088c..65588ad4d8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -94,6 +94,19 @@ APIConnection::~APIConnection() { #endif } +#ifdef HAS_PROTO_MESSAGE_DUMP +void APIConnection::log_batch_item_(const DeferredBatch::BatchItem &item) { + // Set log-only mode + this->log_only_mode_ = true; + + // Call the creator - it will create the message and log it via encode_message_to_buffer + item.creator(item.entity, this, std::numeric_limits::max(), true, item.message_type); + + // Clear log-only mode + this->log_only_mode_ = false; +} +#endif + void APIConnection::loop() { if (this->next_close_) { // requested a disconnect @@ -249,6 +262,14 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) { // including header and footer overhead. Returns 0 if the message doesn't fit. uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // If in log-only mode, just log and return + if (conn->log_only_mode_) { + conn->log_send_message_(msg.message_name(), msg.dump()); + return 1; // Return non-zero to indicate "success" for logging + } +#endif + // Calculate size uint32_t calculated_size = 0; msg.calculate_size(calculated_size); @@ -276,11 +297,6 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes // Encode directly into buffer msg.encode(buffer); -#ifdef HAS_PROTO_MESSAGE_DUMP - // Log the message for VV debugging - conn->log_send_message_(msg.message_name(), msg.dump()); -#endif - // Calculate actual encoded size (not including header that was already added) size_t actual_payload_size = shared_buf.size() - size_before_encode; @@ -1891,6 +1907,15 @@ void APIConnection::process_batch_() { } } +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log messages after send attempt for VV debugging + // It's safe to use the buffer for logging at this point regardless of send result + for (size_t i = 0; i < items_processed; i++) { + const auto &item = this->deferred_batch_.items[i]; + this->log_batch_item_(item); + } +#endif + // Handle remaining items more efficiently if (items_processed < this->deferred_batch_.items.size()) { // Remove processed items from the beginning diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index e872711e95..07e87ab39f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -470,6 +470,10 @@ class APIConnection : public APIServerConnection { bool sent_ping_{false}; bool service_call_subscription_{false}; bool next_close_ = false; +#ifdef HAS_PROTO_MESSAGE_DUMP + // When true, encode_message_to_buffer will only log, not encode + bool log_only_mode_{false}; +#endif uint8_t ping_retries_{0}; // 8 bytes used, no padding needed @@ -627,6 +631,10 @@ class APIConnection : public APIServerConnection { // State for batch buffer allocation bool batch_first_message_{false}; +#ifdef HAS_PROTO_MESSAGE_DUMP + void log_batch_item_(const DeferredBatch::BatchItem &item); +#endif + // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) { this->deferred_batch_.add_item(entity, std::move(creator), message_type); diff --git a/tests/integration/fixtures/api_vv_logging.yaml b/tests/integration/fixtures/api_vv_logging.yaml new file mode 100644 index 0000000000..df1edc796a --- /dev/null +++ b/tests/integration/fixtures/api_vv_logging.yaml @@ -0,0 +1,89 @@ +esphome: + name: vv-logging-test + +host: + +api: + +logger: + level: VERY_VERBOSE + # Enable VV logging for API components where the issue occurs + logs: + api.connection: VERY_VERBOSE + api.service: VERY_VERBOSE + api.proto: VERY_VERBOSE + sensor: VERY_VERBOSE + +# Create many sensors that update frequently to generate API traffic +# This will cause many messages to be batched and sent, triggering the +# code path where VV logging could cause buffer corruption +sensor: + - platform: template + name: "Test Sensor 1" + lambda: 'return millis() / 1000.0;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 2" + lambda: 'return (millis() / 1000.0) + 10;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 3" + lambda: 'return (millis() / 1000.0) + 20;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 4" + lambda: 'return (millis() / 1000.0) + 30;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 5" + lambda: 'return (millis() / 1000.0) + 40;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 6" + lambda: 'return (millis() / 1000.0) + 50;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 7" + lambda: 'return (millis() / 1000.0) + 60;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 8" + lambda: 'return (millis() / 1000.0) + 70;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 9" + lambda: 'return (millis() / 1000.0) + 80;' + update_interval: 50ms + unit_of_measurement: "s" + + - platform: template + name: "Test Sensor 10" + lambda: 'return (millis() / 1000.0) + 90;' + update_interval: 50ms + unit_of_measurement: "s" + +# Add some binary sensors too for variety +binary_sensor: + - platform: template + name: "Test Binary 1" + lambda: 'return (millis() / 1000) % 2 == 0;' + + - platform: template + name: "Test Binary 2" + lambda: 'return (millis() / 1000) % 3 == 0;' diff --git a/tests/integration/test_api_vv_logging.py b/tests/integration/test_api_vv_logging.py new file mode 100644 index 0000000000..19aab2001c --- /dev/null +++ b/tests/integration/test_api_vv_logging.py @@ -0,0 +1,83 @@ +"""Integration test for API with VERY_VERBOSE logging to verify no buffer corruption.""" + +from __future__ import annotations + +import asyncio +from typing import Any + +from aioesphomeapi import LogLevel +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_vv_logging( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that VERY_VERBOSE logging doesn't cause buffer corruption with API messages.""" + + # Track that we're receiving VV log messages and sensor updates + vv_logs_received = 0 + sensor_updates_received = 0 + errors_detected = [] + + def on_log(msg: Any) -> None: + """Capture log messages.""" + nonlocal vv_logs_received + # msg is a SubscribeLogsResponse object with 'message' attribute + # The message field is always bytes + message_text = msg.message.decode("utf-8", errors="replace") + + # Only count VV logs specifically + if "[VV]" in message_text: + vv_logs_received += 1 + + # Check for assertion or error messages + if "assert" in message_text.lower() or "error" in message_text.lower(): + errors_detected.append(message_text) + + # Write, compile and run the ESPHome device + async with run_compiled(yaml_config), api_client_connected() as client: + # Subscribe to VERY_VERBOSE logs - this enables the code path that could cause corruption + client.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE) + + # Wait for device to be ready + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "vv-logging-test" + + # Subscribe to sensor states + states = {} + + def on_state(state): + nonlocal sensor_updates_received + sensor_updates_received += 1 + states[state.key] = state + + client.subscribe_states(on_state) + + # List entities to find our test sensors + entity_info, _ = await client.list_entities_services() + + # Count sensors + sensor_count = sum(1 for e in entity_info if hasattr(e, "unit_of_measurement")) + assert sensor_count >= 10, f"Expected at least 10 sensors, got {sensor_count}" + + # Wait for sensor updates to flow with VV logging active + # The sensors update every 50ms, so we should get many updates + await asyncio.sleep(0.25) + + # Verify we received both VV logs and sensor updates + assert vv_logs_received > 0, "Expected to receive VERY_VERBOSE log messages" + assert sensor_updates_received > 10, ( + f"Expected many sensor updates, got {sensor_updates_received}" + ) + + # Check for any errors + if errors_detected: + pytest.fail(f"Errors detected during test: {errors_detected}") + + # The test passes if we didn't hit any assertions or buffer corruption From 3f1f99cf37cfc621a44ee69d339d2401a50f70a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jun 2025 15:08:33 -0500 Subject: [PATCH 093/115] Extract lock-free queue and event pool to core helpers (#9238) --- esphome/components/esp32_ble/ble.cpp | 1 - esphome/components/esp32_ble/ble.h | 8 +- esphome/components/esp32_ble/ble_event.h | 12 +- esphome/components/esp32_ble/ble_event_pool.h | 72 ---------- esphome/components/esp32_ble/queue.h | 85 ----------- esphome/core/event_pool.h | 81 +++++++++++ esphome/core/lock_free_queue.h | 132 ++++++++++++++++++ 7 files changed, 223 insertions(+), 168 deletions(-) delete mode 100644 esphome/components/esp32_ble/ble_event_pool.h delete mode 100644 esphome/components/esp32_ble/queue.h create mode 100644 esphome/core/event_pool.h create mode 100644 esphome/core/lock_free_queue.h diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index b10d1fe10a..8b0cf4da98 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -1,7 +1,6 @@ #ifdef USE_ESP32 #include "ble.h" -#include "ble_event_pool.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 9fe996086e..ce452d65c4 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -12,8 +12,8 @@ #include "esphome/core/helpers.h" #include "ble_event.h" -#include "ble_event_pool.h" -#include "queue.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" #ifdef USE_ESP32 @@ -148,8 +148,8 @@ class ESP32BLE : public Component { std::vector ble_status_event_handlers_; BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; - LockFreeQueue ble_events_; - BLEEventPool ble_event_pool_; + esphome::LockFreeQueue ble_events_; + esphome::EventPool ble_event_pool_; BLEAdvertising *advertising_{}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; uint32_t advertising_cycle_time_{}; diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index dd3ec3da42..9268c710f3 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -134,13 +134,13 @@ class BLEEvent { } // Destructor to clean up heap allocations - ~BLEEvent() { this->cleanup_heap_data(); } + ~BLEEvent() { this->release(); } // Default constructor for pre-allocation in pool BLEEvent() : type_(GAP) {} - // Clean up any heap-allocated data - void cleanup_heap_data() { + // Invoked on return to EventPool - clean up any heap-allocated data + void release() { if (this->type_ == GAP) { return; } @@ -161,19 +161,19 @@ class BLEEvent { // 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->release(); 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->release(); 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->release(); this->type_ = GATTS; this->init_gatts_data_(e, i, p); } diff --git a/esphome/components/esp32_ble/ble_event_pool.h b/esphome/components/esp32_ble/ble_event_pool.h deleted file mode 100644 index ef123b1325..0000000000 --- a/esphome/components/esp32_ble/ble_event_pool.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include -#include -#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 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 allocator(RAMAllocator::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 free_list_; // Free events ready for reuse - uint8_t total_created_; // Total events created (high water mark) -}; - -} // namespace esp32_ble -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h deleted file mode 100644 index 75bf1eef25..0000000000 --- a/esphome/components/esp32_ble/queue.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include -#include - -/* - * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than using mutex-based locking, this lock-free queue allows the BLE - * task to enqueue events without blocking. The main loop() then processes - * these events at a safer time. - * - * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer. - * The BLE task is the only producer, and the main loop() is the only consumer. - */ - -namespace esphome { -namespace esp32_ble { - -template class LockFreeQueue { - public: - LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} - - bool push(T *element) { - if (element == nullptr) - return false; - - 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 - dropped_count_.fetch_add(1, std::memory_order_relaxed); - return false; - } - - buffer_[current_tail] = element; - tail_.store(next_tail, std::memory_order_release); - return true; - } - - T *pop() { - uint8_t current_head = head_.load(std::memory_order_relaxed); - - if (current_head == tail_.load(std::memory_order_acquire)) { - return nullptr; // Empty - } - - T *element = buffer_[current_head]; - head_.store((current_head + 1) % SIZE, std::memory_order_release); - return element; - } - - size_t size() const { - uint8_t tail = tail_.load(std::memory_order_acquire); - uint8_t head = head_.load(std::memory_order_acquire); - return (tail - head + SIZE) % SIZE; - } - - 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 { - 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]; - // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) - std::atomic 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 head_; - // Atomic: written by producer (push), read by consumer (pop) to check if empty - std::atomic tail_; -}; - -} // namespace esp32_ble -} // namespace esphome - -#endif diff --git a/esphome/core/event_pool.h b/esphome/core/event_pool.h new file mode 100644 index 0000000000..69e03bafac --- /dev/null +++ b/esphome/core/event_pool.h @@ -0,0 +1,81 @@ +#pragma once + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#include +#include +#include "esphome/core/helpers.h" +#include "esphome/core/lock_free_queue.h" + +namespace esphome { + +// Event Pool - On-demand pool of objects to avoid heap fragmentation +// Events are allocated on first use and reused thereafter, growing to peak usage +// @tparam T The type of objects managed by the pool (must have a release() method) +// @tparam SIZE The maximum number of objects in the pool (1-255, limited by uint8_t) +template class EventPool { + public: + EventPool() : total_created_(0) {} + + ~EventPool() { + // Clean up any remaining events in the free list + // IMPORTANT: This destructor assumes no concurrent access. The EventPool must not + // be destroyed while any thread might still call allocate() or release(). + // In practice, this is typically ensured by destroying the pool only during + // component shutdown when all producer/consumer threads have been stopped. + T *event; + RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); + while ((event = this->free_list_.pop()) != nullptr) { + // Call destructor + event->~T(); + // Deallocate using RAMAllocator + allocator.deallocate(event, 1); + } + } + + // Allocate an event from the pool + // Returns nullptr if pool is full + T *allocate() { + // Try to get from free list first + T *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 allocator(RAMAllocator::ALLOC_INTERNAL); + event = allocator.allocate(1); + + if (event == nullptr) { + // Memory allocation failed + return nullptr; + } + + // Placement new to construct the object + new (event) T(); + this->total_created_++; + return event; + } + + // Return an event to the pool for reuse + void release(T *event) { + if (event != nullptr) { + // Clean up the event's allocated memory + event->release(); + this->free_list_.push(event); + } + } + + private: + LockFreeQueue free_list_; // Free events ready for reuse + uint8_t total_created_; // Total events created (high water mark, max 255) +}; + +} // namespace esphome + +#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h new file mode 100644 index 0000000000..5460be0fae --- /dev/null +++ b/esphome/core/lock_free_queue.h @@ -0,0 +1,132 @@ +#pragma once + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#include +#include + +#if defined(USE_ESP32) +#include +#include +#elif defined(USE_LIBRETINY) +#include +#include +#endif + +/* + * Lock-free queue for single-producer single-consumer scenarios. + * This allows one thread to push items and another to pop them without + * blocking each other. + * + * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer. + * Available on platforms with FreeRTOS support (ESP32, LibreTiny). + * + * Common use cases: + * - BLE events: BLE task produces, main loop consumes + * - MQTT messages: main task produces, MQTT thread consumes + * + * @tparam T The type of elements stored in the queue (must be a pointer type) + * @tparam SIZE The maximum number of elements (1-255, limited by uint8_t indices) + */ + +namespace esphome { + +template class LockFreeQueue { + public: + LockFreeQueue() : head_(0), tail_(0), dropped_count_(0), task_to_notify_(nullptr) {} + + bool push(T *element) { + if (element == nullptr) + return false; + + uint8_t current_tail = tail_.load(std::memory_order_relaxed); + uint8_t next_tail = (current_tail + 1) % SIZE; + + // Read head before incrementing tail + uint8_t head_before = head_.load(std::memory_order_acquire); + + if (next_tail == head_before) { + // Buffer full + dropped_count_.fetch_add(1, std::memory_order_relaxed); + return false; + } + + // Check if queue was empty before push + bool was_empty = (current_tail == head_before); + + buffer_[current_tail] = element; + tail_.store(next_tail, std::memory_order_release); + + // Notify optimization: only notify if we need to + if (task_to_notify_ != nullptr) { + if (was_empty) { + // Queue was empty - consumer might be going to sleep, must notify + xTaskNotifyGive(task_to_notify_); + } else { + // Queue wasn't empty - check if consumer has caught up to previous tail + uint8_t head_after = head_.load(std::memory_order_acquire); + if (head_after == current_tail) { + // Consumer just caught up to where tail was - might go to sleep, must notify + // Note: There's a benign race here - between reading head_after and calling + // xTaskNotifyGive(), the consumer could advance further. This would result + // in an unnecessary wake-up, but is harmless and extremely rare in practice. + xTaskNotifyGive(task_to_notify_); + } + // Otherwise: consumer is still behind, no need to notify + } + } + + return true; + } + + T *pop() { + uint8_t current_head = head_.load(std::memory_order_relaxed); + + if (current_head == tail_.load(std::memory_order_acquire)) { + return nullptr; // Empty + } + + T *element = buffer_[current_head]; + head_.store((current_head + 1) % SIZE, std::memory_order_release); + return element; + } + + size_t size() const { + uint8_t tail = tail_.load(std::memory_order_acquire); + uint8_t head = head_.load(std::memory_order_acquire); + return (tail - head + SIZE) % SIZE; + } + + 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 { + uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; + return next_tail == head_.load(std::memory_order_acquire); + } + + // Set the FreeRTOS task handle to notify when items are pushed to the queue + // This enables efficient wake-up of a consumer task that's waiting for data + // @param task The FreeRTOS task handle to notify, or nullptr to disable notifications + void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; } + + protected: + T *buffer_[SIZE]; + // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) + std::atomic dropped_count_; // 65535 max - more than enough for drop tracking + // Atomic: written by consumer (pop), read by producer (push) to check if full + // Using uint8_t limits queue size to 255 elements but saves memory and ensures + // atomic operations are efficient on all platforms + std::atomic head_; + // Atomic: written by producer (push), read by consumer (pop) to check if empty + std::atomic tail_; + // Task handle for notification (optional) + TaskHandle_t task_to_notify_; +}; + +} // namespace esphome + +#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) From 86c0fb48a3dc1454cefc8cc31338300e2b971e51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jun 2025 16:08:30 -0500 Subject: [PATCH 094/115] Replace ping retry timer with batch queue fallback (#9207) --- esphome/components/api/api_connection.cpp | 32 ++++++++++++----------- esphome/components/api/api_connection.h | 15 +++++++++-- esphome/components/api/api_server.cpp | 4 +-- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 65588ad4d8..f339a4b26f 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -65,10 +65,6 @@ uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_ void APIConnection::start() { this->last_traffic_ = App.get_loop_component_start_time(); - // Set next_ping_retry_ to prevent immediate ping - // This ensures the first ping happens after the keepalive period - this->next_ping_retry_ = this->last_traffic_ + KEEPALIVE_TIMEOUT_MS; - APIError err = this->helper_->init(); if (err != APIError::OK) { on_fatal_error(); @@ -176,20 +172,15 @@ void APIConnection::loop() { on_fatal_error(); ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); } - } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) { + } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { ESP_LOGVV(TAG, "Sending keepalive PING"); this->sent_ping_ = this->send_message(PingRequest()); if (!this->sent_ping_) { - this->next_ping_retry_ = now + PING_RETRY_INTERVAL; - this->ping_retries_++; - if (this->ping_retries_ >= MAX_PING_RETRIES) { - on_fatal_error(); - ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_); - } else if (this->ping_retries_ >= 10) { - ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); - } else { - ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); - } + // If we can't send the ping request directly (tx_buffer full), + // schedule it at the front of the batch so it will be sent with priority + ESP_LOGW(TAG, "Buffer full, ping queued"); + this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE); + this->sent_ping_ = true; // Mark as sent to avoid scheduling multiple pings } } @@ -1773,6 +1764,11 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c items.emplace_back(entity, std::move(creator), message_type); } +void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type) { + // Insert at front for high priority messages (no deduplication check) + items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type)); +} + bool APIConnection::schedule_batch_() { if (!this->deferred_batch_.batch_scheduled) { this->deferred_batch_.batch_scheduled = true; @@ -1963,6 +1959,12 @@ uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConne return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size, is_single); } +uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single) { + PingRequest req; + return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); +} + uint16_t APIConnection::get_estimated_message_size(uint16_t message_type) { // Use generated ESTIMATED_SIZE constants from each message type switch (message_type) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 07e87ab39f..4397462d8e 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -185,7 +185,6 @@ class APIConnection : public APIServerConnection { void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping - this->ping_retries_ = 0; this->sent_ping_ = false; } void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; @@ -441,13 +440,16 @@ class APIConnection : public APIServerConnection { // Helper function to get estimated message size for buffer pre-allocation static uint16_t get_estimated_message_size(uint16_t message_type); + // Batch message method for ping requests + static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single); + // Pointers first (4 bytes each, naturally aligned) std::unique_ptr helper_; APIServer *parent_; // 4-byte aligned types uint32_t last_traffic_; - uint32_t next_ping_retry_{0}; int state_subs_at_ = -1; // Strings (12 bytes each on 32-bit) @@ -470,6 +472,7 @@ class APIConnection : public APIServerConnection { bool sent_ping_{false}; bool service_call_subscription_{false}; bool next_close_ = false; + // 7 bytes used, 1 byte padding #ifdef HAS_PROTO_MESSAGE_DUMP // When true, encode_message_to_buffer will only log, not encode bool log_only_mode_{false}; @@ -602,6 +605,8 @@ class APIConnection : public APIServerConnection { // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type); + // Add item to the front of the batch (for high priority messages like ping) + void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type); void clear() { items.clear(); batch_scheduled = false; @@ -645,6 +650,12 @@ class APIConnection : public APIServerConnection { bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { return schedule_message_(entity, MessageCreator(function_ptr), message_type); } + + // Helper function to schedule a high priority message at the front of the batch + bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { + this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type); + return this->schedule_batch_(); + } }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 583837af82..a33623b15a 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -526,8 +526,8 @@ void APIServer::on_shutdown() { for (auto &c : this->clients_) { if (!c->send_message(DisconnectRequest())) { // If we can't send the disconnect request directly (tx_buffer full), - // schedule it in the batch so it will be sent with the 5ms timer - c->schedule_message_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE); + // schedule it at the front of the batch so it will be sent with priority + c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE); } } } From 0c249a700610564d96a34896abaffc913b8a48c0 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 29 Jun 2025 06:16:34 -0500 Subject: [PATCH 095/115] [thermostat] Memory optimizations (#9259) --- .../thermostat/thermostat_climate.cpp | 65 ++++++++------- .../thermostat/thermostat_climate.h | 82 ++++++++++--------- 2 files changed, 77 insertions(+), 70 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index fe6ed8b159..404e585aff 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -997,7 +997,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { auto config = this->preset_config_.find(preset); if (config != this->preset_config_.end()) { - ESP_LOGI(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) || this->preset.value() != preset) { // Fire any preset changed trigger if defined @@ -1015,7 +1015,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { this->custom_preset.reset(); this->preset = preset; } else { - ESP_LOGE(TAG, "Preset %s is not configured, ignoring.", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } } @@ -1023,7 +1023,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) auto config = this->custom_preset_config_.find(custom_preset); if (config != this->custom_preset_config_.end()) { - ESP_LOGI(TAG, "Custom preset %s requested", custom_preset.c_str()); + ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); if (this->change_preset_internal_(config->second) || (!this->custom_preset.has_value()) || this->custom_preset.value() != custom_preset) { // Fire any preset changed trigger if defined @@ -1041,7 +1041,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) this->preset.reset(); this->custom_preset = custom_preset; } else { - ESP_LOGE(TAG, "Custom Preset %s is not configured, ignoring.", custom_preset.c_str()); + ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); } } @@ -1298,7 +1298,7 @@ void ThermostatClimate::dump_config() { if (this->supports_two_points_) { ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); } - ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); + ESP_LOGCONFIG(TAG, " Use Start-up Delay: %s", YESNO(this->use_startup_delay_)); if (this->supports_cool_) { ESP_LOGCONFIG(TAG, " Cooling Parameters:\n" @@ -1353,44 +1353,47 @@ void ThermostatClimate::dump_config() { } ESP_LOGCONFIG(TAG, " Minimum Idle Time: %" PRIu32 "s", this->timer_[thermostat::TIMER_IDLE_ON].time / 1000); ESP_LOGCONFIG(TAG, - " Supports AUTO: %s\n" - " Supports HEAT/COOL: %s\n" - " Supports COOL: %s\n" - " Supports DRY: %s\n" - " Supports FAN_ONLY: %s\n" - " Supports FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" - " Supports FAN_ONLY_COOLING: %s", - YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_cool_), - YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), + " Supported MODES:\n" + " AUTO: %s\n" + " HEAT/COOL: %s\n" + " HEAT: %s\n" + " COOL: %s\n" + " DRY: %s\n" + " FAN_ONLY: %s\n" + " FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" + " FAN_ONLY_COOLING: %s", + YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), + YESNO(this->supports_cool_), YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), YESNO(this->supports_fan_only_action_uses_fan_mode_timer_), YESNO(this->supports_fan_only_cooling_)); if (this->supports_cool_) { - ESP_LOGCONFIG(TAG, " Supports FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); + ESP_LOGCONFIG(TAG, " FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); } if (this->supports_heat_) { - ESP_LOGCONFIG(TAG, " Supports FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_)); + ESP_LOGCONFIG(TAG, " FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_)); } - ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, - " Supports FAN MODE ON: %s\n" - " Supports FAN MODE OFF: %s\n" - " Supports FAN MODE AUTO: %s\n" - " Supports FAN MODE LOW: %s\n" - " Supports FAN MODE MEDIUM: %s\n" - " Supports FAN MODE HIGH: %s\n" - " Supports FAN MODE MIDDLE: %s\n" - " Supports FAN MODE FOCUS: %s\n" - " Supports FAN MODE DIFFUSE: %s\n" - " Supports FAN MODE QUIET: %s", + " Supported FAN MODES:\n" + " ON: %s\n" + " OFF: %s\n" + " AUTO: %s\n" + " LOW: %s\n" + " MEDIUM: %s\n" + " HIGH: %s\n" + " MIDDLE: %s\n" + " FOCUS: %s\n" + " DIFFUSE: %s\n" + " QUIET: %s", YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_), YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_), YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_), YESNO(this->supports_fan_mode_middle_), YESNO(this->supports_fan_mode_focus_), YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_)); ESP_LOGCONFIG(TAG, - " Supports SWING MODE BOTH: %s\n" - " Supports SWING MODE OFF: %s\n" - " Supports SWING MODE HORIZONTAL: %s\n" - " Supports SWING MODE VERTICAL: %s\n" + " Supported SWING MODES:\n" + " BOTH: %s\n" + " OFF: %s\n" + " HORIZONTAL: %s\n" + " VERTICAL: %s\n" " Supports TWO SET POINTS: %s", YESNO(this->supports_swing_mode_both_), YESNO(this->supports_swing_mode_off_), YESNO(this->supports_swing_mode_horizontal_), YESNO(this->supports_swing_mode_vertical_), diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 50510cf070..007d7297d5 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -13,7 +13,7 @@ namespace esphome { namespace thermostat { -enum ThermostatClimateTimerIndex : size_t { +enum ThermostatClimateTimerIndex : uint8_t { TIMER_COOLING_MAX_RUN_TIME = 0, TIMER_COOLING_OFF = 1, TIMER_COOLING_ON = 2, @@ -26,7 +26,11 @@ enum ThermostatClimateTimerIndex : size_t { TIMER_IDLE_ON = 9, }; -enum OnBootRestoreFrom : size_t { MEMORY = 0, DEFAULT_PRESET = 1 }; +enum OnBootRestoreFrom : uint8_t { + MEMORY = 0, + DEFAULT_PRESET = 1, +}; + struct ThermostatClimateTimer { bool active; uint32_t time; @@ -65,7 +69,7 @@ class ThermostatClimate : public climate::Climate, public Component { void set_default_preset(const std::string &custom_preset); void set_default_preset(climate::ClimatePreset preset); - void set_on_boot_restore_from(thermostat::OnBootRestoreFrom on_boot_restore_from); + void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from); void set_set_point_minimum_differential(float differential); void set_cool_deadband(float deadband); void set_cool_overrun(float overrun); @@ -240,10 +244,8 @@ class ThermostatClimate : public climate::Climate, public Component { void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, bool is_default_preset); - /// The sensor used for getting the current temperature - sensor::Sensor *sensor_{nullptr}; - /// The sensor used for getting the current humidity - sensor::Sensor *humidity_sensor_{nullptr}; + /// Minimum allowable duration in seconds for action timers + const uint8_t min_timer_duration_{1}; /// Whether the controller supports auto/cooling/drying/fanning/heating. /// @@ -310,6 +312,31 @@ class ThermostatClimate : public climate::Climate, public Component { /// setup_complete_ blocks modifying/resetting the temps immediately after boot bool setup_complete_{false}; + /// Store previously-known temperatures + /// + /// These are used to determine when the temperature change trigger/action needs to be called + float prev_target_temperature_{NAN}; + float prev_target_temperature_low_{NAN}; + float prev_target_temperature_high_{NAN}; + + /// Minimum differential required between set points + float set_point_minimum_differential_{0}; + + /// Hysteresis values used for computing climate actions + float cooling_deadband_{0}; + float cooling_overrun_{0}; + float heating_deadband_{0}; + float heating_overrun_{0}; + + /// Maximum allowable temperature deltas before engaging supplemental cooling/heating actions + float supplemental_cool_delta_{0}; + float supplemental_heat_delta_{0}; + + /// The sensor used for getting the current temperature + sensor::Sensor *sensor_{nullptr}; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; + /// The trigger to call when the controller should switch to cooling action/mode. /// /// A null value for this attribute means that the controller has no cooling action @@ -399,7 +426,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// The trigger to call when the target temperature(s) change(es). Trigger<> *temperature_change_trigger_{nullptr}; - /// The triggr to call when the preset mode changes + /// The trigger to call when the preset mode changes Trigger<> *preset_change_trigger_{nullptr}; /// A reference to the trigger that was previously active. @@ -411,6 +438,10 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_mode_trigger_{nullptr}; Trigger<> *prev_swing_mode_trigger_{nullptr}; + /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior + /// state will attempt to be restored if possible + OnBootRestoreFrom on_boot_restore_from_{OnBootRestoreFrom::MEMORY}; + /// Store previously-known states /// /// These are used to determine when a trigger/action needs to be called @@ -419,28 +450,10 @@ class ThermostatClimate : public climate::Climate, public Component { climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; - /// Store previously-known temperatures - /// - /// These are used to determine when the temperature change trigger/action needs to be called - float prev_target_temperature_{NAN}; - float prev_target_temperature_low_{NAN}; - float prev_target_temperature_high_{NAN}; - - /// Minimum differential required between set points - float set_point_minimum_differential_{0}; - - /// Hysteresis values used for computing climate actions - float cooling_deadband_{0}; - float cooling_overrun_{0}; - float heating_deadband_{0}; - float heating_overrun_{0}; - - /// Maximum allowable temperature deltas before engauging supplemental cooling/heating actions - float supplemental_cool_delta_{0}; - float supplemental_heat_delta_{0}; - - /// Minimum allowable duration in seconds for action timers - const uint8_t min_timer_duration_{1}; + /// Default standard preset to use on start up + climate::ClimatePreset default_preset_{}; + /// Default custom preset to use on start up + std::string default_custom_preset_{}; /// Climate action timers std::vector timer_{ @@ -460,15 +473,6 @@ class ThermostatClimate : public climate::Climate, public Component { std::map preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") std::map custom_preset_config_{}; - - /// Default standard preset to use on start up - climate::ClimatePreset default_preset_{}; - /// Default custom preset to use on start up - std::string default_custom_preset_{}; - - /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior - /// state will attempt to be restored if possible - thermostat::OnBootRestoreFrom on_boot_restore_from_{thermostat::OnBootRestoreFrom::MEMORY}; }; } // namespace thermostat From 53ab016098be0719f182c3586b65d47e0f54a85c Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 29 Jun 2025 06:17:53 -0500 Subject: [PATCH 096/115] [adc] Memory optimizations (#9247) --- esphome/components/adc/adc_sensor.h | 15 ++++++++---- esphome/components/adc/adc_sensor_common.cpp | 2 +- esphome/components/adc/adc_sensor_esp32.cpp | 24 +++++++++++++------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 62f2461245..9ffb6cf856 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -28,19 +28,24 @@ static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11; #endif #endif // USE_ESP32 -enum class SamplingMode : uint8_t { AVG = 0, MIN = 1, MAX = 2 }; +enum class SamplingMode : uint8_t { + AVG = 0, + MIN = 1, + MAX = 2, +}; + const LogString *sampling_mode_to_str(SamplingMode mode); class Aggregator { public: + Aggregator(SamplingMode mode); void add_sample(uint32_t value); uint32_t aggregate(); - Aggregator(SamplingMode mode); protected: - SamplingMode mode_{SamplingMode::AVG}; uint32_t aggr_{0}; uint32_t samples_{0}; + SamplingMode mode_{SamplingMode::AVG}; }; class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { @@ -81,9 +86,9 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #endif // USE_RP2040 protected: - InternalGPIOPin *pin_; - bool output_raw_{false}; uint8_t sample_count_{1}; + bool output_raw_{false}; + InternalGPIOPin *pin_; SamplingMode sampling_mode_{SamplingMode::AVG}; #ifdef USE_RP2040 diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp index c7509c7c7a..797ab75045 100644 --- a/esphome/components/adc/adc_sensor_common.cpp +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -61,7 +61,7 @@ uint32_t Aggregator::aggregate() { void ADCSensor::update() { float value_v = this->sample(); - ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); + ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v); this->publish_state(value_v); } diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index d6cf6e893b..ed1f3329ab 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -55,32 +55,40 @@ void ADCSensor::setup() { } void ADCSensor::dump_config() { + static const char *const ATTEN_AUTO_STR = "auto"; + static const char *const ATTEN_0DB_STR = "0 db"; + static const char *const ATTEN_2_5DB_STR = "2.5 db"; + static const char *const ATTEN_6DB_STR = "6 db"; + static const char *const ATTEN_12DB_STR = "12 db"; + const char *atten_str = ATTEN_AUTO_STR; + LOG_SENSOR("", "ADC Sensor", this); LOG_PIN(" Pin: ", this->pin_); - if (this->autorange_) { - ESP_LOGCONFIG(TAG, " Attenuation: auto"); - } else { + + if (!this->autorange_) { switch (this->attenuation_) { case ADC_ATTEN_DB_0: - ESP_LOGCONFIG(TAG, " Attenuation: 0db"); + atten_str = ATTEN_0DB_STR; break; case ADC_ATTEN_DB_2_5: - ESP_LOGCONFIG(TAG, " Attenuation: 2.5db"); + atten_str = ATTEN_2_5DB_STR; break; case ADC_ATTEN_DB_6: - ESP_LOGCONFIG(TAG, " Attenuation: 6db"); + atten_str = ATTEN_6DB_STR; break; case ADC_ATTEN_DB_12_COMPAT: - ESP_LOGCONFIG(TAG, " Attenuation: 12db"); + atten_str = ATTEN_12DB_STR; break; default: // This is to satisfy the unused ADC_ATTEN_MAX break; } } + ESP_LOGCONFIG(TAG, + " Attenuation: %s\n" " Samples: %i\n" " Sampling mode: %s", - this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); + atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); LOG_UPDATE_INTERVAL(this); } From 21e1f3d1034098e27f9d793a08436955486f7e40 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 29 Jun 2025 06:28:51 -0500 Subject: [PATCH 097/115] [light] Memory optimizations (#9260) --- .../components/light/esp_color_correction.h | 2 +- esphome/components/light/light_call.cpp | 44 +++++++++---------- esphome/components/light/light_state.h | 9 ++-- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 979a1acb07..39ce5700c6 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -69,8 +69,8 @@ class ESPColorCorrection { protected: uint8_t gamma_table_[256]; uint8_t gamma_reverse_table_[256]; - Color max_brightness_; uint8_t local_brightness_{255}; + Color max_brightness_; }; } // namespace light diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index c2600d05c2..78b0ac9feb 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -136,7 +136,7 @@ LightColorValues LightCall::validate_() { // Color mode check if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) { - ESP_LOGW(TAG, "'%s' - This light does not support color mode %s!", name, + ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_.value()))); this->color_mode_.reset(); } @@ -152,20 +152,20 @@ LightColorValues LightCall::validate_() { // Brightness exists check if (this->brightness_.has_value() && *this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { - ESP_LOGW(TAG, "'%s' - This light does not support setting brightness!", name); + ESP_LOGW(TAG, "'%s': setting brightness not supported", name); this->brightness_.reset(); } // Transition length possible check if (this->transition_length_.has_value() && *this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) { - ESP_LOGW(TAG, "'%s' - This light does not support transitions!", name); + ESP_LOGW(TAG, "'%s': transitions not supported", name); this->transition_length_.reset(); } // Color brightness exists check if (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB brightness!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting RGB brightness", name); this->color_brightness_.reset(); } @@ -173,7 +173,7 @@ LightColorValues LightCall::validate_() { if ((this->red_.has_value() && *this->red_ > 0.0f) || (this->green_.has_value() && *this->green_ > 0.0f) || (this->blue_.has_value() && *this->blue_ > 0.0f)) { if (!(color_mode & ColorCapability::RGB)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB color!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name); this->red_.reset(); this->green_.reset(); this->blue_.reset(); @@ -183,14 +183,14 @@ LightColorValues LightCall::validate_() { // White value exists check if (this->white_.has_value() && *this->white_ > 0.0f && !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting white value!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting white value", name); this->white_.reset(); } // Color temperature exists check if (this->color_temperature_.has_value() && !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting color temperature!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting color temperature", name); this->color_temperature_.reset(); } @@ -198,7 +198,7 @@ LightColorValues LightCall::validate_() { if ((this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || (this->warm_white_.has_value() && *this->warm_white_ > 0.0f)) { if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting cold/warm white value!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name); this->cold_white_.reset(); this->warm_white_.reset(); } @@ -208,7 +208,7 @@ LightColorValues LightCall::validate_() { if (name_##_.has_value()) { \ auto val = *name_##_; \ if (val < (min) || val > (max)) { \ - ESP_LOGW(TAG, "'%s' - %s value %.2f is out of range [%.1f - %.1f]!", name, LOG_STR_LITERAL(upper_name), val, \ + ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_LITERAL(upper_name), val, \ (min), (max)); \ name_##_ = clamp(val, (min), (max)); \ } \ @@ -270,7 +270,7 @@ LightColorValues LightCall::validate_() { // Flash length check if (this->has_flash_() && *this->flash_length_ == 0) { - ESP_LOGW(TAG, "'%s' - Flash length must be greater than zero!", name); + ESP_LOGW(TAG, "'%s': flash length must be greater than zero", name); this->flash_length_.reset(); } @@ -284,18 +284,18 @@ LightColorValues LightCall::validate_() { // validate effect index if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' - Invalid effect index %" PRIu32 "!", name, *this->effect_); + ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, *this->effect_); this->effect_.reset(); } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { - ESP_LOGW(TAG, "'%s' - Effect cannot be used together with transition/flash!", name); + ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name); this->transition_length_.reset(); this->flash_length_.reset(); } if (this->has_flash_() && this->has_transition_()) { - ESP_LOGW(TAG, "'%s' - Flash cannot be used together with transition!", name); + ESP_LOGW(TAG, "'%s': flash cannot be used with transition", name); this->transition_length_.reset(); } @@ -311,7 +311,7 @@ LightColorValues LightCall::validate_() { } if (this->has_transition_() && !supports_transition) { - ESP_LOGW(TAG, "'%s' - Light does not support transitions!", name); + ESP_LOGW(TAG, "'%s': transitions not supported", name); this->transition_length_.reset(); } @@ -320,7 +320,7 @@ LightColorValues LightCall::validate_() { // Reason: When user turns off the light in frontend, the effect should also stop if (!this->has_flash_() && !this->state_.value_or(v.is_on())) { if (this->has_effect_()) { - ESP_LOGW(TAG, "'%s' - Cannot start an effect when turning off!", name); + ESP_LOGW(TAG, "'%s': cannot start effect when turning off", name); this->effect_.reset(); } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect @@ -348,7 +348,7 @@ void LightCall::transform_parameters_() { !(*this->color_mode_ & ColorCapability::WHITE) && // !(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) { - ESP_LOGD(TAG, "'%s' - Setting cold/warm white channels using white/color temperature values.", + ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values", this->parent_->get_name().c_str()); if (this->color_temperature_.has_value()) { const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); @@ -388,8 +388,8 @@ ColorMode LightCall::compute_color_mode_() { // Don't change if the current mode is suitable. if (suitable_modes.count(current_mode) > 0) { - ESP_LOGI(TAG, "'%s' - Keeping current color mode %s for call without color mode.", - this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(current_mode))); + ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), + LOG_STR_ARG(color_mode_to_human(current_mode))); return current_mode; } @@ -398,7 +398,7 @@ ColorMode LightCall::compute_color_mode_() { if (supported_modes.count(mode) == 0) continue; - ESP_LOGI(TAG, "'%s' - Using color mode %s for call without color mode.", this->parent_->get_name().c_str(), + ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode; } @@ -406,8 +406,8 @@ ColorMode LightCall::compute_color_mode_() { // There's no supported mode for this call, so warn, use the current more or a mode at random and let validation strip // out whatever we don't support. auto color_mode = current_mode != ColorMode::UNKNOWN ? current_mode : *supported_modes.begin(); - ESP_LOGW(TAG, "'%s' - No color mode suitable for this call supported, defaulting to %s!", - this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(color_mode))); + ESP_LOGW(TAG, "'%s': no suitable color mode supported; defaulting to %s", this->parent_->get_name().c_str(), + LOG_STR_ARG(color_mode_to_human(color_mode))); return color_mode; } std::set LightCall::get_suitable_color_modes_() { @@ -472,7 +472,7 @@ LightCall &LightCall::set_effect(const std::string &effect) { } } if (!found) { - ESP_LOGW(TAG, "'%s' - No such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); + ESP_LOGW(TAG, "'%s': no such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); } return *this; } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index b93823feac..f21fb8a06e 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -225,6 +225,11 @@ class LightState : public EntityBase, public Component { /// Gamma correction factor for the light. float gamma_correct_{}; + /// 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; + /// Object used to store the persisted values of the light. ESPPreferenceObject rtc_; @@ -247,10 +252,6 @@ class LightState : public EntityBase, public Component { /// 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; }; } // namespace light From 921d0888cdce6e94a29c024c604812e54aa59d40 Mon Sep 17 00:00:00 2001 From: Rezoran <30475103+Rezoran@users.noreply.github.com> Date: Sun, 29 Jun 2025 17:05:23 +0200 Subject: [PATCH 098/115] [uart] fix: missing uart_config_t struct initialisation (#9235) --- esphome/components/uart/uart_component_esp_idf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 84bd48d530..8fae63a603 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -42,7 +42,7 @@ uart_config_t IDFUARTComponent::get_config_() { break; } - uart_config_t uart_config; + uart_config_t uart_config{}; uart_config.baud_rate = this->baud_rate_; uart_config.data_bits = data_bits; uart_config.parity = parity; From ddbcf8549c21c68a48aade8824a22f55c055f03e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 13:29:18 -0500 Subject: [PATCH 099/115] Reduce web_server code duplication by extracting detail parameter parsing (#9257) --- esphome/components/web_server/web_server.cpp | 126 ++++--------------- 1 file changed, 26 insertions(+), 100 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index becb5bc2c7..9f42253794 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -370,6 +370,12 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { set_json_value(root, obj, sensor, value, start_config); \ (root)["state"] = state; +// Helper to get request detail parameter +static JsonDetail get_request_detail(AsyncWebServerRequest *request) { + auto *param = request->getParam("detail"); + return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE; +} + #ifdef USE_SENSOR void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { if (this->events_.empty()) @@ -381,11 +387,7 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -435,11 +437,7 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->text_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -483,11 +481,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->switch_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { @@ -534,11 +528,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->button_json(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "press") { @@ -584,11 +574,7 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->binary_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -632,11 +618,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->fan_json(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { @@ -722,11 +704,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->light_json(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "toggle") { @@ -847,11 +825,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->cover_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -937,11 +911,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->number_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -1016,11 +986,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->date_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1084,11 +1050,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->time_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1151,11 +1113,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->datetime_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1220,11 +1178,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->text_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -1290,11 +1244,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->select_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -1358,11 +1308,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->climate_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1526,11 +1472,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->lock_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); } else if (match.method == "lock") { @@ -1583,11 +1525,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->valve_json(obj, detail); request->send(200, "application/json", data.c_str()); return; @@ -1664,11 +1602,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->alarm_control_panel_json(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); return; @@ -1740,11 +1674,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->event_json(obj, "", detail); request->send(200, "application/json", data.c_str()); return; @@ -1795,11 +1725,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + auto detail = get_request_detail(request); std::string data = this->update_json(obj, detail); request->send(200, "application/json", data.c_str()); return; From ed9850c4a4937b0276cb0c2565a4ac81caf387b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 13:46:28 -0500 Subject: [PATCH 100/115] Remove redundant get_setup_priority() overrides returning default value (#9253) --- esphome/components/ade7880/ade7880.h | 2 -- esphome/components/ads1115/ads1115.h | 1 - esphome/components/ads1118/ads1118.h | 1 - esphome/components/ags10/ags10.h | 2 -- esphome/components/aic3204/aic3204.h | 1 - esphome/components/alpha3/alpha3.h | 1 - esphome/components/am43/cover/am43_cover.h | 1 - esphome/components/am43/sensor/am43_sensor.h | 1 - .../analog_threshold/analog_threshold_binary_sensor.h | 2 -- esphome/components/anova/anova.h | 1 - esphome/components/as5600/as5600.h | 1 - esphome/components/atc_mithermometer/atc_mithermometer.h | 1 - esphome/components/b_parasite/b_parasite.h | 1 - esphome/components/ble_client/output/ble_binary_output.h | 1 - esphome/components/ble_client/sensor/ble_rssi_sensor.h | 1 - esphome/components/ble_client/sensor/ble_sensor.h | 1 - esphome/components/ble_client/switch/ble_switch.h | 1 - esphome/components/ble_client/text_sensor/ble_text_sensor.h | 1 - esphome/components/ble_presence/ble_presence_device.h | 1 - esphome/components/ble_rssi/ble_rssi_sensor.h | 1 - esphome/components/ble_scanner/ble_scanner.h | 1 - esphome/components/bmp581/bmp581.h | 2 -- esphome/components/cap1188/cap1188.h | 1 - esphome/components/ccs811/ccs811.h | 2 -- esphome/components/copy/binary_sensor/copy_binary_sensor.h | 1 - esphome/components/copy/button/copy_button.h | 1 - esphome/components/copy/cover/copy_cover.h | 1 - esphome/components/copy/fan/copy_fan.h | 1 - esphome/components/copy/lock/copy_lock.h | 1 - esphome/components/copy/number/copy_number.h | 1 - esphome/components/copy/select/copy_select.h | 1 - esphome/components/copy/sensor/copy_sensor.h | 1 - esphome/components/copy/switch/copy_switch.h | 1 - esphome/components/copy/text/copy_text.h | 1 - esphome/components/copy/text_sensor/copy_text_sensor.h | 1 - esphome/components/cs5460a/cs5460a.h | 1 - esphome/components/duty_time/duty_time_sensor.h | 1 - esphome/components/ens160_base/ens160_base.h | 1 - esphome/components/es7210/es7210.h | 1 - esphome/components/es7243e/es7243e.h | 1 - esphome/components/es8156/es8156.h | 1 - esphome/components/es8311/es8311.h | 1 - esphome/components/es8388/es8388.h | 1 - esphome/components/esp32_touch/esp32_touch.h | 1 - esphome/components/ezo/ezo.h | 1 - esphome/components/ezo_pmp/ezo_pmp.h | 1 - esphome/components/feedback/feedback_cover.h | 1 - esphome/components/fs3000/fs3000.h | 1 - esphome/components/gcja5/gcja5.h | 1 - esphome/components/gp8403/gp8403.h | 1 - esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h | 2 -- esphome/components/he60r/he60r.h | 1 - esphome/components/honeywellabp2_i2c/honeywellabp2.h | 1 - esphome/components/i2c_device/i2c_device.h | 1 - esphome/components/iaqcore/iaqcore.h | 2 -- esphome/components/ina260/ina260.h | 2 -- esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h | 1 - esphome/components/integration/integration_sensor.h | 1 - esphome/components/interval/interval.h | 2 -- esphome/components/ltr390/ltr390.h | 1 - esphome/components/ltr501/ltr501.h | 1 - esphome/components/ltr_als_ps/ltr_als_ps.h | 1 - esphome/components/max9611/max9611.h | 1 - esphome/components/mcp9600/mcp9600.h | 2 -- esphome/components/mopeka_pro_check/mopeka_pro_check.h | 1 - esphome/components/mopeka_std_check/mopeka_std_check.h | 1 - esphome/components/mpl3115a2/mpl3115a2.h | 2 -- esphome/components/ms8607/ms8607.h | 1 - esphome/components/pmsa003i/pmsa003i.h | 1 - esphome/components/pmsx003/pmsx003.h | 1 - esphome/components/pn7150/pn7150.h | 1 - esphome/components/pn7160/pn7160.h | 1 - esphome/components/pulse_counter/pulse_counter_sensor.h | 1 - esphome/components/pulse_width/pulse_width.h | 1 - esphome/components/pvvx_mithermometer/display/pvvx_display.h | 2 -- esphome/components/pvvx_mithermometer/pvvx_mithermometer.h | 1 - esphome/components/qwiic_pir/qwiic_pir.h | 1 - esphome/components/rc522/rc522.h | 1 - esphome/components/rdm6300/rdm6300.h | 2 -- esphome/components/remote_receiver/remote_receiver.h | 1 - esphome/components/resistance/resistance_sensor.h | 1 - esphome/components/ruuvitag/ruuvitag.h | 1 - esphome/components/scd30/scd30.h | 1 - esphome/components/scd4x/scd4x.h | 1 - esphome/components/script/script.h | 2 -- esphome/components/sen5x/sen5x.h | 1 - esphome/components/senseair/senseair.h | 1 - esphome/components/servo/servo.h | 1 - esphome/components/sfa30/sfa30.h | 1 - esphome/components/sgp30/sgp30.h | 1 - esphome/components/sgp4x/sgp4x.h | 1 - esphome/components/sht4x/sht4x.h | 1 - esphome/components/sm300d2/sm300d2.h | 2 -- esphome/components/sps30/sps30.h | 1 - esphome/components/status/status_binary_sensor.h | 2 -- esphome/components/switch/binary_sensor/switch_binary_sensor.h | 1 - esphome/components/tmp1075/tmp1075.h | 2 -- esphome/components/tof10120/tof10120_sensor.h | 1 - esphome/components/tormatic/tormatic_cover.h | 1 - esphome/components/total_daily_energy/total_daily_energy.h | 1 - esphome/components/ttp229_bsf/ttp229_bsf.h | 1 - esphome/components/ttp229_lsf/ttp229_lsf.h | 1 - esphome/components/vbus/vbus.h | 1 - esphome/components/veml3235/veml3235.h | 1 - esphome/components/veml7700/veml7700.h | 1 - esphome/components/vl53l0x/vl53l0x_sensor.h | 1 - esphome/components/xiaomi_cgd1/xiaomi_cgd1.h | 1 - esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h | 1 - esphome/components/xiaomi_cgg1/xiaomi_cgg1.h | 1 - esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h | 1 - esphome/components/xiaomi_gcls002/xiaomi_gcls002.h | 1 - esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h | 1 - esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h | 1 - esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h | 1 - esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h | 1 - esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h | 1 - esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h | 1 - esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h | 1 - esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h | 1 - esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h | 1 - esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h | 1 - esphome/components/xiaomi_miscale/xiaomi_miscale.h | 1 - esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h | 1 - esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h | 1 - esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h | 1 - esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h | 1 - esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h | 1 - esphome/components/zio_ultrasonic/zio_ultrasonic.h | 2 -- esphome/components/zyaura/zyaura.h | 1 - 129 files changed, 147 deletions(-) diff --git a/esphome/components/ade7880/ade7880.h b/esphome/components/ade7880/ade7880.h index a565357dc5..40bc22e54a 100644 --- a/esphome/components/ade7880/ade7880.h +++ b/esphome/components/ade7880/ade7880.h @@ -85,8 +85,6 @@ class ADE7880 : public i2c::I2CDevice, public PollingComponent { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: ADE7880Store store_{}; InternalGPIOPin *irq0_pin_{nullptr}; diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h index e65835a386..e827a739d2 100644 --- a/esphome/components/ads1115/ads1115.h +++ b/esphome/components/ads1115/ads1115.h @@ -49,7 +49,6 @@ class ADS1115Component : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; /// HARDWARE_LATE setup priority - float get_setup_priority() const override { return setup_priority::DATA; } void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; } /// Helper method to request a measurement from a sensor. diff --git a/esphome/components/ads1118/ads1118.h b/esphome/components/ads1118/ads1118.h index 8b9aa15cd2..e96baab386 100644 --- a/esphome/components/ads1118/ads1118.h +++ b/esphome/components/ads1118/ads1118.h @@ -34,7 +34,6 @@ class ADS1118 : public Component, ADS1118() = default; void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } /// Helper method to request a measurement from a sensor. float request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode); diff --git a/esphome/components/ags10/ags10.h b/esphome/components/ags10/ags10.h index f2201fe70c..3e184ae176 100644 --- a/esphome/components/ags10/ags10.h +++ b/esphome/components/ags10/ags10.h @@ -31,8 +31,6 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - /** * Modifies target address of AGS10. * diff --git a/esphome/components/aic3204/aic3204.h b/esphome/components/aic3204/aic3204.h index 783a58a2b9..28006e33fc 100644 --- a/esphome/components/aic3204/aic3204.h +++ b/esphome/components/aic3204/aic3204.h @@ -66,7 +66,6 @@ class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDev public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } bool set_mute_off() override; bool set_mute_on() override; diff --git a/esphome/components/alpha3/alpha3.h b/esphome/components/alpha3/alpha3.h index 325c70a538..7189ecbc33 100644 --- a/esphome/components/alpha3/alpha3.h +++ b/esphome/components/alpha3/alpha3.h @@ -41,7 +41,6 @@ class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponen void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; } void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } diff --git a/esphome/components/am43/cover/am43_cover.h b/esphome/components/am43/cover/am43_cover.h index f33f2d1734..d6d020e98c 100644 --- a/esphome/components/am43/cover/am43_cover.h +++ b/esphome/components/am43/cover/am43_cover.h @@ -22,7 +22,6 @@ class Am43Component : public cover::Cover, public esphome::ble_client::BLEClient void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } cover::CoverTraits get_traits() override; void set_pin(uint16_t pin) { this->pin_ = pin; } void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; } diff --git a/esphome/components/am43/sensor/am43_sensor.h b/esphome/components/am43/sensor/am43_sensor.h index 8dfe83e3a3..91973d8e33 100644 --- a/esphome/components/am43/sensor/am43_sensor.h +++ b/esphome/components/am43/sensor/am43_sensor.h @@ -22,7 +22,6 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_battery(sensor::Sensor *battery) { battery_ = battery; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index efb8e3c90c..55d6b15c36 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -12,8 +12,6 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina void dump_config() override; void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_sensor(sensor::Sensor *analog_sensor); template void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; } template void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; } diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h index 3d1394980a..560d96baa7 100644 --- a/esphome/components/anova/anova.h +++ b/esphome/components/anova/anova.h @@ -26,7 +26,6 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); diff --git a/esphome/components/as5600/as5600.h b/esphome/components/as5600/as5600.h index fbfd18db40..914a4431bd 100644 --- a/esphome/components/as5600/as5600.h +++ b/esphome/components/as5600/as5600.h @@ -50,7 +50,6 @@ class AS5600Component : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; /// HARDWARE_LATE setup priority - float get_setup_priority() const override { return setup_priority::DATA; } // configuration setters void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; } diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h index 31fb77ac7f..d22e3f069b 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.h +++ b/esphome/components/atc_mithermometer/atc_mithermometer.h @@ -25,7 +25,6 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice 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) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/b_parasite/b_parasite.h b/esphome/components/b_parasite/b_parasite.h index 70ee4ab23c..7dd08968ec 100644 --- a/esphome/components/b_parasite/b_parasite.h +++ b/esphome/components/b_parasite/b_parasite.h @@ -16,7 +16,6 @@ class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListene 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_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } diff --git a/esphome/components/ble_client/output/ble_binary_output.h b/esphome/components/ble_client/output/ble_binary_output.h index 0a1e186b26..5e8bd6da62 100644 --- a/esphome/components/ble_client/output/ble_binary_output.h +++ b/esphome/components/ble_client/output/ble_binary_output.h @@ -16,7 +16,6 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi public: void dump_config() override; void loop() override {} - float get_setup_priority() const override { return setup_priority::DATA; } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.h b/esphome/components/ble_client/sensor/ble_rssi_sensor.h index 5dd3fc7af9..76cd8345a6 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.h +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.h @@ -18,7 +18,6 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ void loop() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h index b11a010ee4..24d1ed2fd2 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.h +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -24,7 +24,6 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h index 2e19c8aeef..9809f904e7 100644 --- a/esphome/components/ble_client/switch/ble_switch.h +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -19,7 +19,6 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie void loop() override {} void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void write_state(bool state) override; diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.h b/esphome/components/ble_client/text_sensor/ble_text_sensor.h index cb34043b46..c75a4df952 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.h +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.h @@ -20,7 +20,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 3ed60d1b49..70ecc67c32 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -105,7 +105,6 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, this->set_found_(false); } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void set_found_(bool state) { diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 89e4f33aca..80245a1fe1 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -99,7 +99,6 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi return false; } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_IRK, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h index b330eff696..8bb51fcff2 100644 --- a/esphome/components/ble_scanner/ble_scanner.h +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -29,7 +29,6 @@ class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESP return true; } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } }; } // namespace ble_scanner diff --git a/esphome/components/bmp581/bmp581.h b/esphome/components/bmp581/bmp581.h index 7327be44ae..1d7e932fa1 100644 --- a/esphome/components/bmp581/bmp581.h +++ b/esphome/components/bmp581/bmp581.h @@ -61,8 +61,6 @@ enum IIRFilter { class BMP581Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } - void dump_config() override; void setup() override; diff --git a/esphome/components/cap1188/cap1188.h b/esphome/components/cap1188/cap1188.h index fa0ed622fa..baefd1c48f 100644 --- a/esphome/components/cap1188/cap1188.h +++ b/esphome/components/cap1188/cap1188.h @@ -46,7 +46,6 @@ class CAP1188Component : public Component, public i2c::I2CDevice { void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; protected: diff --git a/esphome/components/ccs811/ccs811.h b/esphome/components/ccs811/ccs811.h index 8a0d60d002..675ba7da97 100644 --- a/esphome/components/ccs811/ccs811.h +++ b/esphome/components/ccs811/ccs811.h @@ -25,8 +25,6 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: optional read_status_() { return this->read_byte(0x00); } bool status_has_error_() { return this->read_status_().value_or(1) & 1; } diff --git a/esphome/components/copy/binary_sensor/copy_binary_sensor.h b/esphome/components/copy/binary_sensor/copy_binary_sensor.h index d62ed13c76..fc1e368b38 100644 --- a/esphome/components/copy/binary_sensor/copy_binary_sensor.h +++ b/esphome/components/copy/binary_sensor/copy_binary_sensor.h @@ -11,7 +11,6 @@ class CopyBinarySensor : public binary_sensor::BinarySensor, public Component { void set_source(binary_sensor::BinarySensor *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: binary_sensor::BinarySensor *source_; diff --git a/esphome/components/copy/button/copy_button.h b/esphome/components/copy/button/copy_button.h index 9996ca0c65..79d5dbcf04 100644 --- a/esphome/components/copy/button/copy_button.h +++ b/esphome/components/copy/button/copy_button.h @@ -10,7 +10,6 @@ class CopyButton : public button::Button, public Component { public: void set_source(button::Button *source) { source_ = source; } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void press_action() override; diff --git a/esphome/components/copy/cover/copy_cover.h b/esphome/components/copy/cover/copy_cover.h index fb278523ff..ec27b6782a 100644 --- a/esphome/components/copy/cover/copy_cover.h +++ b/esphome/components/copy/cover/copy_cover.h @@ -11,7 +11,6 @@ class CopyCover : public cover::Cover, public Component { void set_source(cover::Cover *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } cover::CoverTraits get_traits() override; diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h index 1a69810510..b474975bc4 100644 --- a/esphome/components/copy/fan/copy_fan.h +++ b/esphome/components/copy/fan/copy_fan.h @@ -11,7 +11,6 @@ class CopyFan : public fan::Fan, public Component { void set_source(fan::Fan *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } fan::FanTraits get_traits() override; diff --git a/esphome/components/copy/lock/copy_lock.h b/esphome/components/copy/lock/copy_lock.h index 0554013674..8799eebb4a 100644 --- a/esphome/components/copy/lock/copy_lock.h +++ b/esphome/components/copy/lock/copy_lock.h @@ -11,7 +11,6 @@ class CopyLock : public lock::Lock, public Component { void set_source(lock::Lock *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(const lock::LockCall &call) override; diff --git a/esphome/components/copy/number/copy_number.h b/esphome/components/copy/number/copy_number.h index 1ad956fec4..09b65e2cbf 100644 --- a/esphome/components/copy/number/copy_number.h +++ b/esphome/components/copy/number/copy_number.h @@ -11,7 +11,6 @@ class CopyNumber : public number::Number, public Component { void set_source(number::Number *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(float value) override; diff --git a/esphome/components/copy/select/copy_select.h b/esphome/components/copy/select/copy_select.h index c8666cd394..fb0aee86f6 100644 --- a/esphome/components/copy/select/copy_select.h +++ b/esphome/components/copy/select/copy_select.h @@ -11,7 +11,6 @@ class CopySelect : public select::Select, public Component { void set_source(select::Select *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(const std::string &value) override; diff --git a/esphome/components/copy/sensor/copy_sensor.h b/esphome/components/copy/sensor/copy_sensor.h index 1ae790ada3..500e6872fe 100644 --- a/esphome/components/copy/sensor/copy_sensor.h +++ b/esphome/components/copy/sensor/copy_sensor.h @@ -11,7 +11,6 @@ class CopySensor : public sensor::Sensor, public Component { void set_source(sensor::Sensor *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: sensor::Sensor *source_; diff --git a/esphome/components/copy/switch/copy_switch.h b/esphome/components/copy/switch/copy_switch.h index 26cb254ab3..80310af03f 100644 --- a/esphome/components/copy/switch/copy_switch.h +++ b/esphome/components/copy/switch/copy_switch.h @@ -11,7 +11,6 @@ class CopySwitch : public switch_::Switch, public Component { void set_source(switch_::Switch *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void write_state(bool state) override; diff --git a/esphome/components/copy/text/copy_text.h b/esphome/components/copy/text/copy_text.h index beb8610dfe..9eaebae4be 100644 --- a/esphome/components/copy/text/copy_text.h +++ b/esphome/components/copy/text/copy_text.h @@ -11,7 +11,6 @@ class CopyText : public text::Text, public Component { void set_source(text::Text *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(const std::string &value) override; diff --git a/esphome/components/copy/text_sensor/copy_text_sensor.h b/esphome/components/copy/text_sensor/copy_text_sensor.h index fe91fe948b..489986c59d 100644 --- a/esphome/components/copy/text_sensor/copy_text_sensor.h +++ b/esphome/components/copy/text_sensor/copy_text_sensor.h @@ -11,7 +11,6 @@ class CopyTextSensor : public text_sensor::TextSensor, public Component { void set_source(text_sensor::TextSensor *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: text_sensor::TextSensor *source_; diff --git a/esphome/components/cs5460a/cs5460a.h b/esphome/components/cs5460a/cs5460a.h index 763ddc14fa..15ae04f3c6 100644 --- a/esphome/components/cs5460a/cs5460a.h +++ b/esphome/components/cs5460a/cs5460a.h @@ -77,7 +77,6 @@ class CS5460AComponent : public Component, void setup() override; void loop() override {} - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; protected: diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index 38655f104a..18280f8e21 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -19,7 +19,6 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent { void update() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void start(); void stop(); diff --git a/esphome/components/ens160_base/ens160_base.h b/esphome/components/ens160_base/ens160_base.h index 729225a5ae..ae850c8180 100644 --- a/esphome/components/ens160_base/ens160_base.h +++ b/esphome/components/ens160_base/ens160_base.h @@ -18,7 +18,6 @@ class ENS160Component : public PollingComponent, public sensor::Sensor { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void send_env_data_(); diff --git a/esphome/components/es7210/es7210.h b/esphome/components/es7210/es7210.h index 8f6d9d8136..7071a547ec 100644 --- a/esphome/components/es7210/es7210.h +++ b/esphome/components/es7210/es7210.h @@ -25,7 +25,6 @@ class ES7210 : public audio_adc::AudioAdc, public Component, public i2c::I2CDevi */ public: void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; void set_bits_per_sample(ES7210BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } diff --git a/esphome/components/es7243e/es7243e.h b/esphome/components/es7243e/es7243e.h index 41a8acac8d..f7c9d67371 100644 --- a/esphome/components/es7243e/es7243e.h +++ b/esphome/components/es7243e/es7243e.h @@ -14,7 +14,6 @@ class ES7243E : public audio_adc::AudioAdc, public Component, public i2c::I2CDev */ public: void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; bool set_mic_gain(float mic_gain) override; diff --git a/esphome/components/es8156/es8156.h b/esphome/components/es8156/es8156.h index e973599a7a..082514485c 100644 --- a/esphome/components/es8156/es8156.h +++ b/esphome/components/es8156/es8156.h @@ -14,7 +14,6 @@ class ES8156 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ///////////////////////// void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; //////////////////////// diff --git a/esphome/components/es8311/es8311.h b/esphome/components/es8311/es8311.h index 840a07204c..5eccc48004 100644 --- a/esphome/components/es8311/es8311.h +++ b/esphome/components/es8311/es8311.h @@ -50,7 +50,6 @@ class ES8311 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ///////////////////////// void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; //////////////////////// diff --git a/esphome/components/es8388/es8388.h b/esphome/components/es8388/es8388.h index 45944f68bd..373f71b437 100644 --- a/esphome/components/es8388/es8388.h +++ b/esphome/components/es8388/es8388.h @@ -38,7 +38,6 @@ class ES8388 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ///////////////////////// void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; //////////////////////// diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 0eac590ce7..3fce8a7e18 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -52,7 +52,6 @@ class ESP32TouchComponent : public Component { void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void on_shutdown() override; diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h index 28b46643e9..00dd98fc80 100644 --- a/esphome/components/ezo/ezo.h +++ b/esphome/components/ezo/ezo.h @@ -38,7 +38,6 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 void loop() override; void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; }; // I2C void set_address(uint8_t address); diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h index b41710cd78..671e124810 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.h +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -23,7 +23,6 @@ namespace ezo_pmp { class EzoPMP : public PollingComponent, public i2c::I2CDevice { public: void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void loop() override; void update() override; diff --git a/esphome/components/feedback/feedback_cover.h b/esphome/components/feedback/feedback_cover.h index 7e107aebcd..199d3b520a 100644 --- a/esphome/components/feedback/feedback_cover.h +++ b/esphome/components/feedback/feedback_cover.h @@ -16,7 +16,6 @@ class FeedbackCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; Trigger<> *get_open_trigger() const { return this->open_trigger_; } Trigger<> *get_close_trigger() const { return this->close_trigger_; } diff --git a/esphome/components/fs3000/fs3000.h b/esphome/components/fs3000/fs3000.h index be3680e7e1..e33c72215f 100644 --- a/esphome/components/fs3000/fs3000.h +++ b/esphome/components/fs3000/fs3000.h @@ -18,7 +18,6 @@ class FS3000Component : public PollingComponent, public i2c::I2CDevice, public s void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_model(FS3000Model model) { this->model_ = model; } diff --git a/esphome/components/gcja5/gcja5.h b/esphome/components/gcja5/gcja5.h index ea1fb78bf0..30bc877169 100644 --- a/esphome/components/gcja5/gcja5.h +++ b/esphome/components/gcja5/gcja5.h @@ -12,7 +12,6 @@ class GCJA5Component : public Component, public uart::UARTDevice { public: void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 65182ef301..9f493d39e3 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -15,7 +15,6 @@ class GP8403 : public Component, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h index 1987d33f37..aab881bd05 100644 --- a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h @@ -22,8 +22,6 @@ class GroveGasMultichannelV2Component : public PollingComponent, public i2c::I2C void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: enum ErrorCode { UNKNOWN, diff --git a/esphome/components/he60r/he60r.h b/esphome/components/he60r/he60r.h index e41e2203c1..02a2b44e66 100644 --- a/esphome/components/he60r/he60r.h +++ b/esphome/components/he60r/he60r.h @@ -13,7 +13,6 @@ class HE60rCover : public cover::Cover, public Component, public uart::UARTDevic void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.h b/esphome/components/honeywellabp2_i2c/honeywellabp2.h index bc81524ac2..274de847ac 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.h +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.h @@ -18,7 +18,6 @@ class HONEYWELLABP2Sensor : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }; void loop() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void dump_config() override; void read_sensor_data(); diff --git a/esphome/components/i2c_device/i2c_device.h b/esphome/components/i2c_device/i2c_device.h index ab118e3e89..9944ca9204 100644 --- a/esphome/components/i2c_device/i2c_device.h +++ b/esphome/components/i2c_device/i2c_device.h @@ -9,7 +9,6 @@ namespace i2c_device { class I2CDeviceComponent : public Component, public i2c::I2CDevice { public: void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: }; diff --git a/esphome/components/iaqcore/iaqcore.h b/esphome/components/iaqcore/iaqcore.h index f343c2a705..bb0bfcc754 100644 --- a/esphome/components/iaqcore/iaqcore.h +++ b/esphome/components/iaqcore/iaqcore.h @@ -16,8 +16,6 @@ class IAQCore : public PollingComponent, public i2c::I2CDevice { void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: sensor::Sensor *co2_{nullptr}; sensor::Sensor *tvoc_{nullptr}; diff --git a/esphome/components/ina260/ina260.h b/esphome/components/ina260/ina260.h index 8bad1cba6d..6cbc157cf3 100644 --- a/esphome/components/ina260/ina260.h +++ b/esphome/components/ina260/ina260.h @@ -13,8 +13,6 @@ class INA260Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { this->bus_voltage_sensor_ = bus_voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h index bdca2d0cac..cd2ea99717 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h @@ -16,7 +16,6 @@ class InkbirdIbstH1Mini : public Component, public esp32_ble_tracker::ESPBTDevic 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) { temperature_ = temperature; } void set_external_temperature(sensor::Sensor *external_temperature) { external_temperature_ = external_temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index e84d7a8ed1..d9f2f5e50f 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -27,7 +27,6 @@ class IntegrationSensor : public sensor::Sensor, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_sensor(Sensor *sensor) { sensor_ = sensor; } void set_time(IntegrationSensorTime time) { time_ = time; } void set_method(IntegrationMethod method) { method_ = method; } diff --git a/esphome/components/interval/interval.h b/esphome/components/interval/interval.h index 5b8bc3081f..8f904b104d 100644 --- a/esphome/components/interval/interval.h +++ b/esphome/components/interval/interval.h @@ -23,8 +23,6 @@ class IntervalTrigger : public Trigger<>, public PollingComponent { void set_startup_delay(const uint32_t startup_delay) { this->startup_delay_ = startup_delay; } - float get_setup_priority() const override { return setup_priority::DATA; } - protected: uint32_t startup_delay_{0}; bool started_{false}; diff --git a/esphome/components/ltr390/ltr390.h b/esphome/components/ltr390/ltr390.h index 7359cbd336..7db73d68ff 100644 --- a/esphome/components/ltr390/ltr390.h +++ b/esphome/components/ltr390/ltr390.h @@ -44,7 +44,6 @@ enum LTR390RESOLUTION { class LTR390Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 07b69fa0d0..849ff6bc23 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -25,7 +25,6 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { // // EspHome framework functions // - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 4cbbcea54c..2c768009ab 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -25,7 +25,6 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { // // EspHome framework functions // - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/max9611/max9611.h b/esphome/components/max9611/max9611.h index 017f56b1a7..1eb7542aee 100644 --- a/esphome/components/max9611/max9611.h +++ b/esphome/components/max9611/max9611.h @@ -38,7 +38,6 @@ class MAX9611Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; void set_voltage_sensor(sensor::Sensor *vs) { voltage_sensor_ = vs; } void set_current_sensor(sensor::Sensor *cs) { current_sensor_ = cs; } diff --git a/esphome/components/mcp9600/mcp9600.h b/esphome/components/mcp9600/mcp9600.h index 92612cc26d..c414653ea6 100644 --- a/esphome/components/mcp9600/mcp9600.h +++ b/esphome/components/mcp9600/mcp9600.h @@ -24,8 +24,6 @@ class MCP9600Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_hot_junction(sensor::Sensor *hot_junction) { this->hot_junction_sensor_ = hot_junction; } void set_cold_junction(sensor::Sensor *cold_junction) { this->cold_junction_sensor_ = cold_junction; } void set_thermocouple_type(MCP9600ThermocoupleType thermocouple_type) { diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index c58406ac18..4cbe8f2afe 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -34,7 +34,6 @@ class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi 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_min_signal_quality(SensorReadQuality min) { this->min_signal_quality_ = min; }; void set_level(sensor::Sensor *level) { level_ = level; }; diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index 2a1d9d2dfc..b92445df34 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -48,7 +48,6 @@ class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi 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_level(sensor::Sensor *level) { this->level_ = level; }; void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }; diff --git a/esphome/components/mpl3115a2/mpl3115a2.h b/esphome/components/mpl3115a2/mpl3115a2.h index 00a6d90c52..05da71f830 100644 --- a/esphome/components/mpl3115a2/mpl3115a2.h +++ b/esphome/components/mpl3115a2/mpl3115a2.h @@ -91,8 +91,6 @@ class MPL3115A2Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *altitude_{nullptr}; diff --git a/esphome/components/ms8607/ms8607.h b/esphome/components/ms8607/ms8607.h index 0bee7e97b7..67ce2817fa 100644 --- a/esphome/components/ms8607/ms8607.h +++ b/esphome/components/ms8607/ms8607.h @@ -37,7 +37,6 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } diff --git a/esphome/components/pmsa003i/pmsa003i.h b/esphome/components/pmsa003i/pmsa003i.h index 59f39a7314..cd106704a6 100644 --- a/esphome/components/pmsa003i/pmsa003i.h +++ b/esphome/components/pmsa003i/pmsa003i.h @@ -32,7 +32,6 @@ class PMSA003IComponent : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_standard_units(bool standard_units) { this->standard_units_ = standard_units; } diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index e422d4165b..ba607b4487 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -31,7 +31,6 @@ enum PMSX003State { class PMSX003Component : public uart::UARTDevice, public Component { public: PMSX003Component() = default; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; void loop() override; diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index 87af7d629b..42cd7a6ef7 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -146,7 +146,6 @@ class PN7150 : public nfc::Nfcc, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index ff8a492b7b..fc00296a71 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -161,7 +161,6 @@ class PN7160 : public nfc::Nfcc, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; void set_dwl_req_pin(GPIOPin *dwl_req_pin) { this->dwl_req_pin_ = dwl_req_pin; } diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index cea9fa7bf9..5ba59cca2a 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -76,7 +76,6 @@ class PulseCounterSensor : public sensor::Sensor, public PollingComponent { /// Unit of measurement is "pulses/min". void setup() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; protected: diff --git a/esphome/components/pulse_width/pulse_width.h b/esphome/components/pulse_width/pulse_width.h index 822688ec88..c6b896988d 100644 --- a/esphome/components/pulse_width/pulse_width.h +++ b/esphome/components/pulse_width/pulse_width.h @@ -32,7 +32,6 @@ class PulseWidthSensor : public sensor::Sensor, public PollingComponent { void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void setup() override { this->store_.setup(this->pin_); } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; protected: diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.h b/esphome/components/pvvx_mithermometer/display/pvvx_display.h index dfeb49c49d..9739362024 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.h +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -39,8 +39,6 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void update() override; void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h index 99455a1663..9614a3c586 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -25,7 +25,6 @@ class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevic 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) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/qwiic_pir/qwiic_pir.h b/esphome/components/qwiic_pir/qwiic_pir.h index d58d67734f..797ded2cc6 100644 --- a/esphome/components/qwiic_pir/qwiic_pir.h +++ b/esphome/components/qwiic_pir/qwiic_pir.h @@ -36,7 +36,6 @@ class QwiicPIRComponent : public Component, public i2c::I2CDevice, public binary void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_debounce_time(uint16_t debounce_time) { this->debounce_time_ = debounce_time; } void set_debounce_mode(DebounceMode mode) { this->debounce_mode_ = mode; } diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h index c6c5e119f0..437cea808b 100644 --- a/esphome/components/rc522/rc522.h +++ b/esphome/components/rc522/rc522.h @@ -19,7 +19,6 @@ class RC522 : public PollingComponent { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void loop() override; diff --git a/esphome/components/rdm6300/rdm6300.h b/esphome/components/rdm6300/rdm6300.h index 1a1a0c0cd6..24a808b62c 100644 --- a/esphome/components/rdm6300/rdm6300.h +++ b/esphome/components/rdm6300/rdm6300.h @@ -21,8 +21,6 @@ class RDM6300Component : public Component, public uart::UARTDevice { void register_card(RDM6300BinarySensor *obj) { this->cards_.push_back(obj); } void register_trigger(RDM6300Trigger *trig) { this->triggers_.push_back(trig); } - float get_setup_priority() const override { return setup_priority::DATA; } - protected: int8_t read_state_{-1}; uint8_t buffer_[6]{}; diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 9d844eee66..45e06e664a 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -59,7 +59,6 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } #ifdef USE_ESP32 void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } diff --git a/esphome/components/resistance/resistance_sensor.h b/esphome/components/resistance/resistance_sensor.h index b57f90b59c..a3b6e92c59 100644 --- a/esphome/components/resistance/resistance_sensor.h +++ b/esphome/components/resistance/resistance_sensor.h @@ -24,7 +24,6 @@ class ResistanceSensor : public Component, public sensor::Sensor { this->process_(this->sensor_->state); } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void process_(float value); diff --git a/esphome/components/ruuvitag/ruuvitag.h b/esphome/components/ruuvitag/ruuvitag.h index 63029ebb4d..dfe393724c 100644 --- a/esphome/components/ruuvitag/ruuvitag.h +++ b/esphome/components/ruuvitag/ruuvitag.h @@ -48,7 +48,6 @@ class RuuviTag : public Component, public esp32_ble_tracker::ESPBTDeviceListener } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_pressure(sensor::Sensor *pressure) { pressure_ = pressure; } diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index 40f075e673..ed3f5e7e9a 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -26,7 +26,6 @@ class SCD30Component : public Component, public sensirion_common::SensirionI2CDe void setup() override; void update(); void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: bool is_data_ready_(); diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 22055e78d0..f2efb28ac1 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -19,7 +19,6 @@ enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RH class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 165f90ed11..60175ec933 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -239,8 +239,6 @@ template class ScriptWaitAction : public Action, this->play_next_tuple_(this->var_); } - float get_setup_priority() const override { return setup_priority::DATA; } - void play(Ts... x) override { /* ignore - see play_complex */ } diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index 6d90636a89..0fa31605e6 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -48,7 +48,6 @@ struct TemperatureCompensation { class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h index bcec638f79..9f939d5b07 100644 --- a/esphome/components/senseair/senseair.h +++ b/esphome/components/senseair/senseair.h @@ -10,7 +10,6 @@ namespace senseair { class SenseAirComponent : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } void update() override; diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index 92d18bf601..ff1708dc53 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -20,7 +20,6 @@ class Servo : public Component { void detach(); void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_min_level(float min_level) { min_level_ = min_level; } void set_idle_level(float idle_level) { idle_level_ = idle_level; } void set_max_level(float max_level) { max_level_ = max_level; } diff --git a/esphome/components/sfa30/sfa30.h b/esphome/components/sfa30/sfa30.h index fa2c59f624..2b744b8da4 100644 --- a/esphome/components/sfa30/sfa30.h +++ b/esphome/components/sfa30/sfa30.h @@ -11,7 +11,6 @@ class SFA30Component : public PollingComponent, public sensirion_common::Sensiri enum ErrorCode { DEVICE_MARKING_READ_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index 9e882e6b05..e6429a7bfa 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -32,7 +32,6 @@ class SGP30Component : public PollingComponent, public sensirion_common::Sensiri void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void send_env_data_(); diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index 45ee66af68..8b31bca28c 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -75,7 +75,6 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se void update() override; void take_sample(); void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; } void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; } diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index 98e0629b50..accc7323be 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -17,7 +17,6 @@ enum SHT4XHEATERTIME : uint16_t { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME class SHT4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/sm300d2/sm300d2.h b/esphome/components/sm300d2/sm300d2.h index 88c04e9813..4e97b54988 100644 --- a/esphome/components/sm300d2/sm300d2.h +++ b/esphome/components/sm300d2/sm300d2.h @@ -9,8 +9,6 @@ namespace sm300d2 { class SM300D2Sensor : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } - void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } void set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sensor) { formaldehyde_sensor_ = formaldehyde_sensor; } void set_tvoc_sensor(sensor::Sensor *tvoc_sensor) { tvoc_sensor_ = tvoc_sensor; } diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index cf2e7a7d4f..04189247e8 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -26,7 +26,6 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } bool start_fan_cleaning(); diff --git a/esphome/components/status/status_binary_sensor.h b/esphome/components/status/status_binary_sensor.h index 08aa0fb32f..feda8b6328 100644 --- a/esphome/components/status/status_binary_sensor.h +++ b/esphome/components/status/status_binary_sensor.h @@ -13,8 +13,6 @@ class StatusBinarySensor : public binary_sensor::BinarySensor, public Component void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - bool is_status_binary_sensor() const override { return true; } }; diff --git a/esphome/components/switch/binary_sensor/switch_binary_sensor.h b/esphome/components/switch/binary_sensor/switch_binary_sensor.h index 5a947c2fb4..53b07da903 100644 --- a/esphome/components/switch/binary_sensor/switch_binary_sensor.h +++ b/esphome/components/switch/binary_sensor/switch_binary_sensor.h @@ -12,7 +12,6 @@ class SwitchBinarySensor : public binary_sensor::BinarySensor, public Component void set_source(Switch *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: Switch *source_; diff --git a/esphome/components/tmp1075/tmp1075.h b/esphome/components/tmp1075/tmp1075.h index 84e2e8abe4..b5fd60c08e 100644 --- a/esphome/components/tmp1075/tmp1075.h +++ b/esphome/components/tmp1075/tmp1075.h @@ -58,8 +58,6 @@ class TMP1075Sensor : public PollingComponent, public sensor::Sensor, public i2c void setup() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void dump_config() override; // Call write_config() after calling any of these to send the new config to diff --git a/esphome/components/tof10120/tof10120_sensor.h b/esphome/components/tof10120/tof10120_sensor.h index 90bad8ed07..d0cca19d4c 100644 --- a/esphome/components/tof10120/tof10120_sensor.h +++ b/esphome/components/tof10120/tof10120_sensor.h @@ -12,7 +12,6 @@ class TOF10120Sensor : public sensor::Sensor, public PollingComponent, public i2 void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; }; } // namespace tof10120 diff --git a/esphome/components/tormatic/tormatic_cover.h b/esphome/components/tormatic/tormatic_cover.h index 33a2e1db8f..534d4bef14 100644 --- a/esphome/components/tormatic/tormatic_cover.h +++ b/esphome/components/tormatic/tormatic_cover.h @@ -16,7 +16,6 @@ class Tormatic : public cover::Cover, public uart::UARTDevice, public PollingCom void loop() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index 1a9d5d1a49..1145f54f95 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -23,7 +23,6 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { void set_method(TotalDailyEnergyMethod method) { method_ = method; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; void publish_state_and_save(float state); diff --git a/esphome/components/ttp229_bsf/ttp229_bsf.h b/esphome/components/ttp229_bsf/ttp229_bsf.h index 2663afcec9..fea4356b55 100644 --- a/esphome/components/ttp229_bsf/ttp229_bsf.h +++ b/esphome/components/ttp229_bsf/ttp229_bsf.h @@ -25,7 +25,6 @@ class TTP229BSFComponent : public Component { void register_channel(TTP229BSFChannel *channel) { this->channels_.push_back(channel); } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override { // check datavalid if sdo is high if (!this->sdo_pin_->digital_read()) { diff --git a/esphome/components/ttp229_lsf/ttp229_lsf.h b/esphome/components/ttp229_lsf/ttp229_lsf.h index f8775a17f0..7cc4bfca89 100644 --- a/esphome/components/ttp229_lsf/ttp229_lsf.h +++ b/esphome/components/ttp229_lsf/ttp229_lsf.h @@ -23,7 +23,6 @@ class TTP229LSFComponent : public Component, public i2c::I2CDevice { void register_channel(TTP229Channel *channel) { this->channels_.push_back(channel); } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; protected: diff --git a/esphome/components/vbus/vbus.h b/esphome/components/vbus/vbus.h index 7e97b5049a..0a253f1bdb 100644 --- a/esphome/components/vbus/vbus.h +++ b/esphome/components/vbus/vbus.h @@ -30,7 +30,6 @@ class VBus : public uart::UARTDevice, public Component { public: void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void register_listener(VBusListener *listener) { this->listeners_.push_back(listener); } diff --git a/esphome/components/veml3235/veml3235.h b/esphome/components/veml3235/veml3235.h index 2b0d6b23ea..b57e1571f1 100644 --- a/esphome/components/veml3235/veml3235.h +++ b/esphome/components/veml3235/veml3235.h @@ -65,7 +65,6 @@ class VEML3235Sensor : public sensor::Sensor, public PollingComponent, public i2 void setup() override; void dump_config() override; void update() override { this->publish_state(this->read_lx_()); } - float get_setup_priority() const override { return setup_priority::DATA; } // Used by ESPHome framework. Does NOT actually set the value on the device. void set_auto_gain(bool auto_gain) { this->auto_gain_ = auto_gain; } diff --git a/esphome/components/veml7700/veml7700.h b/esphome/components/veml7700/veml7700.h index 17fee6b851..b0d1451cf0 100644 --- a/esphome/components/veml7700/veml7700.h +++ b/esphome/components/veml7700/veml7700.h @@ -102,7 +102,6 @@ class VEML7700Component : public PollingComponent, public i2c::I2CDevice { // // EspHome framework functions // - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index dd76e8e0ab..2bf90015fe 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -30,7 +30,6 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; void loop() override; diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h index d05cffc4d1..393795439b 100644 --- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h @@ -17,7 +17,6 @@ class XiaomiCGD1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen 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) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h index 8fd9946537..1f5ef89869 100644 --- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h +++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h @@ -17,7 +17,6 @@ class XiaomiCGDK2 : public Component, public esp32_ble_tracker::ESPBTDeviceListe 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) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index 966c05ac79..52904fd75e 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -18,7 +18,6 @@ class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen 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) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h index eff4b1c6fb..124f9411a1 100644 --- a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h +++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h @@ -21,7 +21,6 @@ class XiaomiCGPR1 : public Component, 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_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; } diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h index 08e1bd7e54..83c8f15ace 100644 --- a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h @@ -17,7 +17,6 @@ class XiaomiGCLS002 : public Component, public esp32_ble_tracker::ESPBTDeviceLis 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) { temperature_ = temperature; } void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h index aa99cc004a..96ea9217fb 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h @@ -17,7 +17,6 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL 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) { temperature_ = temperature; } void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } diff --git a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h index bc1e580ce4..bd4ad75c1d 100644 --- a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h +++ b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h @@ -16,7 +16,6 @@ class XiaomiHHCCJCY10 : public Component, public esp32_ble_tracker::ESPBTDeviceL 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_moisture(sensor::Sensor *moisture) { this->moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { this->conductivity_ = conductivity; } diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h index ce746b9ee0..0ec34b1871 100644 --- a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h @@ -17,7 +17,6 @@ class XiaomiHHCCPOT002 : public Component, public esp32_ble_tracker::ESPBTDevice 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_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h index ca1ad0f27e..e9c44800f2 100644 --- a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h @@ -17,7 +17,6 @@ class XiaomiJQJCY01YM : public Component, public esp32_ble_tracker::ESPBTDeviceL 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) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_formaldehyde(sensor::Sensor *formaldehyde) { formaldehyde_ = formaldehyde; } diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h index 641a02bd5a..772b389a92 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h @@ -17,7 +17,6 @@ class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceLis 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) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h index 19092aa2a9..e1e0fcae40 100644 --- a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h @@ -18,7 +18,6 @@ class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDevice 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; } diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h index 95710a1508..3c7907479a 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h @@ -17,7 +17,6 @@ class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDevice 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) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h index cbc76f9dd3..cf90db937f 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h @@ -17,7 +17,6 @@ class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceLi 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) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h index d0304f7894..c3b8e7d68f 100644 --- a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h +++ b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h @@ -17,7 +17,6 @@ class XiaomiMHOC303 : public Component, public esp32_ble_tracker::ESPBTDeviceLis 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) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h index 4ab882b2af..1acdaa88af 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h @@ -17,7 +17,6 @@ class XiaomiMHOC401 : public Component, public esp32_ble_tracker::ESPBTDeviceLis 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) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.h b/esphome/components/xiaomi_miscale/xiaomi_miscale.h index 4523bbc82b..10d308ef6c 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.h +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.h @@ -23,7 +23,6 @@ class XiaomiMiscale : public Component, public esp32_ble_tracker::ESPBTDeviceLis 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_weight(sensor::Sensor *weight) { weight_ = weight; } void set_impedance(sensor::Sensor *impedance) { impedance_ = impedance; } void set_clear_impedance(bool clear_impedance) { clear_impedance_ = clear_impedance; } diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h index 34b1fe4af0..e1b4055696 100644 --- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h @@ -21,7 +21,6 @@ class XiaomiMJYD02YLA : public Component, 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_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h index 904c575ae6..f1da0705d0 100644 --- a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h @@ -19,7 +19,6 @@ class XiaomiMUE4094RT : public Component, 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_time(uint16_t timeout) { timeout_ = timeout; } protected: diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h index a16c5209d9..ae00a28ac9 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h @@ -23,7 +23,6 @@ class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceL bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } #ifdef USE_BINARY_SENSOR void set_motion(binary_sensor::BinarySensor *motion) { this->motion_ = motion; } diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h index 297c7ab47d..081705fd50 100644 --- a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h @@ -20,7 +20,6 @@ class XiaomiWX08ZM : public Component, 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_tablet(sensor::Sensor *tablet) { tablet_ = tablet; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h index 9ce02bb64e..ed0458ce49 100644 --- a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h @@ -18,7 +18,6 @@ class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDevic 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; } diff --git a/esphome/components/zio_ultrasonic/zio_ultrasonic.h b/esphome/components/zio_ultrasonic/zio_ultrasonic.h index 84c8d44c65..23057b2ab0 100644 --- a/esphome/components/zio_ultrasonic/zio_ultrasonic.h +++ b/esphome/components/zio_ultrasonic/zio_ultrasonic.h @@ -11,8 +11,6 @@ namespace zio_ultrasonic { class ZioUltrasonicComponent : public i2c::I2CDevice, public PollingComponent, public sensor::Sensor { public: - float get_setup_priority() const override { return setup_priority::DATA; } - void dump_config() override; void update() override; diff --git a/esphome/components/zyaura/zyaura.h b/esphome/components/zyaura/zyaura.h index 85c31ec75a..3070aa90c5 100644 --- a/esphome/components/zyaura/zyaura.h +++ b/esphome/components/zyaura/zyaura.h @@ -69,7 +69,6 @@ class ZyAuraSensor : public PollingComponent { void setup() override { this->store_.setup(this->pin_clock_, this->pin_data_); } void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: ZaSensorStore store_; From a4cc6166a0c8f014b3e821910f81a52e15070fae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 14:20:52 -0500 Subject: [PATCH 101/115] Bump aioesphomeapi from 33.1.1 to 34.0.0 (#9265) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3f306fe4fa..12f3b84359 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==33.1.1 +aioesphomeapi==34.0.0 zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.14 # dashboard_import From b743577ebe37063992506272d74431ad8a14e0ca Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 30 Jun 2025 08:07:29 +1200 Subject: [PATCH 102/115] Fix api log client crashing when api encryption is dynamic (#9245) --- esphome/components/api/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 7f8e2529a5..2d4bc37c89 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -35,8 +35,8 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: port: int = int(conf[CONF_PORT]) password: str = conf[CONF_PASSWORD] noise_psk: str | None = None - if CONF_ENCRYPTION in conf: - noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] + if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)): + noise_psk = key _LOGGER.info("Starting log output from %s using esphome API", address) cli = APIClient( address, From 971bbd088c9eaa0590d4ce5c51c840a246b99c3d Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Sun, 29 Jun 2025 21:34:59 +0100 Subject: [PATCH 103/115] Fix MQTT blocking main loop for multiple seconds at a time (#8325) Co-authored-by: patagona Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../components/mqtt/mqtt_backend_esp32.cpp | 118 +++++++++++++++++- esphome/components/mqtt/mqtt_backend_esp32.h | 110 +++++++++++++++- 2 files changed, 223 insertions(+), 5 deletions(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 64dc27d84b..62b153e676 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -6,6 +6,7 @@ #include #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace mqtt { @@ -100,9 +101,24 @@ bool MQTTBackendESP32::initialize_() { handler_.reset(mqtt_client); is_initalized_ = true; esp_mqtt_client_register_event(mqtt_client, MQTT_EVENT_ANY, mqtt_event_handler, this); +#if defined(USE_MQTT_IDF_ENQUEUE) + // Create the task only after MQTT client is initialized successfully + // Use larger stack size when TLS is enabled + size_t stack_size = this->ca_certificate_.has_value() ? TASK_STACK_SIZE_TLS : TASK_STACK_SIZE; + xTaskCreate(esphome_mqtt_task, "esphome_mqtt", stack_size, (void *) this, TASK_PRIORITY, &this->task_handle_); + if (this->task_handle_ == nullptr) { + ESP_LOGE(TAG, "Failed to create MQTT task"); + // Clean up MQTT client since we can't start the async task + handler_.reset(); + is_initalized_ = false; + return false; + } + // Set the task handle so the queue can notify it + this->mqtt_queue_.set_task_to_notify(this->task_handle_); +#endif return true; } else { - ESP_LOGE(TAG, "Failed to initialize IDF-MQTT"); + ESP_LOGE(TAG, "Failed to init client"); return false; } } @@ -115,6 +131,26 @@ void MQTTBackendESP32::loop() { mqtt_event_handler_(event); mqtt_events_.pop(); } + +#if defined(USE_MQTT_IDF_ENQUEUE) + // Periodically log dropped messages to avoid blocking during spikes. + // During high load, many messages can be dropped in quick succession. + // Logging each drop immediately would flood the logs and potentially + // cause more drops if MQTT logging is enabled (cascade effect). + // Instead, we accumulate the count and log a summary periodically. + // IMPORTANT: Don't move this to the scheduler - if drops are due to memory + // pressure, the scheduler's heap allocations would make things worse. + uint32_t now = App.get_loop_component_start_time(); + // Handle rollover: (now - last_time) works correctly with unsigned arithmetic + // even when now < last_time due to rollover + if ((now - this->last_dropped_log_time_) >= DROP_LOG_INTERVAL_MS) { + uint16_t dropped = this->mqtt_queue_.get_and_reset_dropped_count(); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %u messages (%us)", dropped, DROP_LOG_INTERVAL_MS / 1000); + } + this->last_dropped_log_time_ = now; + } +#endif } void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { @@ -188,6 +224,86 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b } } +#if defined(USE_MQTT_IDF_ENQUEUE) +void MQTTBackendESP32::esphome_mqtt_task(void *params) { + MQTTBackendESP32 *this_mqtt = (MQTTBackendESP32 *) params; + + while (true) { + // Wait for notification indefinitely + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + // Process all queued items + struct QueueElement *elem; + while ((elem = this_mqtt->mqtt_queue_.pop()) != nullptr) { + if (this_mqtt->is_connected_) { + switch (elem->type) { + case MQTT_QUEUE_TYPE_SUBSCRIBE: + esp_mqtt_client_subscribe(this_mqtt->handler_.get(), elem->topic, elem->qos); + break; + + case MQTT_QUEUE_TYPE_UNSUBSCRIBE: + esp_mqtt_client_unsubscribe(this_mqtt->handler_.get(), elem->topic); + break; + + case MQTT_QUEUE_TYPE_PUBLISH: + esp_mqtt_client_publish(this_mqtt->handler_.get(), elem->topic, elem->payload, elem->payload_len, elem->qos, + elem->retain); + break; + + default: + ESP_LOGE(TAG, "Invalid operation type from MQTT queue"); + break; + } + } + this_mqtt->mqtt_event_pool_.release(elem); + } + } + + // Clean up any remaining items in the queue + struct QueueElement *elem; + while ((elem = this_mqtt->mqtt_queue_.pop()) != nullptr) { + this_mqtt->mqtt_event_pool_.release(elem); + } + + // Note: EventPool destructor will clean up the pool itself + // Task will delete itself + vTaskDelete(nullptr); +} + +bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload, + size_t len) { + auto *elem = this->mqtt_event_pool_.allocate(); + + if (!elem) { + // Queue is full - increment counter but don't log immediately. + // Logging here can cause a cascade effect: if MQTT logging is enabled, + // each dropped message would generate a log message, which could itself + // be sent via MQTT, causing more drops and more logs in a feedback loop + // that eventually triggers a watchdog reset. Instead, we log periodically + // in loop() to prevent blocking the event loop during spikes. + this->mqtt_queue_.increment_dropped_count(); + return false; + } + + elem->type = type; + elem->qos = qos; + elem->retain = retain; + + // Use the helper to allocate and copy data + if (!elem->set_data(topic, payload, len)) { + // Allocation failed, return elem to pool + this->mqtt_event_pool_.release(elem); + // Increment counter without logging to avoid cascade effect during memory pressure + this->mqtt_queue_.increment_dropped_count(); + return false; + } + + // Push to queue - always succeeds since we allocated from the pool + this->mqtt_queue_.push(elem); + return true; +} +#endif // USE_MQTT_IDF_ENQUEUE + } // namespace mqtt } // namespace esphome #endif // USE_ESP32 diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index 9054702115..57286a24b2 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -6,9 +6,14 @@ #include #include +#include #include +#include +#include #include "esphome/components/network/ip_address.h" #include "esphome/core/helpers.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" namespace esphome { namespace mqtt { @@ -42,9 +47,79 @@ struct Event { error_handle(*event.error_handle) {} }; +enum MqttQueueTypeT : uint8_t { + MQTT_QUEUE_TYPE_NONE = 0, + MQTT_QUEUE_TYPE_SUBSCRIBE, + MQTT_QUEUE_TYPE_UNSUBSCRIBE, + MQTT_QUEUE_TYPE_PUBLISH, +}; + +struct QueueElement { + char *topic; + char *payload; + uint16_t payload_len; // MQTT max payload is 64KiB + uint8_t type : 2; + uint8_t qos : 2; // QoS only needs values 0-2 + uint8_t retain : 1; + uint8_t reserved : 3; // Reserved for future use + + QueueElement() : topic(nullptr), payload(nullptr), payload_len(0), qos(0), retain(0), reserved(0) {} + + // Helper to set topic/payload (uses RAMAllocator) + bool set_data(const char *topic_str, const char *payload_data, size_t len) { + // Check payload size limit (MQTT max is 64KiB) + if (len > std::numeric_limits::max()) { + return false; + } + + // Use RAMAllocator with default flags (tries external RAM first, falls back to internal) + RAMAllocator allocator; + + // Allocate and copy topic + size_t topic_len = strlen(topic_str) + 1; + topic = allocator.allocate(topic_len); + if (!topic) + return false; + memcpy(topic, topic_str, topic_len); + + if (payload_data && len) { + payload = allocator.allocate(len); + if (!payload) { + allocator.deallocate(topic, topic_len); + topic = nullptr; + return false; + } + memcpy(payload, payload_data, len); + payload_len = static_cast(len); + } else { + payload = nullptr; + payload_len = 0; + } + return true; + } + + // Helper to release (uses RAMAllocator) + void release() { + RAMAllocator allocator; + if (topic) { + allocator.deallocate(topic, strlen(topic) + 1); + topic = nullptr; + } + if (payload) { + allocator.deallocate(payload, payload_len); + payload = nullptr; + } + payload_len = 0; + } +}; + class MQTTBackendESP32 final : public MQTTBackend { public: static const size_t MQTT_BUFFER_SIZE = 4096; + static const size_t TASK_STACK_SIZE = 2048; + static const size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations + static const ssize_t TASK_PRIORITY = 5; + static const uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } void set_client_id(const char *client_id) final { this->client_id_ = client_id; } @@ -105,15 +180,23 @@ class MQTTBackendESP32 final : public MQTTBackend { } bool subscribe(const char *topic, uint8_t qos) final { +#if defined(USE_MQTT_IDF_ENQUEUE) + return enqueue_(MQTT_QUEUE_TYPE_SUBSCRIBE, topic, qos); +#else return esp_mqtt_client_subscribe(handler_.get(), topic, qos) != -1; +#endif + } + bool unsubscribe(const char *topic) final { +#if defined(USE_MQTT_IDF_ENQUEUE) + return enqueue_(MQTT_QUEUE_TYPE_UNSUBSCRIBE, topic); +#else + return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; +#endif } - bool unsubscribe(const char *topic) final { return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; } bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final { #if defined(USE_MQTT_IDF_ENQUEUE) - // use the non-blocking version - // it can delay sending a couple of seconds but won't block - return esp_mqtt_client_enqueue(handler_.get(), topic, payload, length, qos, retain, true) != -1; + return enqueue_(MQTT_QUEUE_TYPE_PUBLISH, topic, qos, retain, payload, length); #else // might block for several seconds, either due to network timeout (10s) // or if publishing payloads longer than internal buffer (due to message fragmentation) @@ -129,6 +212,12 @@ class MQTTBackendESP32 final : public MQTTBackend { void set_cl_key(const std::string &key) { cl_key_ = key; } void set_skip_cert_cn_check(bool skip_check) { skip_cert_cn_check_ = skip_check; } + // No destructor needed: ESPHome components live for the entire device runtime. + // The MQTT task and queue will run until the device reboots or loses power, + // at which point the entire process terminates and FreeRTOS cleans up all tasks. + // Implementing a destructor would add complexity and potential race conditions + // for a scenario that never occurs in practice. + protected: bool initialize_(); void mqtt_event_handler_(const Event &event); @@ -160,6 +249,14 @@ class MQTTBackendESP32 final : public MQTTBackend { optional cl_certificate_; optional cl_key_; bool skip_cert_cn_check_{false}; +#if defined(USE_MQTT_IDF_ENQUEUE) + static void esphome_mqtt_task(void *params); + EventPool mqtt_event_pool_; + LockFreeQueue mqtt_queue_; + TaskHandle_t task_handle_{nullptr}; + bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL, + size_t len = 0); +#endif // callbacks CallbackManager on_connect_; @@ -169,6 +266,11 @@ class MQTTBackendESP32 final : public MQTTBackend { CallbackManager on_message_; CallbackManager on_publish_; std::queue mqtt_events_; + +#if defined(USE_MQTT_IDF_ENQUEUE) + uint32_t last_dropped_log_time_{0}; + static constexpr uint32_t DROP_LOG_INTERVAL_MS = 10000; // Log every 10 seconds +#endif }; } // namespace mqtt From d592208c74d80e573471774f6401b8244effd5b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 17:45:41 -0500 Subject: [PATCH 104/115] Fix crash when event last_event_type is null in web_server (#9266) --- esphome/components/web_server/web_server.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 9f42253794..927659e621 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1683,12 +1683,15 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } +static std::string get_event_type(event::Event *event) { return event->last_event_type ? *event->last_event_type : ""; } + std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { - return web_server->event_json((event::Event *) (source), *(((event::Event *) (source))->last_event_type), - DETAIL_STATE); + auto *event = static_cast(source); + return web_server->event_json(event, get_event_type(event), DETAIL_STATE); } std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) { - return web_server->event_json((event::Event *) (source), *(((event::Event *) (source))->last_event_type), DETAIL_ALL); + auto *event = static_cast(source); + return web_server->event_json(event, get_event_type(event), DETAIL_ALL); } std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) { return json::build_json([this, obj, event_type, start_config](JsonObject root) { From d78b7203503012529680e624efcc7573b366ab41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:38:11 -0500 Subject: [PATCH 105/115] Remove single-use send_*_info wrappers in API connection (#9255) --- esphome/components/api/api_connection.cpp | 70 --------- esphome/components/api/api_connection.h | 23 +-- esphome/components/api/list_entities.cpp | 173 +++++++-------------- esphome/components/api/list_entities.h | 52 ++++--- esphome/components/api/subscribe_state.cpp | 52 +++---- esphome/components/api/subscribe_state.h | 45 +++--- 6 files changed, 132 insertions(+), 283 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index f339a4b26f..8550d45bfc 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -304,10 +304,6 @@ bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary return this->schedule_message_(binary_sensor, &APIConnection::try_send_binary_sensor_state, BinarySensorStateResponse::MESSAGE_TYPE); } -void APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) { - this->schedule_message_(binary_sensor, &APIConnection::try_send_binary_sensor_info, - ListEntitiesBinarySensorResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -335,9 +331,6 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne bool APIConnection::send_cover_state(cover::Cover *cover) { return this->schedule_message_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE); } -void APIConnection::send_cover_info(cover::Cover *cover) { - this->schedule_message_(cover, &APIConnection::try_send_cover_info, ListEntitiesCoverResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *cover = static_cast(entity); @@ -399,9 +392,6 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { bool APIConnection::send_fan_state(fan::Fan *fan) { return this->schedule_message_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE); } -void APIConnection::send_fan_info(fan::Fan *fan) { - this->schedule_message_(fan, &APIConnection::try_send_fan_info, ListEntitiesFanResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *fan = static_cast(entity); @@ -461,9 +451,6 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { bool APIConnection::send_light_state(light::LightState *light) { return this->schedule_message_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE); } -void APIConnection::send_light_info(light::LightState *light) { - this->schedule_message_(light, &APIConnection::try_send_light_info, ListEntitiesLightResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *light = static_cast(entity); @@ -556,9 +543,6 @@ void APIConnection::light_command(const LightCommandRequest &msg) { bool APIConnection::send_sensor_state(sensor::Sensor *sensor) { return this->schedule_message_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE); } -void APIConnection::send_sensor_info(sensor::Sensor *sensor) { - this->schedule_message_(sensor, &APIConnection::try_send_sensor_info, ListEntitiesSensorResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -591,9 +575,6 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * bool APIConnection::send_switch_state(switch_::Switch *a_switch) { return this->schedule_message_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE); } -void APIConnection::send_switch_info(switch_::Switch *a_switch) { - this->schedule_message_(a_switch, &APIConnection::try_send_switch_info, ListEntitiesSwitchResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -632,10 +613,6 @@ bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) return this->schedule_message_(text_sensor, &APIConnection::try_send_text_sensor_state, TextSensorStateResponse::MESSAGE_TYPE); } -void APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) { - this->schedule_message_(text_sensor, &APIConnection::try_send_text_sensor_info, - ListEntitiesTextSensorResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -696,9 +673,6 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection resp.target_humidity = climate->target_humidity; return encode_message_to_buffer(resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_climate_info(climate::Climate *climate) { - this->schedule_message_(climate, &APIConnection::try_send_climate_info, ListEntitiesClimateResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *climate = static_cast(entity); @@ -766,9 +740,6 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { bool APIConnection::send_number_state(number::Number *number) { return this->schedule_message_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE); } -void APIConnection::send_number_info(number::Number *number) { - this->schedule_message_(number, &APIConnection::try_send_number_info, ListEntitiesNumberResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -820,9 +791,6 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c fill_entity_state_base(date, resp); return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_date_info(datetime::DateEntity *date) { - this->schedule_message_(date, &APIConnection::try_send_date_info, ListEntitiesDateResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *date = static_cast(entity); @@ -857,9 +825,6 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c fill_entity_state_base(time, resp); return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_time_info(datetime::TimeEntity *time) { - this->schedule_message_(time, &APIConnection::try_send_time_info, ListEntitiesTimeResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *time = static_cast(entity); @@ -896,9 +861,6 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio fill_entity_state_base(datetime, resp); return encode_message_to_buffer(resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { - this->schedule_message_(datetime, &APIConnection::try_send_datetime_info, ListEntitiesDateTimeResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *datetime = static_cast(entity); @@ -922,9 +884,6 @@ void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { bool APIConnection::send_text_state(text::Text *text) { return this->schedule_message_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE); } -void APIConnection::send_text_info(text::Text *text) { - this->schedule_message_(text, &APIConnection::try_send_text_info, ListEntitiesTextResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -963,9 +922,6 @@ void APIConnection::text_command(const TextCommandRequest &msg) { bool APIConnection::send_select_state(select::Select *select) { return this->schedule_message_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE); } -void APIConnection::send_select_info(select::Select *select) { - this->schedule_message_(select, &APIConnection::try_send_select_info, ListEntitiesSelectResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -999,9 +955,6 @@ void APIConnection::select_command(const SelectCommandRequest &msg) { #endif #ifdef USE_BUTTON -void esphome::api::APIConnection::send_button_info(button::Button *button) { - this->schedule_message_(button, &APIConnection::try_send_button_info, ListEntitiesButtonResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *button = static_cast(entity); @@ -1024,9 +977,6 @@ void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg bool APIConnection::send_lock_state(lock::Lock *a_lock) { return this->schedule_message_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE); } -void APIConnection::send_lock_info(lock::Lock *a_lock) { - this->schedule_message_(a_lock, &APIConnection::try_send_lock_info, ListEntitiesLockResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1080,9 +1030,6 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection * fill_entity_state_base(valve, resp); return encode_message_to_buffer(resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_valve_info(valve::Valve *valve) { - this->schedule_message_(valve, &APIConnection::try_send_valve_info, ListEntitiesValveResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *valve = static_cast(entity); @@ -1128,10 +1075,6 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne fill_entity_state_base(media_player, resp); return encode_message_to_buffer(resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { - this->schedule_message_(media_player, &APIConnection::try_send_media_player_info, - ListEntitiesMediaPlayerResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *media_player = static_cast(entity); @@ -1183,9 +1126,6 @@ void APIConnection::set_camera_state(std::shared_ptr image->was_requested_by(esphome::esp32_camera::IDLE)) this->image_reader_.set_image(std::move(image)); } -void APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { - this->schedule_message_(camera, &APIConnection::try_send_camera_info, ListEntitiesCameraResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *camera = static_cast(entity); @@ -1392,10 +1332,6 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A fill_entity_state_base(a_alarm_control_panel, resp); return encode_message_to_buffer(resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - this->schedule_message_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_info, - ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *a_alarm_control_panel = static_cast(entity); @@ -1446,9 +1382,6 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe void APIConnection::send_event(event::Event *event, const std::string &event_type) { this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE); } -void APIConnection::send_event_info(event::Event *event) { - this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { EventResponse resp; @@ -1494,9 +1427,6 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection fill_entity_state_base(update, resp); return encode_message_to_buffer(resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_update_info(update::UpdateEntity *update) { - this->schedule_message_(update, &APIConnection::try_send_update_info, ListEntitiesUpdateResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *update = static_cast(entity); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 4397462d8e..c9f24a7759 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -22,6 +22,7 @@ static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; class APIConnection : public APIServerConnection { public: friend class APIServer; + friend class ListEntitiesIterator; APIConnection(std::unique_ptr socket, APIServer *parent); virtual ~APIConnection(); @@ -34,93 +35,74 @@ class APIConnection : public APIServerConnection { } #ifdef USE_BINARY_SENSOR bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor); - void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor); #endif #ifdef USE_COVER bool send_cover_state(cover::Cover *cover); - void send_cover_info(cover::Cover *cover); void cover_command(const CoverCommandRequest &msg) override; #endif #ifdef USE_FAN bool send_fan_state(fan::Fan *fan); - void send_fan_info(fan::Fan *fan); void fan_command(const FanCommandRequest &msg) override; #endif #ifdef USE_LIGHT bool send_light_state(light::LightState *light); - void send_light_info(light::LightState *light); void light_command(const LightCommandRequest &msg) override; #endif #ifdef USE_SENSOR bool send_sensor_state(sensor::Sensor *sensor); - void send_sensor_info(sensor::Sensor *sensor); #endif #ifdef USE_SWITCH bool send_switch_state(switch_::Switch *a_switch); - void send_switch_info(switch_::Switch *a_switch); void switch_command(const SwitchCommandRequest &msg) override; #endif #ifdef USE_TEXT_SENSOR bool send_text_sensor_state(text_sensor::TextSensor *text_sensor); - void send_text_sensor_info(text_sensor::TextSensor *text_sensor); #endif #ifdef USE_ESP32_CAMERA void set_camera_state(std::shared_ptr image); - void send_camera_info(esp32_camera::ESP32Camera *camera); void camera_image(const CameraImageRequest &msg) override; #endif #ifdef USE_CLIMATE bool send_climate_state(climate::Climate *climate); - void send_climate_info(climate::Climate *climate); void climate_command(const ClimateCommandRequest &msg) override; #endif #ifdef USE_NUMBER bool send_number_state(number::Number *number); - void send_number_info(number::Number *number); void number_command(const NumberCommandRequest &msg) override; #endif #ifdef USE_DATETIME_DATE bool send_date_state(datetime::DateEntity *date); - void send_date_info(datetime::DateEntity *date); void date_command(const DateCommandRequest &msg) override; #endif #ifdef USE_DATETIME_TIME bool send_time_state(datetime::TimeEntity *time); - void send_time_info(datetime::TimeEntity *time); void time_command(const TimeCommandRequest &msg) override; #endif #ifdef USE_DATETIME_DATETIME bool send_datetime_state(datetime::DateTimeEntity *datetime); - void send_datetime_info(datetime::DateTimeEntity *datetime); void datetime_command(const DateTimeCommandRequest &msg) override; #endif #ifdef USE_TEXT bool send_text_state(text::Text *text); - void send_text_info(text::Text *text); void text_command(const TextCommandRequest &msg) override; #endif #ifdef USE_SELECT bool send_select_state(select::Select *select); - void send_select_info(select::Select *select); void select_command(const SelectCommandRequest &msg) override; #endif #ifdef USE_BUTTON - void send_button_info(button::Button *button); void button_command(const ButtonCommandRequest &msg) override; #endif #ifdef USE_LOCK bool send_lock_state(lock::Lock *a_lock); - void send_lock_info(lock::Lock *a_lock); void lock_command(const LockCommandRequest &msg) override; #endif #ifdef USE_VALVE bool send_valve_state(valve::Valve *valve); - void send_valve_info(valve::Valve *valve); void valve_command(const ValveCommandRequest &msg) override; #endif #ifdef USE_MEDIA_PLAYER bool send_media_player_state(media_player::MediaPlayer *media_player); - void send_media_player_info(media_player::MediaPlayer *media_player); void media_player_command(const MediaPlayerCommandRequest &msg) override; #endif bool try_send_log_message(int level, const char *tag, const char *line); @@ -167,18 +149,15 @@ class APIConnection : public APIServerConnection { #ifdef USE_ALARM_CONTROL_PANEL bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); - void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; #endif #ifdef USE_EVENT void send_event(event::Event *event, const std::string &event_type); - void send_event_info(event::Event *event); #endif #ifdef USE_UPDATE bool send_update_state(update::UpdateEntity *update); - void send_update_info(update::UpdateEntity *update); void update_command(const UpdateCommandRequest &msg) override; #endif diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index ceee3f00b8..3f84ef306e 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -1,6 +1,7 @@ #include "list_entities.h" #ifdef USE_API #include "api_connection.h" +#include "api_pb2.h" #include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -8,155 +9,85 @@ namespace esphome { namespace api { +// Generate entity handler implementations using macros #ifdef USE_BINARY_SENSOR -bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { - this->client_->send_binary_sensor_info(binary_sensor); - return true; -} +LIST_ENTITIES_HANDLER(binary_sensor, binary_sensor::BinarySensor, ListEntitiesBinarySensorResponse) #endif #ifdef USE_COVER -bool ListEntitiesIterator::on_cover(cover::Cover *cover) { - this->client_->send_cover_info(cover); - return true; -} +LIST_ENTITIES_HANDLER(cover, cover::Cover, ListEntitiesCoverResponse) #endif #ifdef USE_FAN -bool ListEntitiesIterator::on_fan(fan::Fan *fan) { - this->client_->send_fan_info(fan); - return true; -} +LIST_ENTITIES_HANDLER(fan, fan::Fan, ListEntitiesFanResponse) #endif #ifdef USE_LIGHT -bool ListEntitiesIterator::on_light(light::LightState *light) { - this->client_->send_light_info(light); - return true; -} +LIST_ENTITIES_HANDLER(light, light::LightState, ListEntitiesLightResponse) #endif #ifdef USE_SENSOR -bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { - this->client_->send_sensor_info(sensor); - return true; -} +LIST_ENTITIES_HANDLER(sensor, sensor::Sensor, ListEntitiesSensorResponse) #endif #ifdef USE_SWITCH -bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { - this->client_->send_switch_info(a_switch); - return true; -} +LIST_ENTITIES_HANDLER(switch, switch_::Switch, ListEntitiesSwitchResponse) #endif #ifdef USE_BUTTON -bool ListEntitiesIterator::on_button(button::Button *button) { - this->client_->send_button_info(button); - return true; -} +LIST_ENTITIES_HANDLER(button, button::Button, ListEntitiesButtonResponse) #endif #ifdef USE_TEXT_SENSOR -bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { - this->client_->send_text_sensor_info(text_sensor); - return true; -} +LIST_ENTITIES_HANDLER(text_sensor, text_sensor::TextSensor, ListEntitiesTextSensorResponse) #endif #ifdef USE_LOCK -bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { - this->client_->send_lock_info(a_lock); - return true; -} +LIST_ENTITIES_HANDLER(lock, lock::Lock, ListEntitiesLockResponse) #endif #ifdef USE_VALVE -bool ListEntitiesIterator::on_valve(valve::Valve *valve) { - this->client_->send_valve_info(valve); - return true; -} +LIST_ENTITIES_HANDLER(valve, valve::Valve, ListEntitiesValveResponse) +#endif +#ifdef USE_ESP32_CAMERA +LIST_ENTITIES_HANDLER(camera, esp32_camera::ESP32Camera, ListEntitiesCameraResponse) +#endif +#ifdef USE_CLIMATE +LIST_ENTITIES_HANDLER(climate, climate::Climate, ListEntitiesClimateResponse) +#endif +#ifdef USE_NUMBER +LIST_ENTITIES_HANDLER(number, number::Number, ListEntitiesNumberResponse) +#endif +#ifdef USE_DATETIME_DATE +LIST_ENTITIES_HANDLER(date, datetime::DateEntity, ListEntitiesDateResponse) +#endif +#ifdef USE_DATETIME_TIME +LIST_ENTITIES_HANDLER(time, datetime::TimeEntity, ListEntitiesTimeResponse) +#endif +#ifdef USE_DATETIME_DATETIME +LIST_ENTITIES_HANDLER(datetime, datetime::DateTimeEntity, ListEntitiesDateTimeResponse) +#endif +#ifdef USE_TEXT +LIST_ENTITIES_HANDLER(text, text::Text, ListEntitiesTextResponse) +#endif +#ifdef USE_SELECT +LIST_ENTITIES_HANDLER(select, select::Select, ListEntitiesSelectResponse) +#endif +#ifdef USE_MEDIA_PLAYER +LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMediaPlayerResponse) +#endif +#ifdef USE_ALARM_CONTROL_PANEL +LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel, + ListEntitiesAlarmControlPanelResponse) +#endif +#ifdef USE_EVENT +LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse) +#endif +#ifdef USE_UPDATE +LIST_ENTITIES_HANDLER(update, update::UpdateEntity, ListEntitiesUpdateResponse) #endif +// Special cases that don't follow the pattern bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } + ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} + bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); return this->client_->send_message(resp); } -#ifdef USE_ESP32_CAMERA -bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) { - this->client_->send_camera_info(camera); - return true; -} -#endif - -#ifdef USE_CLIMATE -bool ListEntitiesIterator::on_climate(climate::Climate *climate) { - this->client_->send_climate_info(climate); - return true; -} -#endif - -#ifdef USE_NUMBER -bool ListEntitiesIterator::on_number(number::Number *number) { - this->client_->send_number_info(number); - return true; -} -#endif - -#ifdef USE_DATETIME_DATE -bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { - this->client_->send_date_info(date); - return true; -} -#endif - -#ifdef USE_DATETIME_TIME -bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { - this->client_->send_time_info(time); - return true; -} -#endif - -#ifdef USE_DATETIME_DATETIME -bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) { - this->client_->send_datetime_info(datetime); - return true; -} -#endif - -#ifdef USE_TEXT -bool ListEntitiesIterator::on_text(text::Text *text) { - this->client_->send_text_info(text); - return true; -} -#endif - -#ifdef USE_SELECT -bool ListEntitiesIterator::on_select(select::Select *select) { - this->client_->send_select_info(select); - return true; -} -#endif - -#ifdef USE_MEDIA_PLAYER -bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) { - this->client_->send_media_player_info(media_player); - return true; -} -#endif -#ifdef USE_ALARM_CONTROL_PANEL -bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - this->client_->send_alarm_control_panel_info(a_alarm_control_panel); - return true; -} -#endif -#ifdef USE_EVENT -bool ListEntitiesIterator::on_event(event::Event *event) { - this->client_->send_event_info(event); - return true; -} -#endif -#ifdef USE_UPDATE -bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { - this->client_->send_update_info(update); - return true; -} -#endif - } // namespace api } // namespace esphome #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index e77f21c7a1..b9506073d2 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -9,75 +9,83 @@ namespace api { class APIConnection; +// Macro for generating ListEntitiesIterator handlers +// Calls schedule_message_ with try_send_*_info +#define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \ + bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \ + return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \ + ResponseType::MESSAGE_TYPE); \ + } + class ListEntitiesIterator : public ComponentIterator { public: ListEntitiesIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR - bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; + bool on_binary_sensor(binary_sensor::BinarySensor *entity) override; #endif #ifdef USE_COVER - bool on_cover(cover::Cover *cover) override; + bool on_cover(cover::Cover *entity) override; #endif #ifdef USE_FAN - bool on_fan(fan::Fan *fan) override; + bool on_fan(fan::Fan *entity) override; #endif #ifdef USE_LIGHT - bool on_light(light::LightState *light) override; + bool on_light(light::LightState *entity) override; #endif #ifdef USE_SENSOR - bool on_sensor(sensor::Sensor *sensor) override; + bool on_sensor(sensor::Sensor *entity) override; #endif #ifdef USE_SWITCH - bool on_switch(switch_::Switch *a_switch) override; + bool on_switch(switch_::Switch *entity) override; #endif #ifdef USE_BUTTON - bool on_button(button::Button *button) override; + bool on_button(button::Button *entity) override; #endif #ifdef USE_TEXT_SENSOR - bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; + bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif bool on_service(UserServiceDescriptor *service) override; #ifdef USE_ESP32_CAMERA - bool on_camera(esp32_camera::ESP32Camera *camera) override; + bool on_camera(esp32_camera::ESP32Camera *entity) override; #endif #ifdef USE_CLIMATE - bool on_climate(climate::Climate *climate) override; + bool on_climate(climate::Climate *entity) override; #endif #ifdef USE_NUMBER - bool on_number(number::Number *number) override; + bool on_number(number::Number *entity) override; #endif #ifdef USE_DATETIME_DATE - bool on_date(datetime::DateEntity *date) override; + bool on_date(datetime::DateEntity *entity) override; #endif #ifdef USE_DATETIME_TIME - bool on_time(datetime::TimeEntity *time) override; + bool on_time(datetime::TimeEntity *entity) override; #endif #ifdef USE_DATETIME_DATETIME - bool on_datetime(datetime::DateTimeEntity *datetime) override; + bool on_datetime(datetime::DateTimeEntity *entity) override; #endif #ifdef USE_TEXT - bool on_text(text::Text *text) override; + bool on_text(text::Text *entity) override; #endif #ifdef USE_SELECT - bool on_select(select::Select *select) override; + bool on_select(select::Select *entity) override; #endif #ifdef USE_LOCK - bool on_lock(lock::Lock *a_lock) override; + bool on_lock(lock::Lock *entity) override; #endif #ifdef USE_VALVE - bool on_valve(valve::Valve *valve) override; + bool on_valve(valve::Valve *entity) override; #endif #ifdef USE_MEDIA_PLAYER - bool on_media_player(media_player::MediaPlayer *media_player) override; + bool on_media_player(media_player::MediaPlayer *entity) override; #endif #ifdef USE_ALARM_CONTROL_PANEL - bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; #endif #ifdef USE_EVENT - bool on_event(event::Event *event) override; + bool on_event(event::Event *entity) override; #endif #ifdef USE_UPDATE - bool on_update(update::UpdateEntity *update) override; + bool on_update(update::UpdateEntity *entity) override; #endif bool on_end() override; bool completed() { return this->state_ == IteratorState::NONE; } diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 4180435fcc..12accf4613 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -6,73 +6,67 @@ namespace esphome { namespace api { +// Generate entity handler implementations using macros #ifdef USE_BINARY_SENSOR -bool InitialStateIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { - return this->client_->send_binary_sensor_state(binary_sensor); -} +INITIAL_STATE_HANDLER(binary_sensor, binary_sensor::BinarySensor) #endif #ifdef USE_COVER -bool InitialStateIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_state(cover); } +INITIAL_STATE_HANDLER(cover, cover::Cover) #endif #ifdef USE_FAN -bool InitialStateIterator::on_fan(fan::Fan *fan) { return this->client_->send_fan_state(fan); } +INITIAL_STATE_HANDLER(fan, fan::Fan) #endif #ifdef USE_LIGHT -bool InitialStateIterator::on_light(light::LightState *light) { return this->client_->send_light_state(light); } +INITIAL_STATE_HANDLER(light, light::LightState) #endif #ifdef USE_SENSOR -bool InitialStateIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_state(sensor); } +INITIAL_STATE_HANDLER(sensor, sensor::Sensor) #endif #ifdef USE_SWITCH -bool InitialStateIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_state(a_switch); } +INITIAL_STATE_HANDLER(switch, switch_::Switch) #endif #ifdef USE_TEXT_SENSOR -bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { - return this->client_->send_text_sensor_state(text_sensor); -} +INITIAL_STATE_HANDLER(text_sensor, text_sensor::TextSensor) #endif #ifdef USE_CLIMATE -bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); } +INITIAL_STATE_HANDLER(climate, climate::Climate) #endif #ifdef USE_NUMBER -bool InitialStateIterator::on_number(number::Number *number) { return this->client_->send_number_state(number); } +INITIAL_STATE_HANDLER(number, number::Number) #endif #ifdef USE_DATETIME_DATE -bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_state(date); } +INITIAL_STATE_HANDLER(date, datetime::DateEntity) #endif #ifdef USE_DATETIME_TIME -bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); } +INITIAL_STATE_HANDLER(time, datetime::TimeEntity) #endif #ifdef USE_DATETIME_DATETIME -bool InitialStateIterator::on_datetime(datetime::DateTimeEntity *datetime) { - return this->client_->send_datetime_state(datetime); -} +INITIAL_STATE_HANDLER(datetime, datetime::DateTimeEntity) #endif #ifdef USE_TEXT -bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text); } +INITIAL_STATE_HANDLER(text, text::Text) #endif #ifdef USE_SELECT -bool InitialStateIterator::on_select(select::Select *select) { return this->client_->send_select_state(select); } +INITIAL_STATE_HANDLER(select, select::Select) #endif #ifdef USE_LOCK -bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock); } +INITIAL_STATE_HANDLER(lock, lock::Lock) #endif #ifdef USE_VALVE -bool InitialStateIterator::on_valve(valve::Valve *valve) { return this->client_->send_valve_state(valve); } +INITIAL_STATE_HANDLER(valve, valve::Valve) #endif #ifdef USE_MEDIA_PLAYER -bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) { - return this->client_->send_media_player_state(media_player); -} +INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer) #endif #ifdef USE_ALARM_CONTROL_PANEL -bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - return this->client_->send_alarm_control_panel_state(a_alarm_control_panel); -} +INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel) #endif #ifdef USE_UPDATE -bool InitialStateIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_state(update); } +INITIAL_STATE_HANDLER(update, update::UpdateEntity) #endif + +// Special cases (button and event) are already defined inline in subscribe_state.h + InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 3966c97af5..2b7b508056 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -10,71 +10,78 @@ namespace api { class APIConnection; +// Macro for generating InitialStateIterator handlers +// Calls send_*_state +#define INITIAL_STATE_HANDLER(entity_type, EntityClass) \ + bool InitialStateIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \ + return this->client_->send_##entity_type##_state(entity); \ + } + class InitialStateIterator : public ComponentIterator { public: InitialStateIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR - bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; + bool on_binary_sensor(binary_sensor::BinarySensor *entity) override; #endif #ifdef USE_COVER - bool on_cover(cover::Cover *cover) override; + bool on_cover(cover::Cover *entity) override; #endif #ifdef USE_FAN - bool on_fan(fan::Fan *fan) override; + bool on_fan(fan::Fan *entity) override; #endif #ifdef USE_LIGHT - bool on_light(light::LightState *light) override; + bool on_light(light::LightState *entity) override; #endif #ifdef USE_SENSOR - bool on_sensor(sensor::Sensor *sensor) override; + bool on_sensor(sensor::Sensor *entity) override; #endif #ifdef USE_SWITCH - bool on_switch(switch_::Switch *a_switch) override; + bool on_switch(switch_::Switch *entity) override; #endif #ifdef USE_BUTTON bool on_button(button::Button *button) override { return true; }; #endif #ifdef USE_TEXT_SENSOR - bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; + bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif #ifdef USE_CLIMATE - bool on_climate(climate::Climate *climate) override; + bool on_climate(climate::Climate *entity) override; #endif #ifdef USE_NUMBER - bool on_number(number::Number *number) override; + bool on_number(number::Number *entity) override; #endif #ifdef USE_DATETIME_DATE - bool on_date(datetime::DateEntity *date) override; + bool on_date(datetime::DateEntity *entity) override; #endif #ifdef USE_DATETIME_TIME - bool on_time(datetime::TimeEntity *time) override; + bool on_time(datetime::TimeEntity *entity) override; #endif #ifdef USE_DATETIME_DATETIME - bool on_datetime(datetime::DateTimeEntity *datetime) override; + bool on_datetime(datetime::DateTimeEntity *entity) override; #endif #ifdef USE_TEXT - bool on_text(text::Text *text) override; + bool on_text(text::Text *entity) override; #endif #ifdef USE_SELECT - bool on_select(select::Select *select) override; + bool on_select(select::Select *entity) override; #endif #ifdef USE_LOCK - bool on_lock(lock::Lock *a_lock) override; + bool on_lock(lock::Lock *entity) override; #endif #ifdef USE_VALVE - bool on_valve(valve::Valve *valve) override; + bool on_valve(valve::Valve *entity) override; #endif #ifdef USE_MEDIA_PLAYER - bool on_media_player(media_player::MediaPlayer *media_player) override; + bool on_media_player(media_player::MediaPlayer *entity) override; #endif #ifdef USE_ALARM_CONTROL_PANEL - bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; #endif #ifdef USE_EVENT bool on_event(event::Event *event) override { return true; }; #endif #ifdef USE_UPDATE - bool on_update(update::UpdateEntity *update) override; + bool on_update(update::UpdateEntity *entity) override; #endif bool completed() { return this->state_ == IteratorState::NONE; } From 24bbfcdce7004b4241393b54034182a8eb6b7ab5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:42:57 -0500 Subject: [PATCH 106/115] Reduce API memory footprint through bitfield consolidation and type sizing (#9252) --- esphome/components/api/__init__.py | 7 +- esphome/components/api/api_connection.cpp | 51 ++++++----- esphome/components/api/api_connection.h | 104 ++++++++++++---------- esphome/components/api/api_server.cpp | 10 +-- esphome/components/api/api_server.h | 8 +- 5 files changed, 96 insertions(+), 84 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index bd131ef8de..501b707678 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -110,9 +110,10 @@ CONFIG_SCHEMA = cv.All( ): ACTIONS_SCHEMA, cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, cv.Optional(CONF_ENCRYPTION): _encryption_schema, - cv.Optional( - CONF_BATCH_DELAY, default="100ms" - ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(milliseconds=65535)), + ), cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( single=True ), diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8550d45bfc..e5847e50f7 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -93,21 +93,21 @@ APIConnection::~APIConnection() { #ifdef HAS_PROTO_MESSAGE_DUMP void APIConnection::log_batch_item_(const DeferredBatch::BatchItem &item) { // Set log-only mode - this->log_only_mode_ = true; + this->flags_.log_only_mode = true; // Call the creator - it will create the message and log it via encode_message_to_buffer item.creator(item.entity, this, std::numeric_limits::max(), true, item.message_type); // Clear log-only mode - this->log_only_mode_ = false; + this->flags_.log_only_mode = false; } #endif void APIConnection::loop() { - if (this->next_close_) { + if (this->flags_.next_close) { // requested a disconnect this->helper_->close(); - this->remove_ = true; + this->flags_.remove = true; return; } @@ -148,15 +148,14 @@ void APIConnection::loop() { } else { this->read_message(0, buffer.type, nullptr); } - if (this->remove_) + if (this->flags_.remove) return; } } } // Process deferred batch if scheduled - if (this->deferred_batch_.batch_scheduled && - now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { + if (this->flags_.batch_scheduled && now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { this->process_batch_(); } @@ -166,7 +165,7 @@ void APIConnection::loop() { this->initial_state_iterator_.advance(); } - if (this->sent_ping_) { + if (this->flags_.sent_ping) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { on_fatal_error(); @@ -174,13 +173,13 @@ void APIConnection::loop() { } } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { ESP_LOGVV(TAG, "Sending keepalive PING"); - this->sent_ping_ = this->send_message(PingRequest()); - if (!this->sent_ping_) { + this->flags_.sent_ping = this->send_message(PingRequest()); + if (!this->flags_.sent_ping) { // If we can't send the ping request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority ESP_LOGW(TAG, "Buffer full, ping queued"); this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE); - this->sent_ping_ = true; // Mark as sent to avoid scheduling multiple pings + this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings } } @@ -240,13 +239,13 @@ DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { // don't close yet, we still need to send the disconnect response // close will happen on next loop ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str()); - this->next_close_ = true; + this->flags_.next_close = true; DisconnectResponse resp; return resp; } void APIConnection::on_disconnect_response(const DisconnectResponse &value) { this->helper_->close(); - this->remove_ = true; + this->flags_.remove = true; } // Encodes a message to the buffer and returns the total number of bytes used, @@ -255,7 +254,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes uint32_t remaining_size, bool is_single) { #ifdef HAS_PROTO_MESSAGE_DUMP // If in log-only mode, just log and return - if (conn->log_only_mode_) { + if (conn->flags_.log_only_mode) { conn->log_send_message_(msg.message_name(), msg.dump()); return 1; // Return non-zero to indicate "success" for logging } @@ -1118,7 +1117,7 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { #ifdef USE_ESP32_CAMERA void APIConnection::set_camera_state(std::shared_ptr image) { - if (!this->state_subscription_) + if (!this->flags_.state_subscription) return; if (this->image_reader_.available()) return; @@ -1459,7 +1458,7 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { #endif bool APIConnection::try_send_log_message(int level, const char *tag, const char *line) { - if (this->log_subscription_ < level) + if (this->flags_.log_subscription < level) return false; // Pre-calculate message size to avoid reallocations @@ -1500,7 +1499,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.name = App.get_name(); - this->connection_state_ = ConnectionState::CONNECTED; + this->flags_.connection_state = static_cast(ConnectionState::CONNECTED); return resp; } ConnectResponse APIConnection::connect(const ConnectRequest &msg) { @@ -1511,7 +1510,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { resp.invalid_password = !correct; if (correct) { ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); - this->connection_state_ = ConnectionState::AUTHENTICATED; + this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { @@ -1625,7 +1624,7 @@ void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistant state_subs_at_ = 0; } bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { - if (this->remove_) + if (this->flags_.remove) return false; if (this->helper_->can_write_without_blocking()) return true; @@ -1675,7 +1674,7 @@ void APIConnection::on_no_setup_connection() { } void APIConnection::on_fatal_error() { this->helper_->close(); - this->remove_ = true; + this->flags_.remove = true; } void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type) { @@ -1700,8 +1699,8 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCre } bool APIConnection::schedule_batch_() { - if (!this->deferred_batch_.batch_scheduled) { - this->deferred_batch_.batch_scheduled = true; + if (!this->flags_.batch_scheduled) { + this->flags_.batch_scheduled = true; this->deferred_batch_.batch_start_time = App.get_loop_component_start_time(); } return true; @@ -1710,14 +1709,14 @@ bool APIConnection::schedule_batch_() { ProtoWriteBuffer APIConnection::allocate_single_message_buffer(uint16_t size) { return this->create_buffer(size); } ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) { - ProtoWriteBuffer result = this->prepare_message_buffer(size, this->batch_first_message_); - this->batch_first_message_ = false; + ProtoWriteBuffer result = this->prepare_message_buffer(size, this->flags_.batch_first_message); + this->flags_.batch_first_message = false; return result; } void APIConnection::process_batch_() { if (this->deferred_batch_.empty()) { - this->deferred_batch_.batch_scheduled = false; + this->flags_.batch_scheduled = false; return; } @@ -1770,7 +1769,7 @@ void APIConnection::process_batch_() { // Reserve based on estimated size (much more accurate than 24-byte worst-case) this->parent_->get_shared_buffer_ref().reserve(total_estimated_size + total_overhead); - this->batch_first_message_ = true; + this->flags_.batch_first_message = true; size_t items_processed = 0; uint16_t remaining_size = std::numeric_limits::max(); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index c9f24a7759..410a9ad3a5 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -107,7 +107,7 @@ class APIConnection : public APIServerConnection { #endif bool try_send_log_message(int level, const char *tag, const char *line); void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { - if (!this->service_call_subscription_) + if (!this->flags_.service_call_subscription) return; this->send_message(call); } @@ -164,7 +164,7 @@ class APIConnection : public APIServerConnection { void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping - this->sent_ping_ = false; + this->flags_.sent_ping = false; } void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; #ifdef USE_HOMEASSISTANT_TIME @@ -177,16 +177,16 @@ class APIConnection : public APIServerConnection { DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } void subscribe_states(const SubscribeStatesRequest &msg) override { - this->state_subscription_ = true; + this->flags_.state_subscription = true; this->initial_state_iterator_.begin(); } void subscribe_logs(const SubscribeLogsRequest &msg) override { - this->log_subscription_ = msg.level; + this->flags_.log_subscription = msg.level; if (msg.dump_config) App.schedule_dump_config(); } void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override { - this->service_call_subscription_ = true; + this->flags_.service_call_subscription = true; } void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; GetTimeResponse get_time(const GetTimeRequest &msg) override { @@ -198,9 +198,12 @@ class APIConnection : public APIServerConnection { NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; #endif - bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; } + bool is_authenticated() override { + return static_cast(this->flags_.connection_state) == ConnectionState::AUTHENTICATED; + } bool is_connection_setup() override { - return this->connection_state_ == ConnectionState ::CONNECTED || this->is_authenticated(); + return static_cast(this->flags_.connection_state) == ConnectionState::CONNECTED || + this->is_authenticated(); } void on_fatal_error() override; void on_unauthenticated_access() override; @@ -423,49 +426,28 @@ class APIConnection : public APIServerConnection { static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - // Pointers first (4 bytes each, naturally aligned) + // === Optimal member ordering for 32-bit systems === + + // Group 1: Pointers (4 bytes each on 32-bit) std::unique_ptr helper_; APIServer *parent_; - // 4-byte aligned types - uint32_t last_traffic_; - int state_subs_at_ = -1; - - // Strings (12 bytes each on 32-bit) - std::string client_info_; - std::string client_peername_; - - // 2-byte aligned types - uint16_t client_api_version_major_{0}; - uint16_t client_api_version_minor_{0}; - - // Group all 1-byte types together to minimize padding - enum class ConnectionState : uint8_t { - WAITING_FOR_HELLO, - CONNECTED, - AUTHENTICATED, - } connection_state_{ConnectionState::WAITING_FOR_HELLO}; - uint8_t log_subscription_{ESPHOME_LOG_LEVEL_NONE}; - bool remove_{false}; - bool state_subscription_{false}; - bool sent_ping_{false}; - bool service_call_subscription_{false}; - bool next_close_ = false; - // 7 bytes used, 1 byte padding -#ifdef HAS_PROTO_MESSAGE_DUMP - // When true, encode_message_to_buffer will only log, not encode - bool log_only_mode_{false}; -#endif - uint8_t ping_retries_{0}; - // 8 bytes used, no padding needed - - // Larger objects at the end + // Group 2: Larger objects (must be 4-byte aligned) + // These contain vectors/pointers internally, so putting them early ensures good alignment InitialStateIterator initial_state_iterator_; ListEntitiesIterator list_entities_iterator_; #ifdef USE_ESP32_CAMERA esp32_camera::CameraImageReader image_reader_; #endif + // Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned) + std::string client_info_; + std::string client_peername_; + + // Group 4: 4-byte types + uint32_t last_traffic_; + int state_subs_at_ = -1; + // Function pointer type for message encoding using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); @@ -575,7 +557,6 @@ class APIConnection : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - bool batch_scheduled{false}; DeferredBatch() { // Pre-allocate capacity for typical batch sizes to avoid reallocation @@ -588,13 +569,47 @@ class APIConnection : public APIServerConnection { void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type); void clear() { items.clear(); - batch_scheduled = false; batch_start_time = 0; } bool empty() const { return items.empty(); } }; + // DeferredBatch here (16 bytes, 4-byte aligned) DeferredBatch deferred_batch_; + + // ConnectionState enum for type safety + enum class ConnectionState : uint8_t { + WAITING_FOR_HELLO = 0, + CONNECTED = 1, + AUTHENTICATED = 2, + }; + + // Group 5: Pack all small members together to minimize padding + // This group starts at a 4-byte boundary after DeferredBatch + struct APIFlags { + // Connection state only needs 2 bits (3 states) + uint8_t connection_state : 2; + // Log subscription needs 3 bits (log levels 0-7) + uint8_t log_subscription : 3; + // Boolean flags (1 bit each) + uint8_t remove : 1; + uint8_t state_subscription : 1; + uint8_t sent_ping : 1; + + uint8_t service_call_subscription : 1; + uint8_t next_close : 1; + uint8_t batch_scheduled : 1; + uint8_t batch_first_message : 1; // For batch buffer allocation +#ifdef HAS_PROTO_MESSAGE_DUMP + uint8_t log_only_mode : 1; +#endif + } flags_{}; // 2 bytes total + + // 2-byte types immediately after flags_ (no padding between them) + uint16_t client_api_version_major_{0}; + uint16_t client_api_version_minor_{0}; + // Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary + uint32_t get_batch_delay_ms_() const; // Message will use 8 more bytes than the minimum size, and typical // MTU is 1500. Sometimes users will see as low as 1460 MTU. @@ -612,9 +627,6 @@ class APIConnection : public APIServerConnection { bool schedule_batch_(); void process_batch_(); - // State for batch buffer allocation - bool batch_first_message_{false}; - #ifdef HAS_PROTO_MESSAGE_DUMP void log_batch_item_(const DeferredBatch::BatchItem &item); #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a33623b15a..b17faf7607 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -104,7 +104,7 @@ void APIServer::setup() { return; } for (auto &c : this->clients_) { - if (!c->remove_) + if (!c->flags_.remove) c->try_send_log_message(level, tag, message); } }); @@ -116,7 +116,7 @@ void APIServer::setup() { esp32_camera::global_esp32_camera->add_image_callback( [this](const std::shared_ptr &image) { for (auto &c : this->clients_) { - if (!c->remove_) + if (!c->flags_.remove) c->set_camera_state(image); } }); @@ -176,7 +176,7 @@ void APIServer::loop() { while (client_index < this->clients_.size()) { auto &client = this->clients_[client_index]; - if (!client->remove_) { + if (!client->flags_.remove) { // Common case: process active client client->loop(); client_index++; @@ -431,7 +431,7 @@ void APIServer::set_port(uint16_t port) { this->port_ = port; } void APIServer::set_password(const std::string &password) { this->password_ = password; } -void APIServer::set_batch_delay(uint32_t batch_delay) { this->batch_delay_ = batch_delay; } +void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { for (auto &client : this->clients_) { @@ -502,7 +502,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { #ifdef USE_HOMEASSISTANT_TIME void APIServer::request_time() { for (auto &client : this->clients_) { - if (!client->remove_ && client->is_authenticated()) + if (!client->flags_.remove && client->is_authenticated()) client->send_time_request(); } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 27341dc596..85c1260448 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -40,8 +40,8 @@ class APIServer : public Component, public Controller { void set_port(uint16_t port); void set_password(const std::string &password); void set_reboot_timeout(uint32_t reboot_timeout); - void set_batch_delay(uint32_t batch_delay); - uint32_t get_batch_delay() const { return batch_delay_; } + void set_batch_delay(uint16_t batch_delay); + uint16_t get_batch_delay() const { return batch_delay_; } // Get reference to shared buffer for API connections std::vector &get_shared_buffer_ref() { return shared_write_buffer_; } @@ -150,7 +150,6 @@ class APIServer : public Component, public Controller { // 4-byte aligned types uint32_t reboot_timeout_{300000}; - uint32_t batch_delay_{100}; // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; @@ -161,8 +160,9 @@ class APIServer : public Component, public Controller { // Group smaller types together uint16_t port_{6053}; + uint16_t batch_delay_{100}; bool shutting_down_ = false; - // 3 bytes used, 1 byte padding + // 5 bytes used, 3 bytes padding #ifdef USE_API_NOISE std::shared_ptr noise_ctx_ = std::make_shared(); From a4b57c7e44415e425d90c698c71093d3b81e5d78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:43:47 -0500 Subject: [PATCH 107/115] Reduce flash usage by making add_message_object non-template (#9258) --- esphome/components/api/api_pb2_size.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_pb2_size.h b/esphome/components/api/api_pb2_size.h index e591a7350f..f371be13a5 100644 --- a/esphome/components/api/api_pb2_size.h +++ b/esphome/components/api/api_pb2_size.h @@ -316,15 +316,13 @@ class ProtoSize { /** * @brief Calculates and adds the size of a nested message field to the total message size * - * This templated version directly takes a message object, calculates its size internally, + * This version takes a ProtoMessage object, calculates its size internally, * and updates the total_size reference. This eliminates the need for a temporary variable * at the call site. * - * @tparam MessageType The type of the nested message (inferred from parameter) * @param message The nested message object */ - template - static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const MessageType &message, + static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message, bool force = false) { uint32_t nested_size = 0; message.calculate_size(nested_size); From e907050a173011e1eb071963cbe962e907b8ec89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:45:03 -0500 Subject: [PATCH 108/115] Remove unused return value from read_message and fix ifdef placement in generated API code (#9256) --- esphome/components/api/api_pb2_service.cpp | 161 ++++++++++----------- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/proto.h | 2 +- script/api_protobuf/api_protobuf.py | 30 ++-- 4 files changed, 97 insertions(+), 98 deletions(-) diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 03017fdfff..de8e6574b2 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -14,7 +14,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str } #endif -bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { +void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { HelloRequest msg; @@ -106,50 +106,50 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_subscribe_logs_request(msg); break; } - case 30: { #ifdef USE_COVER + case 30: { CoverCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_cover_command_request: %s", msg.dump().c_str()); #endif this->on_cover_command_request(msg); -#endif break; } - case 31: { +#endif #ifdef USE_FAN + case 31: { FanCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_fan_command_request: %s", msg.dump().c_str()); #endif this->on_fan_command_request(msg); -#endif break; } - case 32: { +#endif #ifdef USE_LIGHT + case 32: { LightCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_light_command_request: %s", msg.dump().c_str()); #endif this->on_light_command_request(msg); -#endif break; } - case 33: { +#endif #ifdef USE_SWITCH + case 33: { SwitchCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_switch_command_request: %s", msg.dump().c_str()); #endif this->on_switch_command_request(msg); -#endif break; } +#endif case 34: { SubscribeHomeassistantServicesRequest msg; msg.decode(msg_data, msg_size); @@ -204,395 +204,394 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_execute_service_request(msg); break; } - case 45: { #ifdef USE_ESP32_CAMERA + case 45: { CameraImageRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_camera_image_request: %s", msg.dump().c_str()); #endif this->on_camera_image_request(msg); -#endif break; } - case 48: { +#endif #ifdef USE_CLIMATE + case 48: { ClimateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str()); #endif this->on_climate_command_request(msg); -#endif break; } - case 51: { +#endif #ifdef USE_NUMBER + case 51: { NumberCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str()); #endif this->on_number_command_request(msg); -#endif break; } - case 54: { +#endif #ifdef USE_SELECT + case 54: { SelectCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); #endif this->on_select_command_request(msg); -#endif break; } - case 57: { +#endif #ifdef USE_SIREN + case 57: { SirenCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_siren_command_request: %s", msg.dump().c_str()); #endif this->on_siren_command_request(msg); -#endif break; } - case 60: { +#endif #ifdef USE_LOCK + case 60: { LockCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_lock_command_request: %s", msg.dump().c_str()); #endif this->on_lock_command_request(msg); -#endif break; } - case 62: { +#endif #ifdef USE_BUTTON + case 62: { ButtonCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str()); #endif this->on_button_command_request(msg); -#endif break; } - case 65: { +#endif #ifdef USE_MEDIA_PLAYER + case 65: { MediaPlayerCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str()); #endif this->on_media_player_command_request(msg); -#endif break; } - case 66: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 66: { SubscribeBluetoothLEAdvertisementsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str()); #endif this->on_subscribe_bluetooth_le_advertisements_request(msg); -#endif break; } - case 68: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 68: { BluetoothDeviceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_device_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_device_request(msg); -#endif break; } - case 70: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 70: { BluetoothGATTGetServicesRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_get_services_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_get_services_request(msg); -#endif break; } - case 73: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 73: { BluetoothGATTReadRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_read_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_read_request(msg); -#endif break; } - case 75: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 75: { BluetoothGATTWriteRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_write_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_write_request(msg); -#endif break; } - case 76: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 76: { BluetoothGATTReadDescriptorRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_read_descriptor_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_read_descriptor_request(msg); -#endif break; } - case 77: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 77: { BluetoothGATTWriteDescriptorRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_write_descriptor_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_write_descriptor_request(msg); -#endif break; } - case 78: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 78: { BluetoothGATTNotifyRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_notify_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_notify_request(msg); -#endif break; } - case 80: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 80: { SubscribeBluetoothConnectionsFreeRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_bluetooth_connections_free_request: %s", msg.dump().c_str()); #endif this->on_subscribe_bluetooth_connections_free_request(msg); -#endif break; } - case 87: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 87: { UnsubscribeBluetoothLEAdvertisementsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_unsubscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str()); #endif this->on_unsubscribe_bluetooth_le_advertisements_request(msg); -#endif break; } - case 89: { +#endif #ifdef USE_VOICE_ASSISTANT + case 89: { SubscribeVoiceAssistantRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_voice_assistant_request: %s", msg.dump().c_str()); #endif this->on_subscribe_voice_assistant_request(msg); -#endif break; } - case 91: { +#endif #ifdef USE_VOICE_ASSISTANT + case 91: { VoiceAssistantResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_response(msg); -#endif break; } - case 92: { +#endif #ifdef USE_VOICE_ASSISTANT + case 92: { VoiceAssistantEventResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_event_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_event_response(msg); -#endif break; } - case 96: { +#endif #ifdef USE_ALARM_CONTROL_PANEL + case 96: { AlarmControlPanelCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_alarm_control_panel_command_request: %s", msg.dump().c_str()); #endif this->on_alarm_control_panel_command_request(msg); -#endif break; } - case 99: { +#endif #ifdef USE_TEXT + case 99: { TextCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_text_command_request: %s", msg.dump().c_str()); #endif this->on_text_command_request(msg); -#endif break; } - case 102: { +#endif #ifdef USE_DATETIME_DATE + case 102: { DateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_date_command_request: %s", msg.dump().c_str()); #endif this->on_date_command_request(msg); -#endif break; } - case 105: { +#endif #ifdef USE_DATETIME_TIME + case 105: { TimeCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_time_command_request: %s", msg.dump().c_str()); #endif this->on_time_command_request(msg); -#endif break; } - case 106: { +#endif #ifdef USE_VOICE_ASSISTANT + case 106: { VoiceAssistantAudio msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_audio: %s", msg.dump().c_str()); #endif this->on_voice_assistant_audio(msg); -#endif break; } - case 111: { +#endif #ifdef USE_VALVE + case 111: { ValveCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_valve_command_request: %s", msg.dump().c_str()); #endif this->on_valve_command_request(msg); -#endif break; } - case 114: { +#endif #ifdef USE_DATETIME_DATETIME + case 114: { DateTimeCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_date_time_command_request: %s", msg.dump().c_str()); #endif this->on_date_time_command_request(msg); -#endif break; } - case 115: { +#endif #ifdef USE_VOICE_ASSISTANT + case 115: { VoiceAssistantTimerEventResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_timer_event_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_timer_event_response(msg); -#endif break; } - case 118: { +#endif #ifdef USE_UPDATE + case 118: { UpdateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str()); #endif this->on_update_command_request(msg); -#endif break; } - case 119: { +#endif #ifdef USE_VOICE_ASSISTANT + case 119: { VoiceAssistantAnnounceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str()); #endif this->on_voice_assistant_announce_request(msg); -#endif break; } - case 121: { +#endif #ifdef USE_VOICE_ASSISTANT + case 121: { VoiceAssistantConfigurationRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str()); #endif this->on_voice_assistant_configuration_request(msg); -#endif break; } - case 123: { +#endif #ifdef USE_VOICE_ASSISTANT + case 123: { VoiceAssistantSetConfiguration msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str()); #endif this->on_voice_assistant_set_configuration(msg); -#endif break; } - case 124: { +#endif #ifdef USE_API_NOISE + case 124: { NoiseEncryptionSetKeyRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str()); #endif this->on_noise_encryption_set_key_request(msg); -#endif break; } - case 127: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 127: { BluetoothScannerSetModeRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_scanner_set_mode_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_scanner_set_mode_request(msg); -#endif break; } +#endif default: - return false; + break; } - return true; } void APIServerConnection::on_hello_request(const HelloRequest &msg) { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 047c56198a..3cc774f91c 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -199,7 +199,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_update_command_request(const UpdateCommandRequest &value){}; #endif protected: - bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; + void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; }; class APIServerConnection : public APIServerConnectionBase { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d9c9e3c85d..764bac2f39 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -364,7 +364,7 @@ class ProtoService { */ virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) = 0; - virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; + virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; // Optimized method that pre-allocates buffer based on message size bool send_message_(const ProtoMessage &msg, uint16_t message_type) { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 419b5aa97d..ad8e41ba5e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1034,7 +1034,7 @@ SOURCE_BOTH = 0 SOURCE_SERVER = 1 SOURCE_CLIENT = 2 -RECEIVE_CASES: dict[int, str] = {} +RECEIVE_CASES: dict[int, tuple[str, str | None]] = {} ifdefs: dict[str, str] = {} @@ -1208,8 +1208,6 @@ def build_service_message_type( func = f"on_{snake}" hout += f"virtual void {func}(const {mt.name} &value){{}};\n" case = "" - if ifdef is not None: - case += f"#ifdef {ifdef}\n" case += f"{mt.name} msg;\n" case += "msg.decode(msg_data, msg_size);\n" if log: @@ -1217,10 +1215,9 @@ def build_service_message_type( case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' case += "#endif\n" case += f"this->{func}(msg);\n" - if ifdef is not None: - case += "#endif\n" case += "break;" - RECEIVE_CASES[id_] = case + # Store the ifdef with the case for later use + RECEIVE_CASES[id_] = (case, ifdef) # Only close ifdef if we opened it if ifdef is not None: @@ -1379,18 +1376,21 @@ def main() -> None: cases = list(RECEIVE_CASES.items()) cases.sort() hpp += " protected:\n" - hpp += " bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" - out = f"bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" + out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" out += " switch (msg_type) {\n" - for i, case in cases: - c = f"case {i}: {{\n" - c += indent(case) + "\n" - c += "}" - out += indent(c, " ") + "\n" + for i, (case, ifdef) in cases: + if ifdef is not None: + out += f"#ifdef {ifdef}\n" + c = f" case {i}: {{\n" + c += indent(case, " ") + "\n" + c += " }" + out += c + "\n" + if ifdef is not None: + out += "#endif\n" out += " default:\n" - out += " return false;\n" + out += " break;\n" out += " }\n" - out += " return true;\n" out += "}\n" cpp += out hpp += "};\n" From 687cb1cd2bcd2ef03f7d50ff1c16243d44def214 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:47:20 -0500 Subject: [PATCH 109/115] Reduce web_server RAM usage by 96 bytes with conditional sorting compilation (#9227) --- esphome/components/web_server/__init__.py | 2 + esphome/components/web_server/web_server.cpp | 155 ++++-------------- esphome/components/web_server/web_server.h | 6 + .../web_server_idf/web_server_idf.cpp | 2 + esphome/core/defines.h | 1 + 5 files changed, 46 insertions(+), 120 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index d846a3418b..8ff7ce1d16 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -211,6 +211,7 @@ async def add_entity_config(entity, config): sorting_weight = config.get(CONF_SORTING_WEIGHT, 50) sorting_group_hash = hash(config.get(CONF_SORTING_GROUP_ID)) + cg.add_define("USE_WEBSERVER_SORTING") cg.add( web_server.add_entity_config( entity, @@ -296,4 +297,5 @@ async def to_code(config): cg.add_define("USE_WEBSERVER_LOCAL") if (sorting_group_config := config.get(CONF_SORTING_GROUPS)) is not None: + cg.add_define("USE_WEBSERVER_SORTING") add_sorting_groups(var, sorting_group_config) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 927659e621..1bf3ed11cb 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -184,6 +184,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(WebServer *ws, DeferredUp std::string message = ws->get_config_json(); source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); +#ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { message = json::build_json([group](JsonObject root) { root["name"] = group.second.name; @@ -193,6 +194,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(WebServer *ws, DeferredUp // up to 31 groups should be able to be queued initially without defer source->try_send_nodefer(message.c_str(), "sorting_group"); } +#endif source->entities_iterator_.begin(ws->include_internal_); @@ -413,12 +415,7 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail } set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); if (!obj->get_unit_of_measurement().empty()) root["uom"] = obj->get_unit_of_measurement(); } @@ -458,12 +455,7 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -511,12 +503,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { root["assumed_state"] = obj->assumed_state(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -552,12 +539,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -595,12 +577,7 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -681,12 +658,7 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { if (obj->get_traits().supports_oscillation()) root["oscillation"] = obj->oscillating; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -802,12 +774,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi for (auto const &option : obj->get_effects()) { opt.add(option->get_name()); } - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -888,12 +855,7 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { if (obj->get_traits().get_supports_tilt()) root["tilt"] = obj->tilt; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -954,12 +916,7 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail root["mode"] = (int) obj->traits.get_mode(); if (!obj->traits.get_unit_of_measurement().empty()) root["uom"] = obj->traits.get_unit_of_measurement(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } if (std::isnan(value)) { root["value"] = "\"NaN\""; @@ -1028,12 +985,7 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con root["value"] = value; root["state"] = value; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1091,12 +1043,7 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con root["value"] = value; root["state"] = value; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1155,12 +1102,7 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s root["value"] = value; root["state"] = value; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1221,12 +1163,7 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json root["value"] = value; if (start_config == DETAIL_ALL) { root["mode"] = (int) obj->traits.get_mode(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1282,12 +1219,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value for (auto &option : obj->traits.get_options()) { opt.add(option); } - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1404,12 +1336,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); } - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } bool has_state = false; @@ -1502,12 +1429,7 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1579,12 +1501,7 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { if (obj->get_traits().get_supports_position()) root["position"] = obj->position; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1652,12 +1569,7 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(), PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1705,12 +1617,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty event_types.add(event_type); } root["device_class"] = obj->get_device_class(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1774,12 +1681,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c root["title"] = obj->update_info.title; root["summary"] = obj->update_info.summary; root["release_url"] = obj->update_info.release_url; - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -2089,6 +1991,18 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { bool WebServer::isRequestHandlerTrivial() const { return false; } +void WebServer::add_sorting_info_(JsonObject &root, EntityBase *entity) { +#ifdef USE_WEBSERVER_SORTING + if (this->sorting_entitys_.find(entity) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[entity].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[entity].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[entity].group_id].name; + } + } +#endif +} + +#ifdef USE_WEBSERVER_SORTING void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) { this->sorting_entitys_[entity] = SortingComponents{weight, group}; } @@ -2096,6 +2010,7 @@ void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t gro void WebServer::add_sorting_group(uint64_t group_id, const std::string &group_name, float weight) { this->sorting_groups_[group_id] = SortingGroup{group_name, weight}; } +#endif void WebServer::schedule_(std::function &&f) { #ifdef USE_ESP32 diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 53ee4d1212..3be99eebae 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -46,6 +46,7 @@ struct UrlMatch { bool valid; ///< Whether this match is valid }; +#ifdef USE_WEBSERVER_SORTING struct SortingComponents { float weight; uint64_t group_id; @@ -55,6 +56,7 @@ struct SortingGroup { std::string name; float weight; }; +#endif enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; @@ -474,14 +476,18 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// This web handle is not trivial. bool isRequestHandlerTrivial() const override; // NOLINT(readability-identifier-naming) +#ifdef USE_WEBSERVER_SORTING 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); std::map sorting_entitys_; std::map sorting_groups_; +#endif + bool include_internal_{false}; protected: + void add_sorting_info_(JsonObject &root, EntityBase *entity); void schedule_(std::function &&f); web_server_base::WebServerBase *base_; #ifdef USE_ARDUINO diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 90fdf720cd..30c6b04fb2 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -338,6 +338,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * std::string message = ws->get_config_json(); this->try_send_nodefer(message.c_str(), "ping", millis(), 30000); +#ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { message = json::build_json([group](JsonObject root) { root["name"] = group.second.name; @@ -348,6 +349,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * // since the only thing in the send buffer at this point is the initial ping/config this->try_send_nodefer(message.c_str(), "sorting_group"); } +#endif this->entities_iterator_->begin(ws->include_internal_); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8abd6598f7..22454249aa 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -151,6 +151,7 @@ #define USE_VOICE_ASSISTANT #define USE_WEBSERVER #define USE_WEBSERVER_PORT 80 // NOLINT +#define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT #ifdef USE_ARDUINO From 2289073a1e0b8013cc6b840ea47cb0002078d94f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:47:50 -0500 Subject: [PATCH 110/115] Add interrupt support to GPIO binary sensors (#9115) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/gpio/binary_sensor/__init__.py | 17 ++++ .../gpio/binary_sensor/gpio_binary_sensor.cpp | 80 ++++++++++++++++++- .../gpio/binary_sensor/gpio_binary_sensor.h | 40 ++++++++++ 3 files changed, 134 insertions(+), 3 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 23f2781095..9f50fd779a 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -10,11 +10,24 @@ GPIOBinarySensor = gpio_ns.class_( "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component ) +CONF_USE_INTERRUPT = "use_interrupt" +CONF_INTERRUPT_TYPE = "interrupt_type" + +INTERRUPT_TYPES = { + "RISING": gpio_ns.INTERRUPT_RISING_EDGE, + "FALLING": gpio_ns.INTERRUPT_FALLING_EDGE, + "ANY": gpio_ns.INTERRUPT_ANY_EDGE, +} + CONFIG_SCHEMA = ( binary_sensor.binary_sensor_schema(GPIOBinarySensor) .extend( { cv.Required(CONF_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean, + cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum( + INTERRUPT_TYPES, upper=True + ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -27,3 +40,7 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) + + cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT])) + if config[CONF_USE_INTERRUPT]: + cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index cf4b088580..4b8369cd59 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -6,17 +6,91 @@ namespace gpio { static const char *const TAG = "gpio.binary_sensor"; +void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { + bool new_state = arg->isr_pin_.digital_read(); + if (new_state != arg->last_state_) { + arg->state_ = new_state; + arg->last_state_ = new_state; + arg->changed_ = true; + // Wake up the component from its disabled loop state + if (arg->component_ != nullptr) { + arg->component_->enable_loop_soon_any_context(); + } + } +} + +void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) { + pin->setup(); + this->isr_pin_ = pin->to_isr(); + this->component_ = component; + + // Read initial state + this->last_state_ = pin->digital_read(); + this->state_ = this->last_state_; + + // Attach interrupt - from this point on, any changes will be caught by the interrupt + pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type); +} + void GPIOBinarySensor::setup() { - this->pin_->setup(); - this->publish_initial_state(this->pin_->digital_read()); + if (this->use_interrupt_ && !this->pin_->is_internal()) { + ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode"); + this->use_interrupt_ = false; + } + + if (this->use_interrupt_) { + auto *internal_pin = static_cast(this->pin_); + this->store_.setup(internal_pin, this->interrupt_type_, this); + this->publish_initial_state(this->store_.get_state()); + } else { + this->pin_->setup(); + this->publish_initial_state(this->pin_->digital_read()); + } } void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); + const char *mode = this->use_interrupt_ ? "interrupt" : "polling"; + ESP_LOGCONFIG(TAG, " Mode: %s", mode); + if (this->use_interrupt_) { + const char *interrupt_type; + switch (this->interrupt_type_) { + case gpio::INTERRUPT_RISING_EDGE: + interrupt_type = "RISING_EDGE"; + break; + case gpio::INTERRUPT_FALLING_EDGE: + interrupt_type = "FALLING_EDGE"; + break; + case gpio::INTERRUPT_ANY_EDGE: + interrupt_type = "ANY_EDGE"; + break; + default: + interrupt_type = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Interrupt Type: %s", interrupt_type); + } } -void GPIOBinarySensor::loop() { this->publish_state(this->pin_->digital_read()); } +void GPIOBinarySensor::loop() { + if (this->use_interrupt_) { + if (this->store_.is_changed()) { + // Clear the flag immediately to minimize the window where we might miss changes + this->store_.clear_changed(); + // Read the state and publish it + // Note: If the ISR fires between clear_changed() and get_state(), that's fine - + // we'll process the new change on the next loop iteration + bool state = this->store_.get_state(); + this->publish_state(state); + } else { + // No changes, disable the loop until the next interrupt + this->disable_loop(); + } + } else { + this->publish_state(this->pin_->digital_read()); + } +} float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 33a173fe2e..8cf52f540b 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -2,14 +2,51 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { namespace gpio { +// Store class for ISR data (no vtables, ISR-safe) +class GPIOBinarySensorStore { + public: + void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component); + + static void gpio_intr(GPIOBinarySensorStore *arg); + + bool get_state() const { + // No lock needed: state_ is atomically updated by ISR + // Volatile ensures we read the latest value + return this->state_; + } + + bool is_changed() const { + // Simple read of volatile bool - no clearing here + return this->changed_; + } + + void clear_changed() { + // Separate method to clear the flag + this->changed_ = false; + } + + protected: + ISRInternalGPIOPin isr_pin_; + volatile bool state_{false}; + volatile bool last_state_{false}; + volatile bool changed_{false}; + Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context() +}; + class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { public: + // No destructor needed: ESPHome components are created at boot and live forever. + // Interrupts are only detached on reboot when memory is cleared anyway. + void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; } + void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup pin @@ -22,6 +59,9 @@ class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { protected: GPIOPin *pin_; + bool use_interrupt_{true}; + gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; + GPIOBinarySensorStore store_; }; } // namespace gpio From 53e9ffe656c920218ff858c3072c9f1df962a562 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:48:19 +1200 Subject: [PATCH 111/115] [pi4ioe5v6408] Add new IO Expander (#8888) Co-authored-by: Keith Burzinski --- CODEOWNERS | 1 + esphome/components/pi4ioe5v6408/__init__.py | 84 +++++++++ .../components/pi4ioe5v6408/pi4ioe5v6408.cpp | 171 ++++++++++++++++++ .../components/pi4ioe5v6408/pi4ioe5v6408.h | 70 +++++++ tests/components/pi4ioe5v6408/common.yaml | 22 +++ .../pi4ioe5v6408/test.esp32-ard.yaml | 5 + .../pi4ioe5v6408/test.esp32-idf.yaml | 5 + .../pi4ioe5v6408/test.rp2040-ard.yaml | 5 + 8 files changed, 363 insertions(+) create mode 100644 esphome/components/pi4ioe5v6408/__init__.py create mode 100644 esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp create mode 100644 esphome/components/pi4ioe5v6408/pi4ioe5v6408.h create mode 100644 tests/components/pi4ioe5v6408/common.yaml create mode 100644 tests/components/pi4ioe5v6408/test.esp32-ard.yaml create mode 100644 tests/components/pi4ioe5v6408/test.esp32-idf.yaml create mode 100644 tests/components/pi4ioe5v6408/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 832c571ae4..b3c66c775b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -332,6 +332,7 @@ esphome/components/pca6416a/* @Mat931 esphome/components/pca9554/* @clydebarrow @hwstar esphome/components/pcf85063/* @brogon esphome/components/pcf8563/* @KoenBreeman +esphome/components/pi4ioe5v6408/* @jesserockz esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 esphome/components/pm1006/* @habbie diff --git a/esphome/components/pi4ioe5v6408/__init__.py b/esphome/components/pi4ioe5v6408/__init__.py new file mode 100644 index 0000000000..c64f923823 --- /dev/null +++ b/esphome/components/pi4ioe5v6408/__init__.py @@ -0,0 +1,84 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, + CONF_RESET, +) + +AUTO_LOAD = ["gpio_expander"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + + +pi4ioe5v6408_ns = cg.esphome_ns.namespace("pi4ioe5v6408") +PI4IOE5V6408Component = pi4ioe5v6408_ns.class_( + "PI4IOE5V6408Component", cg.Component, i2c.I2CDevice +) +PI4IOE5V6408GPIOPin = pi4ioe5v6408_ns.class_("PI4IOE5V6408GPIOPin", cg.GPIOPin) + +CONF_PI4IOE5V6408 = "pi4ioe5v6408" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(PI4IOE5V6408Component), + cv.Optional(CONF_RESET, default=True): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x43)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_reset(config[CONF_RESET])) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + return value + + +PI4IOE5V6408_PIN_SCHEMA = pins.gpio_base_schema( + PI4IOE5V6408GPIOPin, + cv.int_range(min=0, max=7), + modes=[ + CONF_INPUT, + CONF_OUTPUT, + CONF_PULLUP, + CONF_PULLDOWN, + ], + mode_validator=validate_mode, +).extend( + { + cv.Required(CONF_PI4IOE5V6408): cv.use_id(PI4IOE5V6408Component), + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_PI4IOE5V6408, PI4IOE5V6408_PIN_SCHEMA) +async def pi4ioe5v6408_pin_schema(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_PI4IOE5V6408]) + + cg.add(var.set_pin(config[CONF_NUMBER])) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp new file mode 100644 index 0000000000..55b8edffc8 --- /dev/null +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -0,0 +1,171 @@ +#include "pi4ioe5v6408.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pi4ioe5v6408 { + +static const uint8_t PI4IOE5V6408_REGISTER_DEVICE_ID = 0x01; +static const uint8_t PI4IOE5V6408_REGISTER_IO_DIR = 0x03; +static const uint8_t PI4IOE5V6408_REGISTER_OUT_SET = 0x05; +static const uint8_t PI4IOE5V6408_REGISTER_OUT_HIGH_IMPEDENCE = 0x07; +static const uint8_t PI4IOE5V6408_REGISTER_IN_DEFAULT_STATE = 0x09; +static const uint8_t PI4IOE5V6408_REGISTER_PULL_ENABLE = 0x0B; +static const uint8_t PI4IOE5V6408_REGISTER_PULL_SELECT = 0x0D; +static const uint8_t PI4IOE5V6408_REGISTER_IN_STATE = 0x0F; +static const uint8_t PI4IOE5V6408_REGISTER_INTERRUPT_ENABLE_MASK = 0x11; +static const uint8_t PI4IOE5V6408_REGISTER_INTERRUPT_STATUS = 0x13; + +static const char *const TAG = "pi4ioe5v6408"; + +void PI4IOE5V6408Component::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + if (this->reset_) { + this->reg(PI4IOE5V6408_REGISTER_DEVICE_ID) |= 0b00000001; + this->reg(PI4IOE5V6408_REGISTER_OUT_HIGH_IMPEDENCE) = 0b00000000; + } else { + if (!this->read_gpio_modes_()) { + this->mark_failed(); + ESP_LOGE(TAG, "Failed to read GPIO modes"); + return; + } + if (!this->read_gpio_outputs_()) { + this->mark_failed(); + ESP_LOGE(TAG, "Failed to read GPIO outputs"); + return; + } + } +} +void PI4IOE5V6408Component::dump_config() { + ESP_LOGCONFIG(TAG, "PI4IOE5V6408:"); + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} +void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags & gpio::FLAG_OUTPUT) { + // Set mode mask bit + this->mode_mask_ |= 1 << pin; + } else if (flags & gpio::FLAG_INPUT) { + // Clear mode mask bit + this->mode_mask_ &= ~(1 << pin); + if (flags & gpio::FLAG_PULLUP) { + this->pull_up_down_mask_ |= 1 << pin; + this->pull_enable_mask_ |= 1 << pin; + } else if (flags & gpio::FLAG_PULLDOWN) { + this->pull_up_down_mask_ &= ~(1 << pin); + this->pull_enable_mask_ |= 1 << pin; + } + } + // Write GPIO to enable input mode + this->write_gpio_modes_(); +} + +void PI4IOE5V6408Component::loop() { this->reset_pin_cache_(); } + +bool PI4IOE5V6408Component::read_gpio_outputs_() { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_OUT_SET, &data)) { + this->status_set_warning("Failed to read output register"); + return false; + } + this->output_mask_ = data; + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::read_gpio_modes_() { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_IO_DIR, &data)) { + this->status_set_warning("Failed to read GPIO modes"); + return false; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Read GPIO modes: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(data)); +#endif + this->mode_mask_ = data; + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::digital_read_hw(uint8_t pin) { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_IN_STATE, &data)) { + this->status_set_warning("Failed to read GPIO state"); + return false; + } + this->input_mask_ = data; + this->status_clear_warning(); + return true; +} + +void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) { + if (this->is_failed()) + return; + + if (value) { + this->output_mask_ |= (1 << pin); + } else { + this->output_mask_ &= ~(1 << pin); + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_)) { + this->status_set_warning("Failed to write output register"); + return; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Wrote GPIO output: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(this->output_mask_)); +#endif + this->status_clear_warning(); +} + +bool PI4IOE5V6408Component::write_gpio_modes_() { + if (this->is_failed()) + return false; + + if (!this->write_byte(PI4IOE5V6408_REGISTER_IO_DIR, this->mode_mask_)) { + this->status_set_warning("Failed to write GPIO modes"); + return false; + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_SELECT, this->pull_up_down_mask_)) { + this->status_set_warning("Failed to write GPIO pullup/pulldown"); + return false; + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_ENABLE, this->pull_enable_mask_)) { + this->status_set_warning("Failed to write GPIO pull enable"); + return false; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, + "Wrote GPIO modes: 0b" BYTE_TO_BINARY_PATTERN "\n" + "Wrote GPIO pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" + "Wrote GPIO pull enable: 0b" BYTE_TO_BINARY_PATTERN, + BYTE_TO_BINARY(this->mode_mask_), BYTE_TO_BINARY(this->pull_up_down_mask_), + BYTE_TO_BINARY(this->pull_enable_mask_)); +#endif + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::digital_read_cache(uint8_t pin) { return (this->input_mask_ & (1 << pin)); } + +float PI4IOE5V6408Component::get_setup_priority() const { return setup_priority::IO; } + +void PI4IOE5V6408GPIOPin::setup() { this->pin_mode(this->flags_); } +void PI4IOE5V6408GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool PI4IOE5V6408GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void PI4IOE5V6408GPIOPin::digital_write(bool value) { + this->parent_->digital_write(this->pin_, value != this->inverted_); +} +std::string PI4IOE5V6408GPIOPin::dump_summary() const { return str_sprintf("%u via PI4IOE5V6408", this->pin_); } + +} // namespace pi4ioe5v6408 +} // namespace esphome diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h new file mode 100644 index 0000000000..82b3076fab --- /dev/null +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pi4ioe5v6408 { +class PI4IOE5V6408Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { + public: + PI4IOE5V6408Component() = default; + + void setup() override; + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + void dump_config() override; + void loop() override; + + /// Indicate if the component should reset the state during setup + void set_reset(bool reset) { this->reset_ = reset; } + + protected: + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + + /// Mask for the pin mode - 1 means output, 0 means input + uint8_t mode_mask_{0x00}; + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint8_t output_mask_{0x00}; + /// The state read in digital_read_hw - 1 means HIGH, 0 means LOW + uint8_t input_mask_{0x00}; + /// The mask to write as input buffer state - 1 means enabled, 0 means disabled + uint8_t pull_enable_mask_{0x00}; + /// The mask to write as pullup state - 1 means pullup, 0 means pulldown + uint8_t pull_up_down_mask_{0x00}; + + bool reset_{true}; + + bool read_gpio_modes_(); + bool write_gpio_modes_(); + bool read_gpio_outputs_(); +}; + +class PI4IOE5V6408GPIOPin : public GPIOPin, public Parented { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_inverted(bool inverted) { this->inverted_ = inverted; } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } + + gpio::Flags get_flags() const override { return this->flags_; } + + protected: + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace pi4ioe5v6408 +} // namespace esphome diff --git a/tests/components/pi4ioe5v6408/common.yaml b/tests/components/pi4ioe5v6408/common.yaml new file mode 100644 index 0000000000..4130dc2652 --- /dev/null +++ b/tests/components/pi4ioe5v6408/common.yaml @@ -0,0 +1,22 @@ +i2c: + id: i2c_pi4ioe5v6408 + sda: ${i2c_sda} + scl: ${i2c_scl} + +pi4ioe5v6408: + id: pi4ioe1 + address: 0x44 + +switch: + - platform: gpio + id: switch1 + pin: + pi4ioe5v6408: pi4ioe1 + number: 0 + +binary_sensor: + - platform: gpio + id: sensor1 + pin: + pi4ioe5v6408: pi4ioe1 + number: 1 diff --git a/tests/components/pi4ioe5v6408/test.esp32-ard.yaml b/tests/components/pi4ioe5v6408/test.esp32-ard.yaml new file mode 100644 index 0000000000..55e6edfbf3 --- /dev/null +++ b/tests/components/pi4ioe5v6408/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_sda: GPIO21 + i2c_scl: GPIO22 + +<<: !include common.yaml diff --git a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml new file mode 100644 index 0000000000..55e6edfbf3 --- /dev/null +++ b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_sda: GPIO21 + i2c_scl: GPIO22 + +<<: !include common.yaml diff --git a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b7b6b13bfe --- /dev/null +++ b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_sda: GPIO4 + i2c_scl: GPIO5 + +<<: !include common.yaml From 0b1b8f05e11e2ec11218f80384b20edabad022db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 18:49:31 -0500 Subject: [PATCH 112/115] Reduce loop enable/disable log spam by using very verbose level (#9267) --- esphome/core/application.cpp | 2 +- esphome/core/component.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 328de00640..1599c648e7 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -376,7 +376,7 @@ void Application::enable_pending_loops_() { // Clear the pending flag and enable the loop component->pending_enable_loop_ = false; - ESP_LOGD(TAG, "%s loop enabled from ISR", component->get_component_source()); + ESP_LOGVV(TAG, "%s loop enabled from ISR", component->get_component_source()); component->component_state_ &= ~COMPONENT_STATE_MASK; component->component_state_ |= COMPONENT_STATE_LOOP; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 625a7b2125..8fa63de84e 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -149,7 +149,7 @@ void Component::mark_failed() { } void Component::disable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { - ESP_LOGD(TAG, "%s loop disabled", this->get_component_source()); + ESP_LOGVV(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); @@ -157,7 +157,7 @@ void Component::disable_loop() { } 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()); + ESP_LOGVV(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); From 7f8dd4b2540b406e51150e750d8d92abf4843435 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 19:19:18 -0500 Subject: [PATCH 113/115] Fix thread-safe cleanup of event source connections in ESP-IDF web server (#9268) --- .../web_server_idf/web_server_idf.cpp | 39 ++++++++++++++----- .../web_server_idf/web_server_idf.h | 3 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 30c6b04fb2..409230806c 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -292,21 +292,38 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { } void AsyncEventSource::loop() { - for (auto *ses : this->sessions_) { - ses->loop(); + // Clean up dead sessions safely + // This follows the ESP-IDF pattern where free_ctx marks resources as dead + // and the main loop handles the actual cleanup to avoid race conditions + auto it = this->sessions_.begin(); + while (it != this->sessions_.end()) { + auto *ses = *it; + // If the session has a dead socket (marked by destroy callback) + if (ses->fd_.load() == 0) { + ESP_LOGD(TAG, "Removing dead event source session"); + it = this->sessions_.erase(it); + delete ses; // NOLINT(cppcoreguidelines-owning-memory) + } else { + ses->loop(); + ++it; + } } } void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) { for (auto *ses : this->sessions_) { - ses->try_send_nodefer(message, event, id, reconnect); + if (ses->fd_.load() != 0) { // Skip dead sessions + ses->try_send_nodefer(message, event, id, reconnect); + } } } void AsyncEventSource::deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator) { for (auto *ses : this->sessions_) { - ses->deferrable_send_state(source, event_type, message_generator); + if (ses->fd_.load() != 0) { // Skip dead sessions + ses->deferrable_send_state(source, event_type, message_generator); + } } } @@ -331,7 +348,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * req->free_ctx = AsyncEventSourceResponse::destroy; this->hd_ = req->handle; - this->fd_ = httpd_req_to_sockfd(req); + this->fd_.store(httpd_req_to_sockfd(req)); // Configure reconnect timeout and send config // this should always go through since the tcp send buffer is empty on connect @@ -362,8 +379,10 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); - rsp->server_->sessions_.erase(rsp); - delete rsp; // NOLINT(cppcoreguidelines-owning-memory) + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load()); + // Mark as dead by setting fd to 0 - will be cleaned up in the main loop + rsp->fd_.store(0); + // Note: We don't delete or remove from set here to avoid race conditions } // helper for allowing only unique entries in the queue @@ -403,9 +422,11 @@ void AsyncEventSourceResponse::process_buffer_() { return; } - int bytes_sent = httpd_socket_send(this->hd_, this->fd_, event_buffer_.c_str() + event_bytes_sent_, + int bytes_sent = httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, event_buffer_.size() - event_bytes_sent_, 0); if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT || bytes_sent == HTTPD_SOCK_ERR_FAIL) { + // Socket error - just return, the connection will be closed by httpd + // and our destroy callback will be called return; } event_bytes_sent_ += bytes_sent; @@ -425,7 +446,7 @@ void AsyncEventSourceResponse::loop() { bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) { - if (this->fd_ == 0) { + if (this->fd_.load() == 0) { return false; } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 8dafdf11ef..7547117224 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -4,6 +4,7 @@ #include "esphome/core/defines.h" #include +#include #include #include #include @@ -271,7 +272,7 @@ class AsyncEventSourceResponse { static void destroy(void *p); AsyncEventSource *server_; httpd_handle_t hd_{}; - int fd_{}; + std::atomic fd_{}; std::vector deferred_queue_; esphome::web_server::WebServer *web_server_; std::unique_ptr entities_iterator_; From 6a354d7c946d5f81e0efd355e38d4a4267f39152 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 22:33:35 -0500 Subject: [PATCH 114/115] Reduce API component memory usage with conditional compilation (#9262) --- esphome/components/api/__init__.py | 34 +-- esphome/components/api/api_connection.cpp | 2 + esphome/components/api/api_server.cpp | 2 + esphome/components/api/api_server.h | 38 +++- esphome/core/defines.h | 3 + .../fixtures/api_conditional_memory.yaml | 71 ++++++ .../test_api_conditional_memory.py | 205 ++++++++++++++++++ 7 files changed, 338 insertions(+), 17 deletions(-) create mode 100644 tests/integration/fixtures/api_conditional_memory.yaml create mode 100644 tests/integration/test_api_conditional_memory.py diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 501b707678..ae83129c21 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -136,23 +136,26 @@ async def to_code(config): cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) - for conf in config.get(CONF_ACTIONS, []): - template_args = [] - func_args = [] - service_arg_names = [] - for name, var_ in conf[CONF_VARIABLES].items(): - native = SERVICE_ARG_NATIVE_TYPES[var_] - template_args.append(native) - func_args.append((native, name)) - service_arg_names.append(name) - templ = cg.TemplateArguments(*template_args) - trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names - ) - cg.add(var.register_user_service(trigger)) - await automation.build_automation(trigger, func_args, conf) + if actions := config.get(CONF_ACTIONS, []): + cg.add_define("USE_API_YAML_SERVICES") + for conf in actions: + template_args = [] + func_args = [] + service_arg_names = [] + for name, var_ in conf[CONF_VARIABLES].items(): + native = SERVICE_ARG_NATIVE_TYPES[var_] + template_args.append(native) + func_args.append((native, name)) + service_arg_names.append(name) + templ = cg.TemplateArguments(*template_args) + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names + ) + cg.add(var.register_user_service(trigger)) + await automation.build_automation(trigger, func_args, conf) if CONF_ON_CLIENT_CONNECTED in config: + cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") await automation.build_automation( var.get_client_connected_trigger(), [(cg.std_string, "client_info"), (cg.std_string, "client_address")], @@ -160,6 +163,7 @@ async def to_code(config): ) if CONF_ON_CLIENT_DISCONNECTED in config: + cg.add_define("USE_API_CLIENT_DISCONNECTED_TRIGGER") await automation.build_automation( var.get_client_disconnected_trigger(), [(cg.std_string, "client_info"), (cg.std_string, "client_address")], diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e5847e50f7..6a40f21f99 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1511,7 +1511,9 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { if (correct) { ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); +#endif #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { this->send_time_request(); diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index b17faf7607..ebe80604dc 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -184,7 +184,9 @@ void APIServer::loop() { } // Rare case: handle disconnection +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); +#endif ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); // Swap with the last element and pop (avoids expensive vector shifts) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 85c1260448..5a9b0677bc 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -105,7 +105,18 @@ class APIServer : public Component, public Controller { void on_media_player_update(media_player::MediaPlayer *obj) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); - void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } + void register_user_service(UserServiceDescriptor *descriptor) { +#ifdef USE_API_YAML_SERVICES + // Vector is pre-allocated when services are defined in YAML + this->user_services_.push_back(descriptor); +#else + // Lazy allocate vector on first use for CustomAPIDevice + if (!this->user_services_) { + this->user_services_ = std::make_unique>(); + } + this->user_services_->push_back(descriptor); +#endif + } #ifdef USE_HOMEASSISTANT_TIME void request_time(); #endif @@ -134,19 +145,34 @@ class APIServer : public Component, public Controller { void get_home_assistant_state(std::string entity_id, optional attribute, std::function f); const std::vector &get_state_subs() const; - const std::vector &get_user_services() const { return this->user_services_; } + const std::vector &get_user_services() const { +#ifdef USE_API_YAML_SERVICES + return this->user_services_; +#else + static const std::vector EMPTY; + return this->user_services_ ? *this->user_services_ : EMPTY; +#endif + } +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } +#endif +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER Trigger *get_client_disconnected_trigger() const { return this->client_disconnected_trigger_; } +#endif protected: void schedule_reboot_timeout_(); // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger *client_connected_trigger_ = new Trigger(); +#endif +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER Trigger *client_disconnected_trigger_ = new Trigger(); +#endif // 4-byte aligned types uint32_t reboot_timeout_{300000}; @@ -156,7 +182,15 @@ class APIServer : public Component, public Controller { std::string password_; std::vector shared_write_buffer_; // Shared proto write buffer for all connections std::vector state_subs_; +#ifdef USE_API_YAML_SERVICES + // When services are defined in YAML, we know at compile time that services will be registered std::vector user_services_; +#else + // Services can still be registered at runtime by CustomAPIDevice components even when not + // defined in YAML. Using unique_ptr allows lazy allocation, saving 12 bytes in the common + // case where no services (YAML or custom) are used. + std::unique_ptr> user_services_; +#endif // Group smaller types together uint16_t port_{6053}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 22454249aa..ea3c8bdc17 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -101,8 +101,11 @@ #define USE_AUDIO_FLAC_SUPPORT #define USE_AUDIO_MP3_SUPPORT #define USE_API +#define USE_API_CLIENT_CONNECTED_TRIGGER +#define USE_API_CLIENT_DISCONNECTED_TRIGGER #define USE_API_NOISE #define USE_API_PLAINTEXT +#define USE_API_YAML_SERVICES #define USE_MD5 #define USE_MQTT #define USE_NETWORK diff --git a/tests/integration/fixtures/api_conditional_memory.yaml b/tests/integration/fixtures/api_conditional_memory.yaml new file mode 100644 index 0000000000..4bbba5084b --- /dev/null +++ b/tests/integration/fixtures/api_conditional_memory.yaml @@ -0,0 +1,71 @@ +esphome: + name: api-conditional-memory-test +host: +api: + actions: + - action: test_simple_service + then: + - logger.log: "Simple service called" + - binary_sensor.template.publish: + id: service_called_sensor + state: ON + - action: test_service_with_args + variables: + arg_string: string + arg_int: int + arg_bool: bool + arg_float: float + then: + - logger.log: + format: "Service called with: %s, %d, %d, %.2f" + args: [arg_string.c_str(), arg_int, arg_bool, arg_float] + - sensor.template.publish: + id: service_arg_sensor + state: !lambda 'return arg_float;' + on_client_connected: + - logger.log: + format: "Client %s connected from %s" + args: [client_info.c_str(), client_address.c_str()] + - binary_sensor.template.publish: + id: client_connected + state: ON + - text_sensor.template.publish: + id: last_client_info + state: !lambda 'return client_info;' + on_client_disconnected: + - logger.log: + format: "Client %s disconnected from %s" + args: [client_info.c_str(), client_address.c_str()] + - binary_sensor.template.publish: + id: client_connected + state: OFF + - binary_sensor.template.publish: + id: client_disconnected_event + state: ON + +logger: + level: DEBUG + +binary_sensor: + - platform: template + name: "Client Connected" + id: client_connected + device_class: connectivity + - platform: template + name: "Client Disconnected Event" + id: client_disconnected_event + - platform: template + name: "Service Called" + id: service_called_sensor + +sensor: + - platform: template + name: "Service Argument Value" + id: service_arg_sensor + unit_of_measurement: "" + accuracy_decimals: 2 + +text_sensor: + - platform: template + name: "Last Client Info" + id: last_client_info diff --git a/tests/integration/test_api_conditional_memory.py b/tests/integration/test_api_conditional_memory.py new file mode 100644 index 0000000000..b85e8d91af --- /dev/null +++ b/tests/integration/test_api_conditional_memory.py @@ -0,0 +1,205 @@ +"""Integration test for API conditional memory optimization with triggers and services.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import ( + BinarySensorInfo, + EntityState, + SensorInfo, + TextSensorInfo, + UserService, + UserServiceArgType, +) +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_conditional_memory( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test API triggers and services work correctly with conditional compilation.""" + loop = asyncio.get_running_loop() + # Keep ESPHome process running throughout the test + async with run_compiled(yaml_config): + # First connection + async with api_client_connected() as client: + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "api-conditional-memory-test" + + # List entities and services + entity_info, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our entities + client_connected: BinarySensorInfo | None = None + client_disconnected_event: BinarySensorInfo | None = None + service_called_sensor: BinarySensorInfo | None = None + service_arg_sensor: SensorInfo | None = None + last_client_info: TextSensorInfo | None = None + + for entity in entity_info: + if isinstance(entity, BinarySensorInfo): + if entity.object_id == "client_connected": + client_connected = entity + elif entity.object_id == "client_disconnected_event": + client_disconnected_event = entity + elif entity.object_id == "service_called": + service_called_sensor = entity + elif isinstance(entity, SensorInfo): + if entity.object_id == "service_argument_value": + service_arg_sensor = entity + elif isinstance(entity, TextSensorInfo): + if entity.object_id == "last_client_info": + last_client_info = entity + + # Verify all entities exist + assert client_connected is not None, "client_connected sensor not found" + assert client_disconnected_event is not None, ( + "client_disconnected_event sensor not found" + ) + assert service_called_sensor is not None, "service_called sensor not found" + assert service_arg_sensor is not None, "service_arg_sensor not found" + assert last_client_info is not None, "last_client_info sensor not found" + + # Verify services exist + assert len(services) == 2, f"Expected 2 services, found {len(services)}" + + # Find our services + simple_service: UserService | None = None + service_with_args: UserService | None = None + + for service in services: + if service.name == "test_simple_service": + simple_service = service + elif service.name == "test_service_with_args": + service_with_args = service + + assert simple_service is not None, "test_simple_service not found" + assert service_with_args is not None, "test_service_with_args not found" + + # Verify service arguments + assert len(service_with_args.args) == 4, ( + f"Expected 4 args, found {len(service_with_args.args)}" + ) + + # Check arg types + arg_types = {arg.name: arg.type for arg in service_with_args.args} + assert arg_types["arg_string"] == UserServiceArgType.STRING + assert arg_types["arg_int"] == UserServiceArgType.INT + assert arg_types["arg_bool"] == UserServiceArgType.BOOL + assert arg_types["arg_float"] == UserServiceArgType.FLOAT + + # Track state changes + states: dict[int, EntityState] = {} + states_future: asyncio.Future[None] = loop.create_future() + + def on_state(state: EntityState) -> None: + states[state.key] = state + # Check if we have initial states for connection sensors + if ( + client_connected.key in states + and last_client_info.key in states + and not states_future.done() + ): + states_future.set_result(None) + + client.subscribe_states(on_state) + + # Wait for initial states + await asyncio.wait_for(states_future, timeout=5.0) + + # Verify on_client_connected trigger fired + connected_state = states.get(client_connected.key) + assert connected_state is not None + assert connected_state.state is True, "Client should be connected" + + # Verify client info was captured + client_info_state = states.get(last_client_info.key) + assert client_info_state is not None + assert isinstance(client_info_state.state, str) + assert len(client_info_state.state) > 0, "Client info should not be empty" + + # Test simple service + service_future: asyncio.Future[None] = loop.create_future() + + def check_service_called(state: EntityState) -> None: + if state.key == service_called_sensor.key and state.state is True: + if not service_future.done(): + service_future.set_result(None) + + # Update callback to check for service execution + client.subscribe_states(check_service_called) + + # Call simple service + client.execute_service(simple_service, {}) + + # Wait for service to execute + await asyncio.wait_for(service_future, timeout=5.0) + + # Test service with arguments + arg_future: asyncio.Future[None] = loop.create_future() + expected_float = 42.5 + + def check_arg_sensor(state: EntityState) -> None: + if ( + state.key == service_arg_sensor.key + and abs(state.state - expected_float) < 0.01 + ): + if not arg_future.done(): + arg_future.set_result(None) + + client.subscribe_states(check_arg_sensor) + + # Call service with arguments + client.execute_service( + service_with_args, + { + "arg_string": "test_string", + "arg_int": 123, + "arg_bool": True, + "arg_float": expected_float, + }, + ) + + # Wait for service with args to execute + await asyncio.wait_for(arg_future, timeout=5.0) + + # After disconnecting first client, reconnect and verify triggers work + async with api_client_connected() as client2: + # Subscribe to states with new client + states2: dict[int, EntityState] = {} + connected_future: asyncio.Future[None] = loop.create_future() + + def on_state2(state: EntityState) -> None: + states2[state.key] = state + # Check for reconnection + if state.key == client_connected.key and state.state is True: + if not connected_future.done(): + connected_future.set_result(None) + + client2.subscribe_states(on_state2) + + # Wait for connected state + await asyncio.wait_for(connected_future, timeout=5.0) + + # Verify client is connected again (on_client_connected fired) + assert states2[client_connected.key].state is True, ( + "Client should be reconnected" + ) + + # The client_disconnected_event should be ON from when we disconnected + # (it was set ON by on_client_disconnected trigger) + disconnected_state = states2.get(client_disconnected_event.key) + assert disconnected_state is not None + assert disconnected_state.state is True, ( + "Disconnect event should be ON from previous disconnect" + ) From 140ca070a20c9e1b3d3d380f467c0553c2f8a9b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 22:40:36 -0500 Subject: [PATCH 115/115] Optimize scheduler string storage to eliminate heap allocations (#9251) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/component.cpp | 18 +- esphome/core/component.h | 34 ++++ esphome/core/scheduler.cpp | 175 +++++++++++++----- esphome/core/scheduler.h | 119 ++++++++++-- .../fixtures/scheduler_string_test.yaml | 164 ++++++++++++++++ .../integration/test_scheduler_string_test.py | 166 +++++++++++++++++ 6 files changed, 614 insertions(+), 62 deletions(-) create mode 100644 tests/integration/fixtures/scheduler_string_test.yaml create mode 100644 tests/integration/test_scheduler_string_test.py diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 8fa63de84e..6661223e35 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -60,10 +60,18 @@ void Component::set_interval(const std::string &name, uint32_t interval, std::fu App.scheduler.set_interval(this, name, interval, std::move(f)); } +void Component::set_interval(const char *name, uint32_t interval, std::function &&f) { // NOLINT + App.scheduler.set_interval(this, name, interval, std::move(f)); +} + bool Component::cancel_interval(const std::string &name) { // NOLINT return App.scheduler.cancel_interval(this, name); } +bool Component::cancel_interval(const char *name) { // NOLINT + return App.scheduler.cancel_interval(this, name); +} + void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, float backoff_increase_factor) { // NOLINT App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); @@ -77,10 +85,18 @@ void Component::set_timeout(const std::string &name, uint32_t timeout, std::func App.scheduler.set_timeout(this, name, timeout, std::move(f)); } +void Component::set_timeout(const char *name, uint32_t timeout, std::function &&f) { // NOLINT + App.scheduler.set_timeout(this, name, timeout, std::move(f)); +} + bool Component::cancel_timeout(const std::string &name) { // NOLINT return App.scheduler.cancel_timeout(this, name); } +bool Component::cancel_timeout(const char *name) { // NOLINT + return App.scheduler.cancel_timeout(this, name); +} + void Component::call_loop() { this->loop(); } void Component::call_setup() { this->setup(); } void Component::call_dump_config() { @@ -189,7 +205,7 @@ bool Component::is_in_loop_state() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP; } void Component::defer(std::function &&f) { // NOLINT - App.scheduler.set_timeout(this, "", 0, std::move(f)); + App.scheduler.set_timeout(this, static_cast(nullptr), 0, std::move(f)); } bool Component::cancel_defer(const std::string &name) { // NOLINT return App.scheduler.cancel_timeout(this, name); diff --git a/esphome/core/component.h b/esphome/core/component.h index 7f2bdd8414..5b37deeb68 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -260,6 +260,22 @@ class Component { */ void set_interval(const std::string &name, uint32_t interval, std::function &&f); // NOLINT + /** Set an interval function with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the scheduled task + * + * For dynamic strings, use the std::string overload instead. + * + * @param name The identifier for this interval function (must have static lifetime) + * @param interval The interval in ms + * @param f The function to call + */ + void set_interval(const char *name, uint32_t interval, std::function &&f); // NOLINT + void set_interval(uint32_t interval, std::function &&f); // NOLINT /** Cancel an interval function. @@ -268,6 +284,7 @@ class Component { * @return Whether an interval functions was deleted. */ bool cancel_interval(const std::string &name); // NOLINT + bool cancel_interval(const char *name); // NOLINT /** Set an retry function with a unique name. Empty name means no cancelling possible. * @@ -328,6 +345,22 @@ class Component { */ void set_timeout(const std::string &name, uint32_t timeout, std::function &&f); // NOLINT + /** Set a timeout function with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "init") + * - A static const char* variable + * - A pointer with lifetime >= the timeout duration + * + * For dynamic strings, use the std::string overload instead. + * + * @param name The identifier for this timeout function (must have static lifetime) + * @param timeout The timeout in ms + * @param f The function to call + */ + void set_timeout(const char *name, uint32_t timeout, std::function &&f); // NOLINT + void set_timeout(uint32_t timeout, std::function &&f); // NOLINT /** Cancel a timeout function. @@ -336,6 +369,7 @@ class Component { * @return Whether a timeout functions was deleted. */ bool cancel_timeout(const std::string &name); // NOLINT + bool cancel_timeout(const char *name); // NOLINT /** Defer a callback to the next loop() call. * diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 8144435163..5c01b4f3f4 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -7,6 +7,7 @@ #include "esphome/core/log.h" #include #include +#include namespace esphome { @@ -17,75 +18,138 @@ static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // Uncomment to debug scheduler // #define ESPHOME_DEBUG_SCHEDULER +#ifdef ESPHOME_DEBUG_SCHEDULER +// Helper to validate that a pointer looks like it's in static memory +static void validate_static_string(const char *name) { + if (name == nullptr) + return; + + // This is a heuristic check - stack and heap pointers are typically + // much higher in memory than static data + uintptr_t addr = reinterpret_cast(name); + + // Create a stack variable to compare against + int stack_var; + uintptr_t stack_addr = reinterpret_cast(&stack_var); + + // If the string pointer is near our stack variable, it's likely on the stack + // Using 8KB range as ESP32 main task stack is typically 8192 bytes + if (addr > (stack_addr - 0x2000) && addr < (stack_addr + 0x2000)) { + ESP_LOGW(TAG, + "WARNING: Scheduler name '%s' at %p appears to be on the stack - this is unsafe!\n" + " Stack reference at %p", + name, name, &stack_var); + } + + // Also check if it might be on the heap by seeing if it's in a very different range + // This is platform-specific but generally heap is allocated far from static memory + static const char *static_str = "test"; + uintptr_t static_addr = reinterpret_cast(static_str); + + // If the address is very far from known static memory, it might be heap + if (addr > static_addr + 0x100000 || (static_addr > 0x100000 && addr < static_addr - 0x100000)) { + ESP_LOGW(TAG, "WARNING: Scheduler name '%s' at %p might be on heap (static ref at %p)", name, name, static_str); + } +} +#endif + // A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to // them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task, // iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to // avoid the main thread modifying the list while it is being accessed. -void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, - std::function func) { - const auto now = this->millis_(); +// Common implementation for both timeout and interval +void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, + const void *name_ptr, uint32_t delay, std::function func) { + // Get the name as const char* + const char *name_cstr = + is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); - if (!name.empty()) - this->cancel_timeout(component, name); + // Cancel existing timer if name is not empty + if (name_cstr != nullptr && name_cstr[0] != '\0') { + this->cancel_item_(component, name_cstr, type); + } - if (timeout == SCHEDULER_DONT_RUN) + if (delay == SCHEDULER_DONT_RUN) return; + const auto now = this->millis_(); + + // Create and populate the scheduler item auto item = make_unique(); item->component = component; - item->name = name; - item->type = SchedulerItem::TIMEOUT; - item->next_execution_ = now + timeout; + item->set_name(name_cstr, !is_static_string); + item->type = type; item->callback = std::move(func); item->remove = false; + + // Type-specific setup + if (type == SchedulerItem::INTERVAL) { + item->interval = delay; + // Calculate random offset (0 to interval/2) + uint32_t offset = (delay != 0) ? (random_uint32() % delay) / 2 : 0; + item->next_execution_ = now + offset; + } else { + item->interval = 0; + item->next_execution_ = now + delay; + } + #ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "set_timeout(name='%s/%s', timeout=%" PRIu32 ")", item->get_source(), name.c_str(), timeout); + // Validate static strings in debug mode + if (is_static_string && name_cstr != nullptr) { + validate_static_string(name_cstr); + } + + // Debug logging + const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; + if (type == SchedulerItem::TIMEOUT) { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, item->get_source(), + name_cstr ? name_cstr : "(null)", type_str, delay); + } else { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(), + name_cstr ? name_cstr : "(null)", type_str, delay, static_cast(item->next_execution_ - now)); + } #endif + this->push_(std::move(item)); } + +void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function func) { + this->set_timer_common_(component, SchedulerItem::TIMEOUT, true, name, timeout, std::move(func)); +} + +void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, + std::function func) { + this->set_timer_common_(component, SchedulerItem::TIMEOUT, false, &name, timeout, std::move(func)); +} bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) { return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); } +bool HOT Scheduler::cancel_timeout(Component *component, const char *name) { + return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); +} void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, std::function func) { - const auto now = this->millis_(); + this->set_timer_common_(component, SchedulerItem::INTERVAL, false, &name, interval, std::move(func)); +} - if (!name.empty()) - this->cancel_interval(component, name); - - if (interval == SCHEDULER_DONT_RUN) - return; - - // only put offset in lower half - uint32_t offset = 0; - if (interval != 0) - offset = (random_uint32() % interval) / 2; - - auto item = make_unique(); - item->component = component; - item->name = name; - item->type = SchedulerItem::INTERVAL; - item->interval = interval; - item->next_execution_ = now + offset; - item->callback = std::move(func); - item->remove = false; -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "set_interval(name='%s/%s', interval=%" PRIu32 ", offset=%" PRIu32 ")", item->get_source(), - name.c_str(), interval, offset); -#endif - this->push_(std::move(item)); +void HOT Scheduler::set_interval(Component *component, const char *name, uint32_t interval, + std::function func) { + this->set_timer_common_(component, SchedulerItem::INTERVAL, true, name, interval, std::move(func)); } bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) { return this->cancel_item_(component, name, SchedulerItem::INTERVAL); } +bool HOT Scheduler::cancel_interval(Component *component, const char *name) { + return this->cancel_item_(component, name, SchedulerItem::INTERVAL); +} struct RetryArgs { std::function func; uint8_t retry_countdown; uint32_t current_interval; Component *component; - std::string name; + std::string name; // Keep as std::string since retry uses it dynamically float backoff_increase_factor; Scheduler *scheduler; }; @@ -154,7 +218,7 @@ void HOT Scheduler::call() { if (now - last_print > 2000) { last_print = now; std::vector> old_items; - ESP_LOGD(TAG, "Items: count=%u, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, this->last_millis_); while (!this->empty_()) { this->lock_.lock(); @@ -162,8 +226,9 @@ void HOT Scheduler::call() { this->pop_raw_(); this->lock_.unlock(); + 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(), item->name.c_str(), item->interval, + item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, item->next_execution_ - now, item->next_execution_); old_items.push_back(std::move(item)); @@ -220,9 +285,10 @@ void HOT Scheduler::call() { App.set_current_component(item->component); #ifdef ESPHOME_DEBUG_SCHEDULER + 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.c_str(), item->interval, item->next_execution_, - now); + item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval, + item->next_execution_, now); #endif // Warning: During callback(), a lot of stuff can happen, including: @@ -298,19 +364,33 @@ void HOT Scheduler::push_(std::unique_ptr item) { LockGuard guard{this->lock_}; this->to_add_.push_back(std::move(item)); } -bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { +// Common implementation for cancel operations +bool HOT Scheduler::cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, + SchedulerItem::Type type) { + // Get the name as const char* + const char *name_cstr = + is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); + + // Handle null or empty names + if (name_cstr == nullptr) + return false; + // obtain lock because this function iterates and can be called from non-loop task context LockGuard guard{this->lock_}; bool ret = false; + for (auto &it : this->items_) { - if (it->component == component && it->name == name && it->type == type && !it->remove) { + const char *item_name = it->get_name(); + if (it->component == component && item_name != nullptr && strcmp(name_cstr, item_name) == 0 && it->type == type && + !it->remove) { to_remove_++; it->remove = true; ret = true; } } for (auto &it : this->to_add_) { - if (it->component == component && it->name == name && it->type == type) { + const char *item_name = it->get_name(); + if (it->component == component && item_name != nullptr && strcmp(name_cstr, item_name) == 0 && it->type == type) { it->remove = true; ret = true; } @@ -318,6 +398,15 @@ bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, return ret; } + +bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { + return this->cancel_item_common_(component, false, &name, type); +} + +bool HOT Scheduler::cancel_item_(Component *component, const char *name, SchedulerItem::Type type) { + return this->cancel_item_common_(component, true, name, type); +} + uint64_t Scheduler::millis_() { // Get the current 32-bit millis value const uint32_t now = millis(); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 1284bcd4a7..a64968932e 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -12,11 +12,40 @@ class Component; class Scheduler { public: + // Public API - accepts std::string for backward compatibility void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func); - bool cancel_timeout(Component *component, const std::string &name); - void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func); - bool cancel_interval(Component *component, const std::string &name); + /** Set a timeout with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the scheduled task + * + * For dynamic strings, use the std::string overload instead. + */ + void set_timeout(Component *component, const char *name, uint32_t timeout, std::function func); + + bool cancel_timeout(Component *component, const std::string &name); + bool cancel_timeout(Component *component, const char *name); + + void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func); + + /** Set an interval with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the scheduled task + * + * For dynamic strings, use the std::string overload instead. + */ + void set_interval(Component *component, const char *name, uint32_t interval, std::function func); + + bool cancel_interval(Component *component, const std::string &name); + bool cancel_interval(Component *component, const char *name); void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, std::function func, float backoff_increase_factor = 1.0f); bool cancel_retry(Component *component, const std::string &name); @@ -36,32 +65,86 @@ class Scheduler { // with a 16-bit rollover counter to create a 64-bit time that won't roll over for // billions of years. This ensures correct scheduling even when devices run for months. uint64_t next_execution_; - std::string name; - std::function callback; - enum Type : uint8_t { TIMEOUT, INTERVAL } type; - bool remove; - static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); - const char *get_type_str() { - switch (this->type) { - case SchedulerItem::INTERVAL: - return "interval"; - case SchedulerItem::TIMEOUT: - return "timeout"; - default: - return ""; + // Optimized name storage using tagged union + union { + const char *static_name; // For string literals (no allocation) + char *dynamic_name; // For allocated strings + } name_; + + std::function callback; + + // Bit-packed fields to minimize padding + enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; + bool remove : 1; + bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[]) + // 5 bits padding + + // Constructor + SchedulerItem() + : component(nullptr), interval(0), next_execution_(0), type(TIMEOUT), remove(false), name_is_dynamic(false) { + name_.static_name = nullptr; + } + + // Destructor to clean up dynamic names + ~SchedulerItem() { + if (name_is_dynamic) { + delete[] name_.dynamic_name; } } - const char *get_source() { - return this->component != nullptr ? this->component->get_component_source() : "unknown"; + + // Delete copy operations to prevent accidental copies + SchedulerItem(const SchedulerItem &) = delete; + SchedulerItem &operator=(const SchedulerItem &) = delete; + + // Default move operations + SchedulerItem(SchedulerItem &&) = default; + SchedulerItem &operator=(SchedulerItem &&) = default; + + // Helper to get the name regardless of storage type + const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } + + // Helper to set name with proper ownership + void set_name(const char *name, bool make_copy = false) { + // Clean up old dynamic name if any + if (name_is_dynamic && name_.dynamic_name) { + delete[] name_.dynamic_name; + name_is_dynamic = false; + } + + if (!name || !name[0]) { + name_.static_name = nullptr; + } else if (make_copy) { + // Make a copy for dynamic strings + size_t len = strlen(name); + name_.dynamic_name = new char[len + 1]; + memcpy(name_.dynamic_name, name, len + 1); + name_is_dynamic = true; + } else { + // Use static string directly + name_.static_name = name; + } } + + static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); + const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } + const char *get_source() const { return component ? component->get_component_source() : "unknown"; } }; + // Common implementation for both timeout and interval + void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, + uint32_t delay, std::function func); + uint64_t millis_(); void cleanup_(); void pop_raw_(); void push_(std::unique_ptr item); + // Common implementation for cancel operations + bool cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); + bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type); + bool cancel_item_(Component *component, const char *name, SchedulerItem::Type type); + bool empty_() { this->cleanup_(); return this->items_.empty(); diff --git a/tests/integration/fixtures/scheduler_string_test.yaml b/tests/integration/fixtures/scheduler_string_test.yaml new file mode 100644 index 0000000000..1188577e15 --- /dev/null +++ b/tests/integration/fixtures/scheduler_string_test.yaml @@ -0,0 +1,164 @@ +esphome: + name: scheduler-string-test + on_boot: + priority: -100 + then: + - logger.log: "Starting scheduler string tests" + platformio_options: + build_flags: + - "-DESPHOME_DEBUG_SCHEDULER" # Enable scheduler debug logging + +host: +api: +logger: + level: VERBOSE + +globals: + - id: timeout_counter + type: int + initial_value: '0' + - id: interval_counter + type: int + initial_value: '0' + - id: dynamic_counter + type: int + initial_value: '0' + - id: static_tests_done + type: bool + initial_value: 'false' + - id: dynamic_tests_done + type: bool + initial_value: 'false' + - id: results_reported + type: bool + initial_value: 'false' + +script: + - id: test_static_strings + then: + - logger.log: "Testing static string timeouts and intervals" + - lambda: |- + auto *component1 = id(test_sensor1); + // Test 1: Static string literals with set_timeout + App.scheduler.set_timeout(component1, "static_timeout_1", 50, []() { + ESP_LOGI("test", "Static timeout 1 fired"); + id(timeout_counter) += 1; + }); + + // Test 2: Static const char* with set_timeout + static const char* TIMEOUT_NAME = "static_timeout_2"; + App.scheduler.set_timeout(component1, TIMEOUT_NAME, 100, []() { + ESP_LOGI("test", "Static timeout 2 fired"); + id(timeout_counter) += 1; + }); + + // Test 3: Static string literal with set_interval + App.scheduler.set_interval(component1, "static_interval_1", 200, []() { + ESP_LOGI("test", "Static interval 1 fired, count: %d", id(interval_counter)); + id(interval_counter) += 1; + if (id(interval_counter) >= 3) { + App.scheduler.cancel_interval(id(test_sensor1), "static_interval_1"); + ESP_LOGI("test", "Cancelled static interval 1"); + } + }); + + // Test 4: Empty string (should be handled safely) + App.scheduler.set_timeout(component1, "", 150, []() { + ESP_LOGI("test", "Empty string timeout fired"); + }); + + // Test 5: Cancel timeout with const char* literal + App.scheduler.set_timeout(component1, "cancel_static_timeout", 5000, []() { + ESP_LOGI("test", "This static timeout should be cancelled"); + }); + // Cancel using const char* directly + App.scheduler.cancel_timeout(component1, "cancel_static_timeout"); + ESP_LOGI("test", "Cancelled static timeout using const char*"); + + - id: test_dynamic_strings + then: + - logger.log: "Testing dynamic string timeouts and intervals" + - lambda: |- + auto *component2 = id(test_sensor2); + + // Test 6: Dynamic string with set_timeout (std::string) + std::string dynamic_name = "dynamic_timeout_" + std::to_string(id(dynamic_counter)++); + App.scheduler.set_timeout(component2, dynamic_name, 100, []() { + ESP_LOGI("test", "Dynamic timeout fired"); + id(timeout_counter) += 1; + }); + + // Test 7: Dynamic string with set_interval + std::string interval_name = "dynamic_interval_" + std::to_string(id(dynamic_counter)++); + App.scheduler.set_interval(component2, interval_name, 250, [interval_name]() { + ESP_LOGI("test", "Dynamic interval fired: %s", interval_name.c_str()); + id(interval_counter) += 1; + if (id(interval_counter) >= 6) { + App.scheduler.cancel_interval(id(test_sensor2), interval_name); + ESP_LOGI("test", "Cancelled dynamic interval"); + } + }); + + // Test 8: Cancel with different string object but same content + std::string cancel_name = "cancel_test"; + App.scheduler.set_timeout(component2, cancel_name, 2000, []() { + ESP_LOGI("test", "This should be cancelled"); + }); + + // Cancel using a different string object + std::string cancel_name_2 = "cancel_test"; + App.scheduler.cancel_timeout(component2, cancel_name_2); + ESP_LOGI("test", "Cancelled timeout using different string object"); + + - id: report_results + then: + - lambda: |- + ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d", + id(timeout_counter), id(interval_counter)); + +sensor: + - platform: template + name: Test Sensor 1 + id: test_sensor1 + lambda: return 1.0; + update_interval: never + + - platform: template + name: Test Sensor 2 + id: test_sensor2 + lambda: return 2.0; + update_interval: never + +interval: + # Run static string tests after boot - using script to run once + - interval: 0.1s + then: + - if: + condition: + lambda: 'return id(static_tests_done) == false;' + then: + - lambda: 'id(static_tests_done) = true;' + - script.execute: test_static_strings + - logger.log: "Started static string tests" + + # Run dynamic string tests after static tests + - interval: 0.2s + then: + - if: + condition: + lambda: 'return id(static_tests_done) && !id(dynamic_tests_done);' + then: + - lambda: 'id(dynamic_tests_done) = true;' + - delay: 0.2s + - script.execute: test_dynamic_strings + + # Report results after all tests + - interval: 0.2s + then: + - if: + condition: + lambda: 'return id(dynamic_tests_done) && !id(results_reported);' + then: + - lambda: 'id(results_reported) = true;' + - delay: 1s + - script.execute: report_results diff --git a/tests/integration/test_scheduler_string_test.py b/tests/integration/test_scheduler_string_test.py new file mode 100644 index 0000000000..b5ca07f9db --- /dev/null +++ b/tests/integration/test_scheduler_string_test.py @@ -0,0 +1,166 @@ +"""Test scheduler string optimization with static and dynamic strings.""" + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_string_test( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that scheduler handles both static and dynamic strings correctly.""" + # Track counts + timeout_count = 0 + interval_count = 0 + + # Events for each test completion + static_timeout_1_fired = asyncio.Event() + static_timeout_2_fired = asyncio.Event() + static_interval_fired = asyncio.Event() + static_interval_cancelled = asyncio.Event() + empty_string_timeout_fired = asyncio.Event() + static_timeout_cancelled = asyncio.Event() + dynamic_timeout_fired = asyncio.Event() + dynamic_interval_fired = asyncio.Event() + cancel_test_done = asyncio.Event() + final_results_logged = asyncio.Event() + + # Track interval counts + static_interval_count = 0 + dynamic_interval_count = 0 + + def on_log_line(line: str) -> None: + nonlocal \ + timeout_count, \ + interval_count, \ + static_interval_count, \ + dynamic_interval_count + + # Strip ANSI color codes + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + + # Check for static timeout completions + if "Static timeout 1 fired" in clean_line: + static_timeout_1_fired.set() + timeout_count += 1 + + elif "Static timeout 2 fired" in clean_line: + static_timeout_2_fired.set() + timeout_count += 1 + + # Check for static interval + elif "Static interval 1 fired" in clean_line: + match = re.search(r"count: (\d+)", clean_line) + if match: + static_interval_count = int(match.group(1)) + static_interval_fired.set() + + elif "Cancelled static interval 1" in clean_line: + static_interval_cancelled.set() + + # Check for empty string timeout + elif "Empty string timeout fired" in clean_line: + empty_string_timeout_fired.set() + + # Check for static timeout cancellation + elif "Cancelled static timeout using const char*" in clean_line: + static_timeout_cancelled.set() + + # Check for dynamic string tests + elif "Dynamic timeout fired" in clean_line: + dynamic_timeout_fired.set() + timeout_count += 1 + + elif "Dynamic interval fired" in clean_line: + dynamic_interval_count += 1 + dynamic_interval_fired.set() + + # Check for cancel test + elif "Cancelled timeout using different string object" in clean_line: + cancel_test_done.set() + + # Check for final results + elif "Final results" in clean_line: + match = re.search(r"Timeouts: (\d+), Intervals: (\d+)", clean_line) + if match: + timeout_count = int(match.group(1)) + interval_count = int(match.group(2)) + final_results_logged.set() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-string-test" + + # Wait for static string tests + try: + await asyncio.wait_for(static_timeout_1_fired.wait(), timeout=0.5) + except asyncio.TimeoutError: + pytest.fail("Static timeout 1 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(static_timeout_2_fired.wait(), timeout=0.5) + except asyncio.TimeoutError: + pytest.fail("Static timeout 2 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(static_interval_fired.wait(), timeout=1.0) + except asyncio.TimeoutError: + pytest.fail("Static interval did not fire within 1 second") + + try: + await asyncio.wait_for(static_interval_cancelled.wait(), timeout=2.0) + except asyncio.TimeoutError: + pytest.fail("Static interval was not cancelled within 2 seconds") + + # Verify static interval ran at least 3 times + assert static_interval_count >= 2, ( + f"Expected static interval to run at least 3 times, got {static_interval_count + 1}" + ) + + # Verify static timeout was cancelled + assert static_timeout_cancelled.is_set(), ( + "Static timeout should have been cancelled" + ) + + # Wait for dynamic string tests + try: + await asyncio.wait_for(dynamic_timeout_fired.wait(), timeout=1.0) + except asyncio.TimeoutError: + pytest.fail("Dynamic timeout did not fire within 1 second") + + try: + await asyncio.wait_for(dynamic_interval_fired.wait(), timeout=1.5) + except asyncio.TimeoutError: + pytest.fail("Dynamic interval did not fire within 1.5 seconds") + + # Wait for cancel test + try: + await asyncio.wait_for(cancel_test_done.wait(), timeout=1.0) + except asyncio.TimeoutError: + pytest.fail("Cancel test did not complete within 1 second") + + # Wait for final results + try: + await asyncio.wait_for(final_results_logged.wait(), timeout=4.0) + except asyncio.TimeoutError: + pytest.fail("Final results were not logged within 4 seconds") + + # Verify results + assert timeout_count >= 3, f"Expected at least 3 timeouts, got {timeout_count}" + assert interval_count >= 3, ( + f"Expected at least 3 interval fires, got {interval_count}" + ) + + # Empty string timeout DOES fire (scheduler accepts empty names) + assert empty_string_timeout_fired.is_set(), "Empty string timeout should fire"