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 01/74] [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 02/74] [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 03/74] [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 04/74] [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 05/74] [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 06/74] [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 07/74] 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 08/74] [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 09/74] 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 10/74] 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 11/74] 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 12/74] 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 13/74] [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 14/74] [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 15/74] 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 16/74] 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 17/74] 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 18/74] 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 19/74] [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 20/74] [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 21/74] [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 22/74] 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 23/74] 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 24/74] 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 25/74] [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 26/74] 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 27/74] [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 28/74] 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 29/74] [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 30/74] 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 31/74] 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 32/74] 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 33/74] 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 34/74] [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 35/74] [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 36/74] [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 37/74] [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 38/74] [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 39/74] [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 40/74] [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 41/74] [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 42/74] [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 43/74] 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 44/74] 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 45/74] [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 46/74] [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 47/74] [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 48/74] 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 49/74] 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 50/74] 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 51/74] 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 52/74] 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 53/74] 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 54/74] 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 55/74] 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 56/74] [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 57/74] [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 58/74] [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 59/74] [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 60/74] [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 61/74] [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 62/74] [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 63/74] [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 64/74] [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 65/74] [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 66/74] 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 67/74] [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 68/74] 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 69/74] [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 70/74] [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 71/74] 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 72/74] [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 73/74] 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 74/74] [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)