From 3e17767f6a310d10e33e74849e61786bfabeab78 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:50:15 +1000 Subject: [PATCH 001/461] [font][image] Use ESPHome urls for remote images (#11675) --- tests/components/font/common.yaml | 4 ++-- tests/components/font/test.host.yaml | 4 ++-- tests/components/image/common.yaml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/components/font/common.yaml b/tests/components/font/common.yaml index 6ba52e3d975..c156b4aea19 100644 --- a/tests/components/font/common.yaml +++ b/tests/components/font/common.yaml @@ -21,12 +21,12 @@ font: id: roboto_greek size: 20 glyphs: ["\u0300", "\u00C5", "\U000000C7"] - - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + - file: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft size: 20 - file: type: web - url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + url: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft2 size: 24 - file: $component_dir/Monocraft.ttf diff --git a/tests/components/font/test.host.yaml b/tests/components/font/test.host.yaml index c5399f28260..387ea47335d 100644 --- a/tests/components/font/test.host.yaml +++ b/tests/components/font/test.host.yaml @@ -21,12 +21,12 @@ font: id: roboto_greek size: 20 glyphs: ["\u0300", "\u00C5", "\U000000C7"] - - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + - file: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft size: 20 - file: type: web - url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + url: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft2 size: 24 - file: $component_dir/Monocraft.ttf diff --git a/tests/components/image/common.yaml b/tests/components/image/common.yaml index 864ca41c44a..98190689708 100644 --- a/tests/components/image/common.yaml +++ b/tests/components/image/common.yaml @@ -50,16 +50,16 @@ image: transparency: opaque - id: web_svg_image - file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg + file: https://media.esphome.io/logo/logo.svg resize: 256x48 type: BINARY transparency: chroma_key - id: web_tiff_image - file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff + file: https://media.esphome.io/tests/images/SIPI_Jelly_Beans_4.1.07.tiff type: RGB resize: 48x48 - id: web_redirect_image - file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4 + file: https://media.esphome.io/logo/logo.png type: RGB resize: 48x48 - id: mdi_alert From 1509ed8d2391f0cdd16e65b703e2979006515200 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 19:04:06 -0600 Subject: [PATCH 002/461] [esphome][ota] Add write_byte_() helper to reduce code duplication (#11511) --- .../components/esphome/ota/ota_esphome.cpp | 28 ++++++------------- esphome/components/esphome/ota/ota_esphome.h | 1 + 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 569268ea158..b85d6602726 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -281,19 +281,15 @@ void ESPHomeOTAComponent::handle_data_() { #endif // Acknowledge auth OK - 1 byte - buf[0] = ota::OTA_RESPONSE_AUTH_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); // Read size, 4 bytes MSB first if (!this->readall_(buf, 4)) { this->log_read_error_(LOG_STR("size")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } - ota_size = 0; - for (uint8_t i = 0; i < 4; i++) { - ota_size <<= 8; - ota_size |= buf[i]; - } + ota_size = (static_cast(buf[0]) << 24) | (static_cast(buf[1]) << 16) | + (static_cast(buf[2]) << 8) | buf[3]; ESP_LOGV(TAG, "Size is %u bytes", ota_size); // Now that we've passed authentication and are actually @@ -313,8 +309,7 @@ void ESPHomeOTAComponent::handle_data_() { update_started = true; // Acknowledge prepare OK - 1 byte - buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK); // Read binary MD5, 32 bytes if (!this->readall_(buf, 32)) { @@ -326,8 +321,7 @@ void ESPHomeOTAComponent::handle_data_() { this->backend_->set_update_md5(sbuf); // Acknowledge MD5 OK - 1 byte - buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK); while (total < ota_size) { // TODO: timeout check @@ -354,8 +348,7 @@ void ESPHomeOTAComponent::handle_data_() { total += read; #if USE_OTA_VERSION == 2 while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) { - buf[0] = ota::OTA_RESPONSE_CHUNK_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK); size_acknowledged += OTA_BLOCK_SIZE; } #endif @@ -374,8 +367,7 @@ void ESPHomeOTAComponent::handle_data_() { } // Acknowledge receive OK - 1 byte - buf[0] = ota::OTA_RESPONSE_RECEIVE_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK); error_code = this->backend_->end(); if (error_code != ota::OTA_RESPONSE_OK) { @@ -384,8 +376,7 @@ void ESPHomeOTAComponent::handle_data_() { } // Acknowledge Update end OK - 1 byte - buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK; - this->writeall_(buf, 1); + this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK); // Read ACK if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { @@ -404,8 +395,7 @@ void ESPHomeOTAComponent::handle_data_() { App.safe_reboot(); error: - buf[0] = static_cast(error_code); - this->writeall_(buf, 1); + this->write_byte_(static_cast(error_code)); this->cleanup_connection_(); if (this->backend_ != nullptr && update_started) { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index d4a8410d357..057461e6a41 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -53,6 +53,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { #endif // USE_OTA_PASSWORD bool readall_(uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len); + inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); } bool try_read_(size_t to_read, const LogString *desc); bool try_write_(size_t to_write, const LogString *desc); From 17ab20ef6108bcf4c3c2f67d2f022cbe71d9c00d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 19:05:36 -0600 Subject: [PATCH 003/461] [esp32_ble] Optimize loop() to reduce flash usage by ~104 bytes (#11627) --- esphome/components/esp32_ble/ble.cpp | 120 +++++++++++++-------------- 1 file changed, 59 insertions(+), 61 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 5bbd5fe9ed0..69e317ff6de 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -31,6 +31,26 @@ namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; +// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_ +#define GAP_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 + +#define GAP_ADV_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 + +#define GAP_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 + void ESP32BLE::setup() { global_ble = this; if (!ble_pre_setup_()) { @@ -414,60 +434,48 @@ void ESP32BLE::loop() { 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); -#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.scan_complete)); - } -#endif - break; - + GAP_SCAN_COMPLETE_EVENTS: // 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); -#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.adv_complete)); - } -#endif - break; - + GAP_ADV_COMPLETE_EVENTS: // RSSI complete event case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: - ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); -#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.read_rssi_complete)); - } -#endif - 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: + GAP_SECURITY_EVENTS: ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler( - gap_event, reinterpret_cast(&ble_event->event_.gap.security)); + { + esp_ble_gap_cb_param_t *param; + // clang-format off + switch (gap_event) { + // 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) + GAP_SCAN_COMPLETE_EVENTS: + param = reinterpret_cast(&ble_event->event_.gap.scan_complete); + break; + + // All advertising complete events have the same structure with just status + GAP_ADV_COMPLETE_EVENTS: + param = reinterpret_cast(&ble_event->event_.gap.adv_complete); + break; + + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + param = reinterpret_cast(&ble_event->event_.gap.read_rssi_complete); + break; + + GAP_SECURITY_EVENTS: + param = reinterpret_cast(&ble_event->event_.gap.security); + break; + + default: + break; + } + // clang-format on + // Dispatch to all registered handlers + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler(gap_event, param); + } } #endif break; @@ -547,23 +555,13 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa // 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: + GAP_SCAN_COMPLETE_EVENTS: // 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: + GAP_ADV_COMPLETE_EVENTS: // 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: + GAP_SECURITY_EVENTS: enqueue_ble_event(event, param); return; From 01ae86145a7e15f8c537798a6f6f3050596bcf99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 19:06:40 -0600 Subject: [PATCH 004/461] [ble_client] Fix premature disconnections by reading characteristics immediately after service discovery (#11410) --- esphome/components/ble_client/sensor/ble_sensor.cpp | 3 +++ esphome/components/ble_client/text_sensor/ble_text_sensor.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index 6d293528c68..61685c05665 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -77,6 +77,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga } } else { this->node_state = espbt::ClientState::ESTABLISHED; + // For non-notify characteristics, trigger an immediate read after service discovery + // to avoid peripherals disconnecting due to inactivity + this->update(); } break; } diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index e7da297fa05..b7a6d154dbb 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -79,6 +79,9 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } } else { this->node_state = espbt::ClientState::ESTABLISHED; + // For non-notify characteristics, trigger an immediate read after service discovery + // to avoid peripherals disconnecting due to inactivity + this->update(); } break; } From 40f919eaa6f3fb60d461f745220d414e66ca80bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 19:07:03 -0600 Subject: [PATCH 005/461] Add action continuation tests (#11674) --- tests/components/api/common-base.yaml | 96 +++++++ .../fixtures/continuation_actions.yaml | 174 +++++++++++++ .../integration/test_continuation_actions.py | 235 ++++++++++++++++++ 3 files changed, 505 insertions(+) create mode 100644 tests/integration/fixtures/continuation_actions.yaml create mode 100644 tests/integration/test_continuation_actions.py diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml index 6483d5a9975..c90fa4dfefc 100644 --- a/tests/components/api/common-base.yaml +++ b/tests/components/api/common-base.yaml @@ -87,3 +87,99 @@ api: - float_arr.size() - string_arr[0].c_str() - string_arr.size() + # Test ContinuationAction (IfAction with then/else branches) + - action: test_if_action + variables: + condition: bool + value: int + then: + - if: + condition: + lambda: 'return condition;' + then: + - logger.log: + format: "Condition true, value: %d" + args: ['value'] + else: + - logger.log: + format: "Condition false, value: %d" + args: ['value'] + - logger.log: "After if/else" + # Test nested IfAction (multiple ContinuationAction instances) + - action: test_nested_if + variables: + outer: bool + inner: bool + then: + - if: + condition: + lambda: 'return outer;' + then: + - if: + condition: + lambda: 'return inner;' + then: + - logger.log: "Both true" + else: + - logger.log: "Outer true, inner false" + else: + - logger.log: "Outer false" + - logger.log: "After nested if" + # Test WhileLoopContinuation (WhileAction) + - action: test_while_action + variables: + max_count: int + then: + - lambda: 'id(api_continuation_test_counter) = 0;' + - while: + condition: + lambda: 'return id(api_continuation_test_counter) < max_count;' + then: + - logger.log: + format: "While loop iteration: %d" + args: ['id(api_continuation_test_counter)'] + - lambda: 'id(api_continuation_test_counter)++;' + - logger.log: "After while loop" + # Test RepeatLoopContinuation (RepeatAction) + - action: test_repeat_action + variables: + count: int + then: + - repeat: + count: !lambda 'return count;' + then: + - logger.log: + format: "Repeat iteration: %d" + args: ['iteration'] + - logger.log: "After repeat" + # Test combined continuations (if + while + repeat) + - action: test_combined_continuations + variables: + do_loop: bool + loop_count: int + then: + - if: + condition: + lambda: 'return do_loop;' + then: + - repeat: + count: !lambda 'return loop_count;' + then: + - lambda: 'id(api_continuation_test_counter) = iteration;' + - while: + condition: + lambda: 'return id(api_continuation_test_counter) > 0;' + then: + - logger.log: + format: "Combined: repeat=%d, while=%d" + args: ['iteration', 'id(api_continuation_test_counter)'] + - lambda: 'id(api_continuation_test_counter)--;' + else: + - logger.log: "Skipped loops" + - logger.log: "After combined test" + +globals: + - id: api_continuation_test_counter + type: int + restore_value: false + initial_value: '0' diff --git a/tests/integration/fixtures/continuation_actions.yaml b/tests/integration/fixtures/continuation_actions.yaml new file mode 100644 index 00000000000..bdfe149cb7b --- /dev/null +++ b/tests/integration/fixtures/continuation_actions.yaml @@ -0,0 +1,174 @@ +esphome: + name: test-continuation-actions + +host: + +api: + actions: + # Test 1: IfAction with ContinuationAction (then/else branches) + - action: test_if_action + variables: + condition: bool + value: int + then: + - logger.log: + format: "Test if: condition=%s, value=%d" + args: ['YESNO(condition)', 'value'] + - if: + condition: + lambda: 'return condition;' + then: + - logger.log: + format: "if-then executed: value=%d" + args: ['value'] + else: + - logger.log: + format: "if-else executed: value=%d" + args: ['value'] + - logger.log: "if completed" + + # Test 2: Nested IfAction (multiple ContinuationAction instances) + - action: test_nested_if + variables: + outer: bool + inner: bool + then: + - logger.log: + format: "Test nested if: outer=%s, inner=%s" + args: ['YESNO(outer)', 'YESNO(inner)'] + - if: + condition: + lambda: 'return outer;' + then: + - if: + condition: + lambda: 'return inner;' + then: + - logger.log: "nested-both-true" + else: + - logger.log: "nested-outer-true-inner-false" + else: + - logger.log: "nested-outer-false" + - logger.log: "nested if completed" + + # Test 3: WhileAction with WhileLoopContinuation + - action: test_while_action + variables: + max_count: int + then: + - logger.log: + format: "Test while: max_count=%d" + args: ['max_count'] + - globals.set: + id: continuation_test_counter + value: !lambda 'return 0;' + - while: + condition: + lambda: 'return id(continuation_test_counter) < max_count;' + then: + - logger.log: + format: "while-iteration-%d" + args: ['id(continuation_test_counter)'] + - globals.set: + id: continuation_test_counter + value: !lambda 'return id(continuation_test_counter) + 1;' + - logger.log: "while completed" + + # Test 4: RepeatAction with RepeatLoopContinuation + - action: test_repeat_action + variables: + count: int + then: + - logger.log: + format: "Test repeat: count=%d" + args: ['count'] + - repeat: + count: !lambda 'return count;' + then: + - logger.log: + format: "repeat-iteration-%d" + args: ['iteration'] + - logger.log: "repeat completed" + + # Test 5: Combined continuations (if + while + repeat) + - action: test_combined + variables: + do_loop: bool + loop_count: int + then: + - logger.log: + format: "Test combined: do_loop=%s, loop_count=%d" + args: ['YESNO(do_loop)', 'loop_count'] + - if: + condition: + lambda: 'return do_loop;' + then: + - repeat: + count: !lambda 'return loop_count;' + then: + - globals.set: + id: continuation_test_counter + value: !lambda 'return iteration;' + - while: + condition: + lambda: 'return id(continuation_test_counter) > 0;' + then: + - logger.log: + format: "combined-repeat%d-while%d" + args: ['iteration', 'id(continuation_test_counter)'] + - globals.set: + id: continuation_test_counter + value: !lambda 'return id(continuation_test_counter) - 1;' + else: + - logger.log: "combined-skipped" + - logger.log: "combined completed" + + # Test 6: Rapid triggers to verify memory efficiency + - action: test_rapid_if + then: + - logger.log: "=== Rapid if test start ===" + - sensor.template.publish: + id: rapid_sensor + state: 1 + - sensor.template.publish: + id: rapid_sensor + state: 2 + - sensor.template.publish: + id: rapid_sensor + state: 3 + - sensor.template.publish: + id: rapid_sensor + state: 4 + - sensor.template.publish: + id: rapid_sensor + state: 5 + - logger.log: "=== Rapid if test published 5 values ===" + +logger: + level: DEBUG + +globals: + - id: continuation_test_counter + type: int + restore_value: false + initial_value: '0' + +# Sensor to test rapid automation triggers with if/else (ContinuationAction) +sensor: + - platform: template + id: rapid_sensor + on_value: + - if: + condition: + lambda: 'return x > 2;' + then: + - logger.log: + format: "rapid-if-then: value=%d" + args: ['(int)x'] + else: + - logger.log: + format: "rapid-if-else: value=%d" + args: ['(int)x'] + - logger.log: + format: "rapid-if-completed: value=%d" + args: ['(int)x'] diff --git a/tests/integration/test_continuation_actions.py b/tests/integration/test_continuation_actions.py new file mode 100644 index 00000000000..1069ee7581f --- /dev/null +++ b/tests/integration/test_continuation_actions.py @@ -0,0 +1,235 @@ +"""Test continuation actions (ContinuationAction, WhileLoopContinuation, RepeatLoopContinuation).""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_continuation_actions( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """ + Test that continuation actions work correctly for if/while/repeat. + + These continuation classes replace LambdaAction with simple parent pointers, + saving 32-36 bytes per instance and eliminating std::function overhead. + """ + loop = asyncio.get_running_loop() + + # Track test completions + test_results = { + "if_then": False, + "if_else": False, + "if_complete": False, + "nested_both_true": False, + "nested_outer_true_inner_false": False, + "nested_outer_false": False, + "nested_complete": False, + "while_iterations": 0, + "while_complete": False, + "repeat_iterations": 0, + "repeat_complete": False, + "combined_iterations": 0, + "combined_complete": False, + "rapid_then": 0, + "rapid_else": 0, + "rapid_complete": 0, + } + + # Patterns for log messages + if_then_pattern = re.compile(r"if-then executed: value=(\d+)") + if_else_pattern = re.compile(r"if-else executed: value=(\d+)") + if_complete_pattern = re.compile(r"if completed") + nested_both_true_pattern = re.compile(r"nested-both-true") + nested_outer_true_inner_false_pattern = re.compile(r"nested-outer-true-inner-false") + nested_outer_false_pattern = re.compile(r"nested-outer-false") + nested_complete_pattern = re.compile(r"nested if completed") + while_iteration_pattern = re.compile(r"while-iteration-(\d+)") + while_complete_pattern = re.compile(r"while completed") + repeat_iteration_pattern = re.compile(r"repeat-iteration-(\d+)") + repeat_complete_pattern = re.compile(r"repeat completed") + combined_pattern = re.compile(r"combined-repeat(\d+)-while(\d+)") + combined_complete_pattern = re.compile(r"combined completed") + rapid_then_pattern = re.compile(r"rapid-if-then: value=(\d+)") + rapid_else_pattern = re.compile(r"rapid-if-else: value=(\d+)") + rapid_complete_pattern = re.compile(r"rapid-if-completed: value=(\d+)") + + # Test completion futures + test1_complete = loop.create_future() # if action + test2_complete = loop.create_future() # nested if + test3_complete = loop.create_future() # while + test4_complete = loop.create_future() # repeat + test5_complete = loop.create_future() # combined + test6_complete = loop.create_future() # rapid + + def check_output(line: str) -> None: + """Check log output for test messages.""" + # Test 1: IfAction + if if_then_pattern.search(line): + test_results["if_then"] = True + if if_else_pattern.search(line): + test_results["if_else"] = True + if if_complete_pattern.search(line): + test_results["if_complete"] = True + if not test1_complete.done(): + test1_complete.set_result(True) + + # Test 2: Nested IfAction + if nested_both_true_pattern.search(line): + test_results["nested_both_true"] = True + if nested_outer_true_inner_false_pattern.search(line): + test_results["nested_outer_true_inner_false"] = True + if nested_outer_false_pattern.search(line): + test_results["nested_outer_false"] = True + if nested_complete_pattern.search(line): + test_results["nested_complete"] = True + if not test2_complete.done(): + test2_complete.set_result(True) + + # Test 3: WhileAction + if match := while_iteration_pattern.search(line): + test_results["while_iterations"] = max( + test_results["while_iterations"], int(match.group(1)) + 1 + ) + if while_complete_pattern.search(line): + test_results["while_complete"] = True + if not test3_complete.done(): + test3_complete.set_result(True) + + # Test 4: RepeatAction + if match := repeat_iteration_pattern.search(line): + test_results["repeat_iterations"] = max( + test_results["repeat_iterations"], int(match.group(1)) + 1 + ) + if repeat_complete_pattern.search(line): + test_results["repeat_complete"] = True + if not test4_complete.done(): + test4_complete.set_result(True) + + # Test 5: Combined + if combined_pattern.search(line): + test_results["combined_iterations"] += 1 + if combined_complete_pattern.search(line): + test_results["combined_complete"] = True + if not test5_complete.done(): + test5_complete.set_result(True) + + # Test 6: Rapid triggers + if rapid_then_pattern.search(line): + test_results["rapid_then"] += 1 + if rapid_else_pattern.search(line): + test_results["rapid_else"] += 1 + if rapid_complete_pattern.search(line): + test_results["rapid_complete"] += 1 + if test_results["rapid_complete"] == 5 and not test6_complete.done(): + test6_complete.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get services + _, services = await client.list_entities_services() + + # Test 1: IfAction with then branch + test_service = next((s for s in services if s.name == "test_if_action"), None) + assert test_service is not None, "test_if_action service not found" + client.execute_service(test_service, {"condition": True, "value": 42}) + await asyncio.wait_for(test1_complete, timeout=2.0) + assert test_results["if_then"], "IfAction then branch not executed" + assert test_results["if_complete"], "IfAction did not complete" + + # Test 1b: IfAction with else branch + test1_complete = loop.create_future() + test_results["if_complete"] = False + client.execute_service(test_service, {"condition": False, "value": 99}) + await asyncio.wait_for(test1_complete, timeout=2.0) + assert test_results["if_else"], "IfAction else branch not executed" + assert test_results["if_complete"], "IfAction did not complete" + + # Test 2: Nested IfAction - test all branches + test_service = next((s for s in services if s.name == "test_nested_if"), None) + assert test_service is not None, "test_nested_if service not found" + + # Both true + client.execute_service(test_service, {"outer": True, "inner": True}) + await asyncio.wait_for(test2_complete, timeout=2.0) + assert test_results["nested_both_true"], "Nested both true not executed" + + # Outer true, inner false + test2_complete = loop.create_future() + test_results["nested_complete"] = False + client.execute_service(test_service, {"outer": True, "inner": False}) + await asyncio.wait_for(test2_complete, timeout=2.0) + assert test_results["nested_outer_true_inner_false"], ( + "Nested outer true inner false not executed" + ) + + # Outer false + test2_complete = loop.create_future() + test_results["nested_complete"] = False + client.execute_service(test_service, {"outer": False, "inner": True}) + await asyncio.wait_for(test2_complete, timeout=2.0) + assert test_results["nested_outer_false"], "Nested outer false not executed" + + # Test 3: WhileAction + test_service = next( + (s for s in services if s.name == "test_while_action"), None + ) + assert test_service is not None, "test_while_action service not found" + client.execute_service(test_service, {"max_count": 3}) + await asyncio.wait_for(test3_complete, timeout=2.0) + assert test_results["while_iterations"] == 3, ( + f"WhileAction expected 3 iterations, got {test_results['while_iterations']}" + ) + assert test_results["while_complete"], "WhileAction did not complete" + + # Test 4: RepeatAction + test_service = next( + (s for s in services if s.name == "test_repeat_action"), None + ) + assert test_service is not None, "test_repeat_action service not found" + client.execute_service(test_service, {"count": 5}) + await asyncio.wait_for(test4_complete, timeout=2.0) + assert test_results["repeat_iterations"] == 5, ( + f"RepeatAction expected 5 iterations, got {test_results['repeat_iterations']}" + ) + assert test_results["repeat_complete"], "RepeatAction did not complete" + + # Test 5: Combined (if + repeat + while) + test_service = next((s for s in services if s.name == "test_combined"), None) + assert test_service is not None, "test_combined service not found" + client.execute_service(test_service, {"do_loop": True, "loop_count": 2}) + await asyncio.wait_for(test5_complete, timeout=2.0) + # Should execute: repeat 2 times, each iteration does while from iteration down to 0 + # iteration 0: while 0 times = 0 + # iteration 1: while 1 time = 1 + # Total: 1 combined log + assert test_results["combined_iterations"] >= 1, ( + f"Combined expected >=1 iterations, got {test_results['combined_iterations']}" + ) + assert test_results["combined_complete"], "Combined did not complete" + + # Test 6: Rapid triggers (tests memory efficiency of ContinuationAction) + test_service = next((s for s in services if s.name == "test_rapid_if"), None) + assert test_service is not None, "test_rapid_if service not found" + client.execute_service(test_service, {}) + await asyncio.wait_for(test6_complete, timeout=2.0) + # Values 1, 2 should hit else (<=2), values 3, 4, 5 should hit then (>2) + assert test_results["rapid_else"] == 2, ( + f"Rapid test expected 2 else, got {test_results['rapid_else']}" + ) + assert test_results["rapid_then"] == 3, ( + f"Rapid test expected 3 then, got {test_results['rapid_then']}" + ) + assert test_results["rapid_complete"] == 5, ( + f"Rapid test expected 5 completions, got {test_results['rapid_complete']}" + ) From 7a1297ec8428025075b48d52f3d32c1ccd8de8e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 19:08:12 -0600 Subject: [PATCH 006/461] [web_server] Remove redundant assignment in deq_push_back_with_dedup_ (#11642) --- esphome/components/web_server/web_server.cpp | 3 +-- esphome/components/web_server_idf/web_server_idf.cpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1d08ef5a358..61951e26008 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -111,8 +111,7 @@ void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_ // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size for (auto &event : this->deferred_queue_) { if (event == item) { - event = item; - return; + return; // Already in queue, no need to update since items are equal } } this->deferred_queue_.push_back(item); diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index c3ba7ddc2b2..ac0b5bad83b 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -494,8 +494,7 @@ void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_g // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size for (auto &event : this->deferred_queue_) { if (event == item) { - event = item; - return; + return; // Already in queue, no need to update since items are equal } } this->deferred_queue_.push_back(item); From 712421b82b6dc241329f25993d5500656e500f1b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 19:10:18 -0600 Subject: [PATCH 007/461] [web_server] Eliminate nested lambdas in DeferredUpdateEventSourceList (#11641) --- esphome/components/web_server/web_server.cpp | 61 ++++++++++---------- esphome/components/web_server/web_server.h | 2 +- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 61951e26008..cc62b019c30 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -219,50 +219,51 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer DeferredUpdateEventSource *es = new DeferredUpdateEventSource(ws, "/events"); this->push_back(es); - es->onConnect([this, ws, es](AsyncEventSourceClient *client) { - ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); }); - }); + es->onConnect([this, es](AsyncEventSourceClient *client) { this->on_client_connect_(es); }); - es->onDisconnect([this, ws, es](AsyncEventSourceClient *client) { - ws->defer([this, es]() { this->on_client_disconnect_((DeferredUpdateEventSource *) es); }); - }); + es->onDisconnect([this, es](AsyncEventSourceClient *client) { this->on_client_disconnect_(es); }); es->handleRequest(request); } -void DeferredUpdateEventSourceList::on_client_connect_(WebServer *ws, DeferredUpdateEventSource *source) { - // Configure reconnect timeout and send config - // this should always go through since the AsyncEventSourceClient event queue is empty on connect - std::string message = ws->get_config_json(); - source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); +void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource *source) { + WebServer *ws = source->web_server_; + ws->defer([ws, source]() { + // Configure reconnect timeout and send config + // this should always go through since the AsyncEventSourceClient event queue is empty on connect + std::string message = ws->get_config_json(); + source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); #ifdef USE_WEBSERVER_SORTING - for (auto &group : ws->sorting_groups_) { - json::JsonBuilder builder; - JsonObject root = builder.root(); - root["name"] = group.second.name; - root["sorting_weight"] = group.second.weight; - message = builder.serialize(); + for (auto &group : ws->sorting_groups_) { + json::JsonBuilder builder; + JsonObject root = builder.root(); + root["name"] = group.second.name; + root["sorting_weight"] = group.second.weight; + message = builder.serialize(); - // up to 31 groups should be able to be queued initially without defer - source->try_send_nodefer(message.c_str(), "sorting_group"); - } + // up to 31 groups should be able to be queued initially without defer + source->try_send_nodefer(message.c_str(), "sorting_group"); + } #endif - source->entities_iterator_.begin(ws->include_internal_); + source->entities_iterator_.begin(ws->include_internal_); - // just dump them all up-front and take advantage of the deferred queue - // on second thought that takes too long, but leaving the commented code here for debug purposes - // while(!source->entities_iterator_.completed()) { - // source->entities_iterator_.advance(); - //} + // just dump them all up-front and take advantage of the deferred queue + // on second thought that takes too long, but leaving the commented code here for debug purposes + // while(!source->entities_iterator_.completed()) { + // source->entities_iterator_.advance(); + //} + }); } void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSource *source) { - // This method was called via WebServer->defer() and is no longer executing in the - // context of the network callback. The object is now dead and can be safely deleted. - this->remove(source); - delete source; // NOLINT + source->web_server_->defer([this, source]() { + // This method was called via WebServer->defer() and is no longer executing in the + // context of the network callback. The object is now dead and can be safely deleted. + this->remove(source); + delete source; // NOLINT + }); } #endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 2e5d58d3755..c54f5558a96 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -141,7 +141,7 @@ class DeferredUpdateEventSource : public AsyncEventSource { class DeferredUpdateEventSourceList : public std::list { protected: - void on_client_connect_(WebServer *ws, DeferredUpdateEventSource *source); + void on_client_connect_(DeferredUpdateEventSource *source); void on_client_disconnect_(DeferredUpdateEventSource *source); public: From 4838eff3829831d2a6b1390de1c848e52be62fca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 19:12:56 -0600 Subject: [PATCH 008/461] [web_server] Use zero-copy entity ID comparison in request handlers (#11644) --- esphome/components/web_server/web_server.cpp | 52 +++++++++++--------- esphome/components/web_server/web_server.h | 11 ++++- esphome/core/entity_base.h | 5 ++ 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index cc62b019c30..fe0da14435f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -435,9 +435,10 @@ void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (sensor::Sensor *obj : App.get_sensors()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + // Note: request->method() is always HTTP_GET here (canHandle ensures this) + if (match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -477,9 +478,10 @@ void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::s } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (text_sensor::TextSensor *obj : App.get_text_sensors()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + // Note: request->method() is always HTTP_GET here (canHandle ensures this) + if (match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->text_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -516,7 +518,7 @@ void WebServer::on_switch_update(switch_::Switch *obj, bool state) { } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (switch_::Switch *obj : App.get_switches()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -585,7 +587,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail #ifdef USE_BUTTON void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (button::Button *obj : App.get_buttons()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); @@ -627,9 +629,10 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + // Note: request->method() is always HTTP_GET here (canHandle ensures this) + if (match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->binary_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -665,7 +668,7 @@ void WebServer::on_fan_update(fan::Fan *obj) { } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (fan::Fan *obj : App.get_fans()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -739,7 +742,7 @@ void WebServer::on_light_update(light::LightState *obj) { } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (light::LightState *obj : App.get_lights()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -812,7 +815,7 @@ void WebServer::on_cover_update(cover::Cover *obj) { } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (cover::Cover *obj : App.get_covers()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -897,7 +900,7 @@ void WebServer::on_number_update(number::Number *obj, float state) { } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_numbers()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -962,7 +965,7 @@ void WebServer::on_date_update(datetime::DateEntity *obj) { } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_dates()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); @@ -1017,7 +1020,7 @@ void WebServer::on_time_update(datetime::TimeEntity *obj) { } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_times()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); @@ -1071,7 +1074,7 @@ void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_datetimes()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); @@ -1126,7 +1129,7 @@ void WebServer::on_text_update(text::Text *obj, const std::string &state) { } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_texts()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1180,7 +1183,7 @@ void WebServer::on_select_update(select::Select *obj, const std::string &state, } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_selects()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1236,7 +1239,7 @@ void WebServer::on_climate_update(climate::Climate *obj) { } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_climates()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1377,7 +1380,7 @@ void WebServer::on_lock_update(lock::Lock *obj) { } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (lock::Lock *obj : App.get_locks()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1448,7 +1451,7 @@ void WebServer::on_valve_update(valve::Valve *obj) { } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (valve::Valve *obj : App.get_valves()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1529,7 +1532,7 @@ void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { @@ -1608,10 +1611,11 @@ void WebServer::on_event(event::Event *obj, const std::string &event_type) { void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (event::Event *obj : App.get_events()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + // Note: request->method() is always HTTP_GET here (canHandle ensures this) + if (match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->event_json(obj, "", detail); request->send(200, "application/json", data.c_str()); @@ -1673,7 +1677,7 @@ void WebServer::on_update(update::UpdateEntity *obj) { } void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (update::UpdateEntity *obj : App.get_updates()) { - if (!match.id_equals(obj->get_object_id())) + if (!match.id_equals_entity(obj)) continue; if (request->method() == HTTP_GET && match.method_empty()) { diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index c54f5558a96..fb790483dc6 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -48,8 +48,15 @@ struct UrlMatch { return domain && domain_len == strlen(str) && memcmp(domain, str, domain_len) == 0; } - bool id_equals(const std::string &str) const { - return id && id_len == str.length() && memcmp(id, str.c_str(), id_len) == 0; + bool id_equals_entity(EntityBase *entity) const { + // Zero-copy comparison using StringRef + StringRef static_ref = entity->get_object_id_ref_for_api_(); + if (!static_ref.empty()) { + return id && id_len == static_ref.size() && memcmp(id, static_ref.c_str(), id_len) == 0; + } + // Fallback to allocation (rare) + const auto &obj_id = entity->get_object_id(); + return id && id_len == obj_id.length() && memcmp(id, obj_id.c_str(), id_len) == 0; } bool method_equals(const char *str) const { diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 4a6460e708f..80cd6b8e770 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -17,6 +17,10 @@ namespace api { class APIConnection; } // namespace api +namespace web_server { +struct UrlMatch; +} // namespace web_server + enum EntityCategory : uint8_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, @@ -116,6 +120,7 @@ class EntityBase { protected: friend class api::APIConnection; + friend struct web_server::UrlMatch; // Get object_id as StringRef when it's static (for API usage) // Returns empty StringRef if object_id is dynamic (needs allocation) From 34244afea1117c6042e786f5f99a1f026aa0c39c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 19:16:26 -0600 Subject: [PATCH 009/461] =?UTF-8?q?[esp32=5Fble]=20Reduce=20GATT=20event?= =?UTF-8?q?=20latency=20from=208ms=20to=2012=CE=BCs=20with=20notification?= =?UTF-8?q?=20socket=20(#11663)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- esphome/components/esp32_ble/__init__.py | 9 ++ esphome/components/esp32_ble/ble.cpp | 112 +++++++++++++++++++++++ esphome/components/esp32_ble/ble.h | 41 +++++++++ 3 files changed, 162 insertions(+) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 411c2add713..1ae8df6f5ef 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -7,6 +7,7 @@ from typing import Any from esphome import automation import esphome.codegen as cg +from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant import esphome.config_validation as cv from esphome.const import ( @@ -481,6 +482,14 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) + # BLE uses 1 UDP socket for event notification to wake up main loop from select() + # This enables low-latency (~12μs) BLE event processing instead of waiting for + # select() timeout (0-16ms). The socket is created in ble_setup_() and used to + # wake lwip_select() when BLE events arrive from the BLE thread. + # Note: Called during config generation, socket is created at runtime. In practice, + # always used since esp32_ble only runs on ESP32 which always has USE_SOCKET_SELECT_SUPPORT. + socket.consume_sockets(1, "esp32_ble")(config) + # Define max connections for use in C++ code (e.g., ble_server.h) max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 69e317ff6de..eef0db53477 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -27,6 +27,10 @@ extern "C" { #include #endif +#ifdef USE_SOCKET_SELECT_SUPPORT +#include +#endif + namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; @@ -293,10 +297,21 @@ bool ESP32BLE::ble_setup_() { // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT + // Set up notification socket to wake main loop for BLE events + // This enables low-latency (~12μs) event processing instead of waiting for select() timeout +#ifdef USE_SOCKET_SELECT_SUPPORT + this->setup_event_notification_(); +#endif + return true; } bool ESP32BLE::ble_dismantle_() { + // Clean up notification socket first before dismantling BLE stack +#ifdef USE_SOCKET_SELECT_SUPPORT + this->cleanup_event_notification_(); +#endif + esp_err_t err = esp_bluedroid_disable(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err); @@ -394,6 +409,12 @@ void ESP32BLE::loop() { break; } +#ifdef USE_SOCKET_SELECT_SUPPORT + // Drain any notification socket events first + // This clears the socket so it doesn't stay "ready" in subsequent select() calls + this->drain_event_notifications_(); +#endif + BLEEvent *ble_event = this->ble_events_.pop(); while (ble_event != nullptr) { switch (ble_event->type_) { @@ -582,6 +603,10 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { enqueue_ble_event(event, gatts_if, param); + // Wake up main loop to process GATT event immediately +#ifdef USE_SOCKET_SELECT_SUPPORT + global_ble->notify_main_loop_(); +#endif } #endif @@ -589,6 +614,10 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { enqueue_ble_event(event, gattc_if, param); + // Wake up main loop to process GATT event immediately +#ifdef USE_SOCKET_SELECT_SUPPORT + global_ble->notify_main_loop_(); +#endif } #endif @@ -628,6 +657,89 @@ void ESP32BLE::dump_config() { } } +#ifdef USE_SOCKET_SELECT_SUPPORT +void ESP32BLE::setup_event_notification_() { + // Create UDP socket for event notifications + this->notify_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (this->notify_fd_ < 0) { + ESP_LOGW(TAG, "Event socket create failed: %d", errno); + return; + } + + // Bind to loopback with auto-assigned port + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // Auto-assign port + + if (lwip_bind(this->notify_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + ESP_LOGW(TAG, "Event socket bind failed: %d", errno); + lwip_close(this->notify_fd_); + this->notify_fd_ = -1; + return; + } + + // Get the assigned address and connect to it + // Connecting a UDP socket allows using send() instead of sendto() for better performance + struct sockaddr_in notify_addr; + socklen_t len = sizeof(notify_addr); + if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) ¬ify_addr, &len) < 0) { + ESP_LOGW(TAG, "Event socket address failed: %d", errno); + lwip_close(this->notify_fd_); + this->notify_fd_ = -1; + return; + } + + // Connect to self (loopback) - allows using send() instead of sendto() + // After connect(), no need to store notify_addr - the socket remembers it + if (lwip_connect(this->notify_fd_, (struct sockaddr *) ¬ify_addr, sizeof(notify_addr)) < 0) { + ESP_LOGW(TAG, "Event socket connect failed: %d", errno); + lwip_close(this->notify_fd_); + this->notify_fd_ = -1; + return; + } + + // Set non-blocking mode + int flags = lwip_fcntl(this->notify_fd_, F_GETFL, 0); + lwip_fcntl(this->notify_fd_, F_SETFL, flags | O_NONBLOCK); + + // Register with application's select() loop + if (!App.register_socket_fd(this->notify_fd_)) { + ESP_LOGW(TAG, "Event socket register failed"); + lwip_close(this->notify_fd_); + this->notify_fd_ = -1; + return; + } + + ESP_LOGD(TAG, "Event socket ready"); +} + +void ESP32BLE::cleanup_event_notification_() { + if (this->notify_fd_ >= 0) { + App.unregister_socket_fd(this->notify_fd_); + lwip_close(this->notify_fd_); + this->notify_fd_ = -1; + ESP_LOGD(TAG, "Event socket closed"); + } +} + +void ESP32BLE::drain_event_notifications_() { + // Called from main loop to drain any pending notifications + // Must check is_socket_ready() to avoid blocking on empty socket + if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) { + char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE]; + // Drain all pending notifications with non-blocking reads + // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK + // We control both ends of this loopback socket (always write 1 byte per event), + // so no error checking needed - any errors indicate catastrophic system failure + while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { + // Just draining, no action needed - actual BLE events are already queued + } + } +} + +#endif // USE_SOCKET_SELECT_SUPPORT + uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { uint64_t u = 0; u |= uint64_t(address[0] & 0xFF) << 40; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index dc973f0e829..7c3195db6df 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -25,6 +25,10 @@ #include #include +#ifdef USE_SOCKET_SELECT_SUPPORT +#include +#endif + namespace esphome::esp32_ble { // Maximum size of the BLE event queue @@ -162,6 +166,13 @@ class ESP32BLE : public Component { void advertising_init_(); #endif +#ifdef USE_SOCKET_SELECT_SUPPORT + void setup_event_notification_(); // Create notification socket + void cleanup_event_notification_(); // Close and unregister socket + inline void notify_main_loop_(); // Wake up select() from BLE thread (hot path - inlined) + void drain_event_notifications_(); // Read pending notifications in main loop +#endif + private: template friend void enqueue_ble_event(Args... args); @@ -196,6 +207,13 @@ class ESP32BLE : public Component { esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) uint32_t advertising_cycle_time_{}; // 4 bytes +#ifdef USE_SOCKET_SELECT_SUPPORT + // Event notification socket for waking up main loop from BLE thread + // Uses connected UDP loopback socket to wake lwip_select() with ~12μs latency vs 0-16ms timeout + // Socket is connected during setup, allowing use of send() instead of sendto() for efficiency + int notify_fd_{-1}; // 4 bytes (file descriptor) +#endif + // 2-byte aligned members uint16_t appearance_{0}; // 2 bytes @@ -207,6 +225,29 @@ class ESP32BLE : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32BLE *global_ble; +#ifdef USE_SOCKET_SELECT_SUPPORT +// Inline implementations for hot-path functions +// These are called from BLE thread (notify) and main loop (drain) on every event + +// Small buffer for draining notification bytes (1 byte sent per BLE event) +// Size allows draining multiple notifications per recvfrom() without wasting stack +static constexpr size_t BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE = 16; + +inline void ESP32BLE::notify_main_loop_() { + // Called from BLE thread context when events are queued + // Wakes up lwip_select() in main loop by writing to connected loopback socket + if (this->notify_fd_ >= 0) { + const char dummy = 1; + // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway + // No error checking needed: we control both ends of this loopback socket, and the + // BLE event is already queued. Notification is best-effort to reduce latency. + // This is safe to call from BLE thread - send() is thread-safe in lwip + // Socket is already connected to loopback address, so send() is faster than sendto() + lwip_send(this->notify_fd_, &dummy, 1, 0); + } +} +#endif // USE_SOCKET_SELECT_SUPPORT + template class BLEEnabledCondition : public Condition { public: bool check(Ts... x) override { return global_ble->is_active(); } From 3f05fd82e50d43ec878939049a7ef10119571ad2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 19:18:59 -0600 Subject: [PATCH 010/461] [fan] Use std::vector for preset modes, preserve config order (#11483) --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_pb2.cpp | 8 ++-- esphome/components/api/api_pb2.h | 2 +- esphome/components/fan/fan.cpp | 44 +++++++++++++------ esphome/components/fan/fan_traits.h | 38 +++++++--------- esphome/components/hbridge/fan/hbridge_fan.h | 6 +-- esphome/components/speed/fan/speed_fan.h | 6 +-- .../components/template/fan/template_fan.h | 6 +-- 9 files changed, 59 insertions(+), 55 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index f50944ffa4d..20645fc47bf 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -425,7 +425,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 11; - repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"]; + repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } // Deprecated in API version 1.6 - only used in deprecated fields diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 382c4acc169..33d5072d9cb 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -423,7 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); - msg.supported_preset_modes = &traits.supported_preset_modes_for_api_(); + msg.supported_preset_modes = &traits.supported_preset_modes(); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::fan_command(const FanCommandRequest &msg) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 3472707d3ce..0673d355185 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -355,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(10, this->icon_ref_); #endif buffer.encode_uint32(11, static_cast(this->entity_category)); - for (const auto &it : *this->supported_preset_modes) { - buffer.encode_string(12, it, true); + for (const char *it : *this->supported_preset_modes) { + buffer.encode_string(12, it, strlen(it), true); } #ifdef USE_DEVICES buffer.encode_uint32(13, this->device_id); @@ -376,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { #endif size.add_uint32(1, static_cast(this->entity_category)); if (!this->supported_preset_modes->empty()) { - for (const auto &it : *this->supported_preset_modes) { - size.add_length_force(1, it.size()); + for (const char *it : *this->supported_preset_modes) { + size.add_length_force(1, strlen(it)); } } #ifdef USE_DEVICES diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index aa5c031ac73..89f16044d72 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; - const std::set *supported_preset_modes{}; + const std::vector *supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 26065ed6448..5b4f437f99c 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -51,7 +51,14 @@ void FanCall::validate_() { if (!this->preset_mode_.empty()) { const auto &preset_modes = traits.supported_preset_modes(); - if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { + bool found = false; + for (const auto &mode : preset_modes) { + if (strcmp(mode, this->preset_mode_.c_str()) == 0) { + found = true; + break; + } + } + if (!found) { ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); this->preset_mode_.clear(); } @@ -92,11 +99,12 @@ FanCall FanRestoreState::to_call(Fan &fan) { call.set_speed(this->speed); call.set_direction(this->direction); - if (fan.get_traits().supports_preset_modes()) { + auto traits = fan.get_traits(); + if (traits.supports_preset_modes()) { // Use stored preset index to get preset name - const auto &preset_modes = fan.get_traits().supported_preset_modes(); + const auto &preset_modes = traits.supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { - call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); + call.set_preset_mode(preset_modes[this->preset_mode]); } } return call; @@ -107,11 +115,12 @@ void FanRestoreState::apply(Fan &fan) { fan.speed = this->speed; fan.direction = this->direction; - if (fan.get_traits().supports_preset_modes()) { + auto traits = fan.get_traits(); + if (traits.supports_preset_modes()) { // Use stored preset index to get preset name - const auto &preset_modes = fan.get_traits().supported_preset_modes(); + const auto &preset_modes = traits.supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { - fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); + fan.preset_mode = preset_modes[this->preset_mode]; } } fan.publish_state(); @@ -182,18 +191,25 @@ void Fan::save_state_() { return; } + auto traits = this->get_traits(); + FanRestoreState state{}; state.state = this->state; state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; - if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { - const auto &preset_modes = this->get_traits().supported_preset_modes(); + if (traits.supports_preset_modes() && !this->preset_mode.empty()) { + const auto &preset_modes = traits.supported_preset_modes(); // Store index of current preset mode - auto preset_iterator = preset_modes.find(this->preset_mode); - if (preset_iterator != preset_modes.end()) - state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); + size_t i = 0; + for (const auto &mode : preset_modes) { + if (strcmp(mode, this->preset_mode.c_str()) == 0) { + state.preset_mode = i; + break; + } + i++; + } } this->rtc_.save(&state); @@ -216,8 +232,8 @@ void Fan::dump_traits_(const char *tag, const char *prefix) { } if (traits.supports_preset_modes()) { ESP_LOGCONFIG(tag, "%s Supported presets:", prefix); - for (const std::string &s : traits.supported_preset_modes()) - ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str()); + for (const char *s : traits.supported_preset_modes()) + ESP_LOGCONFIG(tag, "%s - %s", prefix, s); } } diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 48509e57059..df345f9b048 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,15 +1,9 @@ -#include -#include - #pragma once -namespace esphome { +#include +#include -#ifdef USE_API -namespace api { -class APIConnection; -} // namespace api -#endif +namespace esphome { namespace fan { @@ -36,27 +30,27 @@ class FanTraits { /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } /// Return the preset modes supported by the fan. - std::set supported_preset_modes() const { return this->preset_modes_; } - /// Set the preset modes supported by the fan. - void set_supported_preset_modes(const std::set &preset_modes) { this->preset_modes_ = preset_modes; } + const std::vector &supported_preset_modes() const { return this->preset_modes_; } + /// Set the preset modes supported by the fan (from initializer list). + void set_supported_preset_modes(std::initializer_list preset_modes) { + this->preset_modes_ = preset_modes; + } + /// Set the preset modes supported by the fan (from vector). + void set_supported_preset_modes(const std::vector &preset_modes) { this->preset_modes_ = preset_modes; } + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_supported_preset_modes(const std::vector &preset_modes) = delete; + void set_supported_preset_modes(std::initializer_list preset_modes) = delete; + /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_.empty(); } protected: -#ifdef USE_API - // The API connection is a friend class to access internal methods - friend class api::APIConnection; - // This method returns a reference to the internal preset modes set. - // It is used by the API to avoid copying data when encoding messages. - // Warning: Do not use this method outside of the API connection code. - // It returns a reference to internal data that can be invalidated. - const std::set &supported_preset_modes_for_api_() const { return this->preset_modes_; } -#endif bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; - std::set preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 4234fccae3e..143c7c18530 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "esphome/core/automation.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -22,7 +20,7 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } - void set_preset_modes(const std::set &presets) { preset_modes_ = presets; } + void set_preset_modes(std::initializer_list presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; @@ -38,7 +36,7 @@ class HBridgeFan : public Component, public fan::Fan { int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; fan::FanTraits traits_; - std::set preset_modes_{}; + std::vector preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 6537bce3f6d..e9a389e0f3f 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "esphome/core/component.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -18,7 +16,7 @@ class SpeedFan : public Component, public fan::Fan { void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + void set_preset_modes(std::initializer_list presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } protected: @@ -30,7 +28,7 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *direction_{nullptr}; int speed_count_{}; fan::FanTraits traits_; - std::set preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace speed diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 7f5305ca485..b09352f4d47 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "esphome/core/component.h" #include "esphome/components/fan/fan.h" @@ -16,7 +14,7 @@ class TemplateFan : public Component, public fan::Fan { void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } void set_speed_count(int count) { this->speed_count_ = count; } - void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + void set_preset_modes(std::initializer_list presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } protected: @@ -26,7 +24,7 @@ class TemplateFan : public Component, public fan::Fan { bool has_direction_{false}; int speed_count_{0}; fan::FanTraits traits_; - std::set preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace template_ From cf76c3a74769f18254e24c9e6173974581966802 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 19:23:03 -0600 Subject: [PATCH 011/461] [web_server_idf] Reduce flash by eliminating temporary string allocations in event formatting (#11658) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- .../web_server_idf/web_server_idf.cpp | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index ac0b5bad83b..0dab5e7e8c2 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -245,8 +246,8 @@ void AsyncWebServerRequest::redirect(const std::string &url) { } void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) { - // Set status code - use constants for common codes to avoid string allocation - const char *status = nullptr; + // Set status code - use constants for common codes, default to 500 for unknown codes + const char *status; switch (code) { case 200: status = HTTPD_200; @@ -258,9 +259,10 @@ void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code status = HTTPD_409; break; default: + status = HTTPD_500; break; } - httpd_resp_set_status(*this, status == nullptr ? to_string(code).c_str() : status); + httpd_resp_set_status(*this, status); if (content_type && *content_type) { httpd_resp_set_type(*this, content_type); @@ -348,7 +350,13 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) { httpd_resp_set_hdr(*this->req_, name, value); } -void AsyncResponseStream::print(float value) { this->print(to_string(value)); } +void AsyncResponseStream::print(float value) { + // Use stack buffer to avoid temporary string allocation + // Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety + char buf[32]; + int len = snprintf(buf, sizeof(buf), "%f", value); + this->content_.append(buf, len); +} void AsyncResponseStream::printf(const char *fmt, ...) { va_list args; @@ -593,16 +601,19 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char event_buffer_.append(chunk_len_header); + // Use stack buffer for formatting numeric fields to avoid temporary string allocations + // Size: "retry: " (7) + max uint32 (10 digits) + CRLF (2) + null (1) = 20 bytes, use 32 for safety + constexpr size_t num_buf_size = 32; + char num_buf[num_buf_size]; + if (reconnect) { - event_buffer_.append("retry: ", sizeof("retry: ") - 1); - event_buffer_.append(to_string(reconnect)); - event_buffer_.append(CRLF_STR, CRLF_LEN); + int len = snprintf(num_buf, num_buf_size, "retry: %" PRIu32 CRLF_STR, reconnect); + event_buffer_.append(num_buf, len); } if (id) { - event_buffer_.append("id: ", sizeof("id: ") - 1); - event_buffer_.append(to_string(id)); - event_buffer_.append(CRLF_STR, CRLF_LEN); + int len = snprintf(num_buf, num_buf_size, "id: %" PRIu32 CRLF_STR, id); + event_buffer_.append(num_buf, len); } if (event && *event) { From 4a5e6576c8eac98dc9120e250325af2394e8ea8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 19:29:29 -0600 Subject: [PATCH 012/461] [scheduler] Refactor call() for improved code organization (#11643) --- esphome/core/scheduler.cpp | 105 +++++++++++-------------------------- esphome/core/scheduler.h | 58 ++++++++++++++++++++ 2 files changed, 88 insertions(+), 75 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 0d4715f6212..11d59c24993 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -316,59 +316,37 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { return 0; return next_exec - now_64; } + +void Scheduler::full_cleanup_removed_items_() { + // We hold the lock for the entire cleanup operation because: + // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout + // 2. Other threads must see either the old state or the new state, not intermediate states + // 3. The operation is already expensive (O(n)), so lock overhead is negligible + // 4. No operations inside can block or take other locks, so no deadlock risk + LockGuard guard{this->lock_}; + + std::vector> valid_items; + + // Move all non-removed items to valid_items, recycle removed ones + for (auto &item : this->items_) { + if (!is_item_removed_(item.get())) { + valid_items.push_back(std::move(item)); + } else { + // Recycle removed items + this->recycle_item_(std::move(item)); + } + } + + // Replace items_ with the filtered list + this->items_ = std::move(valid_items); + // Rebuild the heap structure since items are no longer in heap order + std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); + this->to_remove_ = 0; +} + void HOT Scheduler::call(uint32_t now) { #ifndef ESPHOME_THREAD_SINGLE - // Process defer queue first to guarantee FIFO execution order for deferred items. - // Previously, defer() used the heap which gave undefined order for equal timestamps, - // causing race conditions on multi-core systems (ESP32, BK7200). - // With the defer queue: - // - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_ - // - Items execute in exact order they were deferred (FIFO guarantee) - // - No deferred items exist in to_add_, so processing order doesn't affect correctness - // Single-core platforms don't use this queue and fall back to the heap-based approach. - // - // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still - // processed here. They are skipped during execution by should_skip_item_(). - // This is intentional - no memory leak occurs. - // - // We use an index (defer_queue_front_) to track the read position instead of calling - // erase() on every pop, which would be O(n). The queue is processed once per loop - - // any items added during processing are left for the next loop iteration. - - // Snapshot the queue end point - only process items that existed at loop start - // Items added during processing (by callbacks or other threads) run next loop - // No lock needed: single consumer (main loop), stale read just means we process less this iteration - size_t defer_queue_end = this->defer_queue_.size(); - - while (this->defer_queue_front_ < defer_queue_end) { - std::unique_ptr item; - { - LockGuard lock(this->lock_); - // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. - // This is intentional and safe because: - // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_ - // and has_cancelled_timeout_in_container_ in scheduler.h) - // 3. The lock protects concurrent access, but the nullptr remains until cleanup - item = std::move(this->defer_queue_[this->defer_queue_front_]); - this->defer_queue_front_++; - } - - // Execute callback without holding lock to prevent deadlocks - // if the callback tries to call defer() again - if (!this->should_skip_item_(item.get())) { - now = this->execute_item_(item.get(), now); - } - // Recycle the defer item after execution - this->recycle_item_(std::move(item)); - } - - // If we've consumed all items up to the snapshot point, clean up the dead space - // Single consumer (main loop), so no lock needed for this check - if (this->defer_queue_front_ >= defer_queue_end) { - LockGuard lock(this->lock_); - this->cleanup_defer_queue_locked_(); - } + this->process_defer_queue_(now); #endif /* not ESPHOME_THREAD_SINGLE */ // Convert the fresh timestamp from main loop to 64-bit for scheduler operations @@ -429,30 +407,7 @@ void HOT Scheduler::call(uint32_t now) { // If we still have too many cancelled items, do a full cleanup // This only happens if cancelled items are stuck in the middle/bottom of the heap if (this->to_remove_ >= MAX_LOGICALLY_DELETED_ITEMS) { - // We hold the lock for the entire cleanup operation because: - // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout - // 2. Other threads must see either the old state or the new state, not intermediate states - // 3. The operation is already expensive (O(n)), so lock overhead is negligible - // 4. No operations inside can block or take other locks, so no deadlock risk - LockGuard guard{this->lock_}; - - std::vector> valid_items; - - // Move all non-removed items to valid_items, recycle removed ones - for (auto &item : this->items_) { - if (!is_item_removed_(item.get())) { - valid_items.push_back(std::move(item)); - } else { - // Recycle removed items - this->recycle_item_(std::move(item)); - } - } - - // Replace items_ with the filtered list - this->items_ = std::move(valid_items); - // Rebuild the heap structure since items are no longer in heap order - std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - this->to_remove_ = 0; + this->full_cleanup_removed_items_(); } while (!this->items_.empty()) { // Don't copy-by value yet diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index df0be0e4ced..f6ec07294d2 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -263,7 +263,65 @@ class Scheduler { // Helper to recycle a SchedulerItem void recycle_item_(std::unique_ptr item); + // Helper to perform full cleanup when too many items are cancelled + void full_cleanup_removed_items_(); + #ifndef ESPHOME_THREAD_SINGLE + // Helper to process defer queue - inline for performance in hot path + inline void process_defer_queue_(uint32_t &now) { + // Process defer queue first to guarantee FIFO execution order for deferred items. + // Previously, defer() used the heap which gave undefined order for equal timestamps, + // causing race conditions on multi-core systems (ESP32, BK7200). + // With the defer queue: + // - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_ + // - Items execute in exact order they were deferred (FIFO guarantee) + // - No deferred items exist in to_add_, so processing order doesn't affect correctness + // Single-core platforms don't use this queue and fall back to the heap-based approach. + // + // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still + // processed here. They are skipped during execution by should_skip_item_(). + // This is intentional - no memory leak occurs. + // + // We use an index (defer_queue_front_) to track the read position instead of calling + // erase() on every pop, which would be O(n). The queue is processed once per loop - + // any items added during processing are left for the next loop iteration. + + // Snapshot the queue end point - only process items that existed at loop start + // Items added during processing (by callbacks or other threads) run next loop + // No lock needed: single consumer (main loop), stale read just means we process less this iteration + size_t defer_queue_end = this->defer_queue_.size(); + + while (this->defer_queue_front_ < defer_queue_end) { + std::unique_ptr item; + { + LockGuard lock(this->lock_); + // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. + // This is intentional and safe because: + // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_ + // and has_cancelled_timeout_in_container_ in scheduler.h) + // 3. The lock protects concurrent access, but the nullptr remains until cleanup + item = std::move(this->defer_queue_[this->defer_queue_front_]); + this->defer_queue_front_++; + } + + // Execute callback without holding lock to prevent deadlocks + // if the callback tries to call defer() again + if (!this->should_skip_item_(item.get())) { + now = this->execute_item_(item.get(), now); + } + // Recycle the defer item after execution + this->recycle_item_(std::move(item)); + } + + // If we've consumed all items up to the snapshot point, clean up the dead space + // Single consumer (main loop), so no lock needed for this check + if (this->defer_queue_front_ >= defer_queue_end) { + LockGuard lock(this->lock_); + this->cleanup_defer_queue_locked_(); + } + } + // Helper to cleanup defer_queue_ after processing // IMPORTANT: Caller must hold the scheduler lock before calling this function. inline void cleanup_defer_queue_locked_() { From 0f0cd1f706e31c35961578647f4d63c8e04dc768 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 19:40:13 -0600 Subject: [PATCH 013/461] [core] Avoid redundant millis() calls in base_automation loop methods (#11676) --- esphome/core/base_automation.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index e668a1782aa..28af02a846e 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -103,7 +103,7 @@ template class ForCondition : public Condition, public Co bool check_internal() { bool cond = this->condition_->check(); if (!cond) - this->last_inactive_ = millis(); + this->last_inactive_ = App.get_loop_component_start_time(); return cond; } @@ -380,7 +380,7 @@ template class WaitUntilAction : public Action, public Co if (this->num_running_ == 0) return; - auto now = millis(); + auto now = App.get_loop_component_start_time(); this->var_queue_.remove_if([&](auto &queued) { auto start = std::get(queued); From a72837704c4cdd8765d9bef1ec058741cef38b27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 20:04:37 -0600 Subject: [PATCH 014/461] fix trigge on preset mode cleared --- esphome/components/fan/automation.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 48de8d66fbd..ae0af1a9bd3 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -215,8 +215,9 @@ class FanPresetSetTrigger : public Trigger { const auto *preset_mode = state->get_preset_mode(); auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; - if (should_trigger && preset_mode != nullptr) { - this->trigger(preset_mode); + if (should_trigger) { + // Trigger with empty string when nullptr to maintain backward compatibility + this->trigger(preset_mode != nullptr ? preset_mode : ""); } }); this->last_preset_mode_ = state->get_preset_mode(); From 42c3e7b542f300c11c7549c81e3ef4e829a757c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 20:07:32 -0600 Subject: [PATCH 015/461] fix trigge on preset mode cleared --- esphome/components/fan/fan.cpp | 2 +- esphome/components/fan/fan.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index aefa1858445..959572e9d94 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -177,7 +177,7 @@ void Fan::publish_state() { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } const char *preset = this->get_preset_mode(); - if (traits.supports_preset_modes() && preset != nullptr) { + if (preset != nullptr) { ESP_LOGD(TAG, " Preset Mode: %s", preset); } this->state_callback_.call(); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 33e546b2bb7..e38a80dbbe3 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -156,6 +156,8 @@ class Fan : public EntityBase { CallbackManager state_callback_{}; ESPPreferenceObject rtc_; FanRestoreMode restore_mode_; + + private: const char *preset_mode_{nullptr}; }; From 4dd3c906635ae3964f6b12eab46bcbc7d44990f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 20:55:17 -0600 Subject: [PATCH 016/461] [esp32_ble] Wake main loop for GAP security events (#11677) --- esphome/components/esp32_ble/ble.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index eef0db53477..d6f7e1ce430 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -581,9 +581,17 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa GAP_ADV_COMPLETE_EVENTS: // Connection events - used by ble_client case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + enqueue_ble_event(event, param); + return; + // Security events - used by ble_client and bluetooth_proxy + // These are rare but interactive (pairing/bonding), so notify immediately GAP_SECURITY_EVENTS: enqueue_ble_event(event, param); + // Wake up main loop to process security event immediately +#ifdef USE_SOCKET_SELECT_SUPPORT + global_ble->notify_main_loop_(); +#endif return; // Ignore these GAP events as they are not relevant for our use case From 12077d016d19acf99b1ab59defddb95689bacc2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 21:48:17 -0600 Subject: [PATCH 017/461] [core][esp32_ble] Add wake_loop_threadsafe() helper for background thread wakeups --- esphome/codegen.py | 1 + esphome/components/esp32_ble/__init__.py | 10 +- esphome/components/esp32_ble/ble.cpp | 113 ++--------------------- esphome/components/esp32_ble/ble.h | 40 +------- esphome/core/application.cpp | 79 ++++++++++++++++ esphome/core/application.h | 52 +++++++++++ esphome/cpp_helpers.py | 33 ++++++- tests/unit_tests/test_cpp_helpers.py | 40 ++++++++ 8 files changed, 220 insertions(+), 148 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index 6d55c6023d2..f0deb6e8d36 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -51,6 +51,7 @@ from esphome.cpp_helpers import ( # noqa: F401 past_safe_mode, register_component, register_parented, + require_wake_loop_threadsafe, ) from esphome.cpp_types import ( # noqa: F401 NAN, diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 1ae8df6f5ef..d3db1db70c1 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -7,7 +7,6 @@ from typing import Any from esphome import automation import esphome.codegen as cg -from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant import esphome.config_validation as cv from esphome.const import ( @@ -482,13 +481,10 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) - # BLE uses 1 UDP socket for event notification to wake up main loop from select() + # BLE uses the core wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks # This enables low-latency (~12μs) BLE event processing instead of waiting for - # select() timeout (0-16ms). The socket is created in ble_setup_() and used to - # wake lwip_select() when BLE events arrive from the BLE thread. - # Note: Called during config generation, socket is created at runtime. In practice, - # always used since esp32_ble only runs on ESP32 which always has USE_SOCKET_SELECT_SUPPORT. - socket.consume_sockets(1, "esp32_ble")(config) + # select() timeout (0-16ms). The wake socket is shared across all components. + cg.require_wake_loop_threadsafe() # Define max connections for use in C++ code (e.g., ble_server.h) max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index d6f7e1ce430..ecdd63f5b18 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -297,20 +297,14 @@ bool ESP32BLE::ble_setup_() { // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT - // Set up notification socket to wake main loop for BLE events - // This enables low-latency (~12μs) event processing instead of waiting for select() timeout -#ifdef USE_SOCKET_SELECT_SUPPORT - this->setup_event_notification_(); -#endif + // Wake mechanism is set up by core Application class (wake_loop_threadsafe) + // BLE tasks will call App.wake_loop_threadsafe() to wake main loop when events arrive return true; } bool ESP32BLE::ble_dismantle_() { - // Clean up notification socket first before dismantling BLE stack -#ifdef USE_SOCKET_SELECT_SUPPORT - this->cleanup_event_notification_(); -#endif + // No socket cleanup needed - wake socket is managed by core Application esp_err_t err = esp_bluedroid_disable(); if (err != ESP_OK) { @@ -409,12 +403,6 @@ void ESP32BLE::loop() { break; } -#ifdef USE_SOCKET_SELECT_SUPPORT - // Drain any notification socket events first - // This clears the socket so it doesn't stay "ready" in subsequent select() calls - this->drain_event_notifications_(); -#endif - BLEEvent *ble_event = this->ble_events_.pop(); while (ble_event != nullptr) { switch (ble_event->type_) { @@ -589,8 +577,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa GAP_SECURITY_EVENTS: enqueue_ble_event(event, param); // Wake up main loop to process security event immediately -#ifdef USE_SOCKET_SELECT_SUPPORT - global_ble->notify_main_loop_(); +#ifdef USE_WAKE_LOOP_THREADSAFE + App.wake_loop_threadsafe(); #endif return; @@ -612,8 +600,8 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat esp_ble_gatts_cb_param_t *param) { enqueue_ble_event(event, gatts_if, param); // Wake up main loop to process GATT event immediately -#ifdef USE_SOCKET_SELECT_SUPPORT - global_ble->notify_main_loop_(); +#ifdef USE_WAKE_LOOP_THREADSAFE + App.wake_loop_threadsafe(); #endif } #endif @@ -623,8 +611,8 @@ void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gat esp_ble_gattc_cb_param_t *param) { enqueue_ble_event(event, gattc_if, param); // Wake up main loop to process GATT event immediately -#ifdef USE_SOCKET_SELECT_SUPPORT - global_ble->notify_main_loop_(); +#ifdef USE_WAKE_LOOP_THREADSAFE + App.wake_loop_threadsafe(); #endif } #endif @@ -665,89 +653,6 @@ void ESP32BLE::dump_config() { } } -#ifdef USE_SOCKET_SELECT_SUPPORT -void ESP32BLE::setup_event_notification_() { - // Create UDP socket for event notifications - this->notify_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (this->notify_fd_ < 0) { - ESP_LOGW(TAG, "Event socket create failed: %d", errno); - return; - } - - // Bind to loopback with auto-assigned port - struct sockaddr_in addr = {}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK); - addr.sin_port = 0; // Auto-assign port - - if (lwip_bind(this->notify_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { - ESP_LOGW(TAG, "Event socket bind failed: %d", errno); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - // Get the assigned address and connect to it - // Connecting a UDP socket allows using send() instead of sendto() for better performance - struct sockaddr_in notify_addr; - socklen_t len = sizeof(notify_addr); - if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) ¬ify_addr, &len) < 0) { - ESP_LOGW(TAG, "Event socket address failed: %d", errno); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - // Connect to self (loopback) - allows using send() instead of sendto() - // After connect(), no need to store notify_addr - the socket remembers it - if (lwip_connect(this->notify_fd_, (struct sockaddr *) ¬ify_addr, sizeof(notify_addr)) < 0) { - ESP_LOGW(TAG, "Event socket connect failed: %d", errno); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - // Set non-blocking mode - int flags = lwip_fcntl(this->notify_fd_, F_GETFL, 0); - lwip_fcntl(this->notify_fd_, F_SETFL, flags | O_NONBLOCK); - - // Register with application's select() loop - if (!App.register_socket_fd(this->notify_fd_)) { - ESP_LOGW(TAG, "Event socket register failed"); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - ESP_LOGD(TAG, "Event socket ready"); -} - -void ESP32BLE::cleanup_event_notification_() { - if (this->notify_fd_ >= 0) { - App.unregister_socket_fd(this->notify_fd_); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - ESP_LOGD(TAG, "Event socket closed"); - } -} - -void ESP32BLE::drain_event_notifications_() { - // Called from main loop to drain any pending notifications - // Must check is_socket_ready() to avoid blocking on empty socket - if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) { - char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE]; - // Drain all pending notifications with non-blocking reads - // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK - // We control both ends of this loopback socket (always write 1 byte per event), - // so no error checking needed - any errors indicate catastrophic system failure - while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { - // Just draining, no action needed - actual BLE events are already queued - } - } -} - -#endif // USE_SOCKET_SELECT_SUPPORT - uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { uint64_t u = 0; u |= uint64_t(address[0] & 0xFF) << 40; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 7c3195db6df..3be6a7048d6 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -166,12 +166,10 @@ class ESP32BLE : public Component { void advertising_init_(); #endif -#ifdef USE_SOCKET_SELECT_SUPPORT - void setup_event_notification_(); // Create notification socket - void cleanup_event_notification_(); // Close and unregister socket - inline void notify_main_loop_(); // Wake up select() from BLE thread (hot path - inlined) - void drain_event_notifications_(); // Read pending notifications in main loop -#endif + // BLE uses the core wake_loop_threadsafe() mechanism to wake the main event loop + // from BLE tasks. This enables low-latency (~12μs) event processing instead of + // waiting for select() timeout (0-16ms). The wake socket is shared with other + // components that need this functionality. private: template friend void enqueue_ble_event(Args... args); @@ -207,13 +205,6 @@ class ESP32BLE : public Component { esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) uint32_t advertising_cycle_time_{}; // 4 bytes -#ifdef USE_SOCKET_SELECT_SUPPORT - // Event notification socket for waking up main loop from BLE thread - // Uses connected UDP loopback socket to wake lwip_select() with ~12μs latency vs 0-16ms timeout - // Socket is connected during setup, allowing use of send() instead of sendto() for efficiency - int notify_fd_{-1}; // 4 bytes (file descriptor) -#endif - // 2-byte aligned members uint16_t appearance_{0}; // 2 bytes @@ -225,29 +216,6 @@ class ESP32BLE : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32BLE *global_ble; -#ifdef USE_SOCKET_SELECT_SUPPORT -// Inline implementations for hot-path functions -// These are called from BLE thread (notify) and main loop (drain) on every event - -// Small buffer for draining notification bytes (1 byte sent per BLE event) -// Size allows draining multiple notifications per recvfrom() without wasting stack -static constexpr size_t BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE = 16; - -inline void ESP32BLE::notify_main_loop_() { - // Called from BLE thread context when events are queued - // Wakes up lwip_select() in main loop by writing to connected loopback socket - if (this->notify_fd_ >= 0) { - const char dummy = 1; - // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway - // No error checking needed: we control both ends of this loopback socket, and the - // BLE event is already queued. Notification is best-effort to reduce latency. - // This is safe to call from BLE thread - send() is thread-safe in lwip - // Socket is already connected to loopback address, so send() is faster than sendto() - lwip_send(this->notify_fd_, &dummy, 1, 0); - } -} -#endif // USE_SOCKET_SELECT_SUPPORT - template class BLEEnabledCondition : public Condition { public: bool check(Ts... x) override { return global_ble->is_active(); } diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 61cfcc75853..75814ae2535 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -122,6 +122,11 @@ void Application::setup() { // Clear setup priority overrides to free memory clear_setup_priority_overrides(); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Set up wake socket for waking main loop from tasks + this->setup_wake_loop_threadsafe_(); +#endif + this->schedule_dump_config(); } void Application::loop() { @@ -472,6 +477,11 @@ void Application::enable_pending_loops_() { } void Application::before_loop_tasks_(uint32_t loop_start_time) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Drain wake notifications first to clear socket for next wake + this->drain_wake_notifications_(); +#endif + // Process scheduled tasks this->scheduler.call(loop_start_time); @@ -625,4 +635,73 @@ void Application::yield_with_select_(uint32_t delay_ms) { Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +void Application::setup_wake_loop_threadsafe_() { + // Create UDP socket for wake notifications + this->wake_socket_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (this->wake_socket_fd_ < 0) { + ESP_LOGW(TAG, "Wake socket create failed: %d", errno); + return; + } + + // Bind to loopback with auto-assigned port + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // Auto-assign port + + if (lwip_bind(this->wake_socket_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + ESP_LOGW(TAG, "Wake socket bind failed: %d", errno); + lwip_close(this->wake_socket_fd_); + this->wake_socket_fd_ = -1; + return; + } + + // Get the assigned address and connect to it + // Connecting a UDP socket allows using send() instead of sendto() for better performance + struct sockaddr_in wake_addr; + socklen_t len = sizeof(wake_addr); + if (lwip_getsockname(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, &len) < 0) { + ESP_LOGW(TAG, "Wake socket address failed: %d", errno); + lwip_close(this->wake_socket_fd_); + this->wake_socket_fd_ = -1; + return; + } + + // Connect to self (loopback) - allows using send() instead of sendto() + // After connect(), no need to store wake_addr - the socket remembers it + if (lwip_connect(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) { + ESP_LOGW(TAG, "Wake socket connect failed: %d", errno); + lwip_close(this->wake_socket_fd_); + this->wake_socket_fd_ = -1; + return; + } + + // Set non-blocking mode + int flags = lwip_fcntl(this->wake_socket_fd_, F_GETFL, 0); + lwip_fcntl(this->wake_socket_fd_, F_SETFL, flags | O_NONBLOCK); + + // Register with application's select() loop + if (!this->register_socket_fd(this->wake_socket_fd_)) { + ESP_LOGW(TAG, "Wake socket register failed"); + lwip_close(this->wake_socket_fd_); + this->wake_socket_fd_ = -1; + return; + } +} + +void Application::wake_loop_threadsafe() { + // Called from FreeRTOS task context when events need immediate processing + // Wakes up lwip_select() in main loop by writing to connected loopback socket + if (this->wake_socket_fd_ >= 0) { + const char dummy = 1; + // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway + // No error checking needed: we control both ends of this loopback socket. + // This is safe to call from FreeRTOS tasks - send() is thread-safe in lwip + // Socket is already connected to loopback address, so send() is faster than sendto() + lwip_send(this->wake_socket_fd_, &dummy, 1, 0); + } +} +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + } // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h index 29a734f000b..fdc7f02796f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -21,7 +21,20 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #include + +#ifdef USE_WAKE_LOOP_THREADSAFE +// Inline function drain_wake_notifications_() needs lwip socket functions +#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS +#include +#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS) +#ifdef USE_ESP32 +#include +#else +// True BSD sockets already included via sys/select.h #endif +#endif +#endif // USE_WAKE_LOOP_THREADSAFE +#endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" @@ -429,6 +442,13 @@ class Application { /// Check if there's data available on a socket without blocking /// This function is thread-safe for reading, but should be called after select() has run bool is_socket_ready(int fd) const; + +#ifdef USE_WAKE_LOOP_THREADSAFE + /// Wake the main event loop from a FreeRTOS task + /// Thread-safe, can be called from task context to immediately wake select() + /// IMPORTANT: NOT safe to call from ISR context (socket operations not ISR-safe) + void wake_loop_threadsafe(); +#endif #endif protected: @@ -454,6 +474,11 @@ class Application { /// Perform a delay while also monitoring socket file descriptors for readiness void yield_with_select_(uint32_t delay_ms); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + void setup_wake_loop_threadsafe_(); // Create wake notification socket + inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) +#endif + // === Member variables ordered by size to minimize padding === // Pointer-sized members first @@ -481,6 +506,9 @@ class Application { FixedVector looping_components_{}; #ifdef USE_SOCKET_SELECT_SUPPORT std::vector socket_fds_; // Vector of all monitored socket file descriptors +#ifdef USE_WAKE_LOOP_THREADSAFE + int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks +#endif #endif // std::string members (typically 24-32 bytes each) @@ -597,4 +625,28 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +// Inline implementations for hot-path functions +// drain_wake_notifications_() is called on every loop iteration + +// Small buffer for draining wake notification bytes (1 byte sent per wake) +// Size allows draining multiple notifications per recvfrom() without wasting stack +static constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16; + +inline void Application::drain_wake_notifications_() { + // Called from main loop to drain any pending wake notifications + // Must check is_socket_ready() to avoid blocking on empty socket + if (this->wake_socket_fd_ >= 0 && this->is_socket_ready(this->wake_socket_fd_)) { + char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE]; + // Drain all pending notifications with non-blocking reads + // Multiple wake events may have triggered multiple writes, so drain until EWOULDBLOCK + // We control both ends of this loopback socket (always write 1 byte per wake), + // so no error checking needed - any errors indicate catastrophic system failure + while (lwip_recvfrom(this->wake_socket_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { + // Just draining, no action needed - wake has already occurred + } + } +} +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + } // namespace esphome diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 2698b9b3d58..8b1fd1db27a 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,7 +9,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import LogStringLiteral, add, get_variable +from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable from esphome.cpp_types import App from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -124,3 +124,34 @@ async def past_safe_mode(): yield return await FakeAwaitable(_safe_mode_generator()) + + +# Wake loop threadsafe support tracking +# Components that need to wake the main event loop from FreeRTOS tasks can call require_wake_loop_threadsafe() +KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" + + +def require_wake_loop_threadsafe() -> None: + """Mark that wake_loop_threadsafe support is required by a component. + + Call this from components that need to wake the main event loop from FreeRTOS tasks. + This enables the shared UDP loopback socket mechanism (~208 bytes RAM). + The socket is shared across all components that use this feature. + + IMPORTANT: This is for FreeRTOS task context only, NOT ISR context. + Socket operations are not safe to call from ISR handlers. + + Example: + import esphome.codegen as cg + + async def to_code(config): + cg.require_wake_loop_threadsafe() + """ + # Only set up once (idempotent - multiple components can call this) + if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False): + from esphome.components import socket + + CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + add_define("USE_WAKE_LOOP_THREADSAFE") + # Consume 1 socket for the shared wake notification socket + socket.consume_sockets(1, "core.wake_loop_threadsafe")({}) diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 2618803fecf..89a474f44dd 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -70,3 +70,43 @@ async def test_register_component__with_setup_priority(monkeypatch): assert add_mock.call_count == 4 app_mock.register_component.assert_called_with(var) assert core_mock.component_ids == [] + + +def test_require_wake_loop_threadsafe__first_call() -> None: + """Test that first call sets up define and consumes socket.""" + ch.require_wake_loop_threadsafe() + + # Verify CORE.data was updated + assert ch.CORE.data[ch.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Verify the define was added + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in ch.CORE.defines) + + +def test_require_wake_loop_threadsafe__idempotent() -> None: + """Test that subsequent calls are idempotent.""" + # Set up initial state as if already called + ch.CORE.data[ch.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + + # Call again - should not raise or fail + ch.require_wake_loop_threadsafe() + + # Verify state is still True + assert ch.CORE.data[ch.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Define should not be added since flag was already True + assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in ch.CORE.defines) + + +def test_require_wake_loop_threadsafe__multiple_calls() -> None: + """Test that multiple calls only set up once.""" + # Call three times + ch.require_wake_loop_threadsafe() + ch.require_wake_loop_threadsafe() + ch.require_wake_loop_threadsafe() + + # Verify CORE.data was set + assert ch.CORE.data[ch.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Verify the define was added (only once, but we can just check it exists) + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in ch.CORE.defines) From f11103c895448778a743b35162b972f74df1cc10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 21:50:56 -0600 Subject: [PATCH 018/461] [core][esp32_ble] Add wake_loop_threadsafe() helper for background thread wakeups --- esphome/components/esp32_ble/ble.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index ecdd63f5b18..4221b6331d3 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -304,8 +304,6 @@ bool ESP32BLE::ble_setup_() { } bool ESP32BLE::ble_dismantle_() { - // No socket cleanup needed - wake socket is managed by core Application - esp_err_t err = esp_bluedroid_disable(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err); @@ -577,7 +575,7 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa GAP_SECURITY_EVENTS: enqueue_ble_event(event, param); // Wake up main loop to process security event immediately -#ifdef USE_WAKE_LOOP_THREADSAFE +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); #endif return; @@ -600,7 +598,7 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat esp_ble_gatts_cb_param_t *param) { enqueue_ble_event(event, gatts_if, param); // Wake up main loop to process GATT event immediately -#ifdef USE_WAKE_LOOP_THREADSAFE +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); #endif } @@ -611,7 +609,7 @@ void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gat esp_ble_gattc_cb_param_t *param) { enqueue_ble_event(event, gattc_if, param); // Wake up main loop to process GATT event immediately -#ifdef USE_WAKE_LOOP_THREADSAFE +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); #endif } From 2ac95abea7c31135901c2a3fe4c30760a6b9e4c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 21:51:39 -0600 Subject: [PATCH 019/461] [core][esp32_ble] Add wake_loop_threadsafe() helper for background thread wakeups --- esphome/core/application.h | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index fdc7f02796f..6909eec64a6 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -26,12 +26,8 @@ // Inline function drain_wake_notifications_() needs lwip socket functions #ifdef USE_SOCKET_IMPL_LWIP_SOCKETS #include -#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS) -#ifdef USE_ESP32 +#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS) && defined(USE_ESP32) #include -#else -// True BSD sockets already included via sys/select.h -#endif #endif #endif // USE_WAKE_LOOP_THREADSAFE #endif // USE_SOCKET_SELECT_SUPPORT From acd26600ddcd8682970927d2b55254fe69cd33cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 21:57:57 -0600 Subject: [PATCH 020/461] move to socket --- esphome/codegen.py | 1 - esphome/components/esp32_ble/__init__.py | 6 ++-- esphome/components/socket/__init__.py | 28 ++++++++++++++++ esphome/cpp_helpers.py | 33 +------------------ tests/components/socket/conftest.py | 12 +++++++ tests/components/socket/test_init.py | 42 ++++++++++++++++++++++++ tests/unit_tests/test_cpp_helpers.py | 40 ---------------------- 7 files changed, 87 insertions(+), 75 deletions(-) create mode 100644 tests/components/socket/conftest.py create mode 100644 tests/components/socket/test_init.py diff --git a/esphome/codegen.py b/esphome/codegen.py index f0deb6e8d36..6d55c6023d2 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -51,7 +51,6 @@ from esphome.cpp_helpers import ( # noqa: F401 past_safe_mode, register_component, register_parented, - require_wake_loop_threadsafe, ) from esphome.cpp_types import ( # noqa: F401 NAN, diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index d3db1db70c1..ced7e3fec9b 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -7,6 +7,7 @@ from typing import Any from esphome import automation import esphome.codegen as cg +from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant import esphome.config_validation as cv from esphome.const import ( @@ -21,6 +22,7 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority import esphome.final_validate as fv DEPENDENCIES = ["esp32"] +AUTO_LOAD = ["socket"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] DOMAIN = "esp32_ble" @@ -481,10 +483,10 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) - # BLE uses the core wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks + # BLE uses the socket wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks # This enables low-latency (~12μs) BLE event processing instead of waiting for # select() timeout (0-16ms). The wake socket is shared across all components. - cg.require_wake_loop_threadsafe() + socket.require_wake_loop_threadsafe() # Define max connections for use in C++ code (e.g., ble_server.h) max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index e6a4cfc07ff..4c2ea7f0880 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -3,6 +3,7 @@ from collections.abc import Callable, MutableMapping import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE +from esphome.cpp_generator import add_define CODEOWNERS = ["@esphome/core"] @@ -15,6 +16,9 @@ IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" # Components register their socket needs and platforms read this to configure appropriately KEY_SOCKET_CONSUMERS = "socket_consumers" +# Wake loop threadsafe support tracking +KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" + def consume_sockets( value: int, consumer: str @@ -37,6 +41,30 @@ def consume_sockets( return _consume_sockets +def require_wake_loop_threadsafe() -> None: + """Mark that wake_loop_threadsafe support is required by a component. + + Call this from components that need to wake the main event loop from background threads. + This enables the shared UDP loopback socket mechanism (~208 bytes RAM). + The socket is shared across all components that use this feature. + + IMPORTANT: This is for background thread context only, NOT ISR context. + Socket operations are not safe to call from ISR handlers. + + Example: + from esphome.components import socket + + async def to_code(config): + socket.require_wake_loop_threadsafe() + """ + # Only set up once (idempotent - multiple components can call this) + if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False): + CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + add_define("USE_WAKE_LOOP_THREADSAFE") + # Consume 1 socket for the shared wake notification socket + consume_sockets(1, "socket.wake_loop_threadsafe")({}) + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 8b1fd1db27a..2698b9b3d58 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,7 +9,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable +from esphome.cpp_generator import LogStringLiteral, add, get_variable from esphome.cpp_types import App from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -124,34 +124,3 @@ async def past_safe_mode(): yield return await FakeAwaitable(_safe_mode_generator()) - - -# Wake loop threadsafe support tracking -# Components that need to wake the main event loop from FreeRTOS tasks can call require_wake_loop_threadsafe() -KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" - - -def require_wake_loop_threadsafe() -> None: - """Mark that wake_loop_threadsafe support is required by a component. - - Call this from components that need to wake the main event loop from FreeRTOS tasks. - This enables the shared UDP loopback socket mechanism (~208 bytes RAM). - The socket is shared across all components that use this feature. - - IMPORTANT: This is for FreeRTOS task context only, NOT ISR context. - Socket operations are not safe to call from ISR handlers. - - Example: - import esphome.codegen as cg - - async def to_code(config): - cg.require_wake_loop_threadsafe() - """ - # Only set up once (idempotent - multiple components can call this) - if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False): - from esphome.components import socket - - CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - add_define("USE_WAKE_LOOP_THREADSAFE") - # Consume 1 socket for the shared wake notification socket - socket.consume_sockets(1, "core.wake_loop_threadsafe")({}) diff --git a/tests/components/socket/conftest.py b/tests/components/socket/conftest.py new file mode 100644 index 00000000000..5d93cac232a --- /dev/null +++ b/tests/components/socket/conftest.py @@ -0,0 +1,12 @@ +"""Configuration file for socket component tests.""" + +import pytest + +from esphome.core import CORE + + +@pytest.fixture(autouse=True) +def reset_core(): + """Reset CORE after each test.""" + yield + CORE.reset() diff --git a/tests/components/socket/test_init.py b/tests/components/socket/test_init.py new file mode 100644 index 00000000000..45e5ea22115 --- /dev/null +++ b/tests/components/socket/test_init.py @@ -0,0 +1,42 @@ +from esphome.components import socket +from esphome.core import CORE + + +def test_require_wake_loop_threadsafe__first_call() -> None: + """Test that first call sets up define and consumes socket.""" + socket.require_wake_loop_threadsafe() + + # Verify CORE.data was updated + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Verify the define was added + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) + + +def test_require_wake_loop_threadsafe__idempotent() -> None: + """Test that subsequent calls are idempotent.""" + # Set up initial state as if already called + CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + + # Call again - should not raise or fail + socket.require_wake_loop_threadsafe() + + # Verify state is still True + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Define should not be added since flag was already True + assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) + + +def test_require_wake_loop_threadsafe__multiple_calls() -> None: + """Test that multiple calls only set up once.""" + # Call three times + socket.require_wake_loop_threadsafe() + socket.require_wake_loop_threadsafe() + socket.require_wake_loop_threadsafe() + + # Verify CORE.data was set + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Verify the define was added (only once, but we can just check it exists) + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 89a474f44dd..2618803fecf 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -70,43 +70,3 @@ async def test_register_component__with_setup_priority(monkeypatch): assert add_mock.call_count == 4 app_mock.register_component.assert_called_with(var) assert core_mock.component_ids == [] - - -def test_require_wake_loop_threadsafe__first_call() -> None: - """Test that first call sets up define and consumes socket.""" - ch.require_wake_loop_threadsafe() - - # Verify CORE.data was updated - assert ch.CORE.data[ch.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Verify the define was added - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in ch.CORE.defines) - - -def test_require_wake_loop_threadsafe__idempotent() -> None: - """Test that subsequent calls are idempotent.""" - # Set up initial state as if already called - ch.CORE.data[ch.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - - # Call again - should not raise or fail - ch.require_wake_loop_threadsafe() - - # Verify state is still True - assert ch.CORE.data[ch.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Define should not be added since flag was already True - assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in ch.CORE.defines) - - -def test_require_wake_loop_threadsafe__multiple_calls() -> None: - """Test that multiple calls only set up once.""" - # Call three times - ch.require_wake_loop_threadsafe() - ch.require_wake_loop_threadsafe() - ch.require_wake_loop_threadsafe() - - # Verify CORE.data was set - assert ch.CORE.data[ch.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Verify the define was added (only once, but we can just check it exists) - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in ch.CORE.defines) From 6a48c0f5cf1931fbb817d1ee6dce89182249e413 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 21:59:22 -0600 Subject: [PATCH 021/461] move to socket --- esphome/components/socket/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 4c2ea7f0880..49e074a6ee3 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -3,7 +3,6 @@ from collections.abc import Callable, MutableMapping import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE -from esphome.cpp_generator import add_define CODEOWNERS = ["@esphome/core"] @@ -60,7 +59,7 @@ def require_wake_loop_threadsafe() -> None: # Only set up once (idempotent - multiple components can call this) if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False): CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - add_define("USE_WAKE_LOOP_THREADSAFE") + cg.add_define("USE_WAKE_LOOP_THREADSAFE") # Consume 1 socket for the shared wake notification socket consume_sockets(1, "socket.wake_loop_threadsafe")({}) From 4640198827163c857d53c7bc4db863f6a2a0f746 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 22:01:00 -0600 Subject: [PATCH 022/461] move to socket --- esphome/core/application.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 6909eec64a6..dae44d89027 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -21,15 +21,9 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #include - #ifdef USE_WAKE_LOOP_THREADSAFE -// Inline function drain_wake_notifications_() needs lwip socket functions -#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS -#include -#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS) && defined(USE_ESP32) #include #endif -#endif // USE_WAKE_LOOP_THREADSAFE #endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_BINARY_SENSOR From edd01d5c9cf6dc36a9ebaaa86f0e472e27022e6d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 22:04:14 -0600 Subject: [PATCH 023/461] move to socket --- tests/components/socket/test_init.py | 42 ---------------------------- 1 file changed, 42 deletions(-) delete mode 100644 tests/components/socket/test_init.py diff --git a/tests/components/socket/test_init.py b/tests/components/socket/test_init.py deleted file mode 100644 index 45e5ea22115..00000000000 --- a/tests/components/socket/test_init.py +++ /dev/null @@ -1,42 +0,0 @@ -from esphome.components import socket -from esphome.core import CORE - - -def test_require_wake_loop_threadsafe__first_call() -> None: - """Test that first call sets up define and consumes socket.""" - socket.require_wake_loop_threadsafe() - - # Verify CORE.data was updated - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Verify the define was added - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__idempotent() -> None: - """Test that subsequent calls are idempotent.""" - # Set up initial state as if already called - CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - - # Call again - should not raise or fail - socket.require_wake_loop_threadsafe() - - # Verify state is still True - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Define should not be added since flag was already True - assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__multiple_calls() -> None: - """Test that multiple calls only set up once.""" - # Call three times - socket.require_wake_loop_threadsafe() - socket.require_wake_loop_threadsafe() - socket.require_wake_loop_threadsafe() - - # Verify CORE.data was set - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Verify the define was added (only once, but we can just check it exists) - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) From 8b7ef6cae835a83aeecdb619a04ca4fac5fff418 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 22:04:20 -0600 Subject: [PATCH 024/461] move to socket --- .../socket/test_wake_loop_threadsafe.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/components/socket/test_wake_loop_threadsafe.py diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py new file mode 100644 index 00000000000..45e5ea22115 --- /dev/null +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -0,0 +1,42 @@ +from esphome.components import socket +from esphome.core import CORE + + +def test_require_wake_loop_threadsafe__first_call() -> None: + """Test that first call sets up define and consumes socket.""" + socket.require_wake_loop_threadsafe() + + # Verify CORE.data was updated + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Verify the define was added + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) + + +def test_require_wake_loop_threadsafe__idempotent() -> None: + """Test that subsequent calls are idempotent.""" + # Set up initial state as if already called + CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + + # Call again - should not raise or fail + socket.require_wake_loop_threadsafe() + + # Verify state is still True + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Define should not be added since flag was already True + assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) + + +def test_require_wake_loop_threadsafe__multiple_calls() -> None: + """Test that multiple calls only set up once.""" + # Call three times + socket.require_wake_loop_threadsafe() + socket.require_wake_loop_threadsafe() + socket.require_wake_loop_threadsafe() + + # Verify CORE.data was set + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Verify the define was added (only once, but we can just check it exists) + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) From ee2b10a992ec260ecc9b85d3a9e2bf4161ab5046 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 22:05:15 -0600 Subject: [PATCH 025/461] move to socket --- esphome/components/esp32_ble/ble.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 4221b6331d3..fc26a7fc219 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -297,9 +297,6 @@ bool ESP32BLE::ble_setup_() { // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT - // Wake mechanism is set up by core Application class (wake_loop_threadsafe) - // BLE tasks will call App.wake_loop_threadsafe() to wake main loop when events arrive - return true; } From 8e0721318caae6044b3a9bab422dfd74a36a009a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 22:06:15 -0600 Subject: [PATCH 026/461] analysis --- esphome/core/defines.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 868df6e2547..65069f5b2f8 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -195,6 +195,7 @@ #define USE_PSRAM #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT +#define USE_WAKE_LOOP_THREADSAFE #define USE_SPEAKER #define USE_SPI #define USE_VOICE_ASSISTANT From a41c7b2b3c65aef775ae4d5ff70ce772e28d2c01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 22:16:38 -0600 Subject: [PATCH 027/461] Bump aioesphomeapi from 42.5.0 to 42.6.0 (#11682) 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 660b18c933f..33fa2b64eb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==42.5.0 +aioesphomeapi==42.6.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.16 # dashboard_import From 9da3c08f3b1eff79ed20ff33e9db64500fd910be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 22:43:00 -0600 Subject: [PATCH 028/461] [usb_host] Add wake_loop_threadsafe() for low-latency USB event processing --- esphome/components/usb_host/__init__.py | 8 +++++++- esphome/components/usb_host/usb_host_client.cpp | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index d452e0e9fa8..cccabcf646c 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.components import socket from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, @@ -11,7 +12,7 @@ from esphome.const import CONF_DEVICES, CONF_ID from esphome.cpp_types import Component from esphome.types import ConfigType -AUTO_LOAD = ["bytebuffer"] +AUTO_LOAD = ["bytebuffer", "socket"] CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["esp32"] usb_host_ns = cg.esphome_ns.namespace("usb_host") @@ -71,6 +72,11 @@ async def to_code(config: ConfigType) -> None: max_requests = config[CONF_MAX_TRANSFER_REQUESTS] cg.add_define("USB_HOST_MAX_REQUESTS", max_requests) + # USB uses the socket wake_loop_threadsafe() mechanism to wake the main loop from USB task + # This enables low-latency (~12μs) USB event processing instead of waiting for + # select() timeout (0-16ms). The wake socket is shared across all components. + socket.require_wake_loop_threadsafe() + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for device in config.get(CONF_DEVICES) or (): diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 2139ed869a0..0dda36b9d76 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -3,6 +3,7 @@ #include "usb_host.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/components/bytebuffer/bytebuffer.h" #include @@ -174,6 +175,11 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * // Push to lock-free queue (always succeeds since pool size == queue size) client->event_queue.push(event); + + // Wake main loop immediately to process USB event instead of waiting for select() timeout +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } void USBClient::setup() { usb_host_client_config_t config{.is_synchronous = false, From 42833c85f5fd50313f7dcf965434508aeb030c67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 23:16:39 -0600 Subject: [PATCH 029/461] [climate] Replace std::vector with const char* for custom fan modes and presets (#11621) --- esphome/components/api/api.proto | 4 +- esphome/components/api/api_connection.cpp | 8 +- esphome/components/api/api_pb2.cpp | 16 +- esphome/components/api/api_pb2.h | 4 +- esphome/components/bedjet/bedjet_const.h | 1 - .../bedjet/climate/bedjet_climate.cpp | 68 +++--- .../bedjet/climate/bedjet_climate.h | 12 +- esphome/components/climate/climate.cpp | 223 ++++++++++++------ esphome/components/climate/climate.h | 64 ++++- esphome/components/climate/climate_traits.h | 85 +++++-- esphome/components/demo/demo_climate.h | 22 +- esphome/components/midea/ac_adapter.cpp | 18 +- esphome/components/midea/ac_adapter.h | 14 +- esphome/components/midea/air_conditioner.cpp | 14 +- esphome/components/midea/air_conditioner.h | 8 +- .../thermostat/thermostat_climate.cpp | 47 ++-- .../thermostat/thermostat_climate.h | 2 +- esphome/components/web_server/web_server.cpp | 10 +- script/api_protobuf/api_protobuf.py | 3 +- .../climate_custom_fan_modes_and_presets.yaml | 40 ++++ .../integration/test_climate_custom_modes.py | 42 ++++ 21 files changed, 475 insertions(+), 230 deletions(-) create mode 100644 tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml create mode 100644 tests/integration/test_climate_custom_modes.py diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 20645fc47bf..7a50fa6b179 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1000,9 +1000,9 @@ message ListEntitiesClimateResponse { bool supports_action = 12; // Deprecated: use feature_flags repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; - repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"]; + repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector"]; repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; - repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"]; + repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector"]; bool disabled_by_default = 18; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 20; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 33d5072d9cb..f8490b6ef70 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -637,14 +637,14 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection } if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); - if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) { - resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value())); + if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) { + resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode())); } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } - if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) { - resp.set_custom_preset(StringRef(climate->custom_preset.value())); + if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) { + resp.set_custom_preset(StringRef(climate->get_custom_preset())); } if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 0673d355185..dfa1a1320fe 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1179,14 +1179,14 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { for (const auto &it : *this->supported_swing_modes) { buffer.encode_uint32(14, static_cast(it), true); } - for (const auto &it : *this->supported_custom_fan_modes) { - buffer.encode_string(15, it, true); + for (const char *it : *this->supported_custom_fan_modes) { + buffer.encode_string(15, it, strlen(it), true); } for (const auto &it : *this->supported_presets) { buffer.encode_uint32(16, static_cast(it), true); } - for (const auto &it : *this->supported_custom_presets) { - buffer.encode_string(17, it, true); + for (const char *it : *this->supported_custom_presets) { + buffer.encode_string(17, it, strlen(it), true); } buffer.encode_bool(18, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -1229,8 +1229,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { } } if (!this->supported_custom_fan_modes->empty()) { - for (const auto &it : *this->supported_custom_fan_modes) { - size.add_length_force(1, it.size()); + for (const char *it : *this->supported_custom_fan_modes) { + size.add_length_force(1, strlen(it)); } } if (!this->supported_presets->empty()) { @@ -1239,8 +1239,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { } } if (!this->supported_custom_presets->empty()) { - for (const auto &it : *this->supported_custom_presets) { - size.add_length_force(2, it.size()); + for (const char *it : *this->supported_custom_presets) { + size.add_length_force(2, strlen(it)); } } size.add_bool(2, this->disabled_by_default); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 89f16044d72..716f1a6e9b5 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1384,9 +1384,9 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { bool supports_action{false}; const climate::ClimateFanModeMask *supported_fan_modes{}; const climate::ClimateSwingModeMask *supported_swing_modes{}; - const std::vector *supported_custom_fan_modes{}; + const std::vector *supported_custom_fan_modes{}; const climate::ClimatePresetMask *supported_presets{}; - const std::vector *supported_custom_presets{}; + const std::vector *supported_custom_presets{}; float visual_current_temperature_step{0.0f}; bool supports_current_humidity{false}; bool supports_target_humidity{false}; diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index 0693be10925..10f403dd1ac 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -100,7 +100,6 @@ enum BedjetCommand : uint8_t { static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; -static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; } // namespace bedjet } // namespace esphome diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index f22d312b5ae..716d4d42410 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -8,15 +8,15 @@ namespace bedjet { using namespace esphome::climate; -static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { +static const char *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { if (fan_step < BEDJET_FAN_SPEED_COUNT) - return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; + return BEDJET_FAN_STEP_NAMES[fan_step]; return nullptr; } -static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { +static uint8_t bedjet_fan_speed_to_step(const char *fan_step_percent) { for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) { - if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { + if (strcmp(BEDJET_FAN_STEP_NAMES[i], fan_step_percent) == 0) { return i; } } @@ -48,7 +48,7 @@ void BedJetClimate::dump_config() { ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); } for (const auto &mode : traits.get_supported_custom_fan_modes()) { - ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str()); + ESP_LOGCONFIG(TAG, " - %s (c)", mode); } ESP_LOGCONFIG(TAG, " Supported presets:"); @@ -56,7 +56,7 @@ void BedJetClimate::dump_config() { ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset))); } for (const auto &preset : traits.get_supported_custom_presets()) { - ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str()); + ESP_LOGCONFIG(TAG, " - %s (c)", preset); } } @@ -79,7 +79,7 @@ void BedJetClimate::reset_state_() { this->target_temperature = NAN; this->current_temperature = NAN; this->preset.reset(); - this->custom_preset.reset(); + this->clear_custom_preset_(); this->publish_state(); } @@ -120,7 +120,7 @@ void BedJetClimate::control(const ClimateCall &call) { if (button_result) { this->mode = mode; // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); } } @@ -144,8 +144,7 @@ void BedJetClimate::control(const ClimateCall &call) { if (result) { this->mode = CLIMATE_MODE_HEAT; - this->preset = CLIMATE_PRESET_BOOST; - this->custom_preset.reset(); + this->set_preset_(CLIMATE_PRESET_BOOST); } } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { @@ -153,7 +152,7 @@ void BedJetClimate::control(const ClimateCall &call) { result = this->parent_->send_button(heat_button(this->heating_mode_)); if (result) { this->preset.reset(); - this->custom_preset.reset(); + this->clear_custom_preset_(); } } else { ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'", @@ -164,28 +163,27 @@ void BedJetClimate::control(const ClimateCall &call) { ESP_LOGW(TAG, "Unsupported preset: %d", preset); return; } - } else if (call.get_custom_preset().has_value()) { - std::string preset = *call.get_custom_preset(); + } else if (call.has_custom_preset()) { + const char *preset = call.get_custom_preset(); bool result; - if (preset == "M1") { + if (strcmp(preset, "M1") == 0) { result = this->parent_->button_memory1(); - } else if (preset == "M2") { + } else if (strcmp(preset, "M2") == 0) { result = this->parent_->button_memory2(); - } else if (preset == "M3") { + } else if (strcmp(preset, "M3") == 0) { result = this->parent_->button_memory3(); - } else if (preset == "LTD HT") { + } else if (strcmp(preset, "LTD HT") == 0) { result = this->parent_->button_heat(); - } else if (preset == "EXT HT") { + } else if (strcmp(preset, "EXT HT") == 0) { result = this->parent_->button_ext_heat(); } else { - ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); + ESP_LOGW(TAG, "Unsupported preset: %s", preset); return; } if (result) { - this->custom_preset = preset; - this->preset.reset(); + this->set_custom_preset_(preset); } } @@ -207,19 +205,16 @@ void BedJetClimate::control(const ClimateCall &call) { } if (result) { - this->fan_mode = fan_mode; - this->custom_fan_mode.reset(); + this->set_fan_mode_(fan_mode); } - } else if (call.get_custom_fan_mode().has_value()) { - auto fan_mode = *call.get_custom_fan_mode(); + } else if (call.has_custom_fan_mode()) { + const char *fan_mode = call.get_custom_fan_mode(); auto fan_index = bedjet_fan_speed_to_step(fan_mode); if (fan_index <= 19) { - ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(), - fan_index); + ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index); bool result = this->parent_->set_fan_index(fan_index); if (result) { - this->custom_fan_mode = fan_mode; - this->fan_mode.reset(); + this->set_custom_fan_mode_(fan_mode); } } } @@ -245,7 +240,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); if (fan_mode_name != nullptr) { - this->custom_fan_mode = *fan_mode_name; + this->set_custom_fan_mode_(fan_mode_name); } // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. @@ -255,7 +250,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { this->mode = CLIMATE_MODE_OFF; this->action = CLIMATE_ACTION_IDLE; this->fan_mode = CLIMATE_FAN_OFF; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); break; @@ -266,7 +261,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { if (this->heating_mode_ == HEAT_MODE_EXTENDED) { this->set_custom_preset_("LTD HT"); } else { - this->custom_preset.reset(); + this->clear_custom_preset_(); } break; @@ -275,7 +270,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { this->action = CLIMATE_ACTION_HEATING; this->preset.reset(); if (this->heating_mode_ == HEAT_MODE_EXTENDED) { - this->custom_preset.reset(); + this->clear_custom_preset_(); } else { this->set_custom_preset_("EXT HT"); } @@ -284,20 +279,19 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { case MODE_COOL: this->mode = CLIMATE_MODE_FAN_ONLY; this->action = CLIMATE_ACTION_COOLING; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); break; case MODE_DRY: this->mode = CLIMATE_MODE_DRY; this->action = CLIMATE_ACTION_DRYING; - this->custom_preset.reset(); + this->clear_custom_preset_(); this->preset.reset(); break; case MODE_TURBO: - this->preset = CLIMATE_PRESET_BOOST; - this->custom_preset.reset(); + this->set_preset_(CLIMATE_PRESET_BOOST); this->mode = CLIMATE_MODE_HEAT; this->action = CLIMATE_ACTION_HEATING; break; diff --git a/esphome/components/bedjet/climate/bedjet_climate.h b/esphome/components/bedjet/climate/bedjet_climate.h index dbbb73aeaed..05f4a849e07 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.h +++ b/esphome/components/bedjet/climate/bedjet_climate.h @@ -50,21 +50,13 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. climate::CLIMATE_PRESET_BOOST, }); + // String literals are stored in rodata and valid for program lifetime traits.set_supported_custom_presets({ - // We could fetch biodata from bedjet and set these names that way. - // But then we have to invert the lookup in order to send the right preset. - // For now, we can leave them as M1-3 to match the remote buttons. - // EXT HT added to match remote button. - "EXT HT", + this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT", "M1", "M2", "M3", }); - if (this->heating_mode_ == HEAT_MODE_EXTENDED) { - traits.add_supported_custom_preset("LTD HT"); - } else { - traits.add_supported_custom_preset("EXT HT"); - } traits.set_visual_min_temperature(19.0); traits.set_visual_max_temperature(43.0); traits.set_visual_temperature_step(1.0); diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 944934edbf7..7df38758dce 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -50,21 +50,21 @@ void ClimateCall::perform() { const LogString *mode_s = climate_mode_to_string(*this->mode_); ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); } - if (this->custom_fan_mode_.has_value()) { + if (this->custom_fan_mode_ != nullptr) { this->fan_mode_.reset(); - ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str()); + ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_); } if (this->fan_mode_.has_value()) { - this->custom_fan_mode_.reset(); + this->custom_fan_mode_ = nullptr; const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); } - if (this->custom_preset_.has_value()) { + if (this->custom_preset_ != nullptr) { this->preset_.reset(); - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str()); + ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); } if (this->preset_.has_value()) { - this->custom_preset_.reset(); + this->custom_preset_ = nullptr; const LogString *preset_s = climate_preset_to_string(*this->preset_); ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); } @@ -96,11 +96,10 @@ void ClimateCall::validate_() { this->mode_.reset(); } } - if (this->custom_fan_mode_.has_value()) { - auto custom_fan_mode = *this->custom_fan_mode_; - if (!traits.supports_custom_fan_mode(custom_fan_mode)) { - ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str()); - this->custom_fan_mode_.reset(); + if (this->custom_fan_mode_ != nullptr) { + if (!traits.supports_custom_fan_mode(this->custom_fan_mode_)) { + ESP_LOGW(TAG, " Fan Mode %s not supported", this->custom_fan_mode_); + this->custom_fan_mode_ = nullptr; } } else if (this->fan_mode_.has_value()) { auto fan_mode = *this->fan_mode_; @@ -109,11 +108,10 @@ void ClimateCall::validate_() { this->fan_mode_.reset(); } } - if (this->custom_preset_.has_value()) { - auto custom_preset = *this->custom_preset_; - if (!traits.supports_custom_preset(custom_preset)) { - ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str()); - this->custom_preset_.reset(); + if (this->custom_preset_ != nullptr) { + if (!traits.supports_custom_preset(this->custom_preset_)) { + ESP_LOGW(TAG, " Preset %s not supported", this->custom_preset_); + this->custom_preset_ = nullptr; } } else if (this->preset_.has_value()) { auto preset = *this->preset_; @@ -186,26 +184,29 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) { ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { this->fan_mode_ = fan_mode; - this->custom_fan_mode_.reset(); + this->custom_fan_mode_ = nullptr; return *this; } -ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { +ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { + // Check if it's a standard enum mode first for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { - if (str_equals_case_insensitive(fan_mode, mode_entry.str)) { - this->set_fan_mode(static_cast(mode_entry.value)); - return *this; + if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) { + return this->set_fan_mode(static_cast(mode_entry.value)); } } - if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { - this->custom_fan_mode_ = fan_mode; + // Find the matching pointer from parent climate device + if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) { + this->custom_fan_mode_ = mode_ptr; this->fan_mode_.reset(); - } else { - ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str()); + return *this; } + ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode); return *this; } +ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); } + ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { if (fan_mode.has_value()) { this->set_fan_mode(fan_mode.value()); @@ -215,26 +216,29 @@ ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { this->preset_ = preset; - this->custom_preset_.reset(); + this->custom_preset_ = nullptr; return *this; } -ClimateCall &ClimateCall::set_preset(const std::string &preset) { +ClimateCall &ClimateCall::set_preset(const char *custom_preset) { + // Check if it's a standard enum preset first for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { - if (str_equals_case_insensitive(preset, preset_entry.str)) { - this->set_preset(static_cast(preset_entry.value)); - return *this; + if (str_equals_case_insensitive(custom_preset, preset_entry.str)) { + return this->set_preset(static_cast(preset_entry.value)); } } - if (this->parent_->get_traits().supports_custom_preset(preset)) { - this->custom_preset_ = preset; + // Find the matching pointer from parent climate device + if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) { + this->custom_preset_ = preset_ptr; this->preset_.reset(); - } else { - ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str()); + return *this; } + ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset); return *this; } +ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); } + ClimateCall &ClimateCall::set_preset(optional preset) { if (preset.has_value()) { this->set_preset(preset.value()); @@ -287,8 +291,6 @@ const optional &ClimateCall::get_mode() const { return this->mode_; const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } -const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } -const optional &ClimateCall::get_custom_preset() const { return this->custom_preset_; } ClimateCall &ClimateCall::set_target_temperature_high(optional target_temperature_high) { this->target_temperature_high_ = target_temperature_high; @@ -317,13 +319,13 @@ ClimateCall &ClimateCall::set_mode(optional mode) { ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { this->fan_mode_ = fan_mode; - this->custom_fan_mode_.reset(); + this->custom_fan_mode_ = nullptr; return *this; } ClimateCall &ClimateCall::set_preset(optional preset) { this->preset_ = preset; - this->custom_preset_.reset(); + this->custom_preset_ = nullptr; return *this; } @@ -382,13 +384,13 @@ void Climate::save_state_() { state.uses_custom_fan_mode = false; state.fan_mode = this->fan_mode.value(); } - if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { + if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { state.uses_custom_fan_mode = true; const auto &supported = traits.get_supported_custom_fan_modes(); // std::vector maintains insertion order size_t i = 0; - for (const auto &mode : supported) { - if (mode == custom_fan_mode) { + for (const char *mode : supported) { + if (strcmp(mode, this->custom_fan_mode_) == 0) { state.custom_fan_mode = i; break; } @@ -399,13 +401,13 @@ void Climate::save_state_() { state.uses_custom_preset = false; state.preset = this->preset.value(); } - if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { + if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { state.uses_custom_preset = true; const auto &supported = traits.get_supported_custom_presets(); // std::vector maintains insertion order size_t i = 0; - for (const auto &preset : supported) { - if (preset == custom_preset) { + for (const char *preset : supported) { + if (strcmp(preset, this->custom_preset_) == 0) { state.custom_preset = i; break; } @@ -430,14 +432,14 @@ void Climate::publish_state() { if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } - if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) { - ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode.value().c_str()); + if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { + ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); } if (traits.get_supports_presets() && this->preset.has_value()) { ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } - if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) { - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset.value().c_str()); + if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { + ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); } if (traits.get_supports_swing_modes()) { ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); @@ -527,7 +529,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { if (this->uses_custom_fan_mode) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { call.fan_mode_.reset(); - call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode); + call.custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; } } else if (traits.supports_fan_mode(this->fan_mode)) { call.set_fan_mode(this->fan_mode); @@ -535,7 +537,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { if (this->uses_custom_preset) { if (this->custom_preset < traits.get_supported_custom_presets().size()) { call.preset_.reset(); - call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset); + call.custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; } } else if (traits.supports_preset(this->preset)) { call.set_preset(this->preset); @@ -562,20 +564,20 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { if (this->uses_custom_fan_mode) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { climate->fan_mode.reset(); - climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode); + climate->custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; } } else if (traits.supports_fan_mode(this->fan_mode)) { climate->fan_mode = this->fan_mode; - climate->custom_fan_mode.reset(); + climate->clear_custom_fan_mode_(); } if (this->uses_custom_preset) { if (this->custom_preset < traits.get_supported_custom_presets().size()) { climate->preset.reset(); - climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset); + climate->custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; } } else if (traits.supports_preset(this->preset)) { climate->preset = this->preset; - climate->custom_preset.reset(); + climate->clear_custom_preset_(); } if (traits.supports_swing_mode(this->swing_mode)) { climate->swing_mode = this->swing_mode; @@ -583,28 +585,107 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } -template bool set_alternative(optional &dst, optional &alt, const T1 &src) { - bool is_changed = alt.has_value(); - alt.reset(); - if (is_changed || dst != src) { - dst = src; - is_changed = true; +/** Template helper for setting primary modes (fan_mode, preset) with mutual exclusion. + * + * Climate devices have mutually exclusive mode pairs: + * - fan_mode (enum) vs custom_fan_mode_ (const char*) + * - preset (enum) vs custom_preset_ (const char*) + * + * Only one mode in each pair can be active at a time. This helper ensures setting a primary + * mode automatically clears its corresponding custom mode. + * + * Example state transitions: + * Before: custom_fan_mode_="Turbo", fan_mode=nullopt + * Call: set_fan_mode_(CLIMATE_FAN_HIGH) + * After: custom_fan_mode_=nullptr, fan_mode=CLIMATE_FAN_HIGH + * + * @param primary The primary mode optional (fan_mode or preset) + * @param custom_ptr Reference to the custom mode pointer (custom_fan_mode_ or custom_preset_) + * @param value The new primary mode value to set + * @return true if state changed, false if already set to this value + */ +template bool set_primary_mode(optional &primary, const char *&custom_ptr, T value) { + // Clear the custom mode (mutual exclusion) + bool changed = custom_ptr != nullptr; + custom_ptr = nullptr; + // Set the primary mode + if (changed || !primary.has_value() || primary.value() != value) { + primary = value; + return true; } - return is_changed; + return false; +} + +/** Template helper for setting custom modes (custom_fan_mode_, custom_preset_) with mutual exclusion. + * + * This helper ensures setting a custom mode automatically clears its corresponding primary mode. + * It also validates that the custom mode exists in the device's supported modes (lifetime safety). + * + * Example state transitions: + * Before: fan_mode=CLIMATE_FAN_HIGH, custom_fan_mode_=nullptr + * Call: set_custom_fan_mode_("Turbo") + * After: fan_mode=nullopt, custom_fan_mode_="Turbo" (pointer from traits) + * + * Lifetime Safety: + * - found_ptr must come from traits.find_custom_*_mode_() + * - Only pointers found in traits are stored, ensuring they remain valid + * - Prevents dangling pointers from temporary strings + * + * @param custom_ptr Reference to the custom mode pointer to set + * @param primary The primary mode optional to clear + * @param found_ptr The validated pointer from traits (nullptr if not found) + * @param has_custom Whether a custom mode is currently active + * @return true if state changed, false otherwise + */ +template +bool set_custom_mode(const char *&custom_ptr, optional &primary, const char *found_ptr, bool has_custom) { + if (found_ptr != nullptr) { + // Clear the primary mode (mutual exclusion) + bool changed = primary.has_value(); + primary.reset(); + // Set the custom mode (pointer is validated by caller from traits) + if (changed || custom_ptr != found_ptr) { + custom_ptr = found_ptr; + return true; + } + return false; + } + // Mode not found in supported modes, clear it if currently set + if (has_custom) { + custom_ptr = nullptr; + return true; + } + return false; } bool Climate::set_fan_mode_(ClimateFanMode mode) { - return set_alternative(this->fan_mode, this->custom_fan_mode, mode); + return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode); } -bool Climate::set_custom_fan_mode_(const std::string &mode) { - return set_alternative(this->custom_fan_mode, this->fan_mode, mode); +bool Climate::set_custom_fan_mode_(const char *mode) { + auto traits = this->get_traits(); + return set_custom_mode(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode), + this->has_custom_fan_mode()); } -bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } +void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } -bool Climate::set_custom_preset_(const std::string &preset) { - return set_alternative(this->custom_preset, this->preset, preset); +bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); } + +bool Climate::set_custom_preset_(const char *preset) { + auto traits = this->get_traits(); + return set_custom_mode(this->custom_preset_, this->preset, traits.find_custom_preset_(preset), + this->has_custom_preset()); +} + +void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; } + +const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { + return this->get_traits().find_custom_fan_mode_(custom_fan_mode); +} + +const char *Climate::find_custom_preset_(const char *custom_preset) { + return this->get_traits().find_custom_preset_(custom_preset); } void Climate::dump_traits_(const char *tag) { @@ -656,8 +737,8 @@ void Climate::dump_traits_(const char *tag) { } if (!traits.get_supported_custom_fan_modes().empty()) { ESP_LOGCONFIG(tag, " Supported custom fan modes:"); - for (const std::string &s : traits.get_supported_custom_fan_modes()) - ESP_LOGCONFIG(tag, " - %s", s.c_str()); + for (const char *s : traits.get_supported_custom_fan_modes()) + ESP_LOGCONFIG(tag, " - %s", s); } if (!traits.get_supported_presets().empty()) { ESP_LOGCONFIG(tag, " Supported presets:"); @@ -666,8 +747,8 @@ void Climate::dump_traits_(const char *tag) { } if (!traits.get_supported_custom_presets().empty()) { ESP_LOGCONFIG(tag, " Supported custom presets:"); - for (const std::string &s : traits.get_supported_custom_presets()) - ESP_LOGCONFIG(tag, " - %s", s.c_str()); + for (const char *s : traits.get_supported_custom_presets()) + ESP_LOGCONFIG(tag, " - %s", s); } if (!traits.get_supported_swing_modes().empty()) { ESP_LOGCONFIG(tag, " Supported swing modes:"); diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 0c3e3ebe164..b277877c3e1 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -77,6 +77,8 @@ class ClimateCall { ClimateCall &set_fan_mode(const std::string &fan_mode); /// Set the fan mode of the climate device based on a string. ClimateCall &set_fan_mode(optional fan_mode); + /// Set the custom fan mode of the climate device. + ClimateCall &set_fan_mode(const char *custom_fan_mode); /// Set the swing mode of the climate device. ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); /// Set the swing mode of the climate device. @@ -91,6 +93,8 @@ class ClimateCall { ClimateCall &set_preset(const std::string &preset); /// Set the preset of the climate device based on a string. ClimateCall &set_preset(optional preset); + /// Set the custom preset of the climate device. + ClimateCall &set_preset(const char *custom_preset); void perform(); @@ -103,8 +107,10 @@ class ClimateCall { const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_preset() const; - const optional &get_custom_fan_mode() const; - const optional &get_custom_preset() const; + const char *get_custom_fan_mode() const { return this->custom_fan_mode_; } + const char *get_custom_preset() const { return this->custom_preset_; } + bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } + bool has_custom_preset() const { return this->custom_preset_ != nullptr; } protected: void validate_(); @@ -118,8 +124,10 @@ class ClimateCall { optional fan_mode_; optional swing_mode_; optional preset_; - optional custom_fan_mode_; - optional custom_preset_; + + private: + const char *custom_fan_mode_{nullptr}; + const char *custom_preset_{nullptr}; }; /// Struct used to save the state of the climate device in restore memory. @@ -212,6 +220,12 @@ class Climate : public EntityBase { void set_visual_min_humidity_override(float visual_min_humidity_override); void set_visual_max_humidity_override(float visual_max_humidity_override); + /// Check if a custom fan mode is currently active. + bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } + + /// Check if a custom preset is currently active. + bool has_custom_preset() const { return this->custom_preset_ != nullptr; } + /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; @@ -238,12 +252,6 @@ class Climate : public EntityBase { /// The active preset of the climate device. optional preset; - /// The active custom fan mode of the climate device. - optional custom_fan_mode; - - /// The active custom preset mode of the climate device. - optional custom_preset; - /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; @@ -253,20 +261,37 @@ class Climate : public EntityBase { /// The active swing mode of the climate device. ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; + /// Get the active custom fan mode (read-only access). + const char *get_custom_fan_mode() const { return this->custom_fan_mode_; } + + /// Get the active custom preset (read-only access). + const char *get_custom_preset() const { return this->custom_preset_; } + protected: friend ClimateCall; + friend struct ClimateDeviceRestoreState; /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. bool set_fan_mode_(ClimateFanMode mode); /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. - bool set_custom_fan_mode_(const std::string &mode); + bool set_custom_fan_mode_(const char *mode); + /// Clear custom fan mode. + void clear_custom_fan_mode_(); /// Set preset. Reset custom preset. Return true if preset has been changed. bool set_preset_(ClimatePreset preset); /// Set custom preset. Reset primary preset. Return true if preset has been changed. - bool set_custom_preset_(const std::string &preset); + bool set_custom_preset_(const char *preset); + /// Clear custom preset. + void clear_custom_preset_(); + + /// Find and return the matching custom fan mode pointer from traits, or nullptr if not found. + const char *find_custom_fan_mode_(const char *custom_fan_mode); + + /// Find and return the matching custom preset pointer from traits, or nullptr if not found. + const char *find_custom_preset_(const char *custom_preset); /** Get the default traits of this climate device. * @@ -303,6 +328,21 @@ class Climate : public EntityBase { optional visual_current_temperature_step_override_{}; optional visual_min_humidity_override_{}; optional visual_max_humidity_override_{}; + + private: + /** The active custom fan mode (private - enforces use of safe setters). + * + * Points to an entry in traits.supported_custom_fan_modes_ or nullptr. + * Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify. + */ + const char *custom_fan_mode_{nullptr}; + + /** The active custom preset (private - enforces use of safe setters). + * + * Points to an entry in traits.supported_custom_presets_ or nullptr. + * Use get_custom_preset() to read, set_custom_preset_() to modify. + */ + const char *custom_preset_{nullptr}; }; } // namespace climate diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 1161a54f4ef..0eecf9789fb 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "climate_mode.h" #include "esphome/core/finite_set_mask.h" @@ -18,16 +19,25 @@ using ClimateSwingModeMask = FiniteSetMask>; using ClimatePresetMask = FiniteSetMask>; -// Lightweight linear search for small vectors (1-20 items) +// Lightweight linear search for small vectors (1-20 items) of const char* pointers // Avoids std::find template overhead -template inline bool vector_contains(const std::vector &vec, const T &value) { - for (const auto &item : vec) { - if (item == value) +inline bool vector_contains(const std::vector &vec, const char *value) { + for (const char *item : vec) { + if (strcmp(item, value) == 0) return true; } return false; } +// Find and return matching pointer from vector, or nullptr if not found +inline const char *vector_find(const std::vector &vec, const char *value) { + for (const char *item : vec) { + if (strcmp(item, value) == 0) + return item; + } + return nullptr; +} + /** This class contains all static data for climate devices. * * All climate devices must support these features: @@ -55,7 +65,11 @@ template inline bool vector_contains(const std::vector &vec, cons * - temperature step - the step with which to increase/decrease target temperature. * This also affects with how many decimal places the temperature is shown */ +class Climate; // Forward declaration + class ClimateTraits { + friend class Climate; // Allow Climate to access protected find methods + public: /// Get/set feature flags (see ClimateFeatures enum in climate_mode.h) uint32_t get_feature_flags() const { return this->feature_flags_; } @@ -128,47 +142,61 @@ class ClimateTraits { void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } - void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool get_supports_fan_modes() const { return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); } const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } - void set_supported_custom_fan_modes(std::vector supported_custom_fan_modes) { - this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); + void set_supported_custom_fan_modes(std::initializer_list modes) { + this->supported_custom_fan_modes_ = modes; } - void set_supported_custom_fan_modes(std::initializer_list modes) { + void set_supported_custom_fan_modes(const std::vector &modes) { this->supported_custom_fan_modes_ = modes; } template void set_supported_custom_fan_modes(const char *const (&modes)[N]) { this->supported_custom_fan_modes_.assign(modes, modes + N); } - const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } - bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_supported_custom_fan_modes(const std::vector &modes) = delete; + void set_supported_custom_fan_modes(std::initializer_list modes) = delete; + + const std::vector &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } + bool supports_custom_fan_mode(const char *custom_fan_mode) const { return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); } + bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { + return this->supports_custom_fan_mode(custom_fan_mode.c_str()); + } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } - void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } bool get_supports_presets() const { return !this->supported_presets_.empty(); } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } - void set_supported_custom_presets(std::vector supported_custom_presets) { - this->supported_custom_presets_ = std::move(supported_custom_presets); + void set_supported_custom_presets(std::initializer_list presets) { + this->supported_custom_presets_ = presets; } - void set_supported_custom_presets(std::initializer_list presets) { + void set_supported_custom_presets(const std::vector &presets) { this->supported_custom_presets_ = presets; } template void set_supported_custom_presets(const char *const (&presets)[N]) { this->supported_custom_presets_.assign(presets, presets + N); } - const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } - bool supports_custom_preset(const std::string &custom_preset) const { + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_supported_custom_presets(const std::vector &presets) = delete; + void set_supported_custom_presets(std::initializer_list presets) = delete; + + const std::vector &get_supported_custom_presets() const { return this->supported_custom_presets_; } + bool supports_custom_preset(const char *custom_preset) const { return vector_contains(this->supported_custom_presets_, custom_preset); } + bool supports_custom_preset(const std::string &custom_preset) const { + return this->supports_custom_preset(custom_preset.c_str()); + } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } @@ -227,6 +255,18 @@ class ClimateTraits { } } + /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found + /// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead + const char *find_custom_fan_mode_(const char *custom_fan_mode) const { + return vector_find(this->supported_custom_fan_modes_, custom_fan_mode); + } + + /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found + /// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead + const char *find_custom_preset_(const char *custom_preset) const { + return vector_find(this->supported_custom_presets_, custom_preset); + } + uint32_t feature_flags_{0}; float visual_min_temperature_{10}; float visual_max_temperature_{30}; @@ -239,8 +279,17 @@ class ClimateTraits { climate::ClimateFanModeMask supported_fan_modes_; climate::ClimateSwingModeMask supported_swing_modes_; climate::ClimatePresetMask supported_presets_; - std::vector supported_custom_fan_modes_; - std::vector supported_custom_presets_; + + /** Custom mode storage using const char* pointers to eliminate std::string overhead. + * + * Pointers must remain valid for the ClimateTraits lifetime. Safe patterns: + * - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"}) + * - Static const data: static const char* MODE = "Eco"; + * + * Climate class setters validate pointers are from these vectors before storing. + */ + std::vector supported_custom_fan_modes_; + std::vector supported_custom_presets_; }; } // namespace climate diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index 84b16e7ec58..e2dfb0142be 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -28,16 +28,16 @@ class DemoClimate : public climate::Climate, public Component { this->mode = climate::CLIMATE_MODE_AUTO; this->action = climate::CLIMATE_ACTION_COOLING; this->fan_mode = climate::CLIMATE_FAN_HIGH; - this->custom_preset = {"My Preset"}; + this->set_custom_preset_("My Preset"); break; case DemoClimateType::TYPE_3: this->current_temperature = 21.5; this->target_temperature_low = 21.0; this->target_temperature_high = 22.5; this->mode = climate::CLIMATE_MODE_HEAT_COOL; - this->custom_fan_mode = {"Auto Low"}; + this->set_custom_fan_mode_("Auto Low"); this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; - this->preset = climate::CLIMATE_PRESET_AWAY; + this->set_preset_(climate::CLIMATE_PRESET_AWAY); break; } this->publish_state(); @@ -58,23 +58,19 @@ class DemoClimate : public climate::Climate, public Component { this->target_temperature_high = *call.get_target_temperature_high(); } if (call.get_fan_mode().has_value()) { - this->fan_mode = *call.get_fan_mode(); - this->custom_fan_mode.reset(); + this->set_fan_mode_(*call.get_fan_mode()); } if (call.get_swing_mode().has_value()) { this->swing_mode = *call.get_swing_mode(); } - if (call.get_custom_fan_mode().has_value()) { - this->custom_fan_mode = *call.get_custom_fan_mode(); - this->fan_mode.reset(); + if (call.has_custom_fan_mode()) { + this->set_custom_fan_mode_(call.get_custom_fan_mode()); } if (call.get_preset().has_value()) { - this->preset = *call.get_preset(); - this->custom_preset.reset(); + this->set_preset_(*call.get_preset()); } - if (call.get_custom_preset().has_value()) { - this->custom_preset = *call.get_custom_preset(); - this->preset.reset(); + if (call.has_custom_preset()) { + this->set_custom_preset_(call.get_custom_preset()); } this->publish_state(); } diff --git a/esphome/components/midea/ac_adapter.cpp b/esphome/components/midea/ac_adapter.cpp index 2837713c353..d903db4a1b8 100644 --- a/esphome/components/midea/ac_adapter.cpp +++ b/esphome/components/midea/ac_adapter.cpp @@ -8,9 +8,9 @@ namespace midea { namespace ac { const char *const Constants::TAG = "midea"; -const std::string Constants::FREEZE_PROTECTION = "freeze protection"; -const std::string Constants::SILENT = "silent"; -const std::string Constants::TURBO = "turbo"; +const char *const Constants::FREEZE_PROTECTION = "freeze protection"; +const char *const Constants::SILENT = "silent"; +const char *const Constants::TURBO = "turbo"; ClimateMode Converters::to_climate_mode(MideaMode mode) { switch (mode) { @@ -108,7 +108,7 @@ bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) { } } -const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) { +const char *Converters::to_custom_climate_fan_mode(MideaFanMode mode) { switch (mode) { case MideaFanMode::FAN_SILENT: return Constants::SILENT; @@ -117,8 +117,8 @@ const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) { } } -MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) { - if (mode == Constants::SILENT) +MideaFanMode Converters::to_midea_fan_mode(const char *mode) { + if (strcmp(mode, Constants::SILENT) == 0) return MideaFanMode::FAN_SILENT; return MideaFanMode::FAN_TURBO; } @@ -151,9 +151,9 @@ ClimatePreset Converters::to_climate_preset(MideaPreset preset) { bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; } -const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } +const char *Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } -MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; } +MideaPreset Converters::to_midea_preset(const char *preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; } void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) { if (capabilities.supportAutoMode()) @@ -169,7 +169,7 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea:: if (capabilities.supportEcoPreset()) traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO); if (capabilities.supportFrostProtectionPreset()) - traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION); + traits.set_supported_custom_presets({Constants::FREEZE_PROTECTION}); } } // namespace ac diff --git a/esphome/components/midea/ac_adapter.h b/esphome/components/midea/ac_adapter.h index c17894ae31f..b0589a37f9b 100644 --- a/esphome/components/midea/ac_adapter.h +++ b/esphome/components/midea/ac_adapter.h @@ -20,9 +20,9 @@ using MideaPreset = dudanov::midea::ac::Preset; class Constants { public: static const char *const TAG; - static const std::string FREEZE_PROTECTION; - static const std::string SILENT; - static const std::string TURBO; + static const char *const FREEZE_PROTECTION; + static const char *const SILENT; + static const char *const TURBO; }; class Converters { @@ -32,15 +32,15 @@ class Converters { static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode); static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode); static MideaPreset to_midea_preset(ClimatePreset preset); - static MideaPreset to_midea_preset(const std::string &preset); + static MideaPreset to_midea_preset(const char *preset); static bool is_custom_midea_preset(MideaPreset preset); static ClimatePreset to_climate_preset(MideaPreset preset); - static const std::string &to_custom_climate_preset(MideaPreset preset); + static const char *to_custom_climate_preset(MideaPreset preset); static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode); - static MideaFanMode to_midea_fan_mode(const std::string &fan_mode); + static MideaFanMode to_midea_fan_mode(const char *fan_mode); static bool is_custom_midea_fan_mode(MideaFanMode fan_mode); static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode); - static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode); + static const char *to_custom_climate_fan_mode(MideaFanMode fan_mode); static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities); }; diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 0ad26ebd51e..a6a8d525499 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -64,13 +64,13 @@ void AirConditioner::control(const ClimateCall &call) { ctrl.mode = Converters::to_midea_mode(call.get_mode().value()); if (call.get_preset().has_value()) { ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); - } else if (call.get_custom_preset().has_value()) { - ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value()); + } else if (call.has_custom_preset()) { + ctrl.preset = Converters::to_midea_preset(call.get_custom_preset()); } if (call.get_fan_mode().has_value()) { ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); - } else if (call.get_custom_fan_mode().has_value()) { - ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value()); + } else if (call.has_custom_fan_mode()) { + ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode()); } this->base_.control(ctrl); } @@ -84,8 +84,10 @@ ClimateTraits AirConditioner::traits() { traits.set_supported_modes(this->supported_modes_); traits.set_supported_swing_modes(this->supported_swing_modes_); traits.set_supported_presets(this->supported_presets_); - traits.set_supported_custom_presets(this->supported_custom_presets_); - traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); + if (!this->supported_custom_presets_.empty()) + traits.set_supported_custom_presets(this->supported_custom_presets_); + if (!this->supported_custom_fan_modes_.empty()) + traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); /* + MINIMAL SET OF CAPABILITIES */ traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index 6c2401efe79..70833b8bcca 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -46,8 +46,8 @@ class AirConditioner : public ApplianceBase, void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } - void set_custom_presets(const std::vector &presets) { this->supported_custom_presets_ = presets; } - void set_custom_fan_modes(const std::vector &modes) { this->supported_custom_fan_modes_ = modes; } + void set_custom_presets(std::initializer_list presets) { this->supported_custom_presets_ = presets; } + void set_custom_fan_modes(std::initializer_list modes) { this->supported_custom_fan_modes_ = modes; } protected: void control(const ClimateCall &call) override; @@ -55,8 +55,8 @@ class AirConditioner : public ApplianceBase, ClimateModeMask supported_modes_{}; ClimateSwingModeMask supported_swing_modes_{}; ClimatePresetMask supported_presets_{}; - std::vector supported_custom_presets_{}; - std::vector supported_custom_fan_modes_{}; + std::vector supported_custom_presets_{}; + std::vector supported_custom_fan_modes_{}; Sensor *outdoor_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr}; Sensor *power_sensor_{nullptr}; diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 18efe3984eb..d533ef93eca 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -54,7 +54,7 @@ void ThermostatClimate::setup() { if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) { this->change_preset_(this->default_preset_); } else if (!this->default_custom_preset_.empty()) { - this->change_custom_preset_(this->default_custom_preset_); + this->change_custom_preset_(this->default_custom_preset_.c_str()); } } @@ -218,12 +218,13 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { this->preset = call.get_preset().value(); } } - if (call.get_custom_preset().has_value()) { + if (call.has_custom_preset()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_custom_preset_(call.get_custom_preset().value()); + this->change_custom_preset_(call.get_custom_preset()); } else { - this->custom_preset = call.get_custom_preset().value(); + // Use the base class method which handles pointer lookup internally + this->set_custom_preset_(call.get_custom_preset()); } } @@ -321,9 +322,17 @@ climate::ClimateTraits ThermostatClimate::traits() { for (auto &it : this->preset_config_) { traits.add_supported_preset(it.first); } - for (auto &it : this->custom_preset_config_) { - traits.add_supported_custom_preset(it.first); + + // Extract custom preset names from the custom_preset_config_ map + if (!this->custom_preset_config_.empty()) { + std::vector custom_preset_names; + custom_preset_names.reserve(this->custom_preset_config_.size()); + for (const auto &it : this->custom_preset_config_) { + custom_preset_names.push_back(it.first.c_str()); + } + traits.set_supported_custom_presets(custom_preset_names); } + return traits; } @@ -1153,7 +1162,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { this->preset.value() != preset) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; - this->preset = preset; + this->set_preset_(preset); if (trig != nullptr) { trig->trigger(); } @@ -1163,36 +1172,36 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } else { ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } - this->custom_preset.reset(); - this->preset = preset; } else { ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } } -void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) { +void ThermostatClimate::change_custom_preset_(const char *custom_preset) { auto config = this->custom_preset_config_.find(custom_preset); if (config != this->custom_preset_config_.end()) { - ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); - if (this->change_preset_internal_(config->second) || (!this->custom_preset.has_value()) || - this->custom_preset.value() != custom_preset) { + ESP_LOGV(TAG, "Custom preset %s requested", custom_preset); + if (this->change_preset_internal_(config->second) || !this->has_custom_preset() || + strcmp(this->get_custom_preset(), custom_preset) != 0) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; - this->custom_preset = custom_preset; + // Use the base class method which handles pointer lookup and preset reset internally + this->set_custom_preset_(custom_preset); if (trig != nullptr) { trig->trigger(); } this->refresh(); - ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str()); + ESP_LOGI(TAG, "Custom preset %s applied", custom_preset); } else { - ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str()); + ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset); + // Note: set_custom_preset_() above handles preset.reset() and custom_preset_ assignment internally. + // The old code had these lines here unconditionally, which was a bug (double assignment, state modification + // even when no changes were needed). Now properly handled by the protected setter with mutual exclusion. } - this->preset.reset(); - this->custom_preset = custom_preset; } else { - ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); + ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset); } } diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 42adab7751e..c9795d9666c 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -199,7 +199,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// Change to a provided preset setting; will reset temperature, mode, fan, and swing modes accordingly void change_preset_(climate::ClimatePreset preset); /// Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accordingly - void change_custom_preset_(const std::string &custom_preset); + void change_custom_preset_(const char *custom_preset); /// Applies the temperature, mode, fan, and swing modes of the provided config. /// This is agnostic of custom vs built in preset diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index fe0da14435f..465356db805 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1315,7 +1315,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf for (climate::ClimatePreset m : traits.get_supported_presets()) opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); } - if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { + if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { JsonArray opt = root["custom_presets"].to(); for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); @@ -1336,14 +1336,14 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf if (traits.get_supports_fan_modes() && obj->fan_mode.has_value()) { root["fan_mode"] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); } - if (!traits.get_supported_custom_fan_modes().empty() && obj->custom_fan_mode.has_value()) { - root["custom_fan_mode"] = obj->custom_fan_mode.value().c_str(); + if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) { + root["custom_fan_mode"] = obj->get_custom_fan_mode(); } if (traits.get_supports_presets() && obj->preset.has_value()) { root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); } - if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { - root["custom_preset"] = obj->custom_preset.value().c_str(); + if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { + root["custom_preset"] = obj->get_custom_preset(); } if (traits.get_supports_swing_modes()) { root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 394e92b9a75..3b756095a1a 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1610,8 +1610,9 @@ class RepeatedTypeInfo(TypeInfo): # Other types need the actual value # Special handling for const char* elements if self._use_pointer and "const char" in self._container_no_template: + field_id_size = self.calculate_field_id_size() o += f" for (const char *it : {container_ref}) {{\n" - o += " size.add_length_force(1, strlen(it));\n" + o += f" size.add_length_force({field_id_size}, strlen(it));\n" else: auto_ref = "" if self._ti_is_bool else "&" o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" diff --git a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml new file mode 100644 index 00000000000..bf4ef9eafd5 --- /dev/null +++ b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml @@ -0,0 +1,40 @@ +esphome: + name: climate-custom-modes-test +host: +api: +logger: + +sensor: + - platform: template + id: thermostat_sensor + lambda: "return 22.0;" + +climate: + - platform: thermostat + id: test_thermostat + name: Test Thermostat Custom Modes + sensor: thermostat_sensor + preset: + - name: Away + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C + - name: Eco Plus + default_target_temperature_low: 18°C + default_target_temperature_high: 22°C + - name: Super Saver + default_target_temperature_low: 20°C + default_target_temperature_high: 24°C + - name: Vacation Mode + default_target_temperature_low: 15°C + default_target_temperature_high: 18°C + idle_action: + - logger.log: idle_action + cool_action: + - logger.log: cool_action + heat_action: + - logger.log: heat_action + min_cooling_off_time: 10s + min_cooling_run_time: 10s + min_heating_off_time: 10s + min_heating_run_time: 10s + min_idle_time: 10s diff --git a/tests/integration/test_climate_custom_modes.py b/tests/integration/test_climate_custom_modes.py new file mode 100644 index 00000000000..ce34959d88f --- /dev/null +++ b/tests/integration/test_climate_custom_modes.py @@ -0,0 +1,42 @@ +"""Integration test for climate custom presets.""" + +from __future__ import annotations + +from aioesphomeapi import ClimateInfo, ClimatePreset +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_climate_custom_fan_modes_and_presets( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that custom presets are properly exposed via API.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get entities and services + entities, services = await client.list_entities_services() + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + + test_climate = climate_infos[0] + + # Verify enum presets are exposed (from preset: config map) + assert ClimatePreset.AWAY in test_climate.supported_presets, ( + "Expected AWAY in enum presets" + ) + + # Verify custom string presets are exposed (non-standard preset names from preset map) + custom_presets = test_climate.supported_custom_presets + assert len(custom_presets) == 3, ( + f"Expected 3 custom presets, got {len(custom_presets)}: {custom_presets}" + ) + assert "Eco Plus" in custom_presets, "Expected 'Eco Plus' in custom presets" + assert "Super Saver" in custom_presets, ( + "Expected 'Super Saver' in custom presets" + ) + assert "Vacation Mode" in custom_presets, ( + "Expected 'Vacation Mode' in custom presets" + ) From 0e792d07912346c85dc09e41d6081b8861c2f8c2 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sun, 2 Nov 2025 21:20:08 -0800 Subject: [PATCH 030/461] [nrf52,debug] fix status of nRESET pin, add extra registry from UICR (#11667) Co-authored-by: J. Nick Koston --- esphome/components/debug/debug_zephyr.cpp | 33 ++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 231b39a7111..62fa391e5fa 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -8,8 +8,7 @@ #define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0] -namespace esphome { -namespace debug { +namespace esphome::debug { static const char *const TAG = "debug"; constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; @@ -281,14 +280,18 @@ void DebugComponent::get_device_info_(std::string &device_info) { NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE)); ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); + bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] && + (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected + << UICR_PSELRESET_CONNECT_Pos; ESP_LOGD( TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), - YESNO(((NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) != - (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)) || - ((NRF_UICR->PSELRESET[1] & UICR_PSELRESET_CONNECT_Msk) != - (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)))); - + YESNO(n_reset_enabled)); + if (n_reset_enabled) { + uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos; + uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos; + ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin); + } #ifdef USE_BOOTLOADER_MCUBOOT ESP_LOGD(TAG, "bootloader: mcuboot"); #else @@ -322,10 +325,22 @@ void DebugComponent::get_device_info_(std::string &device_info) { #endif } #endif + auto uicr = [](volatile uint32_t *data, uint8_t size) { + std::string res; + char buf[sizeof(uint32_t) * 2 + 1]; + for (size_t i = 0; i < size; i++) { + if (i > 0) { + res += ' '; + } + res += format_hex_pretty(data[i], '\0', false); + } + return res; + }; + ESP_LOGD(TAG, "NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); + ESP_LOGD(TAG, "NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); } void DebugComponent::update_platform_() {} -} // namespace debug -} // namespace esphome +} // namespace esphome::debug #endif From 7e1cea8e6941f0e5d7df17a23c1213a352331b14 Mon Sep 17 00:00:00 2001 From: Kent Gibson Date: Mon, 3 Nov 2025 22:05:33 +0800 Subject: [PATCH 031/461] [template] alarm_control_panel more ESP_LOGCONFIG reductions (#11691) --- .../template_alarm_control_panel.cpp | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index f7e9872ce10..af662a05a00 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -42,23 +42,21 @@ static const LogString *sensor_type_to_string(AlarmSensorType type) { #endif void TemplateAlarmControlPanel::dump_config() { - ESP_LOGCONFIG(TAG, "TemplateAlarmControlPanel:"); ESP_LOGCONFIG(TAG, + "TemplateAlarmControlPanel:\n" " Current State: %s\n" - " Number of Codes: %u", - LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), this->codes_.size()); - if (!this->codes_.empty()) - ESP_LOGCONFIG(TAG, " Requires Code To Arm: %s", YESNO(this->requires_code_to_arm_)); - ESP_LOGCONFIG(TAG, " Arming Away Time: %" PRIu32 "s", (this->arming_away_time_ / 1000)); - if (this->arming_home_time_ != 0) - ESP_LOGCONFIG(TAG, " Arming Home Time: %" PRIu32 "s", (this->arming_home_time_ / 1000)); - if (this->arming_night_time_ != 0) - ESP_LOGCONFIG(TAG, " Arming Night Time: %" PRIu32 "s", (this->arming_night_time_ / 1000)); - ESP_LOGCONFIG(TAG, + " Number of Codes: %u\n" + " Requires Code To Arm: %s\n" + " Arming Away Time: %" PRIu32 "s\n" + " Arming Home Time: %" PRIu32 "s\n" + " Arming Night Time: %" PRIu32 "s\n" " Pending Time: %" PRIu32 "s\n" " Trigger Time: %" PRIu32 "s\n" " Supported Features: %" PRIu32, - (this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features()); + LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), this->codes_.size(), + YESNO(!this->codes_.empty() && this->requires_code_to_arm_), (this->arming_away_time_ / 1000), + (this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000), + (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR for (auto const &[sensor, info] : this->sensor_map_) { ESP_LOGCONFIG(TAG, From 1ec1692c774173dd73d91bbe6ededc402207274a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 08:23:04 -0600 Subject: [PATCH 032/461] [mqtt] Fix climate custom fan mode and preset compilation errors (#11692) --- esphome/components/mqtt/mqtt_climate.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index a6f4e0a2018..aee2b389421 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -357,8 +357,8 @@ bool MQTTClimateComponent::publish_state_() { payload = "unknown"; } } - if (this->device_->custom_preset.has_value()) - payload = this->device_->custom_preset.value(); + if (this->device_->has_custom_preset()) + payload = this->device_->get_custom_preset(); if (!this->publish(this->get_preset_state_topic(), payload)) success = false; } @@ -429,8 +429,8 @@ bool MQTTClimateComponent::publish_state_() { payload = "unknown"; } } - if (this->device_->custom_fan_mode.has_value()) - payload = this->device_->custom_fan_mode.value(); + if (this->device_->has_custom_fan_mode()) + payload = this->device_->get_custom_fan_mode(); if (!this->publish(this->get_fan_mode_state_topic(), payload)) success = false; } From f05f45af74f2fc02e30445ca67f449c672fee6b4 Mon Sep 17 00:00:00 2001 From: Nathan Bernard <3122054+pixelatedmirror@users.noreply.github.com> Date: Mon, 3 Nov 2025 07:17:28 -0800 Subject: [PATCH 033/461] Add support for Mopeka standard check alternate ID (#10907) Co-authored-by: J. Nick Koston --- esphome/components/mopeka_std_check/mopeka_std_check.cpp | 2 +- esphome/components/mopeka_std_check/mopeka_std_check.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 6685a23c411..0d8340f95fb 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -72,7 +72,7 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const u_int8_t hardware_id = mopeka_data->data_1 & 0xCF; if (static_cast(hardware_id) != STANDARD && static_cast(hardware_id) != XL && - static_cast(hardware_id) != ETRAILER) { + static_cast(hardware_id) != ETRAILER && static_cast(hardware_id) != STANDARD_ALT) { ESP_LOGE(TAG, "[%s] Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id); return false; } diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index b92445df34b..897b5414ed0 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -15,6 +15,7 @@ namespace mopeka_std_check { enum SensorType { STANDARD = 0x02, XL = 0x03, + STANDARD_ALT = 0x44, ETRAILER = 0x46, }; From cb039b42aa235042fb9c51373eadcf2d98af2108 Mon Sep 17 00:00:00 2001 From: Paul Strawder Date: Mon, 3 Nov 2025 17:34:53 +0100 Subject: [PATCH 034/461] [esp32] Make the loop task's stack size configurable (#10564) Co-authored-by: Paul Strawder Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 12 ++++++++++++ esphome/components/esp32/core.cpp | 5 +++-- esphome/core/defines.h | 1 + .../components/esp32/test-stack_size.esp32-idf.yaml | 6 ++++++ 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 tests/components/esp32/test-stack_size.esp32-idf.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index ddb8dbb1f0d..6981662d77e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -558,6 +558,7 @@ CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram" CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" +CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" # VFS requirement tracking # Components that need VFS features can call require_vfs_select() or require_vfs_dir() @@ -654,6 +655,9 @@ FRAMEWORK_SCHEMA = cv.All( ): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, + cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( + min=8192, max=32768 + ), } ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( @@ -926,6 +930,10 @@ async def to_code(config): f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" ), ) + add_idf_sdkconfig_option( + "CONFIG_ARDUINO_LOOP_STACK_SIZE", + conf[CONF_ADVANCED][CONF_LOOP_TASK_STACK_SIZE], + ) add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) @@ -1071,6 +1079,10 @@ async def to_code(config): ) add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True) + cg.add_define( + "ESPHOME_LOOP_TASK_STACK_SIZE", advanced.get(CONF_LOOP_TASK_STACK_SIZE) + ) + cg.add_define( "USE_ESP_IDF_VERSION_CODE", cg.RawExpression( diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 1c8f29fa950..6215ff862f8 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -1,5 +1,6 @@ #ifdef USE_ESP32 +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -97,9 +98,9 @@ void loop_task(void *pv_params) { extern "C" void app_main() { esp32::setup_preferences(); #if CONFIG_FREERTOS_UNICORE - xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle); + xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle); #else - xTaskCreatePinnedToCore(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle, 1); + xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1); #endif } #endif // USE_ESP_IDF diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 868df6e2547..dc37dcbc0eb 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -155,6 +155,7 @@ // IDF-specific feature flags #ifdef USE_ESP_IDF #define USE_MQTT_IDF_ENQUEUE +#define ESPHOME_LOOP_TASK_STACK_SIZE 8192 #endif // ESP32-specific feature flags diff --git a/tests/components/esp32/test-stack_size.esp32-idf.yaml b/tests/components/esp32/test-stack_size.esp32-idf.yaml new file mode 100644 index 00000000000..49535880352 --- /dev/null +++ b/tests/components/esp32/test-stack_size.esp32-idf.yaml @@ -0,0 +1,6 @@ +esp32: + board: esp32dev + framework: + type: esp-idf + advanced: + loop_task_stack_size: 16384 From 06d0787ee06e0780606c6e7010d599f08adaaef2 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 3 Nov 2025 08:42:49 -0800 Subject: [PATCH 035/461] [nrf52, i2c] i2c support for nrf52 (#8150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Ludovic BOUÉ Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/i2c/__init__.py | 70 ++++++++- esphome/components/i2c/i2c.cpp | 3 + esphome/components/i2c/i2c_bus_zephyr.cpp | 133 ++++++++++++++++++ esphome/components/i2c/i2c_bus_zephyr.h | 38 +++++ esphome/components/zephyr/__init__.py | 3 +- .../ads1115/test.nrf52-adafruit.yaml | 4 + .../components/ads1115/test.nrf52-mcumgr.yaml | 4 + .../components/aht10/test.nrf52-adafruit.yaml | 4 + tests/components/i2c/test.nrf52-adafruit.yaml | 4 + tests/components/i2c/test.nrf52-mcumgr.yaml | 4 + tests/components/i2c/test.nrf52-xiao-ble.yaml | 4 + .../common/i2c/nrf52.yaml | 11 ++ 12 files changed, 276 insertions(+), 6 deletions(-) create mode 100644 esphome/components/i2c/i2c_bus_zephyr.cpp create mode 100644 esphome/components/i2c/i2c_bus_zephyr.h create mode 100644 tests/components/ads1115/test.nrf52-adafruit.yaml create mode 100644 tests/components/ads1115/test.nrf52-mcumgr.yaml create mode 100644 tests/components/aht10/test.nrf52-adafruit.yaml create mode 100644 tests/components/i2c/test.nrf52-adafruit.yaml create mode 100644 tests/components/i2c/test.nrf52-mcumgr.yaml create mode 100644 tests/components/i2c/test.nrf52-xiao-ble.yaml create mode 100644 tests/test_build_components/common/i2c/nrf52.yaml diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 3cfec1e94dc..63089237590 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -2,11 +2,18 @@ import logging from esphome import pins import esphome.codegen as cg +from esphome.components.zephyr import ( + zephyr_add_overlay, + zephyr_add_prj_conf, + zephyr_data, +) +from esphome.components.zephyr.const import KEY_BOARD from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, CONF_FREQUENCY, + CONF_I2C, CONF_I2C_ID, CONF_ID, CONF_SCAN, @@ -15,10 +22,12 @@ from esphome.const import ( CONF_TIMEOUT, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_NRF52, PLATFORM_RP2040, PlatformFramework, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj import esphome.final_validate as fv LOGGER = logging.getLogger(__name__) @@ -28,6 +37,7 @@ I2CBus = i2c_ns.class_("I2CBus") InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus) ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component) IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component) +ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component) I2CDevice = i2c_ns.class_("I2CDevice") @@ -41,6 +51,8 @@ def _bus_declare_type(value): return cv.declare_id(ArduinoI2CBus)(value) if CORE.using_esp_idf: return cv.declare_id(IDFI2CBus)(value) + if CORE.using_zephyr: + return cv.declare_id(ZephyrI2CBus)(value) raise NotImplementedError @@ -62,23 +74,70 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( cv.only_with_esp_idf, cv.boolean ), - cv.Optional(CONF_FREQUENCY, default="50kHz"): cv.All( - cv.frequency, cv.Range(min=0, min_included=False) + cv.SplitDefault( + CONF_FREQUENCY, + esp32="50kHz", + esp8266="50kHz", + rp2040="50kHz", + nrf52="100kHz", + ): cv.All( + cv.frequency, + cv.Range(min=0, min_included=False), + ), + cv.Optional(CONF_TIMEOUT): cv.All( + cv.only_with_framework(["arduino", "esp-idf"]), + cv.positive_time_period, ), - cv.Optional(CONF_TIMEOUT): cv.positive_time_period, cv.Optional(CONF_SCAN, default=True): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_NRF52]), validate_config, ) +def _final_validate(config): + full_config = fv.full_config.get()[CONF_I2C] + if CORE.using_zephyr and len(full_config) > 1: + raise cv.Invalid("Second i2c is not implemented on Zephyr yet") + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(CoroPriority.BUS) async def to_code(config): cg.add_global(i2c_ns.using) cg.add_define("USE_I2C") - var = cg.new_Pvariable(config[CONF_ID]) + if CORE.using_zephyr: + zephyr_add_prj_conf("I2C", True) + i2c = "i2c0" + if zephyr_data()[KEY_BOARD] in ["xiao_ble"]: + i2c = "i2c1" + zephyr_add_overlay( + f""" + &pinctrl {{ + {i2c}_default: {i2c}_default {{ + group1 {{ + psels = , + ; + }}; + }}; + {i2c}_sleep: {i2c}_sleep {{ + group1 {{ + psels = , + ; + low-power-enable; + }}; + }}; + }}; + """ + ) + var = cg.new_Pvariable( + config[CONF_ID], MockObj(f"DEVICE_DT_GET(DT_NODELABEL({i2c}))") + ) + else: + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) cg.add(var.set_sda_pin(config[CONF_SDA])) @@ -197,5 +256,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.LN882X_ARDUINO, }, "i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 31c21f398cb..f8c7a1b40bd 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -1,6 +1,7 @@ #include "i2c.h" #include "esphome/core/defines.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include @@ -23,6 +24,8 @@ void I2CBus::i2c_scan_() { } else if (err == ERROR_UNKNOWN) { scan_results_.emplace_back(address, false); } + // it takes 16sec to scan on nrf52. It prevents board reset. + arch_feed_wdt(); } #if defined(USE_ESP32) && defined(USE_LOGGER) esp_log_level_set("*", previous); diff --git a/esphome/components/i2c/i2c_bus_zephyr.cpp b/esphome/components/i2c/i2c_bus_zephyr.cpp new file mode 100644 index 00000000000..658dcee35ce --- /dev/null +++ b/esphome/components/i2c/i2c_bus_zephyr.cpp @@ -0,0 +1,133 @@ +#ifdef USE_ZEPHYR + +#include "i2c_bus_zephyr.h" +#include +#include "esphome/core/log.h" + +namespace esphome::i2c { + +static const char *const TAG = "i2c.zephyr"; + +void ZephyrI2CBus::setup() { + if (!device_is_ready(this->i2c_dev_)) { + ESP_LOGE(TAG, "I2C dev is not ready."); + mark_failed(); + return; + } + + int ret = i2c_configure(this->i2c_dev_, this->dev_config_); + if (ret < 0) { + ESP_LOGE(TAG, "I2C: Failed to configure device"); + } + + this->recovery_result_ = i2c_recover_bus(this->i2c_dev_); + if (this->recovery_result_ != 0) { + ESP_LOGE(TAG, "I2C recover bus failed, err %d", this->recovery_result_); + } + if (this->scan_) { + ESP_LOGV(TAG, "Scanning I2C bus for active devices..."); + this->i2c_scan_(); + } +} + +void ZephyrI2CBus::dump_config() { + auto get_speed = [](uint32_t dev_config) { + switch (I2C_SPEED_GET(dev_config)) { + case I2C_SPEED_STANDARD: + return "100 kHz"; + case I2C_SPEED_FAST: + return "400 kHz"; + case I2C_SPEED_FAST_PLUS: + return "1 MHz"; + case I2C_SPEED_HIGH: + return "3.4 MHz"; + case I2C_SPEED_ULTRA: + return "5 MHz"; + } + return "unknown"; + }; + ESP_LOGCONFIG(TAG, + "I2C Bus:\n" + " SDA Pin: GPIO%u\n" + " SCL Pin: GPIO%u\n" + " Frequency: %s\n" + " Name: %s", + this->sda_pin_, this->scl_pin_, get_speed(this->dev_config_), this->i2c_dev_->name); + + if (this->recovery_result_ != 0) { + ESP_LOGCONFIG(TAG, " Recovery: failed, err %d", this->recovery_result_); + } else { + ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered"); + } + if (this->scan_) { + ESP_LOGI(TAG, "Results from I2C bus scan:"); + if (scan_results_.empty()) { + ESP_LOGI(TAG, "Found no I2C devices!"); + } else { + for (const auto &s : scan_results_) { + if (s.second) { + ESP_LOGI(TAG, "Found I2C device at address 0x%02X", s.first); + } else { + ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first); + } + } + } + } +} + +ErrorCode ZephyrI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, + uint8_t *read_buffer, size_t read_count) { + if (!device_is_ready(this->i2c_dev_)) { + return ERROR_NOT_INITIALIZED; + } + + i2c_msg msgs[2]{}; + size_t cnt = 0; + uint8_t dst = 0x00; // dummy data to not use random value + + if (read_count == 0 && write_count == 0) { + msgs[cnt].buf = &dst; + msgs[cnt].len = 0U; + msgs[cnt++].flags = I2C_MSG_WRITE; + } else { + if (write_count) { + // the same struct is used for read/write — const cast is fine; data isn't modified + msgs[cnt].buf = const_cast(write_buffer); + msgs[cnt].len = write_count; + msgs[cnt++].flags = I2C_MSG_WRITE; + } + if (read_count) { + msgs[cnt].buf = const_cast(read_buffer); + msgs[cnt].len = read_count; + msgs[cnt++].flags = I2C_MSG_READ | I2C_MSG_RESTART; + } + } + + msgs[cnt - 1].flags |= I2C_MSG_STOP; + + auto err = i2c_transfer(this->i2c_dev_, msgs, cnt, address); + + if (err == -EIO) { + return ERROR_NOT_ACKNOWLEDGED; + } + + if (err != 0) { + ESP_LOGE(TAG, "i2c transfer error %d", err); + return ERROR_UNKNOWN; + } + + return ERROR_OK; +} + +void ZephyrI2CBus::set_frequency(uint32_t frequency) { + this->dev_config_ &= ~I2C_SPEED_MASK; + if (frequency >= 400000) { + this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_FAST); + } else { + this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_STANDARD); + } +} + +} // namespace esphome::i2c + +#endif diff --git a/esphome/components/i2c/i2c_bus_zephyr.h b/esphome/components/i2c/i2c_bus_zephyr.h new file mode 100644 index 00000000000..49cac5b9921 --- /dev/null +++ b/esphome/components/i2c/i2c_bus_zephyr.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef USE_ZEPHYR + +#include "i2c_bus.h" +#include "esphome/core/component.h" + +struct device; + +namespace esphome::i2c { + +class ZephyrI2CBus : public InternalI2CBus, public Component { + public: + explicit ZephyrI2CBus(const device *i2c_dev) : i2c_dev_(i2c_dev) {} + void setup() override; + void dump_config() override; + ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void set_scan(bool scan) { scan_ = scan; } + void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; } + void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; } + void set_frequency(uint32_t frequency); + + int get_port() const override { return 0; } + + protected: + const device *i2c_dev_; + int recovery_result_ = 0; + uint8_t sda_pin_{}; + uint8_t scl_pin_{}; + uint32_t dev_config_{}; +}; + +} // namespace esphome::i2c + +#endif diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index a2fb12a5e22..0381fbcba9d 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -1,4 +1,5 @@ from pathlib import Path +import textwrap from typing import TypedDict import esphome.codegen as cg @@ -90,7 +91,7 @@ def zephyr_add_prj_conf( def zephyr_add_overlay(content): - zephyr_data()[KEY_OVERLAY] += content + zephyr_data()[KEY_OVERLAY] += textwrap.dedent(content) def add_extra_build_file(filename: str, path: Path) -> bool: diff --git a/tests/components/ads1115/test.nrf52-adafruit.yaml b/tests/components/ads1115/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/ads1115/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/ads1115/test.nrf52-mcumgr.yaml b/tests/components/ads1115/test.nrf52-mcumgr.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/ads1115/test.nrf52-mcumgr.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/aht10/test.nrf52-adafruit.yaml b/tests/components/aht10/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/aht10/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/i2c/test.nrf52-adafruit.yaml b/tests/components/i2c/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/i2c/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/i2c/test.nrf52-mcumgr.yaml b/tests/components/i2c/test.nrf52-mcumgr.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/i2c/test.nrf52-mcumgr.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/i2c/test.nrf52-xiao-ble.yaml b/tests/components/i2c/test.nrf52-xiao-ble.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/i2c/test.nrf52-xiao-ble.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/test_build_components/common/i2c/nrf52.yaml b/tests/test_build_components/common/i2c/nrf52.yaml new file mode 100644 index 00000000000..b86cdf0d693 --- /dev/null +++ b/tests/test_build_components/common/i2c/nrf52.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for NRF52 tests + +substitutions: + scl_pin: P0.04 + sda_pin: P0.05 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true From 3f12630a6b0227ee6787f92709205692313a629e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 13:13:37 -0600 Subject: [PATCH 036/461] [core][esp32_ble][socket] Add wake_loop_threadsafe() helper for background thread wakeups (#11681) --- esphome/components/esp32_ble/__init__.py | 10 +- esphome/components/esp32_ble/ble.cpp | 112 +----------------- esphome/components/esp32_ble/ble.h | 40 +------ esphome/components/socket/__init__.py | 27 +++++ esphome/core/application.cpp | 79 ++++++++++++ esphome/core/application.h | 42 +++++++ esphome/core/defines.h | 1 + tests/components/socket/conftest.py | 12 ++ .../socket/test_wake_loop_threadsafe.py | 42 +++++++ 9 files changed, 217 insertions(+), 148 deletions(-) create mode 100644 tests/components/socket/conftest.py create mode 100644 tests/components/socket/test_wake_loop_threadsafe.py diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 1ae8df6f5ef..ced7e3fec9b 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -22,6 +22,7 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority import esphome.final_validate as fv DEPENDENCIES = ["esp32"] +AUTO_LOAD = ["socket"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] DOMAIN = "esp32_ble" @@ -482,13 +483,10 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) - # BLE uses 1 UDP socket for event notification to wake up main loop from select() + # BLE uses the socket wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks # This enables low-latency (~12μs) BLE event processing instead of waiting for - # select() timeout (0-16ms). The socket is created in ble_setup_() and used to - # wake lwip_select() when BLE events arrive from the BLE thread. - # Note: Called during config generation, socket is created at runtime. In practice, - # always used since esp32_ble only runs on ESP32 which always has USE_SOCKET_SELECT_SUPPORT. - socket.consume_sockets(1, "esp32_ble")(config) + # select() timeout (0-16ms). The wake socket is shared across all components. + socket.require_wake_loop_threadsafe() # Define max connections for use in C++ code (e.g., ble_server.h) max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index d6f7e1ce430..fc26a7fc219 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -297,21 +297,10 @@ bool ESP32BLE::ble_setup_() { // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT - // Set up notification socket to wake main loop for BLE events - // This enables low-latency (~12μs) event processing instead of waiting for select() timeout -#ifdef USE_SOCKET_SELECT_SUPPORT - this->setup_event_notification_(); -#endif - return true; } bool ESP32BLE::ble_dismantle_() { - // Clean up notification socket first before dismantling BLE stack -#ifdef USE_SOCKET_SELECT_SUPPORT - this->cleanup_event_notification_(); -#endif - esp_err_t err = esp_bluedroid_disable(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err); @@ -409,12 +398,6 @@ void ESP32BLE::loop() { break; } -#ifdef USE_SOCKET_SELECT_SUPPORT - // Drain any notification socket events first - // This clears the socket so it doesn't stay "ready" in subsequent select() calls - this->drain_event_notifications_(); -#endif - BLEEvent *ble_event = this->ble_events_.pop(); while (ble_event != nullptr) { switch (ble_event->type_) { @@ -589,8 +572,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa GAP_SECURITY_EVENTS: enqueue_ble_event(event, param); // Wake up main loop to process security event immediately -#ifdef USE_SOCKET_SELECT_SUPPORT - global_ble->notify_main_loop_(); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); #endif return; @@ -612,8 +595,8 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat esp_ble_gatts_cb_param_t *param) { enqueue_ble_event(event, gatts_if, param); // Wake up main loop to process GATT event immediately -#ifdef USE_SOCKET_SELECT_SUPPORT - global_ble->notify_main_loop_(); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); #endif } #endif @@ -623,8 +606,8 @@ void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gat esp_ble_gattc_cb_param_t *param) { enqueue_ble_event(event, gattc_if, param); // Wake up main loop to process GATT event immediately -#ifdef USE_SOCKET_SELECT_SUPPORT - global_ble->notify_main_loop_(); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); #endif } #endif @@ -665,89 +648,6 @@ void ESP32BLE::dump_config() { } } -#ifdef USE_SOCKET_SELECT_SUPPORT -void ESP32BLE::setup_event_notification_() { - // Create UDP socket for event notifications - this->notify_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (this->notify_fd_ < 0) { - ESP_LOGW(TAG, "Event socket create failed: %d", errno); - return; - } - - // Bind to loopback with auto-assigned port - struct sockaddr_in addr = {}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK); - addr.sin_port = 0; // Auto-assign port - - if (lwip_bind(this->notify_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { - ESP_LOGW(TAG, "Event socket bind failed: %d", errno); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - // Get the assigned address and connect to it - // Connecting a UDP socket allows using send() instead of sendto() for better performance - struct sockaddr_in notify_addr; - socklen_t len = sizeof(notify_addr); - if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) ¬ify_addr, &len) < 0) { - ESP_LOGW(TAG, "Event socket address failed: %d", errno); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - // Connect to self (loopback) - allows using send() instead of sendto() - // After connect(), no need to store notify_addr - the socket remembers it - if (lwip_connect(this->notify_fd_, (struct sockaddr *) ¬ify_addr, sizeof(notify_addr)) < 0) { - ESP_LOGW(TAG, "Event socket connect failed: %d", errno); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - // Set non-blocking mode - int flags = lwip_fcntl(this->notify_fd_, F_GETFL, 0); - lwip_fcntl(this->notify_fd_, F_SETFL, flags | O_NONBLOCK); - - // Register with application's select() loop - if (!App.register_socket_fd(this->notify_fd_)) { - ESP_LOGW(TAG, "Event socket register failed"); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - ESP_LOGD(TAG, "Event socket ready"); -} - -void ESP32BLE::cleanup_event_notification_() { - if (this->notify_fd_ >= 0) { - App.unregister_socket_fd(this->notify_fd_); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - ESP_LOGD(TAG, "Event socket closed"); - } -} - -void ESP32BLE::drain_event_notifications_() { - // Called from main loop to drain any pending notifications - // Must check is_socket_ready() to avoid blocking on empty socket - if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) { - char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE]; - // Drain all pending notifications with non-blocking reads - // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK - // We control both ends of this loopback socket (always write 1 byte per event), - // so no error checking needed - any errors indicate catastrophic system failure - while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { - // Just draining, no action needed - actual BLE events are already queued - } - } -} - -#endif // USE_SOCKET_SELECT_SUPPORT - uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { uint64_t u = 0; u |= uint64_t(address[0] & 0xFF) << 40; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 7c3195db6df..3be6a7048d6 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -166,12 +166,10 @@ class ESP32BLE : public Component { void advertising_init_(); #endif -#ifdef USE_SOCKET_SELECT_SUPPORT - void setup_event_notification_(); // Create notification socket - void cleanup_event_notification_(); // Close and unregister socket - inline void notify_main_loop_(); // Wake up select() from BLE thread (hot path - inlined) - void drain_event_notifications_(); // Read pending notifications in main loop -#endif + // BLE uses the core wake_loop_threadsafe() mechanism to wake the main event loop + // from BLE tasks. This enables low-latency (~12μs) event processing instead of + // waiting for select() timeout (0-16ms). The wake socket is shared with other + // components that need this functionality. private: template friend void enqueue_ble_event(Args... args); @@ -207,13 +205,6 @@ class ESP32BLE : public Component { esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) uint32_t advertising_cycle_time_{}; // 4 bytes -#ifdef USE_SOCKET_SELECT_SUPPORT - // Event notification socket for waking up main loop from BLE thread - // Uses connected UDP loopback socket to wake lwip_select() with ~12μs latency vs 0-16ms timeout - // Socket is connected during setup, allowing use of send() instead of sendto() for efficiency - int notify_fd_{-1}; // 4 bytes (file descriptor) -#endif - // 2-byte aligned members uint16_t appearance_{0}; // 2 bytes @@ -225,29 +216,6 @@ class ESP32BLE : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32BLE *global_ble; -#ifdef USE_SOCKET_SELECT_SUPPORT -// Inline implementations for hot-path functions -// These are called from BLE thread (notify) and main loop (drain) on every event - -// Small buffer for draining notification bytes (1 byte sent per BLE event) -// Size allows draining multiple notifications per recvfrom() without wasting stack -static constexpr size_t BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE = 16; - -inline void ESP32BLE::notify_main_loop_() { - // Called from BLE thread context when events are queued - // Wakes up lwip_select() in main loop by writing to connected loopback socket - if (this->notify_fd_ >= 0) { - const char dummy = 1; - // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway - // No error checking needed: we control both ends of this loopback socket, and the - // BLE event is already queued. Notification is best-effort to reduce latency. - // This is safe to call from BLE thread - send() is thread-safe in lwip - // Socket is already connected to loopback address, so send() is faster than sendto() - lwip_send(this->notify_fd_, &dummy, 1, 0); - } -} -#endif // USE_SOCKET_SELECT_SUPPORT - template class BLEEnabledCondition : public Condition { public: bool check(Ts... x) override { return global_ble->is_active(); } diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index e6a4cfc07ff..49e074a6ee3 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -15,6 +15,9 @@ IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" # Components register their socket needs and platforms read this to configure appropriately KEY_SOCKET_CONSUMERS = "socket_consumers" +# Wake loop threadsafe support tracking +KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" + def consume_sockets( value: int, consumer: str @@ -37,6 +40,30 @@ def consume_sockets( return _consume_sockets +def require_wake_loop_threadsafe() -> None: + """Mark that wake_loop_threadsafe support is required by a component. + + Call this from components that need to wake the main event loop from background threads. + This enables the shared UDP loopback socket mechanism (~208 bytes RAM). + The socket is shared across all components that use this feature. + + IMPORTANT: This is for background thread context only, NOT ISR context. + Socket operations are not safe to call from ISR handlers. + + Example: + from esphome.components import socket + + async def to_code(config): + socket.require_wake_loop_threadsafe() + """ + # Only set up once (idempotent - multiple components can call this) + if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False): + CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + cg.add_define("USE_WAKE_LOOP_THREADSAFE") + # Consume 1 socket for the shared wake notification socket + consume_sockets(1, "socket.wake_loop_threadsafe")({}) + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 61cfcc75853..75814ae2535 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -122,6 +122,11 @@ void Application::setup() { // Clear setup priority overrides to free memory clear_setup_priority_overrides(); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Set up wake socket for waking main loop from tasks + this->setup_wake_loop_threadsafe_(); +#endif + this->schedule_dump_config(); } void Application::loop() { @@ -472,6 +477,11 @@ void Application::enable_pending_loops_() { } void Application::before_loop_tasks_(uint32_t loop_start_time) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Drain wake notifications first to clear socket for next wake + this->drain_wake_notifications_(); +#endif + // Process scheduled tasks this->scheduler.call(loop_start_time); @@ -625,4 +635,73 @@ void Application::yield_with_select_(uint32_t delay_ms) { Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +void Application::setup_wake_loop_threadsafe_() { + // Create UDP socket for wake notifications + this->wake_socket_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (this->wake_socket_fd_ < 0) { + ESP_LOGW(TAG, "Wake socket create failed: %d", errno); + return; + } + + // Bind to loopback with auto-assigned port + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // Auto-assign port + + if (lwip_bind(this->wake_socket_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + ESP_LOGW(TAG, "Wake socket bind failed: %d", errno); + lwip_close(this->wake_socket_fd_); + this->wake_socket_fd_ = -1; + return; + } + + // Get the assigned address and connect to it + // Connecting a UDP socket allows using send() instead of sendto() for better performance + struct sockaddr_in wake_addr; + socklen_t len = sizeof(wake_addr); + if (lwip_getsockname(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, &len) < 0) { + ESP_LOGW(TAG, "Wake socket address failed: %d", errno); + lwip_close(this->wake_socket_fd_); + this->wake_socket_fd_ = -1; + return; + } + + // Connect to self (loopback) - allows using send() instead of sendto() + // After connect(), no need to store wake_addr - the socket remembers it + if (lwip_connect(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) { + ESP_LOGW(TAG, "Wake socket connect failed: %d", errno); + lwip_close(this->wake_socket_fd_); + this->wake_socket_fd_ = -1; + return; + } + + // Set non-blocking mode + int flags = lwip_fcntl(this->wake_socket_fd_, F_GETFL, 0); + lwip_fcntl(this->wake_socket_fd_, F_SETFL, flags | O_NONBLOCK); + + // Register with application's select() loop + if (!this->register_socket_fd(this->wake_socket_fd_)) { + ESP_LOGW(TAG, "Wake socket register failed"); + lwip_close(this->wake_socket_fd_); + this->wake_socket_fd_ = -1; + return; + } +} + +void Application::wake_loop_threadsafe() { + // Called from FreeRTOS task context when events need immediate processing + // Wakes up lwip_select() in main loop by writing to connected loopback socket + if (this->wake_socket_fd_ >= 0) { + const char dummy = 1; + // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway + // No error checking needed: we control both ends of this loopback socket. + // This is safe to call from FreeRTOS tasks - send() is thread-safe in lwip + // Socket is already connected to loopback address, so send() is faster than sendto() + lwip_send(this->wake_socket_fd_, &dummy, 1, 0); + } +} +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + } // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h index 29a734f000b..dae44d89027 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -21,7 +21,10 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #include +#ifdef USE_WAKE_LOOP_THREADSAFE +#include #endif +#endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" @@ -429,6 +432,13 @@ class Application { /// Check if there's data available on a socket without blocking /// This function is thread-safe for reading, but should be called after select() has run bool is_socket_ready(int fd) const; + +#ifdef USE_WAKE_LOOP_THREADSAFE + /// Wake the main event loop from a FreeRTOS task + /// Thread-safe, can be called from task context to immediately wake select() + /// IMPORTANT: NOT safe to call from ISR context (socket operations not ISR-safe) + void wake_loop_threadsafe(); +#endif #endif protected: @@ -454,6 +464,11 @@ class Application { /// Perform a delay while also monitoring socket file descriptors for readiness void yield_with_select_(uint32_t delay_ms); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + void setup_wake_loop_threadsafe_(); // Create wake notification socket + inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) +#endif + // === Member variables ordered by size to minimize padding === // Pointer-sized members first @@ -481,6 +496,9 @@ class Application { FixedVector looping_components_{}; #ifdef USE_SOCKET_SELECT_SUPPORT std::vector socket_fds_; // Vector of all monitored socket file descriptors +#ifdef USE_WAKE_LOOP_THREADSAFE + int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks +#endif #endif // std::string members (typically 24-32 bytes each) @@ -597,4 +615,28 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +// Inline implementations for hot-path functions +// drain_wake_notifications_() is called on every loop iteration + +// Small buffer for draining wake notification bytes (1 byte sent per wake) +// Size allows draining multiple notifications per recvfrom() without wasting stack +static constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16; + +inline void Application::drain_wake_notifications_() { + // Called from main loop to drain any pending wake notifications + // Must check is_socket_ready() to avoid blocking on empty socket + if (this->wake_socket_fd_ >= 0 && this->is_socket_ready(this->wake_socket_fd_)) { + char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE]; + // Drain all pending notifications with non-blocking reads + // Multiple wake events may have triggered multiple writes, so drain until EWOULDBLOCK + // We control both ends of this loopback socket (always write 1 byte per wake), + // so no error checking needed - any errors indicate catastrophic system failure + while (lwip_recvfrom(this->wake_socket_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { + // Just draining, no action needed - wake has already occurred + } + } +} +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index dc37dcbc0eb..2be32058ea0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -196,6 +196,7 @@ #define USE_PSRAM #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT +#define USE_WAKE_LOOP_THREADSAFE #define USE_SPEAKER #define USE_SPI #define USE_VOICE_ASSISTANT diff --git a/tests/components/socket/conftest.py b/tests/components/socket/conftest.py new file mode 100644 index 00000000000..5d93cac232a --- /dev/null +++ b/tests/components/socket/conftest.py @@ -0,0 +1,12 @@ +"""Configuration file for socket component tests.""" + +import pytest + +from esphome.core import CORE + + +@pytest.fixture(autouse=True) +def reset_core(): + """Reset CORE after each test.""" + yield + CORE.reset() diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py new file mode 100644 index 00000000000..45e5ea22115 --- /dev/null +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -0,0 +1,42 @@ +from esphome.components import socket +from esphome.core import CORE + + +def test_require_wake_loop_threadsafe__first_call() -> None: + """Test that first call sets up define and consumes socket.""" + socket.require_wake_loop_threadsafe() + + # Verify CORE.data was updated + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Verify the define was added + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) + + +def test_require_wake_loop_threadsafe__idempotent() -> None: + """Test that subsequent calls are idempotent.""" + # Set up initial state as if already called + CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + + # Call again - should not raise or fail + socket.require_wake_loop_threadsafe() + + # Verify state is still True + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Define should not be added since flag was already True + assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) + + +def test_require_wake_loop_threadsafe__multiple_calls() -> None: + """Test that multiple calls only set up once.""" + # Call three times + socket.require_wake_loop_threadsafe() + socket.require_wake_loop_threadsafe() + socket.require_wake_loop_threadsafe() + + # Verify CORE.data was set + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Verify the define was added (only once, but we can just check it exists) + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) From fb7dbc99103fa1225a88df22833f433d093cc623 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 13:50:39 -0600 Subject: [PATCH 037/461] [usb_host] Add wake_loop_threadsafe() for low-latency USB event processing (#11683) --- esphome/components/usb_host/__init__.py | 8 +++++++- esphome/components/usb_host/usb_host_client.cpp | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index d452e0e9fa8..cccabcf646c 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.components import socket from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, @@ -11,7 +12,7 @@ from esphome.const import CONF_DEVICES, CONF_ID from esphome.cpp_types import Component from esphome.types import ConfigType -AUTO_LOAD = ["bytebuffer"] +AUTO_LOAD = ["bytebuffer", "socket"] CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["esp32"] usb_host_ns = cg.esphome_ns.namespace("usb_host") @@ -71,6 +72,11 @@ async def to_code(config: ConfigType) -> None: max_requests = config[CONF_MAX_TRANSFER_REQUESTS] cg.add_define("USB_HOST_MAX_REQUESTS", max_requests) + # USB uses the socket wake_loop_threadsafe() mechanism to wake the main loop from USB task + # This enables low-latency (~12μs) USB event processing instead of waiting for + # select() timeout (0-16ms). The wake socket is shared across all components. + socket.require_wake_loop_threadsafe() + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for device in config.get(CONF_DEVICES) or (): diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 2139ed869a0..0dda36b9d76 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -3,6 +3,7 @@ #include "usb_host.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/components/bytebuffer/bytebuffer.h" #include @@ -174,6 +175,11 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * // Push to lock-free queue (always succeeds since pool size == queue size) client->event_queue.push(event); + + // Wake main loop immediately to process USB event instead of waiting for select() timeout +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } void USBClient::setup() { usb_host_client_config_t config{.is_synchronous = false, From 9c7cb30ae597a4d132b2242cc1fca55067a6e4b3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:08:50 -0500 Subject: [PATCH 038/461] [esp32_hosted] Initial OTA implementation (#11562) --- CODEOWNERS | 1 + .../esp32_hosted/update/__init__.py | 78 +++++++++ .../update/esp32_hosted_update.cpp | 164 ++++++++++++++++++ .../esp32_hosted/update/esp32_hosted_update.h | 32 ++++ esphome/idf_component.yml | 6 +- script/ci-custom.py | 1 + tests/components/esp32_hosted/.gitattributes | 1 + .../esp32_hosted/test.esp32-p4-idf.yaml | 6 + .../components/esp32_hosted/test_firmware.bin | Bin 0 -> 65536 bytes 9 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 esphome/components/esp32_hosted/update/__init__.py create mode 100644 esphome/components/esp32_hosted/update/esp32_hosted_update.cpp create mode 100644 esphome/components/esp32_hosted/update/esp32_hosted_update.h create mode 100644 tests/components/esp32_hosted/.gitattributes create mode 100644 tests/components/esp32_hosted/test_firmware.bin diff --git a/CODEOWNERS b/CODEOWNERS index 667a44fc03e..fee0e98f46d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -155,6 +155,7 @@ esphome/components/esp32_ble_tracker/* @bdraco esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron esphome/components/esp32_hosted/* @swoboda1337 +esphome/components/esp32_hosted/update/* @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/update/__init__.py b/esphome/components/esp32_hosted/update/__init__.py new file mode 100644 index 00000000000..040f989a646 --- /dev/null +++ b/esphome/components/esp32_hosted/update/__init__.py @@ -0,0 +1,78 @@ +import hashlib +from typing import Any + +import esphome.codegen as cg +from esphome.components import esp32, update +import esphome.config_validation as cv +from esphome.const import CONF_PATH, CONF_RAW_DATA_ID +from esphome.core import CORE, HexInt + +CODEOWNERS = ["@swoboda1337"] +AUTO_LOAD = ["sha256", "watchdog"] +DEPENDENCIES = ["esp32_hosted"] + +CONF_SHA256 = "sha256" + +esp32_hosted_ns = cg.esphome_ns.namespace("esp32_hosted") +Esp32HostedUpdate = esp32_hosted_ns.class_( + "Esp32HostedUpdate", update.UpdateEntity, cg.Component +) + + +def _validate_sha256(value: Any) -> str: + value = cv.string_strict(value) + if len(value) != 64: + raise cv.Invalid("SHA256 must be 64 hexadecimal characters") + try: + bytes.fromhex(value) + except ValueError as e: + raise cv.Invalid(f"SHA256 must be valid hexadecimal: {e}") from e + return value + + +CONFIG_SCHEMA = cv.All( + update.update_schema(Esp32HostedUpdate, device_class="firmware").extend( + { + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + cv.Required(CONF_PATH): cv.file_, + cv.Required(CONF_SHA256): _validate_sha256, + } + ), + esp32.only_on_variant( + supported=[ + esp32.const.VARIANT_ESP32H2, + esp32.const.VARIANT_ESP32P4, + ] + ), +) + + +def _validate_firmware(config: dict[str, Any]) -> None: + path = CORE.relative_config_path(config[CONF_PATH]) + with open(path, "rb") as f: + firmware_data = f.read() + calculated = hashlib.sha256(firmware_data).hexdigest() + expected = config[CONF_SHA256].lower() + if calculated != expected: + raise cv.Invalid( + f"SHA256 mismatch for {config[CONF_PATH]}: expected {expected}, got {calculated}" + ) + + +FINAL_VALIDATE_SCHEMA = _validate_firmware + + +async def to_code(config: dict[str, Any]) -> None: + var = await update.new_update(config) + + path = config[CONF_PATH] + with open(CORE.relative_config_path(path), "rb") as f: + firmware_data = f.read() + rhs = [HexInt(x) for x in firmware_data] + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + + sha256_bytes = bytes.fromhex(config[CONF_SHA256]) + cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes])) + cg.add(var.set_firmware_data(prog_arr)) + cg.add(var.set_firmware_size(len(firmware_data))) + await cg.register_component(var, config) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp new file mode 100644 index 00000000000..adbcc5bf115 --- /dev/null +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -0,0 +1,164 @@ +#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) +#include "esp32_hosted_update.h" +#include "esphome/components/watchdog/watchdog.h" +#include "esphome/components/sha256/sha256.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include +#include +#include + +extern "C" { +#include +} + +namespace esphome::esp32_hosted { + +static const char *const TAG = "esp32_hosted.update"; + +// older coprocessor firmware versions have a 1500-byte limit per RPC call +constexpr size_t CHUNK_SIZE = 1500; + +void Esp32HostedUpdate::setup() { + this->update_info_.title = "ESP32 Hosted Coprocessor"; + + // get coprocessor version + esp_hosted_coprocessor_fwver_t ver_info; + if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { + this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1); + } else { + this->update_info_.current_version = "unknown"; + } + ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str()); + + // get image version + const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t); + if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) { + esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset); + if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) { + ESP_LOGD(TAG, "Firmware version: %s", app_desc->version); + ESP_LOGD(TAG, "Project name: %s", app_desc->project_name); + ESP_LOGD(TAG, "Build date: %s", app_desc->date); + ESP_LOGD(TAG, "Build time: %s", app_desc->time); + ESP_LOGD(TAG, "IDF version: %s", app_desc->idf_ver); + this->update_info_.latest_version = app_desc->version; + if (this->update_info_.latest_version != this->update_info_.current_version) { + this->state_ = update::UPDATE_STATE_AVAILABLE; + } else { + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } + } else { + ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word, + ESP_APP_DESC_MAGIC_WORD); + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } + } else { + ESP_LOGW(TAG, "Firmware too small to contain app description"); + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } + + // publish state + this->status_clear_error(); + this->publish_state(); +} + +void Esp32HostedUpdate::dump_config() { + ESP_LOGCONFIG(TAG, + "ESP32 Hosted Update:\n" + " Current Version: %s\n" + " Latest Version: %s\n" + " Latest Size: %zu bytes", + this->update_info_.current_version.c_str(), this->update_info_.latest_version.c_str(), + this->firmware_size_); +} + +void Esp32HostedUpdate::perform(bool force) { + if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) { + ESP_LOGW(TAG, "Update not available"); + return; + } + + if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) { + ESP_LOGE(TAG, "No firmware data available"); + return; + } + + sha256::SHA256 hasher; + hasher.init(); + hasher.add(this->firmware_data_, this->firmware_size_); + hasher.calculate(); + if (!hasher.equals_bytes(this->firmware_sha256_.data())) { + this->status_set_error("SHA256 verification failed"); + this->publish_state(); + return; + } + + ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_); + + watchdog::WatchdogManager watchdog(20000); + update::UpdateState prev_state = this->state_; + this->state_ = update::UPDATE_STATE_INSTALLING; + this->update_info_.has_progress = false; + this->publish_state(); + + esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err)); + this->state_ = prev_state; + this->status_set_error("Failed to begin OTA"); + this->publish_state(); + return; + } + + uint8_t chunk[CHUNK_SIZE]; + const uint8_t *data_ptr = this->firmware_data_; + size_t remaining = this->firmware_size_; + while (remaining > 0) { + size_t chunk_size = std::min(remaining, static_cast(CHUNK_SIZE)); + memcpy(chunk, data_ptr, chunk_size); + err = esp_hosted_slave_ota_write(chunk, chunk_size); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err)); + esp_hosted_slave_ota_end(); // NOLINT + this->state_ = prev_state; + this->status_set_error("Failed to write OTA data"); + this->publish_state(); + return; + } + data_ptr += chunk_size; + remaining -= chunk_size; + App.feed_wdt(); + } + + err = esp_hosted_slave_ota_end(); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err)); + this->state_ = prev_state; + this->status_set_error("Failed to end OTA"); + this->publish_state(); + return; + } + + // activate new firmware + err = esp_hosted_slave_ota_activate(); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err)); + this->state_ = prev_state; + this->status_set_error("Failed to activate OTA"); + this->publish_state(); + return; + } + + // update state + ESP_LOGI(TAG, "OTA update successful"); + this->state_ = update::UPDATE_STATE_NO_UPDATE; + this->status_clear_error(); + this->publish_state(); + + // schedule a restart to ensure everything is in sync + ESP_LOGI(TAG, "Restarting in 1 second"); + this->set_timeout(1000, []() { App.safe_reboot(); }); +} + +} // namespace esphome::esp32_hosted +#endif diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.h b/esphome/components/esp32_hosted/update/esp32_hosted_update.h new file mode 100644 index 00000000000..9c087bf72aa --- /dev/null +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.h @@ -0,0 +1,32 @@ +#pragma once + +#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) + +#include "esphome/core/component.h" +#include "esphome/components/update/update_entity.h" +#include + +namespace esphome::esp32_hosted { + +class Esp32HostedUpdate : public update::UpdateEntity, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + void perform(bool force) override; + void check() override {} + + void set_firmware_data(const uint8_t *data) { this->firmware_data_ = data; } + void set_firmware_size(size_t size) { this->firmware_size_ = size; } + void set_firmware_sha256(const std::array &sha256) { this->firmware_sha256_ = sha256; } + + protected: + const uint8_t *firmware_data_{nullptr}; + size_t firmware_size_{0}; + std::array firmware_sha256_; +}; + +} // namespace esphome::esp32_hosted + +#endif diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 1a6dc8b97d3..31112caf0ad 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -6,15 +6,15 @@ dependencies: espressif/mdns: version: 1.8.2 espressif/esp_wifi_remote: - version: 0.10.2 + version: 1.1.5 rules: - if: "target in [esp32h2, esp32p4]" espressif/eppp_link: - version: 0.2.0 + version: 1.1.3 rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.0.11 + version: 2.6.1 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: diff --git a/script/ci-custom.py b/script/ci-custom.py index 6b01623d92c..106aa438fe7 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -71,6 +71,7 @@ ignore_types = ( ".apng", ".gif", ".webp", + ".bin", ) LINT_FILE_CHECKS = [] diff --git a/tests/components/esp32_hosted/.gitattributes b/tests/components/esp32_hosted/.gitattributes new file mode 100644 index 00000000000..6abdc561178 --- /dev/null +++ b/tests/components/esp32_hosted/.gitattributes @@ -0,0 +1 @@ +*.bin -text diff --git a/tests/components/esp32_hosted/test.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml index dade44d145b..2a76f17e2fb 100644 --- a/tests/components/esp32_hosted/test.esp32-p4-idf.yaml +++ b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml @@ -1 +1,7 @@ <<: !include common.yaml + +update: + - platform: esp32_hosted + name: "Coprocessor Firmware Update" + path: $component_dir/test_firmware.bin + sha256: de2f256064a0af797747c2b97505dc0b9f3df0de4f489eac731c23ae9ca9cc31 diff --git a/tests/components/esp32_hosted/test_firmware.bin b/tests/components/esp32_hosted/test_firmware.bin new file mode 100644 index 0000000000000000000000000000000000000000..c97c12f9b0a24bfc19c74a2b265a97c924137775 GIT binary patch literal 65536 zcmZP=1*0J_8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8Umvs zFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF z0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71* zAut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@? z8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O zqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8Umvs zFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF z0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71* zAut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@? z8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O zqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8Umvs zFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF V0;3@?8UmvsFd71*A;7@E0000F00961 literal 0 HcmV?d00001 From e65d3da763c82460c144695b9b1efda510996c10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 15:00:37 -0600 Subject: [PATCH 039/461] [micro_wake_word] Add wake_loop_threadsafe() for low-latency wake word detection --- esphome/components/micro_wake_word/__init__.py | 7 ++++++- esphome/components/micro_wake_word/micro_wake_word.cpp | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 8cd71153681..575fb97799b 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin from esphome import automation, external_files, git from esphome.automation import register_action, register_condition import esphome.codegen as cg -from esphome.components import esp32, microphone +from esphome.components import esp32, microphone, socket import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@kahrendt", "@jesserockz"] DEPENDENCIES = ["microphone"] +AUTO_LOAD = ["socket"] DOMAIN = "micro_wake_word" @@ -443,6 +444,10 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + # Enable wake_loop_threadsafe() for low-latency wake word detection + # The inference task queues detection events that need immediate processing + socket.require_wake_loop_threadsafe() + mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE]) cg.add(var.set_microphone_source(mic_source)) diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 6fca48a5bd3..a0547b158ef 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -2,6 +2,7 @@ #ifdef USE_ESP_IDF +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -426,6 +427,12 @@ void MicroWakeWord::process_probabilities_() { if (vad_state.detected) { #endif xQueueSend(this->detection_queue_, &wake_word_state, portMAX_DELAY); + + // Wake main loop immediately to process wake word detection +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + model->reset_probabilities(); #ifdef USE_MICRO_WAKE_WORD_VAD } else { From 8aa8bb8f9898760dfb169c57706b91e9833da785 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 4 Nov 2025 07:45:32 +1000 Subject: [PATCH 040/461] [epaper_spi] Refactoring (#11540) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/display/display.h | 2 +- esphome/components/epaper_spi/display.py | 124 ++++++++-- esphome/components/epaper_spi/epaper_spi.cpp | 210 ++++++++++------- esphome/components/epaper_spi/epaper_spi.h | 104 ++++++--- .../epaper_spi_model_7p3in_spectra_e6.cpp | 42 ---- .../epaper_spi_model_7p3in_spectra_e6.h | 45 ---- .../epaper_spi/epaper_spi_spectra_e6.cpp | 221 ++++++++++-------- .../epaper_spi/epaper_spi_spectra_e6.h | 15 +- .../components/epaper_spi/models/__init__.py | 65 ++++++ .../epaper_spi/models/spectra_e6.py | 51 ++++ esphome/components/mipi/__init__.py | 23 +- .../components/split_buffer/split_buffer.cpp | 43 ++-- .../components/split_buffer/split_buffer.h | 10 +- .../epaper_spi/test.esp32-s3-idf.yaml | 8 +- 14 files changed, 619 insertions(+), 344 deletions(-) delete mode 100644 esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp delete mode 100644 esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h create mode 100644 esphome/components/epaper_spi/models/__init__.py create mode 100644 esphome/components/epaper_spi/models/spectra_e6.py diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index f2d79d12d95..14205da8535 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -210,7 +210,7 @@ class Display : public PollingComponent { /// Fill the entire screen with the given color. virtual void fill(Color color); /// Clear the entire screen by filling it with OFF pixels. - void clear(); + virtual void clear(); /// Get the calculated width of the display in pixels with rotation applied. virtual int get_width() { return this->get_width_internal(); } diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 20549f049dd..9ff393b3975 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -1,21 +1,35 @@ +import importlib +import pkgutil + from esphome import core, pins import esphome.codegen as cg from esphome.components import display, spi +from esphome.components.mipi import flatten_sequence, map_sequence import esphome.config_validation as cv from esphome.const import ( CONF_BUSY_PIN, + CONF_CS_PIN, + CONF_DATA_RATE, CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_HEIGHT, CONF_ID, + CONF_INIT_SEQUENCE, CONF_LAMBDA, CONF_MODEL, - CONF_PAGES, CONF_RESET_DURATION, CONF_RESET_PIN, + CONF_WIDTH, ) +from . import models + AUTO_LOAD = ["split_buffer"] DEPENDENCIES = ["spi"] +CONF_INIT_SEQUENCE_ID = "init_sequence_id" + epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi") EPaperBase = epaper_spi_ns.class_( "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer @@ -24,30 +38,79 @@ EPaperBase = epaper_spi_ns.class_( EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) -MODELS = { - "7.3in-spectra-e6": EPaper7p3InSpectraE6, -} +# Import all models dynamically from the models package +for module_info in pkgutil.iter_modules(models.__path__): + importlib.import_module(f".models.{module_info.name}", package=__package__) -CONFIG_SCHEMA = cv.All( - display.FULL_DISPLAY_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(EPaperBase), - cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"), - cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, - cv.Optional(CONF_RESET_DURATION): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(max=core.TimePeriod(milliseconds=500)), - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(spi.spi_device_schema()), - cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +MODELS = models.EpaperModel.models + +DIMENSION_SCHEMA = cv.Schema( + { + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + } ) + +def model_schema(config): + model = MODELS[config[CONF_MODEL]] + class_name = epaper_spi_ns.class_(model.class_name, EPaperBase) + cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required + return ( + display.FULL_DISPLAY_SCHEMA.extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE0", + default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), + ) + ) + .extend( + { + model.option(pin): pins.gpio_output_pin_schema + for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN) + } + ) + .extend( + { + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(class_name), + cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), + cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA, + model.option(CONF_ENABLE_PIN): cv.ensure_list( + pins.gpio_output_pin_schema + ), + model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( + map_sequence + ), + model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), + } + ) + ) + + +def customise_schema(config): + """ + Create a customised config schema for a specific model and validate the configuration. + :param config: The configuration dictionary to validate + :return: The validated configuration dictionary + :raises cv.Invalid: If the configuration is invalid + """ + config = cv.Schema( + { + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=cv.ALLOW_EXTRA, + )(config) + return model_schema(config)(config) + + +CONFIG_SCHEMA = customise_schema + FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( "epaper_spi", require_miso=False, require_mosi=True ) @@ -56,8 +119,23 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): model = MODELS[config[CONF_MODEL]] - rhs = model.new() - var = cg.Pvariable(config[CONF_ID], rhs, model) + init_sequence = config.get(CONF_INIT_SEQUENCE) + if init_sequence is None: + init_sequence = model.get_init_sequence(config) + init_sequence = flatten_sequence(init_sequence) + init_sequence_length = len(init_sequence) + init_sequence_id = cg.static_const_array( + config[CONF_INIT_SEQUENCE_ID], init_sequence + ) + width, height = model.get_dimensions(config) + var = cg.new_Pvariable( + config[CONF_ID], + model.name, + width, + height, + init_sequence_id, + init_sequence_length, + ) await display.register_display(var, config) await spi.register_spi_device(var, config) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index 9630ea7f8b4..cf6a0b0c3d5 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -8,33 +8,20 @@ namespace esphome::epaper_spi { static const char *const TAG = "epaper_spi"; -static const LogString *epaper_state_to_string(EPaperState state) { - switch (state) { - case EPaperState::IDLE: - return LOG_STR("IDLE"); - case EPaperState::UPDATE: - return LOG_STR("UPDATE"); - case EPaperState::RESET: - return LOG_STR("RESET"); - case EPaperState::INITIALISE: - return LOG_STR("INITIALISE"); - case EPaperState::TRANSFER_DATA: - return LOG_STR("TRANSFER_DATA"); - case EPaperState::POWER_ON: - return LOG_STR("POWER_ON"); - case EPaperState::REFRESH_SCREEN: - return LOG_STR("REFRESH_SCREEN"); - case EPaperState::POWER_OFF: - return LOG_STR("POWER_OFF"); - case EPaperState::DEEP_SLEEP: - return LOG_STR("DEEP_SLEEP"); - default: - return LOG_STR("UNKNOWN"); - } +static constexpr const char *const EPAPER_STATE_STRINGS[] = { + "IDLE", "UPDATE", "RESET", "RESET_END", + + "SHOULD_WAIT", "INITIALISE", "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", +}; + +const char *EPaperBase::epaper_state_to_string_() { + if (auto idx = static_cast(this->state_); idx < std::size(EPAPER_STATE_STRINGS)) + return EPAPER_STATE_STRINGS[idx]; + return "Unknown"; } void EPaperBase::setup() { - if (!this->init_buffer_(this->get_buffer_length())) { + if (!this->init_buffer_(this->buffer_length_)) { this->mark_failed("Failed to initialise buffer"); return; } @@ -50,7 +37,7 @@ bool EPaperBase::init_buffer_(size_t buffer_length) { return true; } -void EPaperBase::setup_pins_() { +void EPaperBase::setup_pins_() const { this->dc_pin_->setup(); // OUTPUT this->dc_pin_->digital_write(false); @@ -81,11 +68,7 @@ void EPaperBase::data(uint8_t value) { // write a command followed by zero or more bytes of data. // The command is the first byte, length is the length of data only in the second byte, followed by the data. // [COMMAND, LENGTH, DATA...] -void EPaperBase::cmd_data(const uint8_t *data) { - const uint8_t command = data[0]; - const uint8_t length = data[1]; - const uint8_t *ptr = data + 2; - +void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) { ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, format_hex_pretty(ptr, length, '.', false).c_str()); @@ -99,91 +82,146 @@ void EPaperBase::cmd_data(const uint8_t *data) { this->disable(); } -bool EPaperBase::is_idle_() { +bool EPaperBase::is_idle_() const { if (this->busy_pin_ == nullptr) { return true; } - return this->busy_pin_->digital_read(); + return !this->busy_pin_->digital_read(); } -void EPaperBase::reset() { +bool EPaperBase::reset_() const { if (this->reset_pin_ != nullptr) { - this->reset_pin_->digital_write(false); - this->disable_loop(); - this->set_timeout(this->reset_duration_, [this] { - this->reset_pin_->digital_write(true); - this->set_timeout(20, [this] { this->enable_loop(); }); - }); + if (this->state_ == EPaperState::RESET) { + this->reset_pin_->digital_write(false); + return false; + } + this->reset_pin_->digital_write(true); } + return true; } void EPaperBase::update() { - if (!this->state_queue_.empty()) { - ESP_LOGE(TAG, "Display update already in progress - %s", - LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front()))); + if (this->state_ != EPaperState::IDLE) { + ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_()); return; } - - this->state_queue_.push(EPaperState::UPDATE); - this->state_queue_.push(EPaperState::RESET); - this->state_queue_.push(EPaperState::INITIALISE); - this->state_queue_.push(EPaperState::TRANSFER_DATA); - this->state_queue_.push(EPaperState::POWER_ON); - this->state_queue_.push(EPaperState::REFRESH_SCREEN); - this->state_queue_.push(EPaperState::POWER_OFF); - this->state_queue_.push(EPaperState::DEEP_SLEEP); - this->state_queue_.push(EPaperState::IDLE); - + this->set_state_(EPaperState::RESET); this->enable_loop(); } +void EPaperBase::wait_for_idle_(bool should_wait) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + if (should_wait) { + this->waiting_for_idle_start_ = millis(); + this->waiting_for_idle_last_print_ = this->waiting_for_idle_start_; + } +#endif + this->waiting_for_idle_ = should_wait; +} + +/** + * Called during the loop task. + * First defer for any pending delays, then check if we are waiting for the display to become idle. + * If not waiting for idle, process the state machine. + */ + void EPaperBase::loop() { + auto now = millis(); + if (this->delay_until_ != 0) { + // using modulus arithmetic to handle wrap-around + int diff = now - this->delay_until_; + if (diff < 0) { + return; + } + this->delay_until_ = 0; + } if (this->waiting_for_idle_) { if (this->is_idle_()) { this->waiting_for_idle_ = false; + ESP_LOGV(TAG, "Screen now idle after %u ms", (unsigned) (millis() - this->waiting_for_idle_start_)); } else { - if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) { - ESP_LOGV(TAG, "Waiting for idle"); - this->waiting_for_idle_last_print_ = App.get_loop_component_start_time(); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + if (now - this->waiting_for_idle_last_print_ >= 1000) { + ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_()); + this->waiting_for_idle_last_print_ = millis(); } +#endif return; } } + this->process_state_(); +} - auto state = this->state_queue_.front(); - - switch (state) { +/** + * Process the state machine. + * Typical state sequence: + * IDLE -> RESET -> RESET_END -> UPDATE -> INITIALISE -> TRANSFER_DATA -> POWER_ON -> REFRESH_SCREEN -> POWER_OFF -> + * DEEP_SLEEP -> IDLE + * + * Should a subclassed class need to override this, the method will need to be made virtual. + */ +void EPaperBase::process_state_() { + ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_()); + switch (this->state_) { + default: + ESP_LOGD(TAG, "Display is in unhandled state %s", epaper_state_to_string_()); + this->disable_loop(); + break; case EPaperState::IDLE: this->disable_loop(); break; + case EPaperState::RESET: + case EPaperState::RESET_END: + if (this->reset_()) { + this->set_state_(EPaperState::UPDATE); + } else { + this->set_state_(EPaperState::RESET_END); + } + break; case EPaperState::UPDATE: this->do_update_(); // Calls ESPHome (current page) lambda - break; - case EPaperState::RESET: - this->reset(); + this->set_state_(EPaperState::INITIALISE); break; case EPaperState::INITIALISE: this->initialise_(); + this->set_state_(EPaperState::TRANSFER_DATA); break; case EPaperState::TRANSFER_DATA: if (!this->transfer_data()) { return; // Not done yet, come back next loop } + this->set_state_(EPaperState::POWER_ON); break; case EPaperState::POWER_ON: this->power_on(); + this->set_state_(EPaperState::REFRESH_SCREEN); break; case EPaperState::REFRESH_SCREEN: this->refresh_screen(); + this->set_state_(EPaperState::POWER_OFF); break; case EPaperState::POWER_OFF: this->power_off(); + this->set_state_(EPaperState::DEEP_SLEEP); break; case EPaperState::DEEP_SLEEP: this->deep_sleep(); + this->set_state_(EPaperState::IDLE); break; } - this->state_queue_.pop(); +} + +void EPaperBase::set_state_(EPaperState state, uint16_t delay) { + ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_()); + this->state_ = state; + this->wait_for_idle_(state > EPaperState::SHOULD_WAIT); + if (delay != 0) { + this->delay_until_ = millis() + delay; + } else { + this->delay_until_ = 0; + } + ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay, + TRUEFALSE(this->waiting_for_idle_)); } void EPaperBase::start_command_() { @@ -203,25 +241,39 @@ void EPaperBase::on_safe_shutdown() { this->deep_sleep(); } void EPaperBase::initialise_() { size_t index = 0; - const auto &sequence = this->init_sequence_; - const size_t sequence_size = this->init_sequence_length_; - while (index != sequence_size) { - if (sequence_size - index < 2) { - this->mark_failed("Malformed init sequence"); - return; - } - const auto *ptr = sequence + index; - const uint8_t length = ptr[1]; - if (sequence_size - index < length + 2) { - this->mark_failed("Malformed init sequence"); - return; - } - this->cmd_data(ptr); - index += length + 2; + auto *sequence = this->init_sequence_; + auto length = this->init_sequence_length_; + while (index != length) { + if (length - index < 2) { + this->mark_failed("Malformed init sequence"); + return; + } + const uint8_t cmd = sequence[index++]; + if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) { + ESP_LOGV(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + const uint8_t num_args = x & 0x7F; + if (length - index < num_args) { + ESP_LOGE(TAG, "Malformed init sequence, cmd = %X, num_args = %u", cmd, num_args); + this->mark_failed(); + return; + } + ESP_LOGV(TAG, "Command %02X, length %d", cmd, num_args); + this->cmd_data(cmd, sequence + index, num_args); + index += num_args; + } } +} - this->power_on(); +void EPaperBase::dump_config() { + LOG_DISPLAY("", "E-Paper SPI", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->name_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); } } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index f6b2d41c656..4745ec7339c 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -8,36 +8,48 @@ #include namespace esphome::epaper_spi { +using namespace display; enum class EPaperState : uint8_t { - IDLE, - UPDATE, - RESET, - INITIALISE, - TRANSFER_DATA, - POWER_ON, - REFRESH_SCREEN, - POWER_OFF, - DEEP_SLEEP, + IDLE, // not doing anything + UPDATE, // update the buffer + RESET, // drive reset low (active) + RESET_END, // drive reset high (inactive) + + SHOULD_WAIT, // states higher than this should wait for the display to be not busy + INITIALISE, // send the init sequence + TRANSFER_DATA, // transfer data to the display + POWER_ON, // power on the display + REFRESH_SCREEN, // send refresh command + POWER_OFF, // power off the display + DEEP_SLEEP, // deep sleep the display }; -static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr uint8_t DELAY_FLAG = 0xFF; -class EPaperBase : public display::DisplayBuffer, +class EPaperBase : public DisplayBuffer, public spi::SPIDevice { public: - EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length) - : init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {} + EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY) + : name_(name), + width_(width), + height_(height), + init_sequence_(init_sequence), + init_sequence_length_(init_sequence_length), + display_type_(display_type) {} void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } float get_setup_priority() const override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } + void dump_config() override; void command(uint8_t value); void data(uint8_t value); - void cmd_data(const uint8_t *data); + void cmd_data(uint8_t command, const uint8_t *ptr, size_t length); void update() override; void loop() override; @@ -46,48 +58,84 @@ class EPaperBase : public display::DisplayBuffer, void on_safe_shutdown() override; + DisplayType get_display_type() override { return this->display_type_; }; + protected: - bool is_idle_(); - void setup_pins_(); - virtual void reset(); + int get_height_internal() override { return this->height_; }; + int get_width_internal() override { return this->width_; }; + void process_state_(); + + const char *epaper_state_to_string_(); + bool is_idle_() const; + void setup_pins_() const; + bool reset_() const; void initialise_(); + void wait_for_idle_(bool should_wait); bool init_buffer_(size_t buffer_length); virtual int get_width_controller() { return this->get_width_internal(); }; - virtual void deep_sleep() = 0; + + /** + * Methods that must be implemented by concrete classes to control the display + */ /** * Send data to the device via SPI - * @return true if done, false if should be called next loop + * @return true if done, false if it should be called next loop */ virtual bool transfer_data() = 0; + /** + * Refresh the screen after data transfer + */ virtual void refresh_screen() = 0; + /** + * Power the display on + */ virtual void power_on() = 0; + /** + * Power the display off + */ virtual void power_off() = 0; - virtual uint32_t get_buffer_length() = 0; + + /** + * Place the display into deep sleep + */ + virtual void deep_sleep() = 0; + + void set_state_(EPaperState state, uint16_t delay = 0); void start_command_(); void end_command_(); void start_data_(); void end_data_(); - const size_t init_sequence_length_{0}; + // properties initialised in the constructor + const char *name_; + uint16_t width_; + uint16_t height_; + const uint8_t *init_sequence_; + size_t init_sequence_length_; + DisplayType display_type_; - size_t current_data_index_{0}; + size_t buffer_length_{}; + size_t current_data_index_{0}; // used by data transfer to track progress uint32_t reset_duration_{200}; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + uint32_t transfer_start_time_{}; uint32_t waiting_for_idle_last_print_{0}; + uint32_t waiting_for_idle_start_{0}; +#endif - GPIOPin *dc_pin_; - GPIOPin *busy_pin_{nullptr}; - GPIOPin *reset_pin_{nullptr}; - - const uint8_t *init_sequence_{nullptr}; + GPIOPin *dc_pin_{}; + GPIOPin *busy_pin_{}; + GPIOPin *reset_pin_{}; bool waiting_for_idle_{false}; + uint32_t delay_until_{0}; split_buffer::SplitBuffer buffer_; - std::queue state_queue_{{EPaperState::IDLE}}; + EPaperState state_{EPaperState::IDLE}; }; } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp deleted file mode 100644 index f6273b392f3..00000000000 --- a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "epaper_spi_model_7p3in_spectra_e6.h" - -namespace esphome::epaper_spi { - -static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6"; - -void EPaper7p3InSpectraE6::power_on() { - ESP_LOGI(TAG, "Power on"); - this->command(0x04); - this->waiting_for_idle_ = true; -} - -void EPaper7p3InSpectraE6::power_off() { - ESP_LOGI(TAG, "Power off"); - this->command(0x02); - this->data(0x00); - this->waiting_for_idle_ = true; -} - -void EPaper7p3InSpectraE6::refresh_screen() { - ESP_LOGI(TAG, "Refresh"); - this->command(0x12); - this->data(0x00); - this->waiting_for_idle_ = true; -} - -void EPaper7p3InSpectraE6::deep_sleep() { - ESP_LOGI(TAG, "Deep sleep"); - this->command(0x07); - this->data(0xA5); -} - -void EPaper7p3InSpectraE6::dump_config() { - LOG_DISPLAY("", "E-Paper SPI", this); - ESP_LOGCONFIG(TAG, " Model: 7.3in Spectra E6"); - LOG_PIN(" Reset Pin: ", this->reset_pin_); - LOG_PIN(" DC Pin: ", this->dc_pin_); - LOG_PIN(" Busy Pin: ", this->busy_pin_); - LOG_UPDATE_INTERVAL(this); -} - -} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h deleted file mode 100644 index 6e850085ac3..00000000000 --- a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include "epaper_spi_spectra_e6.h" - -namespace esphome::epaper_spi { - -class EPaper7p3InSpectraE6 : public EPaperSpectraE6 { - static constexpr const uint16_t WIDTH = 800; - static constexpr const uint16_t HEIGHT = 480; - // clang-format off - - // Command, data length, data - static constexpr uint8_t INIT_SEQUENCE[] = { - 0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18, - 0x01, 1, 0x3F, - 0x00, 2, 0x5F, 0x69, - 0x03, 4, 0x00, 0x54, 0x00, 0x44, - 0x05, 4, 0x40, 0x1F, 0x1F, 0x2C, - 0x06, 4, 0x6F, 0x1F, 0x17, 0x49, - 0x08, 4, 0x6F, 0x1F, 0x1F, 0x22, - 0x30, 1, 0x03, - 0x50, 1, 0x3F, - 0x60, 2, 0x02, 0x00, - 0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256, - 0x84, 1, 0x01, - 0xE3, 1, 0x2F, - }; - // clang-format on - - public: - EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {} - - void dump_config() override; - - protected: - int get_width_internal() override { return WIDTH; }; - int get_height_internal() override { return HEIGHT; }; - - void refresh_screen() override; - void power_on() override; - void power_off() override; - void deep_sleep() override; -}; - -} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp index dccc691252e..8e4cbdde2a3 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp @@ -1,135 +1,166 @@ #include "epaper_spi_spectra_e6.h" +#include + #include "esphome/core/log.h" namespace esphome::epaper_spi { - static constexpr const char *const TAG = "epaper_spi.6c"; +static constexpr size_t MAX_TRANSFER_SIZE = 128; +static constexpr unsigned char GRAY_THRESHOLD = 50; -static inline uint8_t color_to_hex(Color color) { - if (color.red > 127) { - if (color.green > 170) { - if (color.blue > 127) { - return 0x1; // White - } else { - return 0x2; // Yellow - } - } else { - return 0x3; // Red (or Magenta) - } - } else { - if (color.green > 127) { - if (color.blue > 127) { - return 0x5; // Cyan -> Blue - } else { - return 0x6; // Green - } - } else { - if (color.blue > 127) { - return 0x5; // Blue - } else { - return 0x0; // Black - } +enum E6Color { + BLACK, + WHITE, + YELLOW, + RED, + SKIP_1, + BLUE, + GREEN, + CYAN, + SKIP_2, +}; + +static uint8_t color_to_hex(Color color) { + // --- Step 1: Check for Grayscale (Black or White) --- + // We define "grayscale" as a color where the min and max components + // are close to each other. + unsigned char max_rgb = std::max({color.r, color.g, color.b}); + unsigned char min_rgb = std::min({color.r, color.g, color.b}); + + if ((max_rgb - min_rgb) < GRAY_THRESHOLD) { + // It's a shade of gray. Map to BLACK or WHITE. + // We split the luminance at the halfway point (382 = (255*3)/2) + if ((static_cast(color.r) + color.g + color.b) > 382) { + return WHITE; } + return BLACK; } + // --- Step 2: Check for Primary/Secondary Colors --- + // If it's not gray, it's a color. We check which components are + // "on" (over 128) vs "off". This divides the RGB cube into 8 corners. + bool r_on = (color.r > 128); + bool g_on = (color.g > 128); + bool b_on = (color.b > 128); + + if (r_on && g_on && !b_on) { + return YELLOW; + } + if (r_on && !g_on && !b_on) { + return RED; + } + if (!r_on && g_on && !b_on) { + return GREEN; + } + if (!r_on && !g_on && b_on) { + return BLUE; + } + // Handle "impure" colors (Cyan, Magenta) + if (!r_on && g_on && b_on) { + // Cyan (G+B) -> Closest is Green or Blue. Pick Green. + return GREEN; + } + if (r_on && !g_on) { + // Magenta (R+B) -> Closest is Red or Blue. Pick Red. + return RED; + } + // Handle the remaining corners (White-ish, Black-ish) + if (r_on) { + // All high (but not gray) -> White + return WHITE; + } + // !r_on && !g_on && !b_on + // All low (but not gray) -> Black + return BLACK; +} + +void EPaperSpectraE6::power_on() { + ESP_LOGD(TAG, "Power on"); + this->command(0x04); +} + +void EPaperSpectraE6::power_off() { + ESP_LOGD(TAG, "Power off"); + this->command(0x02); + this->data(0x00); +} + +void EPaperSpectraE6::refresh_screen() { + ESP_LOGD(TAG, "Refresh"); + this->command(0x12); + this->data(0x00); +} + +void EPaperSpectraE6::deep_sleep() { + ESP_LOGD(TAG, "Deep sleep"); + this->command(0x07); + this->data(0xA5); } void EPaperSpectraE6::fill(Color color) { - uint8_t pixel_color; - if (color.is_on()) { - pixel_color = color_to_hex(color); - } else { - pixel_color = 0x1; - } + auto pixel_color = color_to_hex(color); - // We store 8 bitset<3> in 3 bytes - // | byte 1 | byte 2 | byte 3 | - // |aaabbbaa|abbbaaab|bbaaabbb| - uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1; - uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2; - uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0; - - const size_t buffer_length = this->get_buffer_length(); - for (size_t i = 0; i < buffer_length; i += 3) { - this->buffer_[i + 0] = byte_1; - this->buffer_[i + 1] = byte_2; - this->buffer_[i + 2] = byte_3; - } + // We store 2 pixels per byte + this->buffer_.fill(pixel_color + (pixel_color << 4)); } -uint32_t EPaperSpectraE6::get_buffer_length() { - // 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes - return this->get_width_controller() * this->get_height_internal() / 8u * 3u; +void EPaperSpectraE6::clear() { + // clear buffer to white, just like real paper. + this->fill(COLOR_ON); } void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) { - if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) + if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) return; - uint8_t pixel_bits = color_to_hex(color); + auto pixel_bits = color_to_hex(color); uint32_t pixel_position = x + y * this->get_width_controller(); - uint32_t first_bit_position = pixel_position * 3; - uint32_t byte_position = first_bit_position / 8u; - uint32_t byte_subposition = first_bit_position % 8u; - - if (byte_subposition <= 5) { - this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) | - (pixel_bits << (5 - byte_subposition)); + uint32_t byte_position = pixel_position / 2; + auto original = this->buffer_[byte_position]; + if ((pixel_position & 1) != 0) { + this->buffer_[byte_position] = (original & 0xF0) | pixel_bits; } else { - this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) | - (pixel_bits >> (byte_subposition - 5)); - - this->buffer_[byte_position + 1] = - (this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) | - (pixel_bits << (13 - byte_subposition)); + this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4); } } bool HOT EPaperSpectraE6::transfer_data() { const uint32_t start_time = App.get_loop_component_start_time(); + const size_t buffer_length = this->buffer_length_; if (this->current_data_index_ == 0) { - ESP_LOGV(TAG, "Sending data"); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + this->transfer_start_time_ = millis(); +#endif + ESP_LOGV(TAG, "Start sending data at %ums", (unsigned) millis()); this->command(0x10); } - uint8_t bytes_to_send[4]{0}; - const size_t buffer_length = this->get_buffer_length(); - for (size_t i = this->current_data_index_; i < buffer_length; i += 3) { - const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]); - // 8 pixels are stored in 3 bytes - // |aaabbbaa|abbbaaab|bbaaabbb| - // | byte 1 | byte 2 | byte 3 | - bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111); - bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111); - bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111); - bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111); + size_t buf_idx = 0; + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + while (this->current_data_index_ != buffer_length) { + bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++]; - this->start_data_(); - this->write_array(bytes_to_send, sizeof(bytes_to_send)); - this->end_data_(); + if (buf_idx == sizeof bytes_to_send) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->end_data_(); + ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis()); + buf_idx = 0; - if (millis() - start_time > MAX_TRANSFER_TIME) { - // Let the main loop run and come back next loop - this->current_data_index_ = i + 3; - return false; + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + return false; + } } } // Finished the entire dataset + if (buf_idx != 0) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->end_data_(); + } this->current_data_index_ = 0; + ESP_LOGV(TAG, "Sent data in %" PRIu32 " ms", millis() - this->transfer_start_time_); return true; } - -void EPaperSpectraE6::reset() { - if (this->reset_pin_ != nullptr) { - this->disable_loop(); - this->reset_pin_->digital_write(true); - this->set_timeout(20, [this] { - this->reset_pin_->digital_write(false); - delay(2); - this->reset_pin_->digital_write(true); - this->set_timeout(20, [this] { this->enable_loop(); }); - }); - } -} - } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h index 9f0652f79d9..48356ad74bb 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -6,18 +6,23 @@ namespace esphome::epaper_spi { class EPaperSpectraE6 : public EPaperBase { public: - EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length) - : EPaperBase(init_sequence, init_sequence_length) {} + EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) { + this->buffer_length_ = width * height / 2; // 2 pixels per byte + } - display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } void fill(Color color) override; + void clear() override; protected: + void refresh_screen() override; + void power_on() override; + void power_off() override; + void deep_sleep() override; void draw_absolute_pixel_internal(int x, int y, Color color) override; - uint32_t get_buffer_length() override; bool transfer_data() override; - void reset() override; }; } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/__init__.py b/esphome/components/epaper_spi/models/__init__.py new file mode 100644 index 00000000000..019eb31d181 --- /dev/null +++ b/esphome/components/epaper_spi/models/__init__.py @@ -0,0 +1,65 @@ +from typing import Any, Self + +import esphome.config_validation as cv +from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH + + +class EpaperModel: + models: dict[str, Self] = {} + + def __init__( + self, + name: str, + class_name: str, + initsequence=None, + **defaults, + ): + name = name.upper() + self.name = name + self.class_name = class_name + self.initsequence = initsequence + self.defaults = defaults + EpaperModel.models[name] = self + + def get_default(self, key, fallback: Any = False) -> Any: + return self.defaults.get(key, fallback) + + def get_init_sequence(self, config: dict): + return self.initsequence + + def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required: + if fallback is None and self.get_default(name, None) is None: + return cv.Required(name) + return cv.Optional(name, default=self.get_default(name, fallback)) + + def get_dimensions(self, config) -> tuple[int, int]: + if CONF_DIMENSIONS in config: + # Explicit dimensions, just use as is + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + width = dimensions[CONF_WIDTH] + height = dimensions[CONF_HEIGHT] + else: + (width, height) = dimensions + + else: + # Default dimensions, use model defaults + width = self.get_default(CONF_WIDTH) + height = self.get_default(CONF_HEIGHT) + return width, height + + def extend(self, name, **kwargs) -> "EpaperModel": + """ + Extend the current model with additional parameters or a modified init sequence. + Parameters supplied here will override the defaults of the current model. + if the initsequence is not provided, the current model's initsequence will be used. + If add_init_sequence is provided, it will be appended to the current initsequence. + :param name: + :param kwargs: + :return: + """ + initsequence = list(kwargs.pop("initsequence", self.initsequence) or ()) + initsequence.extend(kwargs.pop("add_init_sequence", ())) + defaults = self.defaults.copy() + defaults.update(kwargs) + return self.__class__(name, initsequence=tuple(initsequence), **defaults) diff --git a/esphome/components/epaper_spi/models/spectra_e6.py b/esphome/components/epaper_spi/models/spectra_e6.py new file mode 100644 index 00000000000..9f0b673d69a --- /dev/null +++ b/esphome/components/epaper_spi/models/spectra_e6.py @@ -0,0 +1,51 @@ +from typing import Any + +from . import EpaperModel + + +class SpectraE6(EpaperModel): + def __init__(self, name, class_name="EPaperSpectraE6", **kwargs): + super().__init__(name, class_name, **kwargs) + + # fmt: off + def get_init_sequence(self, config: dict): + width, height = self.get_dimensions(config) + return ( + (0xAA, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,), + (0x01, 0x3F,), + (0x00, 0x5F, 0x69,), + (0x03, 0x00, 0x54, 0x00, 0x44,), + (0x05, 0x40, 0x1F, 0x1F, 0x2C,), + (0x06, 0x6F, 0x1F, 0x17, 0x49,), + (0x08, 0x6F, 0x1F, 0x1F, 0x22,), + (0x30, 0x03,), + (0x50, 0x3F,), + (0x60, 0x02, 0x00,), + (0x61, width // 256, width % 256, height // 256, height % 256,), + (0x84, 0x01,), + (0xE3, 0x2F,), + ) + + def get_default(self, key, fallback: Any = False) -> Any: + return self.defaults.get(key, fallback) + + +spectra_e6 = SpectraE6("spectra-e6") + +spectra_e6.extend( + "Seeed-reTerminal-E1002", + width=800, + height=480, + data_rate="20MHz", + cs_pin=10, + dc_pin=11, + reset_pin=12, + busy_pin={ + "number": 13, + "inverted": True, + "mode": { + "input": True, + "pullup": True, + }, + }, +) diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 93d1750cd60..4dbc81caa2b 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -218,6 +218,21 @@ def map_sequence(value): return tuple(value) +def flatten_sequence(sequence: tuple | list): + """ + Flatten an init sequence into a single list of bytes. + :param sequence: The list of tuples + :return: a list of bytes + """ + return sum( + tuple( + (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] + for x in sequence + ), + (), + ) + + def delay(ms): return DELAY_FLAG, ms @@ -456,13 +471,7 @@ class DriverChip: # Flatten the sequence into a list of bytes, with the length of each command # or the delay flag inserted where needed - return sum( - tuple( - (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] - for x in sequence - ), - (), - ), madctl + return flatten_sequence(sequence), madctl def requires_buffer(config) -> bool: diff --git a/esphome/components/split_buffer/split_buffer.cpp b/esphome/components/split_buffer/split_buffer.cpp index a710670a5dd..526a19c71cc 100644 --- a/esphome/components/split_buffer/split_buffer.cpp +++ b/esphome/components/split_buffer/split_buffer.cpp @@ -4,7 +4,6 @@ #include "esphome/core/log.h" namespace esphome::split_buffer { - static constexpr const char *const TAG = "split_buffer"; SplitBuffer::~SplitBuffer() { this->free(); } @@ -102,32 +101,44 @@ void SplitBuffer::free() { this->total_length_ = 0; } -uint8_t &SplitBuffer::operator[](size_t index) { +const uint8_t &SplitBuffer::operator[](size_t index) const { if (index >= this->total_length_) { ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_); - // Return reference to a static dummy byte to avoid crash + // Return reference to a static dummy byte since we can't throw exceptions. + // the byte is non-const since it will also be used by the non-const [] overload. static uint8_t dummy = 0; return dummy; } - size_t buffer_index = index / this->buffer_size_; - size_t offset_in_buffer = index - this->buffer_size_ * buffer_index; + const auto buffer_index = index / this->buffer_size_; + const auto offset_in_buffer = index % this->buffer_size_; return this->buffers_[buffer_index][offset_in_buffer]; } -const uint8_t &SplitBuffer::operator[](size_t index) const { - if (index >= this->total_length_) { - ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_); - // Return reference to a static dummy byte to avoid crash - static const uint8_t DUMMY = 0; - return DUMMY; +// non-const version of operator[] for write access +uint8_t &SplitBuffer::operator[](size_t index) { + // avoid code duplication. These casts are safe since we know the object is not const. + return const_cast(static_cast(this)->operator[](index)); +} + +/** + * Fill the entire buffer with a single byte value + * @param value Fill value + */ +void SplitBuffer::fill(uint8_t value) const { + if (this->buffer_count_ == 0) + return; + // clear all the full sized buffers + size_t i = 0; + for (; i != this->buffer_count_ - 1; i++) { + memset(this->buffers_[i], value, this->buffer_size_); } - - size_t buffer_index = index / this->buffer_size_; - size_t offset_in_buffer = index - this->buffer_size_ * buffer_index; - - return this->buffers_[buffer_index][offset_in_buffer]; + // clear the last, potentially short, buffer. + // `i` is guaranteed to equal the last index since the loop terminates at that value. + // where all buffers are the same size, the modulus must return the size, not 0. + auto size_last = ((this->total_length_ - 1) % this->buffer_size_) + 1; + memset(this->buffers_[i], value, size_last); } } // namespace esphome::split_buffer diff --git a/esphome/components/split_buffer/split_buffer.h b/esphome/components/split_buffer/split_buffer.h index c3490f3d6eb..b615ddce74d 100644 --- a/esphome/components/split_buffer/split_buffer.h +++ b/esphome/components/split_buffer/split_buffer.h @@ -4,7 +4,13 @@ #include namespace esphome::split_buffer { - +/** + * A SplitBuffer allocates a large memory buffer potentially as multiple smaller buffers + * to facilitate allocation of large buffers on devices with fragmented memory spaces. + * Each sub-buffer is the same size, except for the last one which may be smaller. + * Standard array indexing using `[]` is possible on the buffer, but, since the buffer may not be contiguous in memory, + * there is no easy way to access the buffer as a single array, i.e. no `.data()` access like a vector. + */ class SplitBuffer { public: SplitBuffer() = default; @@ -19,13 +25,13 @@ class SplitBuffer { // Access operators uint8_t &operator[](size_t index); const uint8_t &operator[](size_t index) const; + void fill(uint8_t value) const; // Get the total length size_t size() const { return this->total_length_; } // Get buffer information size_t get_buffer_count() const { return this->buffer_count_; } - size_t get_buffer_size() const { return this->buffer_size_; } // Check if successfully initialized bool is_valid() const { return this->buffers_ != nullptr && this->buffer_count_ > 0; } diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index 34aefb82b44..cff1f518978 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -4,7 +4,10 @@ packages: display: - platform: epaper_spi spi_id: spi_bus - model: 7.3in-spectra-e6 + model: spectra-e6 + dimensions: + width: 800 + height: 480 cs_pin: GPIO5 dc_pin: GPIO17 reset_pin: GPIO16 @@ -13,3 +16,6 @@ display: update_interval: 60s lambda: |- it.circle(64, 64, 50, Color::BLACK); + + - platform: epaper_spi + model: seeed-reterminal-e1002 From 57f2e32b00026c372124c5124e3c67eb6765ca1f Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 27 Oct 2025 17:09:45 -0500 Subject: [PATCH 041/461] [uart] Fix order of initialization calls (#11510) --- .../uart/uart_component_esp_idf.cpp | 66 ++++++++++--------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index cffa3308eb4..73813d2d5b7 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -99,10 +99,26 @@ void IDFUARTComponent::setup() { } void IDFUARTComponent::load_settings(bool dump_config) { - uart_config_t uart_config = this->get_config_(); - esp_err_t err = uart_param_config(this->uart_num_, &uart_config); + esp_err_t err; + + if (uart_is_driver_installed(this->uart_num_)) { + err = uart_driver_delete(this->uart_num_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + } + err = uart_driver_install(this->uart_num_, // UART number + this->rx_buffer_size_, // RX ring buffer size + 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will + // block task until all data has been sent out + 20, // event queue size/depth + &this->uart_event_queue_, // event queue + 0 // Flags used to allocate the interrupt + ); if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } @@ -119,10 +135,12 @@ void IDFUARTComponent::load_settings(bool dump_config) { int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; uint32_t invert = 0; - if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) + if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; - if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) + } + if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) { invert |= UART_SIGNAL_RXD_INV; + } err = uart_set_line_inverse(this->uart_num_, invert); if (err != ESP_OK) { @@ -138,26 +156,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } - if (uart_is_driver_installed(this->uart_num_)) { - uart_driver_delete(this->uart_num_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - } - err = uart_driver_install(this->uart_num_, /* UART RX ring buffer size. */ this->rx_buffer_size_, - /* UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will - block task until all data have been sent out.*/ - 0, - /* UART event queue size/depth. */ 20, &(this->uart_event_queue_), - /* Flags used to allocate the interrupt. */ 0); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err)); @@ -173,24 +171,32 @@ void IDFUARTComponent::load_settings(bool dump_config) { } auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART; - err = uart_set_mode(this->uart_num_, mode); + err = uart_set_mode(this->uart_num_, mode); // per docs, must be called only after uart_driver_install() if (err != ESP_OK) { ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } + uart_config_t uart_config = this->get_config_(); + err = uart_param_config(this->uart_num_, &uart_config); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + if (dump_config) { - ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->uart_num_); + ESP_LOGCONFIG(TAG, "Reloaded UART %u", this->uart_num_); this->dump_config(); } } void IDFUARTComponent::dump_config() { ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); - LOG_PIN(" TX Pin: ", tx_pin_); - LOG_PIN(" RX Pin: ", rx_pin_); - LOG_PIN(" Flow Control Pin: ", flow_control_pin_); + LOG_PIN(" TX Pin: ", this->tx_pin_); + LOG_PIN(" RX Pin: ", this->rx_pin_); + LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); if (this->rx_pin_ != nullptr) { ESP_LOGCONFIG(TAG, " RX Buffer Size: %u\n" From 641dd24b214537486337eb82c574278b4428201d Mon Sep 17 00:00:00 2001 From: Anton Sergunov Date: Wed, 29 Oct 2025 07:33:16 +0600 Subject: [PATCH 042/461] Fix the LiberTiny bug with UART pin setup (#11518) --- .../uart/uart_component_libretiny.cpp | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 9c065fe5df7..8d1d28fce46 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -46,40 +46,58 @@ uint16_t LibreTinyUARTComponent::get_config() { } void LibreTinyUARTComponent::setup() { - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); - } - int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin(); int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin(); bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); bool rx_inverted = rx_pin_ != nullptr && rx_pin_->is_inverted(); + auto shouldFallbackToSoftwareSerial = [&]() -> bool { + auto hasFlags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool { + return pin && pin->get_flags() & mask != gpio::Flags::FLAG_NONE; + }; + if (hasFlags(this->tx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) || + hasFlags(this->rx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) { +#if LT_ARD_HAS_SOFTSERIAL + ESP_LOGI(TAG, "Pins has flags set. Using Software Serial"); + return true; +#else + ESP_LOGW(TAG, "Pin flags are set but not supported for hardware serial. Ignoring"); +#endif + } + return false; + }; + if (false) return; #if LT_HW_UART0 - else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX)) { + else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX) && + !shouldFallbackToSoftwareSerial()) { this->serial_ = &Serial0; this->hardware_idx_ = 0; } #endif #if LT_HW_UART1 - else if ((tx_pin == -1 || tx_pin == PIN_SERIAL1_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL1_RX)) { + else if ((tx_pin == -1 || tx_pin == PIN_SERIAL1_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL1_RX) && + !shouldFallbackToSoftwareSerial()) { this->serial_ = &Serial1; this->hardware_idx_ = 1; } #endif #if LT_HW_UART2 - else if ((tx_pin == -1 || tx_pin == PIN_SERIAL2_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL2_RX)) { + else if ((tx_pin == -1 || tx_pin == PIN_SERIAL2_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL2_RX) && + !shouldFallbackToSoftwareSerial()) { this->serial_ = &Serial2; this->hardware_idx_ = 2; } #endif else { #if LT_ARD_HAS_SOFTSERIAL + if (this->rx_pin_) { + this->rx_pin_->setup(); + } + if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { + this->tx_pin_->setup(); + } this->serial_ = new SoftwareSerial(rx_pin, tx_pin, rx_inverted || tx_inverted); #else this->serial_ = &Serial; From db395a662de2f33f64aec4f91c54cc6af717e177 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:37:14 +1000 Subject: [PATCH 043/461] [mipi_rgb] Fix rotation with custom model (#11585) --- esphome/components/mipi/__init__.py | 12 ++++++++ esphome/components/mipi_rgb/display.py | 38 ++++++++++++++------------ esphome/components/mipi_spi/display.py | 15 +--------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 4dff1af62af..93d1750cd60 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -384,6 +384,18 @@ class DriverChip: transform[CONF_TRANSFORM] = True return transform + def swap_xy_schema(self): + uses_swap = self.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED + + def validator(value): + if value: + raise cv.Invalid("Axis swapping not supported by this model") + return cv.boolean(value) + + if uses_swap: + return {cv.Required(CONF_SWAP_XY): cv.boolean} + return {cv.Optional(CONF_SWAP_XY, default=False): validator} + def add_madctl(self, sequence: list, config: dict): # Add the MADCTL command to the sequence based on the configuration. use_flip = config.get(CONF_USE_AXIS_FLIPS) diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 3001d33980e..9d6b1fa7295 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -46,6 +46,7 @@ from esphome.const import ( CONF_DATA_RATE, CONF_DC_PIN, CONF_DIMENSIONS, + CONF_DISABLED, CONF_ENABLE_PIN, CONF_GREEN, CONF_HSYNC_PIN, @@ -117,16 +118,16 @@ def data_pin_set(length): def model_schema(config): model = MODELS[config[CONF_MODEL].upper()] - if transforms := model.transforms: - transform = cv.Schema({cv.Required(x): cv.boolean for x in transforms}) - for x in (CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y): - if x not in transforms: - transform = transform.extend( - {cv.Optional(x): cv.invalid(f"{x} not supported by this model")} - ) - else: - transform = cv.invalid("This model does not support transforms") - + transform = cv.Any( + cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + **model.swap_xy_schema(), + } + ), + cv.one_of(CONF_DISABLED, lower=True), + ) # RPI model does not use an init sequence, indicates with empty list if model.initsequence is None: # Custom model requires an init sequence @@ -135,12 +136,16 @@ def model_schema(config): else: iseqconf = cv.Optional(CONF_INIT_SEQUENCE) uses_spi = CONF_INIT_SEQUENCE in config or len(model.initsequence) != 0 - swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) - - # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden - cv_dimensions = ( - cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required + # Dimensions are optional if the model has a default width and the x-y transform is not overridden + transform_config = config.get(CONF_TRANSFORM, {}) + is_swapped = ( + isinstance(transform_config, dict) + and transform_config.get(CONF_SWAP_XY, False) is True ) + cv_dimensions = ( + cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required + ) + pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, "16", "18") schema = display.FULL_DISPLAY_SCHEMA.extend( { @@ -157,7 +162,7 @@ def model_schema(config): model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of( *pixel_modes, lower=True ), - model.option(CONF_TRANSFORM, cv.UNDEFINED): transform, + cv.Optional(CONF_TRANSFORM): transform, cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), model.option(CONF_INVERT_COLORS, False): cv.boolean, model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean, @@ -270,7 +275,6 @@ async def to_code(config): cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) - index = 0 dpins = [] if CONF_RED in config[CONF_DATA_PINS]: red_pins = config[CONF_DATA_PINS][CONF_RED] diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 891c8b42ffa..50ea826eab6 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -131,19 +131,6 @@ def denominator(config): ) from StopIteration -def swap_xy_schema(model): - uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED - - def validator(value): - if value: - raise cv.Invalid("Axis swapping not supported by this model") - return cv.boolean(value) - - if uses_swap: - return {cv.Required(CONF_SWAP_XY): cv.boolean} - return {cv.Optional(CONF_SWAP_XY, default=False): validator} - - def model_schema(config): model = MODELS[config[CONF_MODEL]] bus_mode = config[CONF_BUS_MODE] @@ -152,7 +139,7 @@ def model_schema(config): { cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean, - **swap_xy_schema(model), + **model.swap_xy_schema(), } ), cv.one_of(CONF_DISABLED, lower=True), From fecc8399a557a577a40d98a110d95d0a2a2c01b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 01:07:48 -0500 Subject: [PATCH 044/461] [lvgl] Fix nested lambdas in automations unable to access parameters (#11583) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/lvgl/defines.py | 22 +++++++++++++-- esphome/components/lvgl/lv_validation.py | 21 ++++++++++++--- esphome/components/lvgl/lvcode.py | 13 ++++++--- esphome/components/lvgl/sensor/__init__.py | 3 +-- tests/components/lvgl/common.yaml | 31 ++++++++++++++++++++++ tests/components/lvgl/lvgl-package.yaml | 23 ++++++++++++++++ 6 files changed, 103 insertions(+), 10 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index baee403b57f..d2b0977e89d 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -5,6 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i """ import logging +from typing import TYPE_CHECKING, Any from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS @@ -12,6 +13,7 @@ from esphome.core import ID, Lambda from esphome.cpp_generator import LambdaExpression, MockObj from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor +from esphome.types import Expression, SafeExpType from .helpers import requires_component @@ -42,7 +44,13 @@ def static_cast(type, value): def call_lambda(lamb: LambdaExpression): expr = lamb.content.strip() if expr.startswith("return") and expr.endswith(";"): - return expr[6:][:-1].strip() + return expr[6:-1].strip() + # If lambda has parameters, call it with those parameter names + # Parameter names come from hardcoded component code (like "x", "it", "event") + # not from user input, so they're safe to use directly + if lamb.parameters and lamb.parameters.parameters: + param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters) + return f"{lamb}({param_names})" return f"{lamb}()" @@ -65,10 +73,20 @@ class LValidator: return cv.returning_lambda(value) return self.validator(value) - async def process(self, value, args=()): + async def process( + self, value: Any, args: list[tuple[SafeExpType, str]] | None = None + ) -> Expression: if value is None: return None if isinstance(value, Lambda): + # Local import to avoid circular import + from .lvcode import CodeContext, LambdaContext + + if TYPE_CHECKING: + # CodeContext does not have get_automation_parameters + # so we need to assert the type here + assert isinstance(CodeContext.code_context, LambdaContext) + args = args or CodeContext.code_context.get_automation_parameters() return cg.RawExpression( call_lambda( await cg.process_lambda(value, args, return_type=self.rtype) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index d345ac70f3b..6f95a32a186 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING, Any + import esphome.codegen as cg from esphome.components import image from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw @@ -17,6 +19,7 @@ from esphome.cpp_generator import MockObj from esphome.cpp_types import ESPTime, int32, uint32 from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor +from esphome.types import Expression, SafeExpType from . import types as ty from .defines import ( @@ -388,11 +391,23 @@ class TextValidator(LValidator): return value return super().__call__(value) - async def process(self, value, args=()): + async def process( + self, value: Any, args: list[tuple[SafeExpType, str]] | None = None + ) -> Expression: + # Local import to avoid circular import at module level + + from .lvcode import CodeContext, LambdaContext + + if TYPE_CHECKING: + # CodeContext does not have get_automation_parameters + # so we need to assert the type here + assert isinstance(CodeContext.code_context, LambdaContext) + args = args or CodeContext.code_context.get_automation_parameters() + if isinstance(value, dict): if format_str := value.get(CONF_FORMAT): - args = [str(x) for x in value[CONF_ARGS]] - arg_expr = cg.RawExpression(",".join(args)) + str_args = [str(x) for x in value[CONF_ARGS]] + arg_expr = cg.RawExpression(",".join(str_args)) format_str = cpp_string_escape(format_str) return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") if time_format := value.get(CONF_TIME_FORMAT): diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 7a5c35f8965..ea38845c078 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -164,6 +164,9 @@ class LambdaContext(CodeContext): code_text.append(text) return code_text + def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: + return self.parameters + async def __aenter__(self): await super().__aenter__() add_line_marks(self.where) @@ -178,9 +181,8 @@ class LvContext(LambdaContext): added_lambda_count = 0 - def __init__(self, args=None): - self.args = args or LVGL_COMP_ARG - super().__init__(parameters=self.args) + def __init__(self): + super().__init__(parameters=LVGL_COMP_ARG) async def __aexit__(self, exc_type, exc_val, exc_tb): await super().__aexit__(exc_type, exc_val, exc_tb) @@ -189,6 +191,11 @@ class LvContext(LambdaContext): cg.add(expression) return expression + def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: + # When generating automations, we don't want the `lv_component` parameter to be passed + # to the lambda. + return [] + def __call__(self, *args): return self.add(*args) diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py index 03b2638ed03..167af9c6e19 100644 --- a/esphome/components/lvgl/sensor/__init__.py +++ b/esphome/components/lvgl/sensor/__init__.py @@ -5,7 +5,6 @@ from ..defines import CONF_WIDGET from ..lvcode import ( API_EVENT, EVENT_ARG, - LVGL_COMP_ARG, UPDATE_EVENT, LambdaContext, LvContext, @@ -30,7 +29,7 @@ async def to_code(config): await wait_for_widgets() async with LambdaContext(EVENT_ARG) as lamb: lv_add(sensor.publish_state(widget.get_value())) - async with LvContext(LVGL_COMP_ARG): + async with LvContext(): lv_add( lvgl_static.add_event_cb( widget.obj, diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index d9b7013a1ec..c70dd7568da 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -52,6 +52,19 @@ number: widget: spinbox_id id: lvgl_spinbox_number name: LVGL Spinbox Number + - platform: template + id: test_brightness + name: "Test Brightness" + min_value: 0 + max_value: 255 + step: 1 + optimistic: true + # Test lambda in automation accessing x parameter directly + # This is a real-world pattern from user configs + on_value: + - lambda: !lambda |- + // Direct use of x parameter in automation + ESP_LOGD("test", "Brightness: %.0f", x); light: - platform: lvgl @@ -110,3 +123,21 @@ text: platform: lvgl widget: hello_label mode: text + +text_sensor: + - platform: template + id: test_text_sensor + name: "Test Text Sensor" + # Test nested lambdas in LVGL actions can access automation parameters + on_value: + - lvgl.label.update: + id: hello_label + text: !lambda return x.c_str(); + - lvgl.label.update: + id: hello_label + text: !lambda |- + // Test complex lambda with conditionals accessing x parameter + if (x == "*") { + return "WILDCARD"; + } + return x.c_str(); diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 582531e943c..14241a16693 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -257,7 +257,30 @@ lvgl: text: "Hello shiny day" text_color: 0xFFFFFF align: bottom_mid + - label: + id: setup_lambda_label + # Test lambda in widget property during setup (LvContext) + # Should NOT receive lv_component parameter + text: !lambda |- + char buf[32]; + snprintf(buf, sizeof(buf), "Setup: %d", 42); + return std::string(buf); + align: top_mid text_font: space16 + - label: + id: chip_info_label + # Test complex setup lambda (real-world pattern) + # Should NOT receive lv_component parameter + text: !lambda |- + // Test conditional compilation and string formatting + char buf[64]; + #ifdef USE_ESP_IDF + snprintf(buf, sizeof(buf), "IDF: v%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR); + #else + snprintf(buf, sizeof(buf), "Arduino"); + #endif + return std::string(buf); + align: top_left - obj: align: center arc_opa: COVER From 51745d1d5ef080493666e592b6abe777cf2a9338 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 1 Nov 2025 10:08:28 +1000 Subject: [PATCH 045/461] [image] Catch and report svg load errors (#11619) --- esphome/components/image/__init__.py | 31 +++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index f880b5f7365..bf25a7cd928 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -671,18 +671,33 @@ async def write_image(config, all_frames=False): resize = config.get(CONF_RESIZE) if is_svg_file(path): # Local import so use of non-SVG files needn't require cairosvg installed + from pyexpat import ExpatError + from xml.etree.ElementTree import ParseError + from cairosvg import svg2png + from cairosvg.helpers import PointError if not resize: resize = (None, None) - with open(path, "rb") as file: - image = svg2png( - file_obj=file, - output_width=resize[0], - output_height=resize[1], - ) - image = Image.open(io.BytesIO(image)) - width, height = image.size + try: + with open(path, "rb") as file: + image = svg2png( + file_obj=file, + output_width=resize[0], + output_height=resize[1], + ) + image = Image.open(io.BytesIO(image)) + width, height = image.size + except ( + ValueError, + ParseError, + IndexError, + ExpatError, + AttributeError, + TypeError, + PointError, + ) as e: + raise core.EsphomeError(f"Could not load SVG image {path}: {e}") from e else: image = Image.open(path) width, height = image.size From 2f5f1da16f0e37f1e774e61c8acdb3d0d4d71730 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:37:07 +1000 Subject: [PATCH 046/461] [lvgl] Fix event for binary sensor (#11636) --- esphome/components/lvgl/binary_sensor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/lvgl/binary_sensor/__init__.py b/esphome/components/lvgl/binary_sensor/__init__.py index ffbdc977b28..f9df7d23fa3 100644 --- a/esphome/components/lvgl/binary_sensor/__init__.py +++ b/esphome/components/lvgl/binary_sensor/__init__.py @@ -31,7 +31,7 @@ async def to_code(config): lvgl_static.add_event_cb( widget.obj, await pressed_ctx.get_lambda(), - LV_EVENT.PRESSING, + LV_EVENT.PRESSED, LV_EVENT.RELEASED, ) ) From 0f6fd9130430d976613f60fc933bc69a5162d7aa Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:45:42 +1000 Subject: [PATCH 047/461] [sdl] Fix keymappings (#11635) --- esphome/components/sdl/binary_sensor.py | 485 ++++++++++++------------ tests/components/sdl/common.yaml | 6 +- 2 files changed, 253 insertions(+), 238 deletions(-) diff --git a/esphome/components/sdl/binary_sensor.py b/esphome/components/sdl/binary_sensor.py index 3ea6c2d2184..e19a4888001 100644 --- a/esphome/components/sdl/binary_sensor.py +++ b/esphome/components/sdl/binary_sensor.py @@ -12,241 +12,256 @@ CODEOWNERS = ["@bdm310"] STATE_ARG = "state" -SDL_KEYMAP = { - "SDLK_UNKNOWN": 0, - "SDLK_FIRST": 0, - "SDLK_BACKSPACE": 8, - "SDLK_TAB": 9, - "SDLK_CLEAR": 12, - "SDLK_RETURN": 13, - "SDLK_PAUSE": 19, - "SDLK_ESCAPE": 27, - "SDLK_SPACE": 32, - "SDLK_EXCLAIM": 33, - "SDLK_QUOTEDBL": 34, - "SDLK_HASH": 35, - "SDLK_DOLLAR": 36, - "SDLK_AMPERSAND": 38, - "SDLK_QUOTE": 39, - "SDLK_LEFTPAREN": 40, - "SDLK_RIGHTPAREN": 41, - "SDLK_ASTERISK": 42, - "SDLK_PLUS": 43, - "SDLK_COMMA": 44, - "SDLK_MINUS": 45, - "SDLK_PERIOD": 46, - "SDLK_SLASH": 47, - "SDLK_0": 48, - "SDLK_1": 49, - "SDLK_2": 50, - "SDLK_3": 51, - "SDLK_4": 52, - "SDLK_5": 53, - "SDLK_6": 54, - "SDLK_7": 55, - "SDLK_8": 56, - "SDLK_9": 57, - "SDLK_COLON": 58, - "SDLK_SEMICOLON": 59, - "SDLK_LESS": 60, - "SDLK_EQUALS": 61, - "SDLK_GREATER": 62, - "SDLK_QUESTION": 63, - "SDLK_AT": 64, - "SDLK_LEFTBRACKET": 91, - "SDLK_BACKSLASH": 92, - "SDLK_RIGHTBRACKET": 93, - "SDLK_CARET": 94, - "SDLK_UNDERSCORE": 95, - "SDLK_BACKQUOTE": 96, - "SDLK_a": 97, - "SDLK_b": 98, - "SDLK_c": 99, - "SDLK_d": 100, - "SDLK_e": 101, - "SDLK_f": 102, - "SDLK_g": 103, - "SDLK_h": 104, - "SDLK_i": 105, - "SDLK_j": 106, - "SDLK_k": 107, - "SDLK_l": 108, - "SDLK_m": 109, - "SDLK_n": 110, - "SDLK_o": 111, - "SDLK_p": 112, - "SDLK_q": 113, - "SDLK_r": 114, - "SDLK_s": 115, - "SDLK_t": 116, - "SDLK_u": 117, - "SDLK_v": 118, - "SDLK_w": 119, - "SDLK_x": 120, - "SDLK_y": 121, - "SDLK_z": 122, - "SDLK_DELETE": 127, - "SDLK_WORLD_0": 160, - "SDLK_WORLD_1": 161, - "SDLK_WORLD_2": 162, - "SDLK_WORLD_3": 163, - "SDLK_WORLD_4": 164, - "SDLK_WORLD_5": 165, - "SDLK_WORLD_6": 166, - "SDLK_WORLD_7": 167, - "SDLK_WORLD_8": 168, - "SDLK_WORLD_9": 169, - "SDLK_WORLD_10": 170, - "SDLK_WORLD_11": 171, - "SDLK_WORLD_12": 172, - "SDLK_WORLD_13": 173, - "SDLK_WORLD_14": 174, - "SDLK_WORLD_15": 175, - "SDLK_WORLD_16": 176, - "SDLK_WORLD_17": 177, - "SDLK_WORLD_18": 178, - "SDLK_WORLD_19": 179, - "SDLK_WORLD_20": 180, - "SDLK_WORLD_21": 181, - "SDLK_WORLD_22": 182, - "SDLK_WORLD_23": 183, - "SDLK_WORLD_24": 184, - "SDLK_WORLD_25": 185, - "SDLK_WORLD_26": 186, - "SDLK_WORLD_27": 187, - "SDLK_WORLD_28": 188, - "SDLK_WORLD_29": 189, - "SDLK_WORLD_30": 190, - "SDLK_WORLD_31": 191, - "SDLK_WORLD_32": 192, - "SDLK_WORLD_33": 193, - "SDLK_WORLD_34": 194, - "SDLK_WORLD_35": 195, - "SDLK_WORLD_36": 196, - "SDLK_WORLD_37": 197, - "SDLK_WORLD_38": 198, - "SDLK_WORLD_39": 199, - "SDLK_WORLD_40": 200, - "SDLK_WORLD_41": 201, - "SDLK_WORLD_42": 202, - "SDLK_WORLD_43": 203, - "SDLK_WORLD_44": 204, - "SDLK_WORLD_45": 205, - "SDLK_WORLD_46": 206, - "SDLK_WORLD_47": 207, - "SDLK_WORLD_48": 208, - "SDLK_WORLD_49": 209, - "SDLK_WORLD_50": 210, - "SDLK_WORLD_51": 211, - "SDLK_WORLD_52": 212, - "SDLK_WORLD_53": 213, - "SDLK_WORLD_54": 214, - "SDLK_WORLD_55": 215, - "SDLK_WORLD_56": 216, - "SDLK_WORLD_57": 217, - "SDLK_WORLD_58": 218, - "SDLK_WORLD_59": 219, - "SDLK_WORLD_60": 220, - "SDLK_WORLD_61": 221, - "SDLK_WORLD_62": 222, - "SDLK_WORLD_63": 223, - "SDLK_WORLD_64": 224, - "SDLK_WORLD_65": 225, - "SDLK_WORLD_66": 226, - "SDLK_WORLD_67": 227, - "SDLK_WORLD_68": 228, - "SDLK_WORLD_69": 229, - "SDLK_WORLD_70": 230, - "SDLK_WORLD_71": 231, - "SDLK_WORLD_72": 232, - "SDLK_WORLD_73": 233, - "SDLK_WORLD_74": 234, - "SDLK_WORLD_75": 235, - "SDLK_WORLD_76": 236, - "SDLK_WORLD_77": 237, - "SDLK_WORLD_78": 238, - "SDLK_WORLD_79": 239, - "SDLK_WORLD_80": 240, - "SDLK_WORLD_81": 241, - "SDLK_WORLD_82": 242, - "SDLK_WORLD_83": 243, - "SDLK_WORLD_84": 244, - "SDLK_WORLD_85": 245, - "SDLK_WORLD_86": 246, - "SDLK_WORLD_87": 247, - "SDLK_WORLD_88": 248, - "SDLK_WORLD_89": 249, - "SDLK_WORLD_90": 250, - "SDLK_WORLD_91": 251, - "SDLK_WORLD_92": 252, - "SDLK_WORLD_93": 253, - "SDLK_WORLD_94": 254, - "SDLK_WORLD_95": 255, - "SDLK_KP0": 256, - "SDLK_KP1": 257, - "SDLK_KP2": 258, - "SDLK_KP3": 259, - "SDLK_KP4": 260, - "SDLK_KP5": 261, - "SDLK_KP6": 262, - "SDLK_KP7": 263, - "SDLK_KP8": 264, - "SDLK_KP9": 265, - "SDLK_KP_PERIOD": 266, - "SDLK_KP_DIVIDE": 267, - "SDLK_KP_MULTIPLY": 268, - "SDLK_KP_MINUS": 269, - "SDLK_KP_PLUS": 270, - "SDLK_KP_ENTER": 271, - "SDLK_KP_EQUALS": 272, - "SDLK_UP": 273, - "SDLK_DOWN": 274, - "SDLK_RIGHT": 275, - "SDLK_LEFT": 276, - "SDLK_INSERT": 277, - "SDLK_HOME": 278, - "SDLK_END": 279, - "SDLK_PAGEUP": 280, - "SDLK_PAGEDOWN": 281, - "SDLK_F1": 282, - "SDLK_F2": 283, - "SDLK_F3": 284, - "SDLK_F4": 285, - "SDLK_F5": 286, - "SDLK_F6": 287, - "SDLK_F7": 288, - "SDLK_F8": 289, - "SDLK_F9": 290, - "SDLK_F10": 291, - "SDLK_F11": 292, - "SDLK_F12": 293, - "SDLK_F13": 294, - "SDLK_F14": 295, - "SDLK_F15": 296, - "SDLK_NUMLOCK": 300, - "SDLK_CAPSLOCK": 301, - "SDLK_SCROLLOCK": 302, - "SDLK_RSHIFT": 303, - "SDLK_LSHIFT": 304, - "SDLK_RCTRL": 305, - "SDLK_LCTRL": 306, - "SDLK_RALT": 307, - "SDLK_LALT": 308, - "SDLK_RMETA": 309, - "SDLK_LMETA": 310, - "SDLK_LSUPER": 311, - "SDLK_RSUPER": 312, - "SDLK_MODE": 313, - "SDLK_COMPOSE": 314, - "SDLK_HELP": 315, - "SDLK_PRINT": 316, - "SDLK_SYSREQ": 317, - "SDLK_BREAK": 318, - "SDLK_MENU": 319, - "SDLK_POWER": 320, - "SDLK_EURO": 321, - "SDLK_UNDO": 322, -} +SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode") + +SDL_KEYS = ( + "SDLK_UNKNOWN", + "SDLK_RETURN", + "SDLK_ESCAPE", + "SDLK_BACKSPACE", + "SDLK_TAB", + "SDLK_SPACE", + "SDLK_EXCLAIM", + "SDLK_QUOTEDBL", + "SDLK_HASH", + "SDLK_PERCENT", + "SDLK_DOLLAR", + "SDLK_AMPERSAND", + "SDLK_QUOTE", + "SDLK_LEFTPAREN", + "SDLK_RIGHTPAREN", + "SDLK_ASTERISK", + "SDLK_PLUS", + "SDLK_COMMA", + "SDLK_MINUS", + "SDLK_PERIOD", + "SDLK_SLASH", + "SDLK_0", + "SDLK_1", + "SDLK_2", + "SDLK_3", + "SDLK_4", + "SDLK_5", + "SDLK_6", + "SDLK_7", + "SDLK_8", + "SDLK_9", + "SDLK_COLON", + "SDLK_SEMICOLON", + "SDLK_LESS", + "SDLK_EQUALS", + "SDLK_GREATER", + "SDLK_QUESTION", + "SDLK_AT", + "SDLK_LEFTBRACKET", + "SDLK_BACKSLASH", + "SDLK_RIGHTBRACKET", + "SDLK_CARET", + "SDLK_UNDERSCORE", + "SDLK_BACKQUOTE", + "SDLK_a", + "SDLK_b", + "SDLK_c", + "SDLK_d", + "SDLK_e", + "SDLK_f", + "SDLK_g", + "SDLK_h", + "SDLK_i", + "SDLK_j", + "SDLK_k", + "SDLK_l", + "SDLK_m", + "SDLK_n", + "SDLK_o", + "SDLK_p", + "SDLK_q", + "SDLK_r", + "SDLK_s", + "SDLK_t", + "SDLK_u", + "SDLK_v", + "SDLK_w", + "SDLK_x", + "SDLK_y", + "SDLK_z", + "SDLK_CAPSLOCK", + "SDLK_F1", + "SDLK_F2", + "SDLK_F3", + "SDLK_F4", + "SDLK_F5", + "SDLK_F6", + "SDLK_F7", + "SDLK_F8", + "SDLK_F9", + "SDLK_F10", + "SDLK_F11", + "SDLK_F12", + "SDLK_PRINTSCREEN", + "SDLK_SCROLLLOCK", + "SDLK_PAUSE", + "SDLK_INSERT", + "SDLK_HOME", + "SDLK_PAGEUP", + "SDLK_DELETE", + "SDLK_END", + "SDLK_PAGEDOWN", + "SDLK_RIGHT", + "SDLK_LEFT", + "SDLK_DOWN", + "SDLK_UP", + "SDLK_NUMLOCKCLEAR", + "SDLK_KP_DIVIDE", + "SDLK_KP_MULTIPLY", + "SDLK_KP_MINUS", + "SDLK_KP_PLUS", + "SDLK_KP_ENTER", + "SDLK_KP_1", + "SDLK_KP_2", + "SDLK_KP_3", + "SDLK_KP_4", + "SDLK_KP_5", + "SDLK_KP_6", + "SDLK_KP_7", + "SDLK_KP_8", + "SDLK_KP_9", + "SDLK_KP_0", + "SDLK_KP_PERIOD", + "SDLK_APPLICATION", + "SDLK_POWER", + "SDLK_KP_EQUALS", + "SDLK_F13", + "SDLK_F14", + "SDLK_F15", + "SDLK_F16", + "SDLK_F17", + "SDLK_F18", + "SDLK_F19", + "SDLK_F20", + "SDLK_F21", + "SDLK_F22", + "SDLK_F23", + "SDLK_F24", + "SDLK_EXECUTE", + "SDLK_HELP", + "SDLK_MENU", + "SDLK_SELECT", + "SDLK_STOP", + "SDLK_AGAIN", + "SDLK_UNDO", + "SDLK_CUT", + "SDLK_COPY", + "SDLK_PASTE", + "SDLK_FIND", + "SDLK_MUTE", + "SDLK_VOLUMEUP", + "SDLK_VOLUMEDOWN", + "SDLK_KP_COMMA", + "SDLK_KP_EQUALSAS400", + "SDLK_ALTERASE", + "SDLK_SYSREQ", + "SDLK_CANCEL", + "SDLK_CLEAR", + "SDLK_PRIOR", + "SDLK_RETURN2", + "SDLK_SEPARATOR", + "SDLK_OUT", + "SDLK_OPER", + "SDLK_CLEARAGAIN", + "SDLK_CRSEL", + "SDLK_EXSEL", + "SDLK_KP_00", + "SDLK_KP_000", + "SDLK_THOUSANDSSEPARATOR", + "SDLK_DECIMALSEPARATOR", + "SDLK_CURRENCYUNIT", + "SDLK_CURRENCYSUBUNIT", + "SDLK_KP_LEFTPAREN", + "SDLK_KP_RIGHTPAREN", + "SDLK_KP_LEFTBRACE", + "SDLK_KP_RIGHTBRACE", + "SDLK_KP_TAB", + "SDLK_KP_BACKSPACE", + "SDLK_KP_A", + "SDLK_KP_B", + "SDLK_KP_C", + "SDLK_KP_D", + "SDLK_KP_E", + "SDLK_KP_F", + "SDLK_KP_XOR", + "SDLK_KP_POWER", + "SDLK_KP_PERCENT", + "SDLK_KP_LESS", + "SDLK_KP_GREATER", + "SDLK_KP_AMPERSAND", + "SDLK_KP_DBLAMPERSAND", + "SDLK_KP_VERTICALBAR", + "SDLK_KP_DBLVERTICALBAR", + "SDLK_KP_COLON", + "SDLK_KP_HASH", + "SDLK_KP_SPACE", + "SDLK_KP_AT", + "SDLK_KP_EXCLAM", + "SDLK_KP_MEMSTORE", + "SDLK_KP_MEMRECALL", + "SDLK_KP_MEMCLEAR", + "SDLK_KP_MEMADD", + "SDLK_KP_MEMSUBTRACT", + "SDLK_KP_MEMMULTIPLY", + "SDLK_KP_MEMDIVIDE", + "SDLK_KP_PLUSMINUS", + "SDLK_KP_CLEAR", + "SDLK_KP_CLEARENTRY", + "SDLK_KP_BINARY", + "SDLK_KP_OCTAL", + "SDLK_KP_DECIMAL", + "SDLK_KP_HEXADECIMAL", + "SDLK_LCTRL", + "SDLK_LSHIFT", + "SDLK_LALT", + "SDLK_LGUI", + "SDLK_RCTRL", + "SDLK_RSHIFT", + "SDLK_RALT", + "SDLK_RGUI", + "SDLK_MODE", + "SDLK_AUDIONEXT", + "SDLK_AUDIOPREV", + "SDLK_AUDIOSTOP", + "SDLK_AUDIOPLAY", + "SDLK_AUDIOMUTE", + "SDLK_MEDIASELECT", + "SDLK_WWW", + "SDLK_MAIL", + "SDLK_CALCULATOR", + "SDLK_COMPUTER", + "SDLK_AC_SEARCH", + "SDLK_AC_HOME", + "SDLK_AC_BACK", + "SDLK_AC_FORWARD", + "SDLK_AC_STOP", + "SDLK_AC_REFRESH", + "SDLK_AC_BOOKMARKS", + "SDLK_BRIGHTNESSDOWN", + "SDLK_BRIGHTNESSUP", + "SDLK_DISPLAYSWITCH", + "SDLK_KBDILLUMTOGGLE", + "SDLK_KBDILLUMDOWN", + "SDLK_KBDILLUMUP", + "SDLK_EJECT", + "SDLK_SLEEP", + "SDLK_APP1", + "SDLK_APP2", + "SDLK_AUDIOREWIND", + "SDLK_AUDIOFASTFORWARD", + "SDLK_SOFTLEFT", + "SDLK_SOFTRIGHT", + "SDLK_CALL", + "SDLK_ENDCALL", +) + +SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS} CONFIG_SCHEMA = ( binary_sensor.binary_sensor_schema(BinarySensor) diff --git a/tests/components/sdl/common.yaml b/tests/components/sdl/common.yaml index 50fa4a5990c..52991d595c0 100644 --- a/tests/components/sdl/common.yaml +++ b/tests/components/sdl/common.yaml @@ -14,10 +14,10 @@ display: binary_sensor: - platform: sdl id: key_up - key: SDLK_a + key: SDLK_UP - platform: sdl id: key_down - key: SDLK_d + key: SDLK_DOWN - platform: sdl id: key_enter - key: SDLK_s + key: SDLK_RETURN From a3583da17d1a59d53db1917fedb151ca5fae1360 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 4 Nov 2025 11:25:33 +1300 Subject: [PATCH 048/461] Bump version to 2025.10.4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index f770defaf20..4f72970e248 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.10.3 +PROJECT_NUMBER = 2025.10.4 # 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 712dc852217..9e8ec487b52 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.10.3" +__version__ = "2025.10.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 99ce989eaedd59fdad9ea28a0468d63d31568d95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 16:30:35 -0600 Subject: [PATCH 049/461] [micro_wake_word] Add wake_loop_threadsafe() for low-latency wake word detection (#11698) --- esphome/components/micro_wake_word/__init__.py | 7 ++++++- esphome/components/micro_wake_word/micro_wake_word.cpp | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 8cd71153681..575fb97799b 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin from esphome import automation, external_files, git from esphome.automation import register_action, register_condition import esphome.codegen as cg -from esphome.components import esp32, microphone +from esphome.components import esp32, microphone, socket import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@kahrendt", "@jesserockz"] DEPENDENCIES = ["microphone"] +AUTO_LOAD = ["socket"] DOMAIN = "micro_wake_word" @@ -443,6 +444,10 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + # Enable wake_loop_threadsafe() for low-latency wake word detection + # The inference task queues detection events that need immediate processing + socket.require_wake_loop_threadsafe() + mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE]) cg.add(var.set_microphone_source(mic_source)) diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 6fca48a5bd3..a0547b158ef 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -2,6 +2,7 @@ #ifdef USE_ESP_IDF +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -426,6 +427,12 @@ void MicroWakeWord::process_probabilities_() { if (vad_state.detected) { #endif xQueueSend(this->detection_queue_, &wake_word_state, portMAX_DELAY); + + // Wake main loop immediately to process wake word detection +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + model->reset_probabilities(); #ifdef USE_MICRO_WAKE_WORD_VAD } else { From 99d1a9cf6ef89fae36d749d3f640891ad1265471 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:23:45 +1000 Subject: [PATCH 050/461] [usb_uart] Fixes for transfer queue allocation (#11548) --- esphome/components/usb_host/usb_host.h | 12 ++--- .../components/usb_host/usb_host_client.cpp | 54 +++++++++---------- esphome/components/usb_uart/usb_uart.cpp | 22 +++++--- tests/components/usb_uart/common.yaml | 3 ++ 4 files changed, 51 insertions(+), 40 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index 43b24a54a50..31bdde2df8c 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -55,7 +55,7 @@ static const uint8_t USB_DIR_IN = 1 << 7; static const uint8_t USB_DIR_OUT = 0; static const size_t SETUP_PACKET_SIZE = 8; -static const size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. +static constexpr size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32"); // Select appropriate bitmask type for tracking allocation of TransferRequest slots. @@ -65,6 +65,7 @@ static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be bet // This is tied to the static_assert above, which enforces MAX_REQUESTS is between 1 and 32. // If MAX_REQUESTS is increased above 32, this logic and the static_assert must be updated. using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type; +static constexpr trq_bitmask_t ALL_REQUESTS_IN_USE = MAX_REQUESTS == 32 ? ~0 : (1 << MAX_REQUESTS) - 1; static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples) @@ -133,11 +134,11 @@ class USBClient : public Component { float get_setup_priority() const override { return setup_priority::IO; } void on_opened(uint8_t addr); void on_removed(usb_device_handle_t handle); - void control_transfer_callback(const usb_transfer_t *xfer) const; - void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); - void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); + bool transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); + bool transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); void dump_config() override; void release_trq(TransferRequest *trq); + trq_bitmask_t get_trq_in_use() const { return trq_in_use_; } bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, const std::vector &data = {}); @@ -147,7 +148,6 @@ class USBClient : public Component { EventPool event_pool; protected: - bool register_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); virtual void on_connected() {} @@ -158,7 +158,7 @@ class USBClient : public Component { // USB task management static void usb_task_fn(void *arg); - void usb_task_loop(); + [[noreturn]] void usb_task_loop() const; TaskHandle_t usb_task_handle_{nullptr}; diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 0dda36b9d76..4c09cf8a498 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -194,9 +194,9 @@ void USBClient::setup() { } // Pre-allocate USB transfer buffers for all slots at startup // This avoids any dynamic allocation during runtime - for (size_t i = 0; i < MAX_REQUESTS; i++) { - usb_host_transfer_alloc(64, 0, &this->requests_[i].transfer); - this->requests_[i].client = this; // Set once, never changes + for (auto &request : this->requests_) { + usb_host_transfer_alloc(64, 0, &request.transfer); + request.client = this; // Set once, never changes } // Create and start USB task @@ -216,8 +216,7 @@ void USBClient::usb_task_fn(void *arg) { auto *client = static_cast(arg); client->usb_task_loop(); } - -void USBClient::usb_task_loop() { +void USBClient::usb_task_loop() const { while (true) { usb_host_client_handle_events(this->handle_, portMAX_DELAY); } @@ -340,22 +339,23 @@ static void control_callback(const usb_transfer_t *xfer) { // This multi-threaded access is intentional for performance - USB task can // immediately restart transfers without waiting for main loop scheduling. TransferRequest *USBClient::get_trq_() { - trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed); + trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire); // Find first available slot (bit = 0) and try to claim it atomically // We use a while loop to allow retrying the same slot after CAS failure - size_t i = 0; - while (i != MAX_REQUESTS) { - if (mask & (static_cast(1) << i)) { - // Slot is in use, move to next slot - i++; - continue; + for (;;) { + if (mask == ALL_REQUESTS_IN_USE) { + ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); + return nullptr; } + // find the least significant zero bit + trq_bitmask_t lsb = ~mask & (mask + 1); // Slot i appears available, try to claim it atomically - trq_bitmask_t desired = mask | (static_cast(1) << i); // Set bit i to mark as in-use + trq_bitmask_t desired = mask | lsb; - if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order_acquire, std::memory_order_relaxed)) { + if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order::acquire)) { + auto i = __builtin_ctz(lsb); // count trailing zeroes // Successfully claimed slot i - prepare the TransferRequest auto *trq = &this->requests_[i]; trq->transfer->context = trq; @@ -364,13 +364,9 @@ TransferRequest *USBClient::get_trq_() { } // CAS failed - another thread modified the bitmask // mask was already updated by compare_exchange_weak with the current value - // No need to reload - the CAS already did that for us - i = 0; } - - ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); - return nullptr; } + void USBClient::disconnect() { this->on_disconnected(); auto err = usb_host_device_close(this->handle_, this->device_handle_); @@ -452,11 +448,11 @@ static void transfer_callback(usb_transfer_t *xfer) { * * @throws None. */ -void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { +bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return; + return false; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -466,7 +462,9 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); + return false; } + return true; } /** @@ -482,11 +480,11 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u * * @throws None. */ -void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { +bool USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return; + return false; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -497,7 +495,9 @@ void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); + return false; } + return true; } void USBClient::dump_config() { ESP_LOGCONFIG(TAG, @@ -511,7 +511,7 @@ void USBClient::dump_config() { // - Main loop: When transfer submission fails // // THREAD SAFETY: Lock-free using atomic AND to clear bit -// Thread-safe atomic operation allows multi-threaded deallocation +// Thread-safe atomic operation allows multithreaded deallocation void USBClient::release_trq(TransferRequest *trq) { if (trq == nullptr) return; @@ -523,10 +523,10 @@ void USBClient::release_trq(TransferRequest *trq) { return; } - // Atomically clear bit i to mark slot as available + // Atomically clear the bit to mark slot as available // fetch_and with inverted bitmask clears the bit atomically - trq_bitmask_t bit = static_cast(1) << index; - this->trq_in_use_.fetch_and(static_cast(~bit), std::memory_order_release); + trq_bitmask_t mask = ~(static_cast(1) << index); + this->trq_in_use_.fetch_and(mask, std::memory_order_release); } } // namespace usb_host diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 29003e071ef..c24fffb11de 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -214,7 +214,7 @@ void USBUartComponent::dump_config() { } } void USBUartComponent::start_input(USBUartChannel *channel) { - if (!channel->initialised_.load() || channel->input_started_.load()) + if (!channel->initialised_.load()) return; // THREAD CONTEXT: Called from both USB task and main loop threads // - USB task: Immediate restart after successful transfer for continuous data flow @@ -226,12 +226,18 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // // The underlying transfer_in() uses lock-free atomic allocation from the // TransferRequest pool, making this multi-threaded access safe + + // if already started, don't restart. A spurious failure in compare_exchange_weak + // is not a problem, as it will be retried on the next read_array() + auto started = false; + if (!channel->input_started_.compare_exchange_weak(started, true)) + return; const auto *ep = channel->cdc_dev_.in_ep; // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback auto callback = [this, channel](const usb_host::TransferStatus &status) { ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); if (!status.success) { - ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); + ESP_LOGE(TAG, "Input transfer failed, status=%s", esp_err_to_name(status.error_code)); // On failure, don't restart - let next read_array() trigger it channel->input_started_.store(false); return; @@ -263,8 +269,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { channel->input_started_.store(false); this->start_input(channel); }; - channel->input_started_.store(true); - this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); + if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) { + channel->input_started_.store(false); + } } void USBUartComponent::start_output(USBUartChannel *channel) { @@ -357,11 +364,12 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); } usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); - channel->initialised_.store(false); - channel->input_started_.store(false); - channel->output_started_.store(false); + // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts + channel->input_started_.store(true); + channel->output_started_.store(true); channel->input_buffer_.clear(); channel->output_buffer_.clear(); + channel->initialised_.store(false); } USBClient::on_disconnected(); } diff --git a/tests/components/usb_uart/common.yaml b/tests/components/usb_uart/common.yaml index 46ad6291f93..474c3f5c8de 100644 --- a/tests/components/usb_uart/common.yaml +++ b/tests/components/usb_uart/common.yaml @@ -1,3 +1,6 @@ +usb_host: + max_transfer_requests: 32 + usb_uart: - id: uart_0 type: cdc_acm From 266e4ae91fb2414528163109e708af6246b57952 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 3 Nov 2025 17:30:37 -0600 Subject: [PATCH 051/461] [helpers] Add `get_mac_address_into_buffer()` (#11700) --- esphome/core/helpers.cpp | 6 ++++++ esphome/core/helpers.h | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index fb8b220b2fa..568acb9f1bb 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -643,6 +643,12 @@ std::string get_mac_address_pretty() { return format_mac_address_pretty(mac); } +void get_mac_address_into_buffer(std::span buf) { + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_lower_no_sep(mac, buf.data()); +} + #ifndef USE_ESP32 bool has_custom_mac_address() { return false; } #endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index cf21ddc16da..91ddc70afaa 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -1027,6 +1028,10 @@ std::string get_mac_address(); /// Get the device MAC address as a string, in colon-separated uppercase hex notation. std::string get_mac_address_pretty(); +/// Get the device MAC address into the given buffer, in lowercase hex notation. +/// Assumes buffer length is 13 (12 digits for hexadecimal representation followed by null terminator). +void get_mac_address_into_buffer(std::span buf); + #ifdef USE_ESP32 /// Set the MAC address to use from the provided byte array (6 bytes). void set_mac_address(uint8_t *mac); From 59326f137ed7c6a932bb63f38a10fe4fa7baaf9b Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 3 Nov 2025 18:29:30 -0600 Subject: [PATCH 052/461] [tinyusb] New component (#11678) --- CODEOWNERS | 1 + esphome/components/tinyusb/__init__.py | 60 ++++++++++++++++ .../components/tinyusb/tinyusb_component.cpp | 44 ++++++++++++ .../components/tinyusb/tinyusb_component.h | 72 +++++++++++++++++++ esphome/idf_component.yml | 4 ++ tests/components/tinyusb/common.yaml | 8 +++ .../components/tinyusb/test.esp32-p4-idf.yaml | 1 + .../components/tinyusb/test.esp32-s2-idf.yaml | 1 + .../components/tinyusb/test.esp32-s3-idf.yaml | 1 + 9 files changed, 192 insertions(+) create mode 100644 esphome/components/tinyusb/__init__.py create mode 100644 esphome/components/tinyusb/tinyusb_component.cpp create mode 100644 esphome/components/tinyusb/tinyusb_component.h create mode 100644 tests/components/tinyusb/common.yaml create mode 100644 tests/components/tinyusb/test.esp32-p4-idf.yaml create mode 100644 tests/components/tinyusb/test.esp32-s2-idf.yaml create mode 100644 tests/components/tinyusb/test.esp32-s3-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index fee0e98f46d..b8a4df6a854 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -480,6 +480,7 @@ esphome/components/template/fan/* @ssieb esphome/components/text/* @mauritskorse esphome/components/thermostat/* @kbx81 esphome/components/time/* @esphome/core +esphome/components/tinyusb/* @kbx81 esphome/components/tlc5947/* @rnauber esphome/components/tlc5971/* @IJIJI esphome/components/tm1621/* @Philippe12 diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py new file mode 100644 index 00000000000..72afc18387b --- /dev/null +++ b/esphome/components/tinyusb/__init__.py @@ -0,0 +1,60 @@ +import esphome.codegen as cg +from esphome.components import esp32 +from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option +from esphome.components.esp32.const import ( + VARIANT_ESP32P4, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@kbx81"] +CONFLICTS_WITH = ["usb_host"] + +CONF_USB_LANG_ID = "usb_lang_id" +CONF_USB_MANUFACTURER_STR = "usb_manufacturer_str" +CONF_USB_PRODUCT_ID = "usb_product_id" +CONF_USB_PRODUCT_STR = "usb_product_str" +CONF_USB_SERIAL_STR = "usb_serial_str" +CONF_USB_VENDOR_ID = "usb_vendor_id" + +tinyusb_ns = cg.esphome_ns.namespace("tinyusb") +TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TinyUSB), + cv.Optional(CONF_USB_PRODUCT_ID, default=0x4001): cv.uint16_t, + cv.Optional(CONF_USB_VENDOR_ID, default=0x303A): cv.uint16_t, + cv.Optional(CONF_USB_LANG_ID, default=0x0409): cv.uint16_t, + cv.Optional(CONF_USB_MANUFACTURER_STR, default="ESPHome"): cv.string, + cv.Optional(CONF_USB_PRODUCT_STR, default="ESPHome"): cv.string, + cv.Optional(CONF_USB_SERIAL_STR, default=""): cv.string, + } + ).extend(cv.COMPONENT_SCHEMA), + esp32.only_on_variant( + supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3], + ), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + # Set USB device descriptor properties + cg.add(var.set_usb_desc_product_id(config[CONF_USB_PRODUCT_ID])) + cg.add(var.set_usb_desc_vendor_id(config[CONF_USB_VENDOR_ID])) + cg.add(var.set_usb_desc_lang_id(config[CONF_USB_LANG_ID])) + cg.add(var.set_usb_desc_manufacturer(config[CONF_USB_MANUFACTURER_STR])) + cg.add(var.set_usb_desc_product(config[CONF_USB_PRODUCT_STR])) + if config[CONF_USB_SERIAL_STR]: + cg.add(var.set_usb_desc_serial(config[CONF_USB_SERIAL_STR])) + + add_idf_component(name="espressif/esp_tinyusb", ref="1.7.6~1") + + add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_ESPRESSIF_VID", False) + add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_DEFAULT_PID", False) + add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_BCD_DEVICE", 0x0100) diff --git a/esphome/components/tinyusb/tinyusb_component.cpp b/esphome/components/tinyusb/tinyusb_component.cpp new file mode 100644 index 00000000000..a2057c90cea --- /dev/null +++ b/esphome/components/tinyusb/tinyusb_component.cpp @@ -0,0 +1,44 @@ +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "tinyusb_component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::tinyusb { + +static const char *TAG = "tinyusb"; + +void TinyUSB::setup() { + // Use the device's MAC address as its serial number if no serial number is defined + if (this->string_descriptor_[SERIAL_NUMBER] == nullptr) { + static char mac_addr_buf[13]; + get_mac_address_into_buffer(mac_addr_buf); + this->string_descriptor_[SERIAL_NUMBER] = mac_addr_buf; + } + + this->tusb_cfg_ = { + .descriptor = &this->usb_descriptor_, + .string_descriptor = this->string_descriptor_, + .string_descriptor_count = SIZE, + .external_phy = false, + }; + + esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_); + if (result != ESP_OK) { + this->mark_failed(); + } +} + +void TinyUSB::dump_config() { + ESP_LOGCONFIG(TAG, + "TinyUSB:\n" + " Product ID: 0x%04X\n" + " Vendor ID: 0x%04X\n" + " Manufacturer: '%s'\n" + " Product: '%s'\n" + " Serial: '%s'\n", + this->usb_descriptor_.idProduct, this->usb_descriptor_.idVendor, this->string_descriptor_[MANUFACTURER], + this->string_descriptor_[PRODUCT], this->string_descriptor_[SERIAL_NUMBER]); +} + +} // namespace esphome::tinyusb +#endif diff --git a/esphome/components/tinyusb/tinyusb_component.h b/esphome/components/tinyusb/tinyusb_component.h new file mode 100644 index 00000000000..56c286f4559 --- /dev/null +++ b/esphome/components/tinyusb/tinyusb_component.h @@ -0,0 +1,72 @@ +#pragma once +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "esphome/core/component.h" + +#include "tinyusb.h" +#include "tusb.h" + +namespace esphome::tinyusb { + +enum USBDStringDescriptor : uint8_t { + LANGUAGE_ID = 0, + MANUFACTURER = 1, + PRODUCT = 2, + SERIAL_NUMBER = 3, + INTERFACE = 4, + TERMINATOR = 5, + SIZE = 6, +}; + +static const char *DEFAULT_USB_STR = "ESPHome"; + +class TinyUSB : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void set_usb_desc_product_id(uint16_t product_id) { this->usb_descriptor_.idProduct = product_id; } + void set_usb_desc_vendor_id(uint16_t vendor_id) { this->usb_descriptor_.idVendor = vendor_id; } + void set_usb_desc_lang_id(uint16_t lang_id) { + this->usb_desc_lang_id_[0] = lang_id & 0xFF; + this->usb_desc_lang_id_[1] = lang_id >> 8; + } + void set_usb_desc_manufacturer(const char *usb_desc_manufacturer) { + this->string_descriptor_[MANUFACTURER] = usb_desc_manufacturer; + } + void set_usb_desc_product(const char *usb_desc_product) { this->string_descriptor_[PRODUCT] = usb_desc_product; } + void set_usb_desc_serial(const char *usb_desc_serial) { this->string_descriptor_[SERIAL_NUMBER] = usb_desc_serial; } + + protected: + char usb_desc_lang_id_[2] = {0x09, 0x04}; // defaults to english + + const char *string_descriptor_[SIZE] = { + this->usb_desc_lang_id_, // 0: supported language is English (0x0409) + DEFAULT_USB_STR, // 1: Manufacturer + DEFAULT_USB_STR, // 2: Product + nullptr, // 3: Serial Number + nullptr, // 4: Interface + nullptr, // 5: Terminator + }; + + tinyusb_config_t tusb_cfg_{}; + tusb_desc_device_t usb_descriptor_{ + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = 0x303A, + .idProduct = 0x4001, + .bcdDevice = CONFIG_TINYUSB_DESC_BCD_DEVICE, + .iManufacturer = 1, + .iProduct = 2, + .iSerialNumber = 3, + .bNumConfigurations = 1, + }; +}; + +} // namespace esphome::tinyusb +#endif diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 31112caf0ad..fcb3a4f4382 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -23,3 +23,7 @@ dependencies: version: "2.0.0" rules: - if: "target in [esp32, esp32p4]" + espressif/esp_tinyusb: + version: "1.7.6~1" + rules: + - if: "target in [esp32s2, esp32s3, esp32p4]" diff --git a/tests/components/tinyusb/common.yaml b/tests/components/tinyusb/common.yaml new file mode 100644 index 00000000000..cb3f48836ab --- /dev/null +++ b/tests/components/tinyusb/common.yaml @@ -0,0 +1,8 @@ +tinyusb: + id: tinyusb_test + usb_lang_id: 0x0123 + usb_manufacturer_str: ESPHomeTestManufacturer + usb_product_id: 0x1234 + usb_product_str: ESPHomeTestProduct + usb_serial_str: ESPHomeTestSerialNumber + usb_vendor_id: 0x2345 diff --git a/tests/components/tinyusb/test.esp32-p4-idf.yaml b/tests/components/tinyusb/test.esp32-p4-idf.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/tinyusb/test.esp32-p4-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tinyusb/test.esp32-s2-idf.yaml b/tests/components/tinyusb/test.esp32-s2-idf.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/tinyusb/test.esp32-s2-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tinyusb/test.esp32-s3-idf.yaml b/tests/components/tinyusb/test.esp32-s3-idf.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/tinyusb/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 6220084fe684f357c99f4261832f09c8cf7a76c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 19:23:04 -0600 Subject: [PATCH 053/461] [ci] Fix memory impact analysis to filter incompatible platform components (#11706) --- script/determine-jobs.py | 33 ++++++++- tests/script/test_determine_jobs.py | 108 ++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 4a0edebb0dd..6f908b7150b 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -94,6 +94,22 @@ class Platform(StrEnum): MEMORY_IMPACT_FALLBACK_COMPONENT = "api" # Representative component for core changes MEMORY_IMPACT_FALLBACK_PLATFORM = Platform.ESP32_IDF # Most representative platform +# Platform-specific components that can only be built on their respective platforms +# These components contain platform-specific code and cannot be cross-compiled +# Regular components (wifi, logger, api, etc.) are cross-platform and not listed here +PLATFORM_SPECIFIC_COMPONENTS = frozenset( + { + "esp32", # ESP32 platform implementation + "esp8266", # ESP8266 platform implementation + "rp2040", # Raspberry Pi Pico / RP2040 platform implementation + "bk72xx", # Beken BK72xx platform implementation (uses LibreTiny) + "rtl87xx", # Realtek RTL87xx platform implementation (uses LibreTiny) + "ln882x", # Winner Micro LN882x platform implementation (uses LibreTiny) + "host", # Host platform (for testing on development machine) + "nrf52", # Nordic nRF52 platform implementation + } +) + # Platform preference order for memory impact analysis # This order is used when no platform-specific hints are detected from filenames # Priority rationale: @@ -568,6 +584,20 @@ def detect_memory_impact_config( ) platform = _select_platform_by_count(platform_counts) + # Filter out platform-specific components that are incompatible with selected platform + # Platform components (esp32, esp8266, rp2040, etc.) can only build on their own platform + # Other components (wifi, logger, etc.) are cross-platform and can build anywhere + compatible_components = [ + component + for component in components_with_tests + if component not in PLATFORM_SPECIFIC_COMPONENTS + or platform in component_platforms_map.get(component, set()) + ] + + # If no components are compatible with the selected platform, don't run + if not compatible_components: + return {"should_run": "false"} + # Debug output print("Memory impact analysis:", file=sys.stderr) print(f" Changed components: {sorted(changed_component_set)}", file=sys.stderr) @@ -579,10 +609,11 @@ def detect_memory_impact_config( print(f" Platform hints from filenames: {platform_hints}", file=sys.stderr) print(f" Common platforms: {sorted(common_platforms)}", file=sys.stderr) print(f" Selected platform: {platform}", file=sys.stderr) + print(f" Compatible components: {compatible_components}", file=sys.stderr) return { "should_run": "true", - "components": components_with_tests, + "components": compatible_components, "platform": platform, "use_merged_config": "true", } diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index e73c1341516..a33eca5b19e 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -1130,3 +1130,111 @@ def test_main_core_files_changed_still_detects_components( assert "select" in output["changed_components"] assert "api" in output["changed_components"] assert len(output["changed_components"]) > 0 + + +def test_detect_memory_impact_config_filters_incompatible_esp32_on_esp8266( + tmp_path: Path, +) -> None: + """Test that ESP32 components are filtered out when ESP8266 platform is selected. + + This test verifies the fix for the issue where ESP32 components were being included + when ESP8266 was selected as the platform, causing build failures in PR 10387. + """ + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # esp32 component only has esp32-idf tests (NOT compatible with esp8266) + esp32_dir = tests_dir / "esp32" + esp32_dir.mkdir(parents=True) + (esp32_dir / "test.esp32-idf.yaml").write_text("test: esp32") + (esp32_dir / "test.esp32-s3-idf.yaml").write_text("test: esp32") + + # esp8266 component only has esp8266-ard test (NOT compatible with esp32) + esp8266_dir = tests_dir / "esp8266" + esp8266_dir.mkdir(parents=True) + (esp8266_dir / "test.esp8266-ard.yaml").write_text("test: esp8266") + + # Mock changed_files to return both esp32 and esp8266 component changes + # Include esp8266-specific filename to trigger esp8266 platform hint + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "tests/components/esp32/common.yaml", + "tests/components/esp8266/test.esp8266-ard.yaml", + "esphome/core/helpers_esp8266.h", # ESP8266-specific file to hint platform + ] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run + assert result["should_run"] == "true" + + # Platform should be esp8266-ard (due to ESP8266 filename hint) + assert result["platform"] == "esp8266-ard" + + # CRITICAL: Only esp8266 component should be included, not esp32 + # This prevents trying to build ESP32 components on ESP8266 platform + assert result["components"] == ["esp8266"], ( + "When esp8266-ard platform is selected, only esp8266 component should be included, " + "not esp32. This prevents trying to build ESP32 components on ESP8266 platform." + ) + + assert result["use_merged_config"] == "true" + + +def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32( + tmp_path: Path, +) -> None: + """Test that ESP8266 components are filtered out when ESP32 platform is selected. + + This is the inverse of the ESP8266 test - ensures filtering works both ways. + """ + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # esp32 component only has esp32-idf tests (NOT compatible with esp8266) + esp32_dir = tests_dir / "esp32" + esp32_dir.mkdir(parents=True) + (esp32_dir / "test.esp32-idf.yaml").write_text("test: esp32") + (esp32_dir / "test.esp32-s3-idf.yaml").write_text("test: esp32") + + # esp8266 component only has esp8266-ard test (NOT compatible with esp32) + esp8266_dir = tests_dir / "esp8266" + esp8266_dir.mkdir(parents=True) + (esp8266_dir / "test.esp8266-ard.yaml").write_text("test: esp8266") + + # Mock changed_files to return both esp32 and esp8266 component changes + # Include MORE esp32-specific filenames to ensure esp32-idf wins the hint count + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "tests/components/esp32/common.yaml", + "tests/components/esp8266/test.esp8266-ard.yaml", + "esphome/components/wifi/wifi_component_esp_idf.cpp", # ESP-IDF hint + "esphome/components/ethernet/ethernet_esp32.cpp", # ESP32 hint + ] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run + assert result["should_run"] == "true" + + # Platform should be esp32-idf (due to more ESP32-IDF hints) + assert result["platform"] == "esp32-idf" + + # CRITICAL: Only esp32 component should be included, not esp8266 + # This prevents trying to build ESP8266 components on ESP32 platform + assert result["components"] == ["esp32"], ( + "When esp32-idf platform is selected, only esp32 component should be included, " + "not esp8266. This prevents trying to build ESP8266 components on ESP32 platform." + ) + + assert result["use_merged_config"] == "true" From 326975ccad0d8c819042981e801b20e79c6c3567 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:09:34 -0500 Subject: [PATCH 054/461] [core] Fix ESPTime crash (#11705) --- esphome/core/time.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/core/time.h b/esphome/core/time.h index ffcfced418c..68826dabdc2 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -84,6 +84,9 @@ struct ESPTime { */ static ESPTime from_epoch_local(time_t epoch) { struct tm *c_tm = ::localtime(&epoch); + if (c_tm == nullptr) { + return ESPTime{}; // Return an invalid ESPTime + } return ESPTime::from_c_tm(c_tm, epoch); } /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. @@ -93,6 +96,9 @@ struct ESPTime { */ static ESPTime from_epoch_utc(time_t epoch) { struct tm *c_tm = ::gmtime(&epoch); + if (c_tm == nullptr) { + return ESPTime{}; // Return an invalid ESPTime + } return ESPTime::from_c_tm(c_tm, epoch); } From 69a1ea43e778e48b1114c92d86751d444b1cbec5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 21:31:03 -0600 Subject: [PATCH 055/461] [network] Store use_address in RODATA to save RAM --- esphome/components/api/api_server.cpp | 2 +- esphome/components/esphome/ota/ota_esphome.cpp | 2 +- esphome/components/ethernet/ethernet_component.cpp | 4 ++-- esphome/components/ethernet/ethernet_component.h | 6 +++--- esphome/components/network/util.cpp | 5 ++--- esphome/components/network/util.h | 2 +- esphome/components/openthread/openthread.cpp | 4 ++-- esphome/components/openthread/openthread.h | 6 +++--- esphome/components/web_server/web_server.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/components/wifi/wifi_component.h | 6 +++--- 11 files changed, 21 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index e618610a75d..e5f0d9795ed 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -224,7 +224,7 @@ void APIServer::dump_config() { " Address: %s:%u\n" " Listen backlog: %u\n" " Max connections: %u", - network::get_use_address().c_str(), this->port_, this->listen_backlog_, this->max_connections_); + network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_); #ifdef USE_API_NOISE ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); if (!this->noise_ctx_->has_psk()) { diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index b85d6602726..eb6c61a69be 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -94,7 +94,7 @@ void ESPHomeOTAComponent::dump_config() { "Over-The-Air updates:\n" " Address: %s:%u\n" " Version: %d", - network::get_use_address().c_str(), this->port_, USE_OTA_VERSION); + network::get_use_address(), this->port_, USE_OTA_VERSION); #ifdef USE_OTA_PASSWORD if (!this->password_.empty()) { ESP_LOGCONFIG(TAG, " Password configured"); diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 24b6e8154ba..893d0285be2 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -691,9 +691,9 @@ void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ // set_use_address() is guaranteed to be called during component setup by Python code generation, // so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const std::string &EthernetComponent::get_use_address() const { return this->use_address_; } +const char *EthernetComponent::get_use_address() const { return this->use_address_; } -void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { esp_err_t err; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index d5dda3e3aef..de136a02eb4 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -88,8 +88,8 @@ class EthernetComponent : public Component { network::IPAddresses get_ip_addresses(); network::IPAddress get_dns_address(uint8_t num); - const std::string &get_use_address() const; - void set_use_address(const std::string &use_address); + const char *get_use_address() const; + void set_use_address(const char *use_address); void get_eth_mac_address_raw(uint8_t *mac); std::string get_eth_mac_address_pretty(); eth_duplex_t get_duplex_mode(); @@ -114,7 +114,7 @@ class EthernetComponent : public Component { /// @brief Set arbitratry PHY registers from config. void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data); - std::string use_address_; + const char *use_address_{""}; #ifdef USE_ETHERNET_SPI uint8_t clk_pin_; uint8_t miso_pin_; diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index cb8f8569add..5e741fd2445 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -85,7 +85,7 @@ network::IPAddresses get_ip_addresses() { return {}; } -const std::string &get_use_address() { +const char *get_use_address() { // Global component pointers are guaranteed to be set by component constructors when USE_* is defined #ifdef USE_ETHERNET return ethernet::global_eth_component->get_use_address(); @@ -105,8 +105,7 @@ const std::string &get_use_address() { #if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) // Fallback when no network component is defined (e.g., host platform) - static const std::string empty; - return empty; + return ""; #endif } diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index b4a92f8bee0..3dc12232aa8 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -12,7 +12,7 @@ bool is_connected(); /// Return whether the network is disabled (only wifi for now) bool is_disabled(); /// Get the active network hostname -const std::string &get_use_address(); +const char *get_use_address(); IPAddresses get_ip_addresses(); } // namespace network diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index db909e6b1ff..d7fb1e1d42d 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -254,9 +254,9 @@ void OpenThreadComponent::on_factory_reset(std::function callback) { // set_use_address() is guaranteed to be called during component setup by Python code generation, // so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const std::string &OpenThreadComponent::get_use_address() const { return this->use_address_; } +const char *OpenThreadComponent::get_use_address() const { return this->use_address_; } -void OpenThreadComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +void OpenThreadComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } } // namespace openthread } // namespace esphome diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 19dbeb46283..d099c321f97 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -33,15 +33,15 @@ class OpenThreadComponent : public Component { void on_factory_reset(std::function callback); void defer_factory_reset_external_callback(); - const std::string &get_use_address() const; - void set_use_address(const std::string &use_address); + const char *get_use_address() const; + void set_use_address(const char *use_address); protected: std::optional get_omr_address_(InstanceLock &lock); bool teardown_started_{false}; bool teardown_complete_{false}; std::function factory_reset_external_callback_; - std::string use_address_; + const char *use_address_{""}; }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 465356db805..acc0f33e610 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -324,7 +324,7 @@ void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:\n" " Address: %s:%u", - network::get_use_address().c_str(), this->base_->get_port()); + network::get_use_address(), this->base_->get_port()); } float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; } diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index b278e5a386e..51b5756f296 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -273,8 +273,8 @@ network::IPAddress WiFiComponent::get_dns_address(int num) { } // set_use_address() is guaranteed to be called during component setup by Python code generation, // so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const std::string &WiFiComponent::get_use_address() const { return this->use_address_; } -void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +const char *WiFiComponent::get_use_address() const { return this->use_address_; } +void WiFiComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } #ifdef USE_WIFI_AP void WiFiComponent::setup_ap_config_() { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 42f78dbfac8..0c079ef1e42 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -283,8 +283,8 @@ class WiFiComponent : public Component { network::IPAddress get_dns_address(int num); network::IPAddresses get_ip_addresses(); - const std::string &get_use_address() const; - void set_use_address(const std::string &use_address); + const char *get_use_address() const; + void set_use_address(const char *use_address); const wifi_scan_vector_t &get_scan_result() const { return scan_result_; } @@ -393,7 +393,7 @@ class WiFiComponent : public Component { void wifi_scan_done_callback_(); #endif - std::string use_address_; + const char *use_address_{""}; FixedVector sta_; std::vector sta_priorities_; wifi_scan_vector_t scan_result_; From 758ac583431af02f9482de3240de2c6a806ac8e6 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:38:43 +1000 Subject: [PATCH 056/461] [psram] Require mode for S3 (#11470) Co-authored-by: clydeps --- esphome/components/esp32_camera/__init__.py | 18 ++++++--- esphome/components/inkplate/display.py | 3 +- esphome/components/psram/__init__.py | 12 ++++++ .../speaker/media_player/__init__.py | 40 +++++++++---------- tests/component_tests/psram/test_psram.py | 12 ++++-- tests/components/inkplate/test.esp32-idf.yaml | 3 ++ .../mipi_spi/test-lvgl.esp32-s3-idf.yaml | 1 + .../common/i2c_camera/esp32-idf.yaml | 2 + 8 files changed, 60 insertions(+), 31 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index d8ba098645e..d9d9bc0a56c 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -4,6 +4,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import i2c from esphome.components.esp32 import add_idf_component +from esphome.components.psram import DOMAIN as psram_domain import esphome.config_validation as cv from esphome.const import ( CONF_BRIGHTNESS, @@ -26,10 +27,9 @@ import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) +AUTO_LOAD = ["camera"] DEPENDENCIES = ["esp32"] -AUTO_LOAD = ["camera", "psram"] - esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData") @@ -163,6 +163,14 @@ CONF_ON_IMAGE = "on_image" camera_range_param = cv.int_range(min=-2, max=2) + +def validate_fb_location_(value): + validator = cv.enum(ENUM_FB_LOCATION, upper=True) + if value.lower() == psram_domain: + validator = cv.All(validator, cv.requires_component(psram_domain)) + return validator(value) + + CONFIG_SCHEMA = cv.All( cv.ENTITY_BASE_SCHEMA.extend( { @@ -236,9 +244,9 @@ CONFIG_SCHEMA = cv.All( cv.framerate, cv.Range(min=0, max=1) ), cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), - cv.Optional(CONF_FRAME_BUFFER_LOCATION, default="PSRAM"): cv.enum( - ENUM_FB_LOCATION, upper=True - ), + cv.Optional( + CONF_FRAME_BUFFER_LOCATION, default="PSRAM" + ): validate_fb_location_, cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( diff --git a/esphome/components/inkplate/display.py b/esphome/components/inkplate/display.py index a0b0265cf16..89518dcfab9 100644 --- a/esphome/components/inkplate/display.py +++ b/esphome/components/inkplate/display.py @@ -20,8 +20,7 @@ import esphome.final_validate as fv from .const import INKPLATE_10_CUSTOM_WAVEFORMS, WAVEFORMS -DEPENDENCIES = ["i2c", "esp32"] -AUTO_LOAD = ["psram"] +DEPENDENCIES = ["i2c", "esp32", "psram"] CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index 8e4f9d7eac7..df49e088793 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -1,4 +1,5 @@ import logging +import textwrap import esphome.codegen as cg from esphome.components.esp32 import ( @@ -104,6 +105,17 @@ def get_config_schema(config): if not speeds: raise cv.Invalid("PSRAM is not supported on this chip") modes = SPIRAM_MODES[variant] + if CONF_MODE not in config and len(modes) != 1: + raise ( + cv.Invalid( + textwrap.dedent( + f""" + {variant} requires PSRAM mode selection; one of {", ".join(modes)} + Selection of the wrong mode for the board will cause a runtime failure to initialise PSRAM + """ + ) + ) + ) return cv.Schema( { cv.GenerateID(): cv.declare_id(PsramComponent), diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 7537a61e4ea..e50656e723e 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -26,21 +26,12 @@ from esphome.const import ( from esphome.core import CORE, HexInt from esphome.core.entity_helpers import inherit_property_from from esphome.external_files import download_content -from esphome.types import ConfigType +from esphome.final_validate import full_config _LOGGER = logging.getLogger(__name__) -def AUTO_LOAD(config: ConfigType) -> list[str]: - load = ["audio"] - if ( - not config - or config.get(CONF_TASK_STACK_IN_PSRAM) - or config.get(CONF_CODEC_SUPPORT_ENABLED) - ): - return load + ["psram"] - return load - +AUTO_LOAD = ["audio"] CODEOWNERS = ["@kahrendt", "@synesthesiam"] DOMAIN = "media_player" @@ -226,12 +217,19 @@ def _validate_repeated_speaker(config): return config -def _validate_supported_local_file(config): +def _final_validate(config): + # Default to using codec if psram is enabled + if (use_codec := config.get(CONF_CODEC_SUPPORT_ENABLED)) is None: + use_codec = psram.DOMAIN in full_config.get() + conf_id = config[CONF_ID].id + core_data = CORE.data.setdefault(DOMAIN, {conf_id: {}}) + core_data[conf_id][CONF_CODEC_SUPPORT_ENABLED] = use_codec + for file_config in config.get(CONF_FILES, []): _, media_file_type = _read_audio_file_and_type(file_config) if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]): raise cv.Invalid("Unsupported local media file") - if not config[CONF_CODEC_SUPPORT_ENABLED] and str(media_file_type) != str( + if not use_codec and str(media_file_type) != str( audio.AUDIO_FILE_TYPE_ENUM["WAV"] ): # Only wav files are supported @@ -290,11 +288,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( min=4000, max=4000000 ), - cv.Optional( - CONF_CODEC_SUPPORT_ENABLED, default=psram.supported() - ): cv.boolean, + cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.boolean, cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), - cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean, + cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( + cv.boolean, cv.requires_component(psram.DOMAIN) + ), cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage, cv.Optional(CONF_VOLUME_INITIAL, default=0.5): cv.percentage, cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage, @@ -317,12 +315,12 @@ FINAL_VALIDATE_SCHEMA = cv.All( }, extra=cv.ALLOW_EXTRA, ), - _validate_supported_local_file, + _final_validate, ) async def to_code(config): - if config[CONF_CODEC_SUPPORT_ENABLED]: + if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]: # Compile all supported audio codecs and optimize the wifi settings cg.add_define("USE_AUDIO_FLAC_SUPPORT", True) @@ -352,8 +350,8 @@ async def to_code(config): cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) - cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM])) - if config[CONF_TASK_STACK_IN_PSRAM]: + if config.get(CONF_TASK_STACK_IN_PSRAM): + cg.add(var.set_task_stack_in_psram(True)) esp32.add_idf_sdkconfig_option( "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True ) diff --git a/tests/component_tests/psram/test_psram.py b/tests/component_tests/psram/test_psram.py index 3e40a8d1920..f8ad013689b 100644 --- a/tests/component_tests/psram/test_psram.py +++ b/tests/component_tests/psram/test_psram.py @@ -34,6 +34,12 @@ SUPPORTED_PSRAM_VARIANTS = [ VARIANT_ESP32S3, VARIANT_ESP32P4, ] +SUPPORTED_PSRAM_MODES = { + VARIANT_ESP32: ["quad"], + VARIANT_ESP32S2: ["quad"], + VARIANT_ESP32S3: ["quad", "octal"], + VARIANT_ESP32P4: ["hex"], +} @pytest.mark.parametrize( @@ -86,7 +92,7 @@ def test_psram_configuration_valid_supported_variants( from esphome.components.psram import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA # This should not raise an exception - config = CONFIG_SCHEMA({}) + config = CONFIG_SCHEMA({"mode": SUPPORTED_PSRAM_MODES[variant][0]}) FINAL_VALIDATE_SCHEMA(config) @@ -122,7 +128,7 @@ def _setup_psram_final_validation_test( ("config", "esp32_config", "expect_error", "error_match"), [ pytest.param( - {"speed": "120MHz"}, + {"mode": "quad", "speed": "120MHz"}, {"cpu_frequency": "160MHz"}, True, r"PSRAM 120MHz requires 240MHz CPU frequency", @@ -143,7 +149,7 @@ def _setup_psram_final_validation_test( id="ecc_only_in_octal_mode", ), pytest.param( - {"speed": "120MHZ"}, + {"mode": "quad", "speed": "120MHZ"}, {"cpu_frequency": "240MHZ"}, False, None, diff --git a/tests/components/inkplate/test.esp32-idf.yaml b/tests/components/inkplate/test.esp32-idf.yaml index b47e39c3898..17e58ce3902 100644 --- a/tests/components/inkplate/test.esp32-idf.yaml +++ b/tests/components/inkplate/test.esp32-idf.yaml @@ -1,4 +1,7 @@ packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml +psram: + mode: quad + <<: !include common.yaml diff --git a/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml index 48f34f34499..14f864d3261 100644 --- a/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml +++ b/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml @@ -9,3 +9,4 @@ display: lvgl: psram: + mode: quad diff --git a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml index a6e7c264cbe..443ebbebd9f 100644 --- a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml +++ b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml @@ -1,4 +1,6 @@ # I2C bus for camera sensor +psram: + i2c: - id: i2c_camera_bus sda: 25 From 0b04361fc0be415a28bf5ffb9c223bf2a262f5b0 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:39:27 +1000 Subject: [PATCH 057/461] [lvgl] Layout improvements (#10149) Co-authored-by: clydeps Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/lvgl/__init__.py | 25 +- esphome/components/lvgl/defines.py | 3 + esphome/components/lvgl/layout.py | 357 +++++++++++++++++++ esphome/components/lvgl/lv_validation.py | 3 + esphome/components/lvgl/lvcode.py | 5 +- esphome/components/lvgl/schemas.py | 259 ++++---------- esphome/components/lvgl/types.py | 31 +- esphome/components/lvgl/widgets/__init__.py | 7 +- esphome/components/lvgl/widgets/canvas.py | 60 ++-- esphome/components/lvgl/widgets/checkbox.py | 9 +- esphome/components/lvgl/widgets/container.py | 39 ++ esphome/components/lvgl/widgets/label.py | 11 +- esphome/components/lvgl/widgets/qrcode.py | 13 +- esphome/components/lvgl/widgets/textarea.py | 17 +- tests/components/lvgl/lvgl-package.yaml | 22 +- 15 files changed, 572 insertions(+), 289 deletions(-) create mode 100644 esphome/components/lvgl/layout.py create mode 100644 esphome/components/lvgl/widgets/container.py diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index aa6935c5fc3..861999d0b74 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -41,10 +41,7 @@ from .lv_validation import lv_bool, lv_images_used from .lvcode import LvContext, LvglComponent, lvgl_static from .schemas import ( DISP_BG_SCHEMA, - FLEX_OBJ_SCHEMA, FULL_STYLE_SCHEMA, - GRID_CELL_SCHEMA, - LAYOUT_SCHEMAS, WIDGET_TYPES, any_widget_schema, container_schema, @@ -78,6 +75,7 @@ from .widgets.button import button_spec from .widgets.buttonmatrix import buttonmatrix_spec from .widgets.canvas import canvas_spec from .widgets.checkbox import checkbox_spec +from .widgets.container import container_spec from .widgets.dropdown import dropdown_spec from .widgets.img import img_spec from .widgets.keyboard import keyboard_spec @@ -130,20 +128,10 @@ for w_type in ( tileview_spec, qr_code_spec, canvas_spec, + container_spec, ): WIDGET_TYPES[w_type.name] = w_type -WIDGET_SCHEMA = any_widget_schema() - -LAYOUT_SCHEMAS[df.TYPE_GRID] = { - cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(GRID_CELL_SCHEMA)) -} -LAYOUT_SCHEMAS[df.TYPE_FLEX] = { - cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(FLEX_OBJ_SCHEMA)) -} -LAYOUT_SCHEMAS[df.TYPE_NONE] = { - cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema()) -} for w_type in WIDGET_TYPES.values(): register_action( f"lvgl.{w_type.name}.update", @@ -410,7 +398,7 @@ def display_schema(config): def add_hello_world(config): if df.CONF_WIDGETS not in config and CONF_PAGES not in config: LOGGER.info("No pages or widgets configured, creating default hello_world page") - config[df.CONF_WIDGETS] = cv.ensure_list(WIDGET_SCHEMA)(get_hello_world()) + config[df.CONF_WIDGETS] = any_widget_schema()(get_hello_world()) return config @@ -450,6 +438,7 @@ LVGL_SCHEMA = cv.All( ), } ), + cv.Optional(CONF_PAGES): cv.ensure_list(container_schema(page_spec)), **{ cv.Optional(x): validate_automation( { @@ -459,12 +448,6 @@ LVGL_SCHEMA = cv.All( ) for x in SIMPLE_TRIGGERS }, - cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list( - WIDGET_SCHEMA - ), - cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list( - container_schema(page_spec) - ), cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA), cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 3241ba9c3f8..f2bcb6cc061 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -394,6 +394,8 @@ LV_FLEX_ALIGNMENTS = LvConstant( "SPACE_BETWEEN", ) +LV_FLEX_CROSS_ALIGNMENTS = LV_FLEX_ALIGNMENTS.extend("STRETCH") + LV_MENU_MODES = LvConstant( "LV_MENU_HEADER_", "TOP_FIXED", @@ -436,6 +438,7 @@ CONF_BUTTONS = "buttons" CONF_BYTE_ORDER = "byte_order" CONF_CHANGE_RATE = "change_rate" CONF_CLOSE_BUTTON = "close_button" +CONF_CONTAINER = "container" CONF_CONTROL = "control" CONF_DEFAULT_FONT = "default_font" CONF_DEFAULT_GROUP = "default_group" diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py new file mode 100644 index 00000000000..0aed525e169 --- /dev/null +++ b/esphome/components/lvgl/layout.py @@ -0,0 +1,357 @@ +import re + +import esphome.config_validation as cv +from esphome.const import CONF_HEIGHT, CONF_TYPE, CONF_WIDTH + +from .defines import ( + CONF_FLEX_ALIGN_CROSS, + CONF_FLEX_ALIGN_MAIN, + CONF_FLEX_ALIGN_TRACK, + CONF_FLEX_FLOW, + CONF_FLEX_GROW, + CONF_GRID_CELL_COLUMN_POS, + CONF_GRID_CELL_COLUMN_SPAN, + CONF_GRID_CELL_ROW_POS, + CONF_GRID_CELL_ROW_SPAN, + CONF_GRID_CELL_X_ALIGN, + CONF_GRID_CELL_Y_ALIGN, + CONF_GRID_COLUMN_ALIGN, + CONF_GRID_COLUMNS, + CONF_GRID_ROW_ALIGN, + CONF_GRID_ROWS, + CONF_LAYOUT, + CONF_PAD_COLUMN, + CONF_PAD_ROW, + CONF_WIDGETS, + FLEX_FLOWS, + LV_CELL_ALIGNMENTS, + LV_FLEX_ALIGNMENTS, + LV_FLEX_CROSS_ALIGNMENTS, + LV_GRID_ALIGNMENTS, + TYPE_FLEX, + TYPE_GRID, + TYPE_NONE, + LvConstant, +) +from .lv_validation import padding, size + +cell_alignments = LV_CELL_ALIGNMENTS.one_of +grid_alignments = LV_GRID_ALIGNMENTS.one_of +flex_alignments = LV_FLEX_ALIGNMENTS.one_of + +FLEX_LAYOUT_SCHEMA = { + cv.Required(CONF_TYPE): cv.one_of(TYPE_FLEX, lower=True), + cv.Optional(CONF_FLEX_FLOW, default="row_wrap"): FLEX_FLOWS.one_of, + cv.Optional(CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, + cv.Optional( + CONF_FLEX_ALIGN_CROSS, default="start" + ): LV_FLEX_CROSS_ALIGNMENTS.one_of, + cv.Optional(CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, + cv.Optional(CONF_PAD_ROW): padding, + cv.Optional(CONF_PAD_COLUMN): padding, + cv.Optional(CONF_FLEX_GROW): cv.int_, +} + +FLEX_HV_STYLE = { + CONF_FLEX_ALIGN_MAIN: "LV_FLEX_ALIGN_SPACE_EVENLY", + CONF_FLEX_ALIGN_TRACK: "LV_FLEX_ALIGN_CENTER", + CONF_FLEX_ALIGN_CROSS: "LV_FLEX_ALIGN_CENTER", + CONF_TYPE: TYPE_FLEX, +} + +FLEX_OBJ_SCHEMA = { + cv.Optional(CONF_FLEX_GROW): cv.int_, +} + + +def flex_hv_schema(dir): + dir = CONF_HEIGHT if dir == "horizontal" else CONF_WIDTH + return { + cv.Optional(CONF_FLEX_GROW, default=1): cv.int_, + cv.Optional(dir, default="100%"): size, + } + + +def grid_free_space(value): + value = cv.Upper(value) + if value.startswith("FR(") and value.endswith(")"): + value = value.removesuffix(")").removeprefix("FR(") + return f"LV_GRID_FR({cv.positive_int(value)})" + raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)") + + +grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space) + +GRID_CELL_SCHEMA = { + cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments, +} + + +class Layout: + """ + Define properties for a layout + The base class is layout "none" + """ + + def get_type(self): + return TYPE_NONE + + def get_layout_schemas(self, config: dict) -> tuple: + """ + Get the layout and child schema for a given widget based on its layout type. + """ + return None, {} + + def validate(self, config): + """ + Validate the layout configuration. This is called late in the schema validation + :param config: The input configuration + :return: The validated configuration + """ + return config + + +class FlexLayout(Layout): + def get_type(self): + return TYPE_FLEX + + def get_layout_schemas(self, config: dict) -> tuple: + layout = config.get(CONF_LAYOUT) + if not isinstance(layout, dict) or layout.get(CONF_TYPE) != TYPE_FLEX: + return None, {} + child_schema = FLEX_OBJ_SCHEMA + if grow := layout.get(CONF_FLEX_GROW): + child_schema = {cv.Optional(CONF_FLEX_GROW, default=grow): cv.int_} + # Polyfill to implement stretch alignment for flex containers + # LVGL does not support this natively, so we add a 100% size property to the children in the cross-axis + if layout.get(CONF_FLEX_ALIGN_CROSS) == "LV_FLEX_ALIGN_STRETCH": + dimension = ( + CONF_WIDTH + if "COLUMN" in layout[CONF_FLEX_FLOW].upper() + else CONF_HEIGHT + ) + child_schema[cv.Optional(dimension, default="100%")] = size + return FLEX_LAYOUT_SCHEMA, child_schema + + def validate(self, config): + """ + Perform validation on the container and its children for this layout + :param config: + :return: + """ + return config + + +class DirectionalLayout(FlexLayout): + def __init__(self, direction: str, flow): + """ + :param direction: "horizontal" or "vertical" + :param flow: "row" or "column" + """ + super().__init__() + self.direction = direction + self.flow = flow + + def get_type(self): + return self.direction + + def get_layout_schemas(self, config: dict) -> tuple: + if config.get(CONF_LAYOUT, "").lower() != self.direction: + return None, {} + return cv.one_of(self.direction, lower=True), flex_hv_schema(self.direction) + + def validate(self, config): + assert config[CONF_LAYOUT].lower() == self.direction + config[CONF_LAYOUT] = { + **FLEX_HV_STYLE, + CONF_FLEX_FLOW: "LV_FLEX_FLOW_" + self.flow.upper(), + } + return config + + +class GridLayout(Layout): + _GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)\s*x\s*(\d+)\s*$") + + def get_type(self): + return TYPE_GRID + + def get_layout_schemas(self, config: dict) -> tuple: + layout = config.get(CONF_LAYOUT) + if isinstance(layout, str): + if GridLayout._GRID_LAYOUT_REGEX.match(layout): + return ( + cv.string, + { + cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional( + CONF_GRID_CELL_ROW_SPAN, default=1 + ): cv.positive_int, + cv.Optional( + CONF_GRID_CELL_COLUMN_SPAN, default=1 + ): cv.positive_int, + cv.Optional( + CONF_GRID_CELL_X_ALIGN, default="center" + ): grid_alignments, + cv.Optional( + CONF_GRID_CELL_Y_ALIGN, default="center" + ): grid_alignments, + }, + ) + # Not a valid grid layout string + return None, {} + + if not isinstance(layout, dict) or layout.get(CONF_TYPE) != TYPE_GRID: + return None, {} + return ( + { + cv.Required(CONF_TYPE): cv.one_of(TYPE_GRID, lower=True), + cv.Required(CONF_GRID_ROWS): [grid_spec], + cv.Required(CONF_GRID_COLUMNS): [grid_spec], + cv.Optional(CONF_GRID_COLUMN_ALIGN): grid_alignments, + cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments, + cv.Optional(CONF_PAD_ROW): padding, + cv.Optional(CONF_PAD_COLUMN): padding, + }, + { + cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments, + }, + ) + + def validate(self, config: dict): + """ + Validate the grid layout. + The `layout:` key may be a dictionary with `rows` and `columns` keys, or a string in the format "rows x columns". + Either all cells must have a row and column, + or none, in which case the grid layout is auto-generated. + :param config: + :return: The config updated with auto-generated values + """ + layout = config.get(CONF_LAYOUT) + if isinstance(layout, str): + # If the layout is a string, assume it is in the format "rows x columns", implying + # a grid layout with the specified number of rows and columns each with CONTENT sizing. + layout = layout.strip() + match = GridLayout._GRID_LAYOUT_REGEX.match(layout) + if match: + rows = int(match.group(1)) + cols = int(match.group(2)) + layout = { + CONF_TYPE: TYPE_GRID, + CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows, + CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols, + } + config[CONF_LAYOUT] = layout + else: + raise cv.Invalid( + f"Invalid grid layout format: {config}, expected 'rows x columns'", + [CONF_LAYOUT], + ) + # should be guaranteed to be a dict at this point + assert isinstance(layout, dict) + assert layout.get(CONF_TYPE) == TYPE_GRID + rows = len(layout[CONF_GRID_ROWS]) + columns = len(layout[CONF_GRID_COLUMNS]) + used_cells = [[None] * columns for _ in range(rows)] + for index, widget in enumerate(config.get(CONF_WIDGETS, [])): + _, w = next(iter(widget.items())) + if (CONF_GRID_CELL_COLUMN_POS in w) != (CONF_GRID_CELL_ROW_POS in w): + raise cv.Invalid( + "Both row and column positions must be specified, or both omitted", + [CONF_WIDGETS, index], + ) + if CONF_GRID_CELL_ROW_POS in w: + row = w[CONF_GRID_CELL_ROW_POS] + column = w[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: + raise cv.Invalid( + "No free cells available in grid layout", [CONF_WIDGETS, index] + ) from None + w[CONF_GRID_CELL_ROW_POS] = row + w[CONF_GRID_CELL_COLUMN_POS] = column + + for i in range(w[CONF_GRID_CELL_ROW_SPAN]): + for j in range(w[CONF_GRID_CELL_COLUMN_SPAN]): + if row + i >= rows or column + j >= columns: + raise cv.Invalid( + f"Cell at {row}/{column} span {w[CONF_GRID_CELL_ROW_SPAN]}x{w[CONF_GRID_CELL_COLUMN_SPAN]} " + f"exceeds grid size {rows}x{columns}", + [CONF_WIDGETS, index], + ) + if used_cells[row + i][column + j] is not None: + raise cv.Invalid( + f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", + [CONF_WIDGETS, index], + ) + used_cells[row + i][column + j] = index + + return config + + +LAYOUT_CLASSES = ( + FlexLayout(), + GridLayout(), + DirectionalLayout("horizontal", "row"), + DirectionalLayout("vertical", "column"), +) +LAYOUT_CHOICES = [x.get_type() for x in LAYOUT_CLASSES] + + +def append_layout_schema(schema, config: dict): + """ + Get the child layout schema for a given widget based on its layout type. + :param config: The config to check + :return: A schema for the layout including a widgets key + """ + # Local import to avoid circular dependencies + if CONF_WIDGETS not in config: + if CONF_LAYOUT in config: + raise cv.Invalid( + f"Layout {config[CONF_LAYOUT]} requires a {CONF_WIDGETS} key", + [CONF_LAYOUT], + ) + return schema + + from .schemas import any_widget_schema + + if CONF_LAYOUT not in config: + # If no layout is specified, return the schema as is + return schema.extend({cv.Optional(CONF_WIDGETS): any_widget_schema()}) + + for layout_class in LAYOUT_CLASSES: + layout_schema, child_schema = layout_class.get_layout_schemas(config) + if layout_schema: + layout_schema = cv.Schema( + { + cv.Required(CONF_LAYOUT): layout_schema, + cv.Required(CONF_WIDGETS): any_widget_schema(child_schema), + } + ) + layout_schema.add_extra(layout_class.validate) + return layout_schema.extend(schema) + + # If no layout class matched, return a default schema + return cv.Schema( + { + cv.Optional(CONF_LAYOUT): cv.one_of(*LAYOUT_CHOICES, lower=True), + cv.Optional(CONF_WIDGETS): any_widget_schema(), + } + ) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 6f95a32a186..9fe72128cee 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,3 +1,4 @@ +import re from typing import TYPE_CHECKING, Any import esphome.codegen as cg @@ -246,6 +247,8 @@ def pixels_or_percent_validator(value): return ["pixels", "..%"] if isinstance(value, str) and value.lower().endswith("px"): value = cv.int_(value[:-2]) + if isinstance(value, str) and re.match(r"^lv_pct\((\d+)\)$", value): + return value value = cv.Any(cv.int_, cv.percentage)(value) if isinstance(value, int): return value diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index ea38845c078..c11597131f1 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -299,6 +299,7 @@ class LvExpr(MockLv): # Top level mock for generic lv_ calls to be recorded lv = MockLv("lv_") +LV = MockLv("LV_") # Just generate an expression lv_expr = LvExpr("lv_") # Mock for lv_obj_ calls @@ -327,7 +328,7 @@ def lv_assign(target, expression): lv_add(AssignmentExpression("", "", target, expression)) -def lv_Pvariable(type, name): +def lv_Pvariable(type, name) -> MockObj: """ Create but do not initialise a pointer variable :param type: Type of the variable target @@ -343,7 +344,7 @@ def lv_Pvariable(type, name): return var -def lv_variable(type, name): +def lv_variable(type, name) -> MockObj: """ Create but do not initialise a variable :param type: Type of the variable target diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 959d203c414..dd248d0b947 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -12,17 +12,21 @@ from esphome.const import ( CONF_TEXT, CONF_TIME, CONF_TRIGGER_ID, - CONF_TYPE, CONF_X, CONF_Y, ) from esphome.core import TimePeriod 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, TYPE_GRID -from .helpers import add_lv_use, requires_component, validate_printf +from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR +from .helpers import requires_component, validate_printf +from .layout import ( + FLEX_OBJ_SCHEMA, + GRID_CELL_SCHEMA, + append_layout_schema, + grid_alignments, +) from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity from .lvcode import LvglComponent, lv_event_t_ptr from .types import ( @@ -72,11 +76,9 @@ def _validate_text(value): # A schema for text properties -TEXT_SCHEMA = cv.Schema( - { - cv.Optional(CONF_TEXT): _validate_text, - } -) +TEXT_SCHEMA = { + cv.Optional(CONF_TEXT): _validate_text, +} LIST_ACTION_SCHEMA = cv.ensure_list( cv.maybe_simple_value( @@ -136,7 +138,7 @@ STYLE_PROPS = { "arc_opa": lvalid.opacity, "arc_color": lvalid.lv_color, "arc_rounded": lvalid.lv_bool, - "arc_width": lvalid.lv_positive_int, + "arc_width": lvalid.pixels, "anim_time": lvalid.lv_milliseconds, "bg_color": lvalid.lv_color, "bg_grad": lv_gradient, @@ -223,10 +225,6 @@ STYLE_REMAP = { "image_recolor_opa": "img_recolor_opa", } -cell_alignments = df.LV_CELL_ALIGNMENTS.one_of -grid_alignments = df.LV_GRID_ALIGNMENTS.one_of -flex_alignments = df.LV_FLEX_ALIGNMENTS.one_of - # Complete object style schema STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( { @@ -266,10 +264,8 @@ def part_schema(parts): :param parts: The parts to include :return: The schema """ - return ( - cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}) - .extend(STATE_SCHEMA) - .extend(FLAG_SCHEMA) + return STATE_SCHEMA.extend(FLAG_SCHEMA).extend( + {cv.Optional(part): STATE_SCHEMA for part in parts} ) @@ -277,10 +273,10 @@ def automation_schema(typ: LvType): events = df.LV_EVENT_TRIGGERS + df.SWIPE_TRIGGERS if typ.has_on_value: events = events + (CONF_ON_VALUE,) - args = typ.get_arg_type() if isinstance(typ, LvType) else [] + args = typ.get_arg_type() args.append(lv_event_t_ptr) - return cv.Schema( - { + return { + **{ cv.Optional(event): validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -289,14 +285,11 @@ def automation_schema(typ: LvType): } ) for event in events - } - ).extend( - { - cv.Optional(CONF_ON_BOOT): validate_automation( - {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger)} - ) - } - ) + }, + cv.Optional(CONF_ON_BOOT): validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger)} + ), + } def base_update_schema(widget_type, parts): @@ -335,75 +328,17 @@ def obj_schema(widget_type: WidgetType): """ return ( part_schema(widget_type.parts) - .extend(LAYOUT_SCHEMA) .extend(ALIGN_TO_SCHEMA) .extend(automation_schema(widget_type.w_type)) .extend( - cv.Schema( - { - cv.Optional(CONF_STATE): SET_STATE_SCHEMA, - cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), - } - ) + { + cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), + } ) ) -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( { @@ -416,57 +351,6 @@ ALIGN_TO_SCHEMA = { } -def grid_free_space(value): - value = cv.Upper(value) - if value.startswith("FR(") and value.endswith(")"): - value = value.removesuffix(")").removeprefix("FR(") - return f"LV_GRID_FR({cv.positive_int(value)})" - raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)") - - -grid_spec = cv.Any( - lvalid.size, df.LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space -) - -LAYOUT_SCHEMA = { - cv.Optional(df.CONF_LAYOUT): cv.typed_schema( - { - df.TYPE_GRID: { - cv.Required(df.CONF_GRID_ROWS): [grid_spec], - cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], - cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, - cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, - cv.Optional(df.CONF_PAD_ROW): lvalid.padding, - cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding, - }, - df.TYPE_FLEX: { - cv.Optional( - df.CONF_FLEX_FLOW, default="row_wrap" - ): df.FLEX_FLOWS.one_of, - cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, - cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, - cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, - cv.Optional(df.CONF_PAD_ROW): lvalid.padding, - cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding, - }, - }, - lower=True, - ) -} - -GRID_CELL_SCHEMA = { - 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, - cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments, -} - -FLEX_OBJ_SCHEMA = { - cv.Optional(df.CONF_FLEX_GROW): cv.int_, -} - DISP_BG_SCHEMA = cv.Schema( { cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any( @@ -498,48 +382,11 @@ ALL_STYLES = { } -def container_validator(schema, widget_type: WidgetType): - """ - Create a validator for a container given the widget type - :param schema: Base schema to extend - :param widget_type: - :return: - """ - - def validator(value): - if w_sch := widget_type.schema: - 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): - raise cv.Invalid("Layout value must be a dict") - ltype = layout.get(CONF_TYPE) - if not ltype: - raise (cv.Invalid("Layout schema requires type:")) - add_lv_use(ltype) - if value == SCHEMA_EXTRACT: - return result - result = result.extend( - LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE]) - ) - value = result(value) - if layout_validator := LAYOUT_VALIDATORS.get(ltype): - value = layout_validator(value) - return value - - return validator - - def container_schema(widget_type: WidgetType, extras=None): """ Create a schema for a container widget of a given type. All obj properties are available, plus the extras passed in, plus any defined for the specific widget being specified. - :param widget_type: The widget type, e.g. "img" + :param widget_type: The widget type, e.g. "image" :param extras: Additional options to be made available, e.g. layout properties for children :return: The schema for this type of widget. """ @@ -549,31 +396,49 @@ def container_schema(widget_type: WidgetType, extras=None): if extras: schema = schema.extend(extras) # Delayed evaluation for recursion - return container_validator(schema, widget_type) + schema = schema.extend(widget_type.schema) -def widget_schema(widget_type: WidgetType, extras=None): - """ - Create a schema for a given widget type - :param widget_type: The name of the widget - :param extras: - :return: - """ - validator = container_schema(widget_type, extras=extras) - if required := widget_type.required_component: - validator = cv.All(validator, requires_component(required)) - return cv.Exclusive(widget_type.name, df.CONF_WIDGETS), validator + def validator(value): + return append_layout_schema(schema, value)(value) - -# All widget schemas must be defined before this is called. + return validator def any_widget_schema(extras=None): """ - Generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of + Dynamically generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of widget under the widgets: key. :param extras: Additional schema to be applied to each generated one - :return: + :return: A validator for the Widgets key """ - return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_TYPES.values())) + + def validator(value): + if isinstance(value, dict): + # Convert to list + value = [{k: v} for k, v in value.items()] + if not isinstance(value, list): + raise cv.Invalid("Expected a list of widgets") + result = [] + for index, entry in enumerate(value): + if not isinstance(entry, dict) or len(entry) != 1: + raise cv.Invalid( + "Each widget must be a dictionary with a single key", path=[index] + ) + [(key, value)] = entry.items() + # Validate the widget against its schema + widget_type = WIDGET_TYPES.get(key) + if not widget_type: + raise cv.Invalid(f"Unknown widget type: {key}", path=[index]) + container_validator = container_schema(widget_type, extras=extras) + if required := widget_type.required_component: + container_validator = cv.All( + container_validator, requires_component(required) + ) + # Apply custom validation + value = widget_type.validate(value or {}) + result.append({key: container_validator(value)}) + return result + + return validator diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 9955b530aa8..8c33e139344 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,6 +1,7 @@ import sys from esphome import automation, codegen as cg +from esphome.config_validation import Schema from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE from esphome.cpp_generator import MockObj, MockObjClass from esphome.cpp_types import esphome_ns @@ -135,14 +136,14 @@ class WidgetType: self.lv_name = lv_name or name self.w_type = w_type self.parts = parts - if schema is None: - self.schema = {} - else: - self.schema = schema + if not isinstance(schema, Schema): + schema = Schema(schema or {}) + self.schema = schema if modify_schema is None: - self.modify_schema = self.schema - else: - self.modify_schema = modify_schema + modify_schema = schema + if not isinstance(modify_schema, Schema): + modify_schema = Schema(modify_schema) + self.modify_schema = modify_schema self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") @property @@ -163,7 +164,6 @@ class WidgetType: :param config: Its configuration :return: Generated code as a list of text lines """ - return [] async def obj_creator(self, parent: MockObjClass, config: dict): """ @@ -174,6 +174,13 @@ class WidgetType: """ return lv_expr.call(f"{self.lv_name}_create", parent) + def on_create(self, var: MockObj, config: dict): + """ + Called from to_code when the widget is created, to set up any initial properties + :param var: The variable representing the widget + :param config: Its configuration + """ + def get_uses(self): """ Get a list of other widgets used by this one @@ -193,6 +200,14 @@ class WidgetType: def get_scale(self, config: dict): return 1.0 + def validate(self, value): + """ + Provides an opportunity for custom validation for a given widget type + :param value: + :return: + """ + return value + class NumberType(WidgetType): def get_max(self, config: dict): diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 1f9cdde0a06..7d9f9cb7dec 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -339,7 +339,10 @@ async def set_obj_properties(w: Widget, config): if layout_type == TYPE_FLEX: lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW])) main = literal(layout[CONF_FLEX_ALIGN_MAIN]) - cross = literal(layout[CONF_FLEX_ALIGN_CROSS]) + cross = layout[CONF_FLEX_ALIGN_CROSS] + if cross == "LV_FLEX_ALIGN_STRETCH": + cross = "LV_FLEX_ALIGN_CENTER" + cross = literal(cross) track = literal(layout[CONF_FLEX_ALIGN_TRACK]) lv_obj.set_flex_align(w.obj, main, cross, track) parts = collect_parts(config) @@ -446,9 +449,11 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): if spec.is_compound(): var = cg.new_Pvariable(wid) lv_add(var.set_obj(creator)) + spec.on_create(var.obj, w_cnfig) else: var = lv_Pvariable(lv_obj_t, wid) lv_assign(var, creator) + spec.on_create(var, w_cnfig) w = Widget.create(wid, var, spec, w_cnfig) if theme := theme_widget_map.get(w_type): diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index f0a9cd35bac..ead352aa77d 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -159,18 +159,15 @@ async def canvas_set_pixel(config, action_id, template_arg, args): ) -DRAW_SCHEMA = cv.Schema( - { - cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), - cv.Required(CONF_X): pixels, - cv.Required(CONF_Y): pixels, - } -) -DRAW_OPA_SCHEMA = DRAW_SCHEMA.extend( - { - cv.Optional(CONF_OPA): opacity, - } -) +DRAW_SCHEMA = { + cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), + cv.Required(CONF_X): pixels, + cv.Required(CONF_Y): pixels, +} +DRAW_OPA_SCHEMA = { + **DRAW_SCHEMA, + cv.Optional(CONF_OPA): opacity, +} async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg, args): @@ -224,12 +221,14 @@ RECT_PROPS = { @automation.register_action( "lvgl.canvas.draw_rectangle", ObjUpdateAction, - DRAW_SCHEMA.extend( + cv.Schema( { + **DRAW_OPA_SCHEMA, cv.Required(CONF_WIDTH): cv.templatable(cv.int_), cv.Required(CONF_HEIGHT): cv.templatable(cv.int_), - }, - ).extend({cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}), + **{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}, + } + ), ) async def canvas_draw_rect(config, action_id, template_arg, args): width = await pixels.process(config[CONF_WIDTH]) @@ -261,13 +260,14 @@ TEXT_PROPS = { @automation.register_action( "lvgl.canvas.draw_text", ObjUpdateAction, - TEXT_SCHEMA.extend(DRAW_OPA_SCHEMA) - .extend( + cv.Schema( { + **TEXT_SCHEMA, + **DRAW_OPA_SCHEMA, cv.Required(CONF_MAX_WIDTH): cv.templatable(cv.int_), + **{cv.Optional(prop): STYLE_PROPS[f"text_{prop}"] for prop in TEXT_PROPS}, }, - ) - .extend({cv.Optional(prop): STYLE_PROPS[f"text_{prop}"] for prop in TEXT_PROPS}), + ), ) async def canvas_draw_text(config, action_id, template_arg, args): text = await lv_text.process(config[CONF_TEXT]) @@ -293,13 +293,15 @@ IMG_PROPS = { @automation.register_action( "lvgl.canvas.draw_image", ObjUpdateAction, - DRAW_OPA_SCHEMA.extend( + cv.Schema( { + **DRAW_OPA_SCHEMA, cv.Required(CONF_SRC): lv_image, cv.Optional(CONF_PIVOT_X, default=0): pixels, cv.Optional(CONF_PIVOT_Y, default=0): pixels, - }, - ).extend({cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}), + **{cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}, + } + ), ) async def canvas_draw_image(config, action_id, template_arg, args): src = await lv_image.process(config[CONF_SRC]) @@ -336,8 +338,9 @@ LINE_PROPS = { cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), cv.Optional(CONF_OPA): opacity, cv.Required(CONF_POINTS): cv.ensure_list(point_schema), - }, - ).extend({cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()}), + **{cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()}, + } + ), ) async def canvas_draw_line(config, action_id, template_arg, args): points = [ @@ -363,8 +366,9 @@ async def canvas_draw_line(config, action_id, template_arg, args): { cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), cv.Required(CONF_POINTS): cv.ensure_list(point_schema), + **{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}, }, - ).extend({cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}), + ), ) async def canvas_draw_polygon(config, action_id, template_arg, args): points = [ @@ -395,13 +399,15 @@ ARC_PROPS = { @automation.register_action( "lvgl.canvas.draw_arc", ObjUpdateAction, - DRAW_OPA_SCHEMA.extend( + cv.Schema( { + **DRAW_OPA_SCHEMA, cv.Required(CONF_RADIUS): pixels, cv.Required(CONF_START_ANGLE): lv_angle_degrees, cv.Required(CONF_END_ANGLE): lv_angle_degrees, + **{cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}, } - ).extend({cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}), + ), ) async def canvas_draw_arc(config, action_id, template_arg, args): radius = await size.process(config[CONF_RADIUS]) diff --git a/esphome/components/lvgl/widgets/checkbox.py b/esphome/components/lvgl/widgets/checkbox.py index c344fbfe75f..ca97e2d8432 100644 --- a/esphome/components/lvgl/widgets/checkbox.py +++ b/esphome/components/lvgl/widgets/checkbox.py @@ -17,11 +17,10 @@ class CheckboxType(WidgetType): CONF_CHECKBOX, LvBoolean("lv_checkbox_t"), (CONF_MAIN, CONF_INDICATOR), - TEXT_SCHEMA.extend( - { - Optional(CONF_PAD_COLUMN): padding, - } - ), + { + **TEXT_SCHEMA, + Optional(CONF_PAD_COLUMN): padding, + }, ) async def to_code(self, w: Widget, config): diff --git a/esphome/components/lvgl/widgets/container.py b/esphome/components/lvgl/widgets/container.py new file mode 100644 index 00000000000..2ac1a3b244b --- /dev/null +++ b/esphome/components/lvgl/widgets/container.py @@ -0,0 +1,39 @@ +import esphome.config_validation as cv +from esphome.const import CONF_HEIGHT, CONF_WIDTH +from esphome.cpp_generator import MockObj + +from ..defines import CONF_CONTAINER, CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR +from ..lv_validation import size +from ..lvcode import lv +from ..types import WidgetType, lv_obj_t + +CONTAINER_SCHEMA = cv.Schema( + { + cv.Optional(CONF_HEIGHT, default="100%"): size, + cv.Optional(CONF_WIDTH, default="100%"): size, + } +) + + +class ContainerType(WidgetType): + """ + A simple container widget that can hold other widgets and which defaults to a 100% size. + Made from an obj with all styles removed + """ + + def __init__(self): + super().__init__( + CONF_CONTAINER, + lv_obj_t, + (CONF_MAIN, CONF_SCROLLBAR), + schema=CONTAINER_SCHEMA, + modify_schema={}, + lv_name=CONF_OBJ, + ) + self.styles = {} + + def on_create(self, var: MockObj, config: dict): + lv.obj_remove_style_all(var) + + +container_spec = ContainerType() diff --git a/esphome/components/lvgl/widgets/label.py b/esphome/components/lvgl/widgets/label.py index 6b042356749..3a3a9977374 100644 --- a/esphome/components/lvgl/widgets/label.py +++ b/esphome/components/lvgl/widgets/label.py @@ -23,12 +23,11 @@ class LabelType(WidgetType): CONF_LABEL, LvText("lv_label_t"), (CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED), - TEXT_SCHEMA.extend( - { - cv.Optional(CONF_RECOLOR): lv_bool, - cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of, - } - ), + { + **TEXT_SCHEMA, + cv.Optional(CONF_RECOLOR): lv_bool, + cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of, + }, ) async def to_code(self, w: Widget, config): diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 028a81b4499..ad46f67c6b6 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -14,13 +14,12 @@ CONF_QRCODE = "qrcode" CONF_DARK_COLOR = "dark_color" CONF_LIGHT_COLOR = "light_color" -QRCODE_SCHEMA = TEXT_SCHEMA.extend( - { - cv.Optional(CONF_DARK_COLOR, default="black"): lv_color, - cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color, - cv.Required(CONF_SIZE): cv.int_, - } -) +QRCODE_SCHEMA = { + **TEXT_SCHEMA, + cv.Optional(CONF_DARK_COLOR, default="black"): lv_color, + cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color, + cv.Required(CONF_SIZE): cv.int_, +} class QrCodeType(WidgetType): diff --git a/esphome/components/lvgl/widgets/textarea.py b/esphome/components/lvgl/widgets/textarea.py index 23d50b3894b..e5ab8846859 100644 --- a/esphome/components/lvgl/widgets/textarea.py +++ b/esphome/components/lvgl/widgets/textarea.py @@ -21,15 +21,14 @@ CONF_TEXTAREA = "textarea" lv_textarea_t = LvText("lv_textarea_t") -TEXTAREA_SCHEMA = TEXT_SCHEMA.extend( - { - cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text, - cv.Optional(CONF_ACCEPTED_CHARS): lv_text, - cv.Optional(CONF_ONE_LINE): lv_bool, - cv.Optional(CONF_PASSWORD_MODE): lv_bool, - cv.Optional(CONF_MAX_LENGTH): lv_int, - } -) +TEXTAREA_SCHEMA = { + **TEXT_SCHEMA, + cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text, + cv.Optional(CONF_ACCEPTED_CHARS): lv_text, + cv.Optional(CONF_ONE_LINE): lv_bool, + cv.Optional(CONF_PASSWORD_MODE): lv_bool, + cv.Optional(CONF_MAX_LENGTH): lv_int, +} class TextareaType(WidgetType): diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 14241a16693..ca669c16e4e 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -113,9 +113,10 @@ lvgl: title: Messagebox bg_color: 0xffff widgets: - - label: - text: Hello Msgbox - id: msgbox_label + # Test single widget without list + label: + text: Hello Msgbox + id: msgbox_label body: text: This is a sample messagebox bg_color: 0x808080 @@ -281,7 +282,7 @@ lvgl: #endif return std::string(buf); align: top_left - - obj: + - container: align: center arc_opa: COVER arc_color: 0xFF0000 @@ -414,6 +415,7 @@ lvgl: - buttons: - id: button_e - button: + layout: 2x1 id: button_button width: 20% height: 10% @@ -430,8 +432,13 @@ lvgl: checked: bg_color: 0x000000 widgets: - - label: - text: Button + # Test parse a dict instead of list + label: + text: Button + align: bottom_right + image: + src: cat_image + align: top_left on_click: - lvgl.widget.focus: spin_up - lvgl.widget.focus: next @@ -539,6 +546,7 @@ lvgl: - logger.log: "tile 1 is now showing" tiles: - id: tile_1 + layout: vertical row: 0 column: 0 dir: ALL @@ -554,6 +562,7 @@ lvgl: bg_color: 0x000000 - id: page2 + layout: vertical widgets: - canvas: id: canvas_id @@ -1005,6 +1014,7 @@ lvgl: r_mod: -20 opa: 0% - id: page3 + layout: horizontal widgets: - keyboard: id: lv_keyboard From 3e086c2127eb51ab4c7af219cb75be5fff00bd76 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:43:27 +1000 Subject: [PATCH 058/461] [lvgl] Fix rotation with unusual width (#11680) --- esphome/components/const/__init__.py | 1 + esphome/components/lvgl/lvgl_esphome.cpp | 24 ++++++++++--------- .../components/lvgl/widgets/buttonmatrix.py | 3 ++- esphome/components/matrix_keypad/__init__.py | 3 ++- esphome/const.py | 1 - 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 19924f0da77..2b88bb43a8d 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -11,4 +11,5 @@ CONF_DRAW_ROUNDING = "draw_rounding" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" +CONF_ROWS = "rows" CONF_USE_PSRAM = "use_psram" diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 947342089cd..05005b02170 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -171,6 +171,7 @@ bool LvPageType::is_showing() const { return this->parent_->get_current_page() = void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); + auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto x1 = area->x1; auto y1 = area->y1; lv_color_t *dst = this->rotate_buf_; @@ -178,13 +179,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { case display::DISPLAY_ROTATION_90_DEGREES: for (lv_coord_t x = height; x-- != 0;) { for (lv_coord_t y = 0; y != width; y++) { - dst[y * height + x] = *ptr++; + dst[y * height_rounded + x] = *ptr++; } } y1 = x1; x1 = this->disp_drv_.ver_res - area->y1 - height; - width = height; - height = lv_area_get_width(area); + height = width; + width = height_rounded; break; case display::DISPLAY_ROTATION_180_DEGREES: @@ -200,13 +201,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { case display::DISPLAY_ROTATION_270_DEGREES: for (lv_coord_t x = 0; x != height; x++) { for (lv_coord_t y = width; y-- != 0;) { - dst[y * height + x] = *ptr++; + dst[y * height_rounded + x] = *ptr++; } } x1 = y1; y1 = this->disp_drv_.hor_res - area->x1 - width; - width = height; - height = lv_area_get_width(area); + height = width; + width = height_rounded; break; default: @@ -443,8 +444,10 @@ LvglComponent::LvglComponent(std::vector displays, float buf void LvglComponent::setup() { auto *display = this->displays_[0]; - auto width = display->get_width(); - auto height = display->get_height(); + auto rounding = this->draw_rounding; + // cater for displays with dimensions that don't divide by the required rounding + auto width = (display->get_width() + rounding - 1) / rounding * rounding; + auto height = (display->get_height() + rounding - 1) / rounding * rounding; auto frac = this->buffer_frac_; if (frac == 0) frac = 1; @@ -469,9 +472,8 @@ void LvglComponent::setup() { } this->buffer_frac_ = frac; lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels); - this->disp_drv_.hor_res = width; - this->disp_drv_.ver_res = height; - // this->setup_driver_(display->get_width(), display->get_height()); + this->disp_drv_.hor_res = display->get_width(); + this->disp_drv_.ver_res = display->get_height(); lv_disp_drv_update(this->disp_, &this->disp_drv_); this->rotation = display->get_rotation(); if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index baeb1c8e3ef..fe421aa4770 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -1,8 +1,9 @@ from esphome import automation import esphome.codegen as cg +from esphome.components.const import CONF_ROWS from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROWS, CONF_TEXT, CONF_WIDTH +from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH from esphome.cpp_generator import MockObj from ..automation import action_to_code diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index 2e123323a08..868b1492115 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -1,8 +1,9 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import key_provider +from esphome.components.const import CONF_ROWS import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_ROWS, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID CODEOWNERS = ["@ssieb"] diff --git a/esphome/const.py b/esphome/const.py index 3bbc6b8b3fd..d0d94ed283f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -838,7 +838,6 @@ CONF_RMT_CHANNEL = "rmt_channel" CONF_RMT_SYMBOLS = "rmt_symbols" CONF_ROTATION = "rotation" CONF_ROW = "row" -CONF_ROWS = "rows" CONF_RS_PIN = "rs_pin" CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance" CONF_RTD_WIRES = "rtd_wires" From 525790049587e58a2cc72dd06b59ba6be350f019 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 21:45:20 -0600 Subject: [PATCH 059/461] [mqtt] Add wake_loop_threadsafe() for low-latency event processing on ESP32 (#11695) --- esphome/components/mqtt/__init__.py | 12 +++++++++--- esphome/components/mqtt/mqtt_backend_esp32.cpp | 5 +++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 641c70a367c..1fc0c30db1c 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -3,7 +3,7 @@ import re from esphome import automation from esphome.automation import Condition import esphome.codegen as cg -from esphome.components import logger +from esphome.components import logger, socket from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv @@ -66,6 +66,9 @@ DEPENDENCIES = ["network"] def AUTO_LOAD(): if CORE.is_esp8266 or CORE.is_libretiny: return ["async_tcp", "json"] + # ESP32 needs socket for wake_loop_threadsafe() + if CORE.is_esp32: + return ["json", "socket"] return ["json"] @@ -213,8 +216,6 @@ def validate_fingerprint(value): def _consume_mqtt_sockets(config: ConfigType) -> ConfigType: """Register socket needs for MQTT component.""" - from esphome.components import socket - # MQTT needs 1 socket for the broker connection socket.consume_sockets(1, "mqtt")(config) return config @@ -341,6 +342,11 @@ async def to_code(config): # https://github.com/heman/async-mqtt-client/blob/master/library.json cg.add_library("heman/AsyncMqttClient-esphome", "2.0.0") + # MQTT on ESP32 uses wake_loop_threadsafe() to wake the main loop from the MQTT event handler + # This enables low-latency MQTT event processing instead of waiting for select() timeout + if CORE.is_esp32: + socket.require_wake_loop_threadsafe() + cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 623206a0cd7..dcc51ed60ee 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -190,6 +190,11 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b if (instance) { auto event = *static_cast(event_data); instance->mqtt_events_.emplace(event); + + // Wake main loop immediately to process MQTT event instead of waiting for select() timeout +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } } From 4c31cb57eaf1d1411c071ef74c190fffdb25be04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 21:45:57 -0600 Subject: [PATCH 060/461] [espnow] Add wake_loop_threadsafe() for low-latency event processing (#11696) --- esphome/components/espnow/__init__.py | 7 ++++++- esphome/components/espnow/espnow_component.cpp | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 9d2f17440c3..cc2c02d4c09 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -1,6 +1,6 @@ from esphome import automation, core import esphome.codegen as cg -from esphome.components import wifi +from esphome.components import socket, wifi from esphome.components.udp import CONF_ON_RECEIVE import esphome.config_validation as cv from esphome.const import ( @@ -17,6 +17,7 @@ from esphome.core import CORE, HexInt from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] +AUTO_LOAD = ["socket"] byte_vector = cg.std_vector.template(cg.uint8) peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6) @@ -120,6 +121,10 @@ async def to_code(config): if CORE.using_arduino: cg.add_library("WiFi", None) + # ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks + # This enables low-latency event processing instead of waiting for select() timeout + socket.require_wake_loop_threadsafe() + cg.add_define("USE_ESPNOW") if wifi_channel := config.get(CONF_CHANNEL): cg.add(var.set_wifi_channel(wifi_channel)) diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index b0d5938dbad..d2f136d1c7a 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -4,6 +4,7 @@ #include "espnow_err.h" +#include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -97,6 +98,11 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); // Push always because we're the only producer and the pool ensures we never exceed queue size + + // Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { @@ -114,6 +120,11 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); // Push always because we're the only producer and the pool ensures we never exceed queue size + + // Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } ESPNowComponent::ESPNowComponent() { global_esp_now = this; } From 4d2f9db8617d542021ed20aa2fea993e8ffeb384 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 21:46:34 -0600 Subject: [PATCH 061/461] [esp32_ble] Remove leftover lwip/sockets.h include (#11702) --- esphome/components/esp32_ble/ble.cpp | 4 ---- esphome/components/esp32_ble/ble.h | 4 ---- 2 files changed, 8 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index fc26a7fc219..8bbb21e3ca2 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -27,10 +27,6 @@ extern "C" { #include #endif -#ifdef USE_SOCKET_SELECT_SUPPORT -#include -#endif - namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 3be6a7048d6..c3e1ec2ce62 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -25,10 +25,6 @@ #include #include -#ifdef USE_SOCKET_SELECT_SUPPORT -#include -#endif - namespace esphome::esp32_ble { // Maximum size of the BLE event queue From 980098ca77dfe249da4d45943ddf5488d4532ae2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 21:47:11 -0600 Subject: [PATCH 062/461] [ci] Fix non-component files incorrectly detected as components (#11701) --- script/helpers.py | 6 +++++- tests/script/test_helpers.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/script/helpers.py b/script/helpers.py index 447d54fa54f..33f95d6f8a9 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -101,7 +101,11 @@ def get_component_from_path(file_path: str) -> str | None: ): parts = file_path.split("/") if len(parts) >= 3 and parts[2]: - return parts[2] + # Verify that parts[2] is actually a component directory, not a file + # like .gitignore or README.md in the components directory itself + component_name = parts[2] + if "." not in component_name: + return component_name return None diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 1046512a141..5eb55c07220 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1093,6 +1093,11 @@ def test_parse_list_components_output(output: str, expected: list[str]) -> None: ("tests/components/", None), # No component name ("esphome/components", None), # No trailing slash ("tests/components", None), # No trailing slash + # Files in component directories that are not components + ("tests/components/.gitignore", None), # Hidden file + ("tests/components/README.md", None), # Documentation file + ("esphome/components/__init__.py", None), # Python init file + ("tests/components/main.cpp", None), # File with extension ], ) def test_get_component_from_path( From 1530e3105d5a3803f46c5ea038c7fb66e39bd96d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 22:25:49 -0600 Subject: [PATCH 063/461] review --- esphome/components/ethernet/ethernet_component.h | 5 +++++ esphome/components/openthread/openthread.h | 4 ++++ esphome/components/wifi/wifi_component.h | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index de136a02eb4..4f30959e407 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -114,7 +114,12 @@ class EthernetComponent : public Component { /// @brief Set arbitratry PHY registers from config. void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data); + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. const char *use_address_{""}; + + protected: #ifdef USE_ETHERNET_SPI uint8_t clk_pin_; uint8_t miso_pin_; diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index d099c321f97..3132e416962 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -41,6 +41,10 @@ class OpenThreadComponent : public Component { bool teardown_started_{false}; bool teardown_complete_{false}; std::function factory_reset_external_callback_; + + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. const char *use_address_{""}; }; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 0c079ef1e42..4aa2ad15d49 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -393,7 +393,12 @@ class WiFiComponent : public Component { void wifi_scan_done_callback_(); #endif + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. const char *use_address_{""}; + + protected: FixedVector sta_; std::vector sta_priorities_; wifi_scan_vector_t scan_result_; From 080bebbe06aca4fa414b0e7b97b8d3660acdc4f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 22:29:58 -0600 Subject: [PATCH 064/461] review --- esphome/components/ethernet/ethernet_component.h | 11 +++++------ esphome/components/wifi/wifi_component.h | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 4f30959e407..31f9fa360a5 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -114,12 +114,6 @@ class EthernetComponent : public Component { /// @brief Set arbitratry PHY registers from config. void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data); - private: - // Stores a pointer to a string literal (static storage duration). - // ONLY set from Python-generated code with string literals - never dynamic strings. - const char *use_address_{""}; - - protected: #ifdef USE_ETHERNET_SPI uint8_t clk_pin_; uint8_t miso_pin_; @@ -163,6 +157,11 @@ class EthernetComponent : public Component { esp_eth_handle_t eth_handle_; esp_eth_phy_t *phy_{nullptr}; optional> fixed_mac_; + + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. + const char *use_address_{""}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4aa2ad15d49..89b7b1fa41c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -393,12 +393,6 @@ class WiFiComponent : public Component { void wifi_scan_done_callback_(); #endif - private: - // Stores a pointer to a string literal (static storage duration). - // ONLY set from Python-generated code with string literals - never dynamic strings. - const char *use_address_{""}; - - protected: FixedVector sta_; std::vector sta_priorities_; wifi_scan_vector_t scan_result_; @@ -450,6 +444,11 @@ class WiFiComponent : public Component { // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; Trigger<> *disconnect_trigger_{new Trigger<>()}; + + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. + const char *use_address_{""}; }; extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From 060bb4159f6c638d3ce03a1e3169607a751ec1d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 22:38:57 -0600 Subject: [PATCH 065/461] [ci] Cache component dependency graph for up to 3.4x faster determine-jobs (#11648) --- .github/workflows/ci.yml | 11 ++ script/helpers.py | 71 +++++++- tests/script/test_determine_jobs.py | 2 + tests/script/test_helpers.py | 260 ++++++++++++++++++++++++++++ 4 files changed, 341 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1756d5b7651..16837b3186e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -192,6 +192,11 @@ jobs: with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} + - name: Restore components graph cache + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: .temp/components_graph.json + key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} - name: Determine which tests to run id: determine env: @@ -216,6 +221,12 @@ jobs: echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT + - name: Save components graph cache + if: github.ref == 'refs/heads/dev' + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: .temp/components_graph.json + key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} integration-tests: name: Run integration tests diff --git a/script/helpers.py b/script/helpers.py index 33f95d6f8a9..5b2fe6cd06a 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable from functools import cache +import hashlib import json import os import os.path @@ -52,6 +53,10 @@ BASE_BUS_COMPONENTS = { "remote_receiver", } +# Cache version for components graph +# Increment this when the cache format or graph building logic changes +COMPONENTS_GRAPH_CACHE_VERSION = 1 + def parse_list_components_output(output: str) -> list[str]: """Parse the output from list-components.py script. @@ -756,20 +761,71 @@ def resolve_auto_load( return auto_load() +@cache +def get_components_graph_cache_key() -> str: + """Generate cache key based on all component Python file hashes. + + Uses git ls-files with sha1 hashes to generate a stable cache key that works + across different machines and CI runs. This is faster and more reliable than + reading file contents or using modification times. + + Returns: + SHA256 hex string uniquely identifying the current component state + """ + + # Use git ls-files -s to get sha1 hashes of all component Python files + # Format: + # This is fast and works consistently across CI and local dev + # We hash all .py files because AUTO_LOAD, DEPENDENCIES, etc. can be defined + # in any Python file, not just __init__.py + cmd = ["git", "ls-files", "-s", "esphome/components/**/*.py"] + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, cwd=root_path, close_fds=False + ) + + # Hash the git output (includes file paths and their sha1 hashes) + # This changes only when component Python files actually change + hasher = hashlib.sha256() + hasher.update(result.stdout.encode()) + + return hasher.hexdigest() + + def create_components_graph() -> dict[str, list[str]]: - """Create a graph of component dependencies. + """Create a graph of component dependencies (cached). + + This function is expensive (5-6 seconds) because it imports all ESPHome components + to extract their DEPENDENCIES and AUTO_LOAD metadata. The result is cached based + on component file modification times, so unchanged components don't trigger a rebuild. Returns: Dictionary mapping parent components to their children (dependencies) """ - from pathlib import Path + # Check cache first - use fixed filename since GitHub Actions cache doesn't support wildcards + cache_file = Path(temp_folder) / "components_graph.json" + + if cache_file.exists(): + try: + cached_data = json.loads(cache_file.read_text()) + except (OSError, json.JSONDecodeError): + # Cache file corrupted or unreadable, rebuild + pass + else: + # Verify cache version matches + if cached_data.get("_version") == COMPONENTS_GRAPH_CACHE_VERSION: + # Verify cache is for current component state + cache_key = get_components_graph_cache_key() + if cached_data.get("_cache_key") == cache_key: + return cached_data.get("graph", {}) + # Cache key mismatch - stale cache, rebuild + # Cache version mismatch - incompatible format, rebuild from esphome import const from esphome.core import CORE from esphome.loader import ComponentManifest, get_component, get_platform # The root directory of the repo - root = Path(__file__).parent.parent + root = Path(root_path) components_dir = root / ESPHOME_COMPONENTS_PATH # Fake some directory so that get_component works CORE.config_path = root @@ -846,6 +902,15 @@ def create_components_graph() -> dict[str, list[str]]: # restore config CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + # Save to cache with version and cache key for validation + cache_data = { + "_version": COMPONENTS_GRAPH_CACHE_VERSION, + "_cache_key": get_components_graph_cache_key(), + "graph": components_graph, + } + cache_file.parent.mkdir(exist_ok=True) + cache_file.write_text(json.dumps(cache_data)) + return components_graph diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index a33eca5b19e..e084e2e3982 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -543,6 +543,7 @@ def test_main_filters_components_without_tests( with ( patch.object(determine_jobs, "root_path", str(tmp_path)), patch.object(helpers, "root_path", str(tmp_path)), + patch.object(helpers, "create_components_graph", return_value={}), patch("sys.argv", ["determine-jobs.py"]), patch.object( determine_jobs, @@ -640,6 +641,7 @@ def test_main_detects_components_with_variant_tests( with ( patch.object(determine_jobs, "root_path", str(tmp_path)), patch.object(helpers, "root_path", str(tmp_path)), + patch.object(helpers, "create_components_graph", return_value={}), patch("sys.argv", ["determine-jobs.py"]), patch.object( determine_jobs, diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 5eb55c07220..1bfffef51cb 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1,5 +1,6 @@ """Unit tests for script/helpers.py module.""" +from collections.abc import Generator import json import os from pathlib import Path @@ -1106,3 +1107,262 @@ def test_get_component_from_path( """Test extraction of component names from file paths.""" result = helpers.get_component_from_path(file_path) assert result == expected_component + + +# Components graph cache tests + + +@pytest.fixture +def mock_git_output() -> str: + """Fixture for mock git ls-files output with realistic component files. + + Includes examples of AUTO_LOAD in sensor.py and binary_sensor.py files, + which is why we need to hash all .py files, not just __init__.py. + """ + return ( + "100644 abc123... 0 esphome/components/wifi/__init__.py\n" + "100644 def456... 0 esphome/components/api/__init__.py\n" + "100644 ghi789... 0 esphome/components/xiaomi_lywsd03mmc/__init__.py\n" + "100644 jkl012... 0 esphome/components/xiaomi_lywsd03mmc/sensor.py\n" + "100644 mno345... 0 esphome/components/xiaomi_cgpr1/__init__.py\n" + "100644 pqr678... 0 esphome/components/xiaomi_cgpr1/binary_sensor.py\n" + ) + + +@pytest.fixture +def mock_cache_file(tmp_path: Path) -> Path: + """Fixture for a temporary cache file path.""" + return tmp_path / "components_graph.json" + + +@pytest.fixture(autouse=True) +def clear_cache_key_cache() -> None: + """Clear the components graph cache key cache before each test.""" + helpers.get_components_graph_cache_key.cache_clear() + + +@pytest.fixture +def mock_subprocess_run() -> Generator[Mock, None, None]: + """Fixture to mock subprocess.run for git commands.""" + with patch("subprocess.run") as mock_run: + yield mock_run + + +def test_cache_key_generation(mock_git_output: str, mock_subprocess_run: Mock) -> None: + """Test that cache key is generated based on git file hashes.""" + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + key = helpers.get_components_graph_cache_key() + + # Should be a 64-character hex string (SHA256) + assert len(key) == 64 + assert all(c in "0123456789abcdef" for c in key) + + +def test_cache_key_consistent_for_same_files( + mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that same git output produces same cache key.""" + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + key1 = helpers.get_components_graph_cache_key() + key2 = helpers.get_components_graph_cache_key() + + assert key1 == key2 + + +def test_cache_key_different_for_changed_files(mock_subprocess_run: Mock) -> None: + """Test that different git output produces different cache key. + + This test demonstrates that changes to any .py file (not just __init__.py) + will invalidate the cache, which is important because AUTO_LOAD can be + defined in sensor.py, binary_sensor.py, etc. + """ + mock_result1 = Mock() + mock_result1.stdout = ( + "100644 abc123... 0 esphome/components/xiaomi_lywsd03mmc/sensor.py\n" + ) + + mock_result2 = Mock() + # Same file, different hash - simulates a change to AUTO_LOAD + mock_result2.stdout = ( + "100644 xyz789... 0 esphome/components/xiaomi_lywsd03mmc/sensor.py\n" + ) + + mock_subprocess_run.return_value = mock_result1 + key1 = helpers.get_components_graph_cache_key() + + helpers.get_components_graph_cache_key.cache_clear() + mock_subprocess_run.return_value = mock_result2 + key2 = helpers.get_components_graph_cache_key() + + assert key1 != key2 + + +def test_cache_key_uses_git_ls_files( + mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that git ls-files command is called correctly.""" + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + helpers.get_components_graph_cache_key() + + # Verify git ls-files was called with correct arguments + mock_subprocess_run.assert_called_once() + call_args = mock_subprocess_run.call_args + assert call_args[0][0] == [ + "git", + "ls-files", + "-s", + "esphome/components/**/*.py", + ] + assert call_args[1]["capture_output"] is True + assert call_args[1]["text"] is True + assert call_args[1]["check"] is True + assert call_args[1]["close_fds"] is False + + +def test_cache_hit_returns_cached_graph( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache hit returns cached data without rebuilding.""" + mock_graph = {"wifi": ["network"], "api": ["socket"]} + cache_key = "a" * 64 + cache_data = { + "_version": helpers.COMPONENTS_GRAPH_CACHE_VERSION, + "_cache_key": cache_key, + "graph": mock_graph, + } + + # Write cache file + cache_file = tmp_path / "components_graph.json" + cache_file.write_text(json.dumps(cache_data)) + + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + with ( + patch("helpers.get_components_graph_cache_key", return_value=cache_key), + patch("helpers.temp_folder", str(tmp_path)), + ): + result = helpers.create_components_graph() + assert result == mock_graph + + +def test_cache_miss_no_cache_file( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache miss rebuilds graph when no cache file exists.""" + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + # Create minimal components directory structure + components_dir = tmp_path / "esphome" / "components" + components_dir.mkdir(parents=True) + + with ( + patch("helpers.root_path", str(tmp_path)), + patch("helpers.temp_folder", str(tmp_path / ".temp")), + patch("helpers.get_components_graph_cache_key", return_value="test_key"), + ): + result = helpers.create_components_graph() + # Should return empty graph for empty components directory + assert result == {} + + +def test_cache_miss_version_mismatch( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache miss rebuilds graph when version doesn't match.""" + cache_data = { + "_version": 999, # Wrong version + "_cache_key": "test_key", + "graph": {"old": ["data"]}, + } + + cache_file = tmp_path / ".temp" / "components_graph.json" + cache_file.parent.mkdir(parents=True) + cache_file.write_text(json.dumps(cache_data)) + + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + # Create minimal components directory structure + components_dir = tmp_path / "esphome" / "components" + components_dir.mkdir(parents=True) + + with ( + patch("helpers.root_path", str(tmp_path)), + patch("helpers.temp_folder", str(tmp_path / ".temp")), + patch("helpers.get_components_graph_cache_key", return_value="test_key"), + ): + result = helpers.create_components_graph() + # Should rebuild and return empty graph, not use cached data + assert result == {} + + +def test_cache_miss_key_mismatch( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache miss rebuilds graph when cache key doesn't match.""" + cache_data = { + "_version": helpers.COMPONENTS_GRAPH_CACHE_VERSION, + "_cache_key": "old_key", + "graph": {"old": ["data"]}, + } + + cache_file = tmp_path / ".temp" / "components_graph.json" + cache_file.parent.mkdir(parents=True) + cache_file.write_text(json.dumps(cache_data)) + + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + # Create minimal components directory structure + components_dir = tmp_path / "esphome" / "components" + components_dir.mkdir(parents=True) + + with ( + patch("helpers.root_path", str(tmp_path)), + patch("helpers.temp_folder", str(tmp_path / ".temp")), + patch("helpers.get_components_graph_cache_key", return_value="new_key"), + ): + result = helpers.create_components_graph() + # Should rebuild and return empty graph, not use cached data with old key + assert result == {} + + +def test_cache_miss_corrupted_json( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache miss rebuilds graph when cache file has invalid JSON.""" + cache_file = tmp_path / ".temp" / "components_graph.json" + cache_file.parent.mkdir(parents=True) + cache_file.write_text("{invalid json") + + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + # Create minimal components directory structure + components_dir = tmp_path / "esphome" / "components" + components_dir.mkdir(parents=True) + + with ( + patch("helpers.root_path", str(tmp_path)), + patch("helpers.temp_folder", str(tmp_path / ".temp")), + patch("helpers.get_components_graph_cache_key", return_value="test_key"), + ): + result = helpers.create_components_graph() + # Should handle corruption gracefully and rebuild + assert result == {} From 13e3c03a6144ea215612640e43784080af5634f1 Mon Sep 17 00:00:00 2001 From: leejoow Date: Tue, 4 Nov 2025 07:30:53 +0100 Subject: [PATCH 066/461] [dallas_temp] add support for index (#11346) --- esphome/components/dallas_temp/dallas_temp.cpp | 2 +- esphome/components/one_wire/__init__.py | 7 +++++-- esphome/components/one_wire/one_wire.cpp | 12 +++++++++++- esphome/components/one_wire/one_wire.h | 7 ++++++- tests/components/dallas_temp/common.yaml | 3 +++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index a518c964894..a3969e081e5 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -70,7 +70,7 @@ bool DallasTemperatureSensor::read_scratch_pad_() { } void DallasTemperatureSensor::setup() { - if (!this->check_address_()) + if (!this->check_address_or_index_()) return; if (!this->read_scratch_pad_()) return; diff --git a/esphome/components/one_wire/__init__.py b/esphome/components/one_wire/__init__.py index 6d95b8fd33e..e12cca3e27f 100644 --- a/esphome/components/one_wire/__init__.py +++ b/esphome/components/one_wire/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ADDRESS +from esphome.const import CONF_ADDRESS, CONF_INDEX CODEOWNERS = ["@ssieb"] @@ -21,7 +21,8 @@ def one_wire_device_schema(): return cv.Schema( { cv.GenerateID(CONF_ONE_WIRE_ID): cv.use_id(OneWireBus), - cv.Optional(CONF_ADDRESS): cv.hex_uint64_t, + cv.Exclusive(CONF_ADDRESS, "index_or_address"): cv.hex_uint64_t, + cv.Exclusive(CONF_INDEX, "index_or_address"): cv.uint8_t, } ) @@ -37,3 +38,5 @@ async def register_one_wire_device(var, config): cg.add(var.set_one_wire_bus(parent)) if (address := config.get(CONF_ADDRESS)) is not None: cg.add(var.set_address(address)) + if (index := config.get(CONF_INDEX)) is not None: + cg.add(var.set_index(index)) diff --git a/esphome/components/one_wire/one_wire.cpp b/esphome/components/one_wire/one_wire.cpp index 96e6145f63f..fd139d0ddc1 100644 --- a/esphome/components/one_wire/one_wire.cpp +++ b/esphome/components/one_wire/one_wire.cpp @@ -18,10 +18,20 @@ bool OneWireDevice::send_command_(uint8_t cmd) { return true; } -bool OneWireDevice::check_address_() { +bool OneWireDevice::check_address_or_index_() { if (this->address_ != 0) return true; auto devices = this->bus_->get_devices(); + + if (this->index_ != INDEX_NOT_SET) { + if (this->index_ >= devices.size()) { + ESP_LOGE(TAG, "Index %d out of range, only %d devices found", this->index_, devices.size()); + return false; + } + this->address_ = devices[this->index_]; + return true; + } + if (devices.empty()) { ESP_LOGE(TAG, "No devices, can't auto-select address"); return false; diff --git a/esphome/components/one_wire/one_wire.h b/esphome/components/one_wire/one_wire.h index e83c6e81e8e..f6a956a92c7 100644 --- a/esphome/components/one_wire/one_wire.h +++ b/esphome/components/one_wire/one_wire.h @@ -17,6 +17,8 @@ class OneWireDevice { /// @param address of the device void set_address(uint64_t address) { this->address_ = address; } + void set_index(uint8_t index) { this->index_ = index; } + /// @brief store the pointer to the OneWireBus to use /// @param bus pointer to the OneWireBus object void set_one_wire_bus(OneWireBus *bus) { this->bus_ = bus; } @@ -25,13 +27,16 @@ class OneWireDevice { const std::string &get_address_name(); protected: + static constexpr uint8_t INDEX_NOT_SET = 255; + uint64_t address_{0}; + uint8_t index_{INDEX_NOT_SET}; OneWireBus *bus_{nullptr}; ///< pointer to OneWireBus instance std::string address_name_; /// @brief find an address if necessary /// should be called from setup - bool check_address_(); + bool check_address_or_index_(); /// @brief send command on the bus /// @param cmd command to send diff --git a/tests/components/dallas_temp/common.yaml b/tests/components/dallas_temp/common.yaml index 6d865d8e930..abd8e0cfa36 100644 --- a/tests/components/dallas_temp/common.yaml +++ b/tests/components/dallas_temp/common.yaml @@ -9,3 +9,6 @@ sensor: resolution: 9 - platform: dallas_temp name: Dallas Temperature 2 + - platform: dallas_temp + name: Dallas Temperature 3 + index: 2 From 84f7cacef95ae53bc1a993914453abffe35406e6 Mon Sep 17 00:00:00 2001 From: Chaser Huang Date: Tue, 4 Nov 2025 10:41:30 -0500 Subject: [PATCH 067/461] [sgp30] Fix reading from preexisting stored baseline even with `store_baseline:false` (#7922) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/sgp30/sgp30.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 0e3aeff8129..9e8d6b332c9 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -78,7 +78,7 @@ void SGP30Component::setup() { uint32_t hash = fnv1_hash(App.get_compilation_time() + std::to_string(this->serial_number_)); this->pref_ = global_preferences->make_preference(hash, true); - if (this->pref_.load(&this->baselines_storage_)) { + if (this->store_baseline_ && this->pref_.load(&this->baselines_storage_)) { ESP_LOGI(TAG, "Loaded eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", this->baselines_storage_.eco2, baselines_storage_.tvoc); this->eco2_baseline_ = this->baselines_storage_.eco2; From 71fa88c9d41efe5a3fbf179e28b8a2aa2584e9a2 Mon Sep 17 00:00:00 2001 From: Cameron Steel Date: Wed, 5 Nov 2025 03:32:23 +1100 Subject: [PATCH 068/461] =?UTF-8?q?[max7219digit]=20support=20`flip=5Fx`?= =?UTF-8?q?=20when=20`rotate=5Fchip`=20is=2090=C2=B0=20or=20270=C2=B0=20(#?= =?UTF-8?q?6109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/max7219digit/max7219digit.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index 6df3c4d7c82..cdceafad501 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -271,7 +271,11 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { } } } else if (this->orientation_ == 1) { - b = pixels[col]; + if (this->flip_x_) { + b = pixels[7 - col]; + } else { + b = pixels[col]; + } } else if (this->orientation_ == 2) { for (uint8_t i = 0; i < 8; i++) { if (this->flip_x_) { @@ -282,7 +286,11 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { } } else { for (uint8_t i = 0; i < 8; i++) { - b |= ((pixels[7 - col] >> i) & 1) << (7 - i); + if (this->flip_x_) { + b |= ((pixels[col] >> i) & 1) << (7 - i); + } else { + b |= ((pixels[7 - col] >> i) & 1) << (7 - i); + } } } // send this byte to display at selected chip From 968df6cb3fd625a87118e4d72ea2970641e9efb9 Mon Sep 17 00:00:00 2001 From: SeByDocKy Date: Tue, 4 Nov 2025 18:16:11 +0100 Subject: [PATCH 069/461] [gp8403] Add gp8413 (15 bits) DAC model (#7726) Co-authored-by: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CODEOWNERS | 2 +- esphome/components/gp8403/__init__.py | 17 +++++--- esphome/components/gp8403/gp8403.cpp | 40 +++++++++++++++++-- esphome/components/gp8403/gp8403.h | 13 +++++- esphome/components/gp8403/output/__init__.py | 4 +- .../gp8403/output/gp8403_output.cpp | 10 +---- .../components/gp8403/output/gp8403_output.h | 4 +- tests/components/gp8403/common.yaml | 9 +++++ 8 files changed, 73 insertions(+), 26 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b8a4df6a854..4d458eceb87 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -181,7 +181,7 @@ esphome/components/gdk101/* @Szewcson esphome/components/gl_r01_i2c/* @pkejval esphome/components/globals/* @esphome/core esphome/components/gp2y1010au0f/* @zry98 -esphome/components/gp8403/* @jesserockz +esphome/components/gp8403/* @jesserockz @sebydocky esphome/components/gpio/* @esphome/core esphome/components/gpio/one_wire/* @ssieb esphome/components/gps/* @coogle @ximex diff --git a/esphome/components/gp8403/__init__.py b/esphome/components/gp8403/__init__.py index 96f1807688d..83859a40302 100644 --- a/esphome/components/gp8403/__init__.py +++ b/esphome/components/gp8403/__init__.py @@ -1,19 +1,25 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_VOLTAGE +from esphome.const import CONF_ID, CONF_MODEL, CONF_VOLTAGE -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@sebydocky"] DEPENDENCIES = ["i2c"] MULTI_CONF = True gp8403_ns = cg.esphome_ns.namespace("gp8403") -GP8403 = gp8403_ns.class_("GP8403", cg.Component, i2c.I2CDevice) +GP8403Component = gp8403_ns.class_("GP8403Component", cg.Component, i2c.I2CDevice) GP8403Voltage = gp8403_ns.enum("GP8403Voltage") +GP8403Model = gp8403_ns.enum("GP8403Model") CONF_GP8403_ID = "gp8403_id" +MODELS = { + "GP8403": GP8403Model.GP8403, + "GP8413": GP8403Model.GP8413, +} + VOLTAGES = { "5V": GP8403Voltage.GP8403_VOLTAGE_5V, "10V": GP8403Voltage.GP8403_VOLTAGE_10V, @@ -22,7 +28,8 @@ VOLTAGES = { CONFIG_SCHEMA = ( cv.Schema( { - cv.GenerateID(): cv.declare_id(GP8403), + cv.GenerateID(): cv.declare_id(GP8403Component), + cv.Optional(CONF_MODEL, default="GP8403"): cv.enum(MODELS, upper=True), cv.Required(CONF_VOLTAGE): cv.enum(VOLTAGES, upper=True), } ) @@ -35,5 +42,5 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - + cg.add(var.set_model(config[CONF_MODEL])) cg.add(var.set_voltage(config[CONF_VOLTAGE])) diff --git a/esphome/components/gp8403/gp8403.cpp b/esphome/components/gp8403/gp8403.cpp index 5107e96dee5..11c2f9a7c06 100644 --- a/esphome/components/gp8403/gp8403.cpp +++ b/esphome/components/gp8403/gp8403.cpp @@ -8,16 +8,48 @@ namespace gp8403 { static const char *const TAG = "gp8403"; static const uint8_t RANGE_REGISTER = 0x01; +static const uint8_t OUTPUT_REGISTER = 0x02; -void GP8403::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); } +const LogString *model_to_string(GP8403Model model) { + switch (model) { + case GP8403Model::GP8403: + return LOG_STR("GP8403"); + case GP8403Model::GP8413: + return LOG_STR("GP8413"); + } + return LOG_STR("Unknown"); +}; -void GP8403::dump_config() { +void GP8403Component::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); } + +void GP8403Component::dump_config() { ESP_LOGCONFIG(TAG, "GP8403:\n" - " Voltage: %dV", - this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10); + " Voltage: %dV\n" + " Model: %s", + this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10, LOG_STR_ARG(model_to_string(this->model_))); LOG_I2C_DEVICE(this); } +void GP8403Component::write_state(float state, uint8_t channel) { + uint16_t val = 0; + switch (this->model_) { + case GP8403Model::GP8403: + val = ((uint16_t) (4095 * state)) << 4; + break; + case GP8403Model::GP8413: + val = ((uint16_t) (32767 * state)) << 1; + break; + default: + ESP_LOGE(TAG, "Unknown model %s", LOG_STR_ARG(model_to_string(this->model_))); + return; + } + ESP_LOGV(TAG, "Calculated DAC value: %" PRIu16, val); + i2c::ErrorCode err = this->write_register(OUTPUT_REGISTER + (2 * channel), (uint8_t *) &val, 2); + if (err != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Error writing to %s, code %d", LOG_STR_ARG(model_to_string(this->model_)), err); + } +} + } // namespace gp8403 } // namespace esphome diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 9f493d39e38..6613187b206 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -11,15 +11,24 @@ enum GP8403Voltage { GP8403_VOLTAGE_10V = 0x11, }; -class GP8403 : public Component, public i2c::I2CDevice { +enum GP8403Model { + GP8403, + GP8413, +}; + +class GP8403Component : public Component, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - + float get_setup_priority() const override { return setup_priority::DATA; } + void set_model(GP8403Model model) { this->model_ = model; } void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } + void write_state(float state, uint8_t channel); + protected: GP8403Voltage voltage_; + GP8403Model model_{GP8403Model::GP8403}; }; } // namespace gp8403 diff --git a/esphome/components/gp8403/output/__init__.py b/esphome/components/gp8403/output/__init__.py index dc57833f4a7..5245c405db6 100644 --- a/esphome/components/gp8403/output/__init__.py +++ b/esphome/components/gp8403/output/__init__.py @@ -3,7 +3,7 @@ from esphome.components import i2c, output import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_ID -from .. import CONF_GP8403_ID, GP8403, gp8403_ns +from .. import CONF_GP8403_ID, GP8403Component, gp8403_ns DEPENDENCIES = ["gp8403"] @@ -14,7 +14,7 @@ GP8403Output = gp8403_ns.class_( CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(GP8403Output), - cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403), + cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403Component), cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=1), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/gp8403/output/gp8403_output.cpp b/esphome/components/gp8403/output/gp8403_output.cpp index edb6972184d..dfdc2d6ccb1 100644 --- a/esphome/components/gp8403/output/gp8403_output.cpp +++ b/esphome/components/gp8403/output/gp8403_output.cpp @@ -7,8 +7,6 @@ namespace gp8403 { static const char *const TAG = "gp8403.output"; -static const uint8_t OUTPUT_REGISTER = 0x02; - void GP8403Output::dump_config() { ESP_LOGCONFIG(TAG, "GP8403 Output:\n" @@ -16,13 +14,7 @@ void GP8403Output::dump_config() { this->channel_); } -void GP8403Output::write_state(float state) { - uint16_t value = ((uint16_t) (state * 4095)) << 4; - i2c::ErrorCode err = this->parent_->write_register(OUTPUT_REGISTER + (2 * this->channel_), (uint8_t *) &value, 2); - if (err != i2c::ERROR_OK) { - ESP_LOGE(TAG, "Error writing to GP8403, code %d", err); - } -} +void GP8403Output::write_state(float state) { this->parent_->write_state(state, this->channel_); } } // namespace gp8403 } // namespace esphome diff --git a/esphome/components/gp8403/output/gp8403_output.h b/esphome/components/gp8403/output/gp8403_output.h index 71e5efb1cb5..c0d6650500e 100644 --- a/esphome/components/gp8403/output/gp8403_output.h +++ b/esphome/components/gp8403/output/gp8403_output.h @@ -8,13 +8,11 @@ namespace esphome { namespace gp8403 { -class GP8403Output : public Component, public output::FloatOutput, public Parented { +class GP8403Output : public Component, public output::FloatOutput, public Parented { public: void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA - 1; } - void set_channel(uint8_t channel) { this->channel_ = channel; } - void write_state(float state) override; protected: diff --git a/tests/components/gp8403/common.yaml b/tests/components/gp8403/common.yaml index b04fbc4fbca..1147316b026 100644 --- a/tests/components/gp8403/common.yaml +++ b/tests/components/gp8403/common.yaml @@ -4,6 +4,11 @@ gp8403: voltage: 5V - id: gp8403_10v i2c_id: i2c_bus + model: GP8403 + voltage: 10V + - id: gp8413 + i2c_id: i2c_bus + model: GP8413 voltage: 10V output: @@ -15,3 +20,7 @@ output: gp8403_id: gp8403_10v id: gp8403_output_1 channel: 1 + - platform: gp8403 + gp8403_id: gp8413 + id: gp8413_output_2 + channel: 1 From 191a88c2dc50dd44f8a9dc3bcb2fbc70bff4fb35 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Tue, 4 Nov 2025 19:38:59 +0100 Subject: [PATCH 070/461] [gt911] Fix gt911 touchscreen with reset pin not initializing when loglevel is set to NONE (#11715) --- esphome/components/gt911/touchscreen/gt911_touchscreen.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 4810867d4ba..992a86cc21a 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -34,8 +34,8 @@ void GT911Touchscreen::setup() { this->interrupt_pin_->digital_write(false); } delay(2); - this->reset_pin_->digital_write(true); // wait 50ms after reset - this->set_timeout(50, [this] { this->setup_internal_(); }); + this->reset_pin_->digital_write(true); // wait at least T3+T4 ms as per the datasheet + this->set_timeout(5 + 50 + 1, [this] { this->setup_internal_(); }); return; } this->setup_internal_(); From aed7505f53c48bcda7206d0ac6f8db6cdfad433d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 15:48:20 -0600 Subject: [PATCH 071/461] [automations] Reduce memory usage in if/while/repeat actions (32-36 bytes per instance) (#11650) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/core/automation.h | 1 + esphome/core/base_automation.h | 91 +++++++++++++++++++++++++++------- 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index aace7889f08..c22b3ca0e35 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -220,6 +220,7 @@ template class Action { protected: friend ActionList; + template friend class ContinuationAction; virtual void play(Ts... x) = 0; void play_next_(Ts... x) { diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 28af02a846e..083bb3ae311 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -216,18 +216,46 @@ template class StatelessLambdaAction : public Action { void (*f_)(Ts...); }; +/// Simple continuation action that calls play_next_ on a parent action. +/// Used internally by IfAction, WhileAction, RepeatAction, etc. to chain actions. +/// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function). +template class ContinuationAction : public Action { + public: + explicit ContinuationAction(Action *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->play_next_(x...); } + + protected: + Action *parent_; +}; + +// Forward declaration for WhileLoopContinuation +template class WhileAction; + +/// Loop continuation for WhileAction that checks condition and repeats or continues. +/// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function). +template class WhileLoopContinuation : public Action { + public: + explicit WhileLoopContinuation(WhileAction *parent) : parent_(parent) {} + + void play(Ts... x) override; + + protected: + WhileAction *parent_; +}; + template class IfAction : public Action { public: explicit IfAction(Condition *condition) : condition_(condition) {} void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); + this->then_.add_action(new ContinuationAction(this)); } void add_else(const std::initializer_list *> &actions) { this->else_.add_actions(actions); - this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); + this->else_.add_action(new ContinuationAction(this)); } void play_complex(Ts... x) override { @@ -268,17 +296,11 @@ template class WhileAction : public Action { void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](Ts... x) { - if (this->num_running_ > 0 && this->condition_->check(x...)) { - // play again - this->then_.play(x...); - } else { - // condition false, play next - this->play_next_(x...); - } - })); + this->then_.add_action(new WhileLoopContinuation(this)); } + friend class WhileLoopContinuation; + void play_complex(Ts... x) override { this->num_running_++; // Initial condition check @@ -304,22 +326,43 @@ template class WhileAction : public Action { ActionList then_; }; +// Implementation of WhileLoopContinuation::play +template void WhileLoopContinuation::play(Ts... x) { + if (this->parent_->num_running_ > 0 && this->parent_->condition_->check(x...)) { + // play again + this->parent_->then_.play(x...); + } else { + // condition false, play next + this->parent_->play_next_(x...); + } +} + +// Forward declaration for RepeatLoopContinuation +template class RepeatAction; + +/// Loop continuation for RepeatAction that increments iteration and repeats or continues. +/// Memory: 4-8 bytes (parent pointer) vs 40 bytes (LambdaAction with std::function). +template class RepeatLoopContinuation : public Action { + public: + explicit RepeatLoopContinuation(RepeatAction *parent) : parent_(parent) {} + + void play(uint32_t iteration, Ts... x) override; + + protected: + RepeatAction *parent_; +}; + template class RepeatAction : public Action { public: TEMPLATABLE_VALUE(uint32_t, count) void add_then(const std::initializer_list *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](uint32_t iteration, Ts... x) { - iteration++; - if (iteration >= this->count_.value(x...)) { - this->play_next_(x...); - } else { - this->then_.play(iteration, x...); - } - })); + this->then_.add_action(new RepeatLoopContinuation(this)); } + friend class RepeatLoopContinuation; + void play_complex(Ts... x) override { this->num_running_++; if (this->count_.value(x...) > 0) { @@ -338,6 +381,16 @@ template class RepeatAction : public Action { ActionList then_; }; +// Implementation of RepeatLoopContinuation::play +template void RepeatLoopContinuation::play(uint32_t iteration, Ts... x) { + iteration++; + if (iteration >= this->parent_->count_.value(x...)) { + this->parent_->play_next_(x...); + } else { + this->parent_->then_.play(iteration, x...); + } +} + /** Wait until a condition is true to continue execution. * * Uses queue-based storage to safely handle concurrent executions. From 531b27582a4422700f166c16b8e2b2bd39647eaa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 15:52:10 -0600 Subject: [PATCH 072/461] [network] Store use_address in RODATA to save RAM (#11707) --- esphome/components/api/api_server.cpp | 2 +- esphome/components/esphome/ota/ota_esphome.cpp | 2 +- esphome/components/ethernet/ethernet_component.cpp | 4 ++-- esphome/components/ethernet/ethernet_component.h | 10 +++++++--- esphome/components/network/util.cpp | 5 ++--- esphome/components/network/util.h | 2 +- esphome/components/openthread/openthread.cpp | 4 ++-- esphome/components/openthread/openthread.h | 10 +++++++--- esphome/components/web_server/web_server.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/components/wifi/wifi_component.h | 10 +++++++--- 11 files changed, 33 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index e618610a75d..e5f0d9795ed 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -224,7 +224,7 @@ void APIServer::dump_config() { " Address: %s:%u\n" " Listen backlog: %u\n" " Max connections: %u", - network::get_use_address().c_str(), this->port_, this->listen_backlog_, this->max_connections_); + network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_); #ifdef USE_API_NOISE ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); if (!this->noise_ctx_->has_psk()) { diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index b85d6602726..eb6c61a69be 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -94,7 +94,7 @@ void ESPHomeOTAComponent::dump_config() { "Over-The-Air updates:\n" " Address: %s:%u\n" " Version: %d", - network::get_use_address().c_str(), this->port_, USE_OTA_VERSION); + network::get_use_address(), this->port_, USE_OTA_VERSION); #ifdef USE_OTA_PASSWORD if (!this->password_.empty()) { ESP_LOGCONFIG(TAG, " Password configured"); diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 24b6e8154ba..893d0285be2 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -691,9 +691,9 @@ void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ // set_use_address() is guaranteed to be called during component setup by Python code generation, // so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const std::string &EthernetComponent::get_use_address() const { return this->use_address_; } +const char *EthernetComponent::get_use_address() const { return this->use_address_; } -void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { esp_err_t err; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index d5dda3e3aef..31f9fa360a5 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -88,8 +88,8 @@ class EthernetComponent : public Component { network::IPAddresses get_ip_addresses(); network::IPAddress get_dns_address(uint8_t num); - const std::string &get_use_address() const; - void set_use_address(const std::string &use_address); + const char *get_use_address() const; + void set_use_address(const char *use_address); void get_eth_mac_address_raw(uint8_t *mac); std::string get_eth_mac_address_pretty(); eth_duplex_t get_duplex_mode(); @@ -114,7 +114,6 @@ class EthernetComponent : public Component { /// @brief Set arbitratry PHY registers from config. void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data); - std::string use_address_; #ifdef USE_ETHERNET_SPI uint8_t clk_pin_; uint8_t miso_pin_; @@ -158,6 +157,11 @@ class EthernetComponent : public Component { esp_eth_handle_t eth_handle_; esp_eth_phy_t *phy_{nullptr}; optional> fixed_mac_; + + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. + const char *use_address_{""}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index cb8f8569add..5e741fd2445 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -85,7 +85,7 @@ network::IPAddresses get_ip_addresses() { return {}; } -const std::string &get_use_address() { +const char *get_use_address() { // Global component pointers are guaranteed to be set by component constructors when USE_* is defined #ifdef USE_ETHERNET return ethernet::global_eth_component->get_use_address(); @@ -105,8 +105,7 @@ const std::string &get_use_address() { #if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) // Fallback when no network component is defined (e.g., host platform) - static const std::string empty; - return empty; + return ""; #endif } diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index b4a92f8bee0..3dc12232aa8 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -12,7 +12,7 @@ bool is_connected(); /// Return whether the network is disabled (only wifi for now) bool is_disabled(); /// Get the active network hostname -const std::string &get_use_address(); +const char *get_use_address(); IPAddresses get_ip_addresses(); } // namespace network diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index db909e6b1ff..d7fb1e1d42d 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -254,9 +254,9 @@ void OpenThreadComponent::on_factory_reset(std::function callback) { // set_use_address() is guaranteed to be called during component setup by Python code generation, // so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const std::string &OpenThreadComponent::get_use_address() const { return this->use_address_; } +const char *OpenThreadComponent::get_use_address() const { return this->use_address_; } -void OpenThreadComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +void OpenThreadComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } } // namespace openthread } // namespace esphome diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 19dbeb46283..3132e416962 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -33,15 +33,19 @@ class OpenThreadComponent : public Component { void on_factory_reset(std::function callback); void defer_factory_reset_external_callback(); - const std::string &get_use_address() const; - void set_use_address(const std::string &use_address); + const char *get_use_address() const; + void set_use_address(const char *use_address); protected: std::optional get_omr_address_(InstanceLock &lock); bool teardown_started_{false}; bool teardown_complete_{false}; std::function factory_reset_external_callback_; - std::string use_address_; + + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. + const char *use_address_{""}; }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 465356db805..acc0f33e610 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -324,7 +324,7 @@ void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:\n" " Address: %s:%u", - network::get_use_address().c_str(), this->base_->get_port()); + network::get_use_address(), this->base_->get_port()); } float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; } diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index b278e5a386e..51b5756f296 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -273,8 +273,8 @@ network::IPAddress WiFiComponent::get_dns_address(int num) { } // set_use_address() is guaranteed to be called during component setup by Python code generation, // so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const std::string &WiFiComponent::get_use_address() const { return this->use_address_; } -void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +const char *WiFiComponent::get_use_address() const { return this->use_address_; } +void WiFiComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } #ifdef USE_WIFI_AP void WiFiComponent::setup_ap_config_() { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 42f78dbfac8..89b7b1fa41c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -283,8 +283,8 @@ class WiFiComponent : public Component { network::IPAddress get_dns_address(int num); network::IPAddresses get_ip_addresses(); - const std::string &get_use_address() const; - void set_use_address(const std::string &use_address); + const char *get_use_address() const; + void set_use_address(const char *use_address); const wifi_scan_vector_t &get_scan_result() const { return scan_result_; } @@ -393,7 +393,6 @@ class WiFiComponent : public Component { void wifi_scan_done_callback_(); #endif - std::string use_address_; FixedVector sta_; std::vector sta_priorities_; wifi_scan_vector_t scan_result_; @@ -445,6 +444,11 @@ class WiFiComponent : public Component { // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; Trigger<> *disconnect_trigger_{new Trigger<>()}; + + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. + const char *use_address_{""}; }; extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From 885508775f6c1a556410c462b95f247c6b249d05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 15:55:37 -0600 Subject: [PATCH 073/461] [fan] Remove duplicate preset mode storage to save RAM (#11632) --- esphome/components/api/api_connection.cpp | 4 +- esphome/components/copy/fan/copy_fan.cpp | 6 +- esphome/components/fan/automation.h | 9 +- esphome/components/fan/fan.cpp | 90 ++++++++++++------- esphome/components/fan/fan.h | 32 +++++-- esphome/components/fan/fan_traits.h | 12 +++ .../components/hbridge/fan/hbridge_fan.cpp | 2 +- esphome/components/speed/fan/speed_fan.cpp | 2 +- .../components/template/fan/template_fan.cpp | 2 +- 9 files changed, 109 insertions(+), 50 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index f8490b6ef70..3f1a0760076 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -410,8 +410,8 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co } if (traits.supports_direction()) msg.direction = static_cast(fan->direction); - if (traits.supports_preset_modes()) - msg.set_preset_mode(StringRef(fan->preset_mode)); + if (traits.supports_preset_modes() && fan->has_preset_mode()) + msg.set_preset_mode(StringRef(fan->get_preset_mode())); return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index 15a7f5e025e..d35ece950bc 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -12,7 +12,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; - this->preset_mode = source_->preset_mode; + this->set_preset_mode_(source_->get_preset_mode()); this->publish_state(); }); @@ -20,7 +20,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; - this->preset_mode = source_->preset_mode; + this->set_preset_mode_(source_->get_preset_mode()); this->publish_state(); } @@ -49,7 +49,7 @@ void CopyFan::control(const fan::FanCall &call) { call2.set_speed(*call.get_speed()); if (call.get_direction().has_value()) call2.set_direction(*call.get_direction()); - if (!call.get_preset_mode().empty()) + if (call.has_preset_mode()) call2.set_preset_mode(call.get_preset_mode()); call2.perform(); } diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 90661c307c5..ae0af1a9bd3 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -212,18 +212,19 @@ class FanPresetSetTrigger : public Trigger { public: FanPresetSetTrigger(Fan *state) { state->add_on_state_callback([this, state]() { - auto preset_mode = state->preset_mode; + const auto *preset_mode = state->get_preset_mode(); auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; if (should_trigger) { - this->trigger(preset_mode); + // Trigger with empty string when nullptr to maintain backward compatibility + this->trigger(preset_mode != nullptr ? preset_mode : ""); } }); - this->last_preset_mode_ = state->preset_mode; + this->last_preset_mode_ = state->get_preset_mode(); } protected: - std::string last_preset_mode_; + const char *last_preset_mode_{nullptr}; }; } // namespace fan diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 5b4f437f99c..959572e9d94 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -17,6 +17,27 @@ const LogString *fan_direction_to_string(FanDirection direction) { } } +FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { return this->set_preset_mode(preset_mode.c_str()); } + +FanCall &FanCall::set_preset_mode(const char *preset_mode) { + if (preset_mode == nullptr || strlen(preset_mode) == 0) { + this->preset_mode_ = nullptr; + return *this; + } + + // Find and validate pointer from traits immediately + auto traits = this->parent_.get_traits(); + const char *validated_mode = traits.find_preset_mode(preset_mode); + if (validated_mode != nullptr) { + this->preset_mode_ = validated_mode; // Store pointer from traits + } else { + // Preset mode not found in traits - log warning and don't set + ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), preset_mode); + this->preset_mode_ = nullptr; + } + return *this; +} + void FanCall::perform() { ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); this->validate_(); @@ -32,8 +53,8 @@ void FanCall::perform() { if (this->direction_.has_value()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); } - if (!this->preset_mode_.empty()) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_.c_str()); + if (this->has_preset_mode()) { + ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_); } this->parent_.control(*this); } @@ -46,30 +67,15 @@ void FanCall::validate_() { // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes // "Manually setting a speed must disable any set preset mode" - this->preset_mode_.clear(); - } - - if (!this->preset_mode_.empty()) { - const auto &preset_modes = traits.supported_preset_modes(); - bool found = false; - for (const auto &mode : preset_modes) { - if (strcmp(mode, this->preset_mode_.c_str()) == 0) { - found = true; - break; - } - } - if (!found) { - ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); - this->preset_mode_.clear(); - } + this->preset_mode_ = nullptr; } // when turning on... if (!this->parent_.state && this->binary_state_.has_value() && *this->binary_state_ // ..,and no preset mode will be active... - && this->preset_mode_.empty() && - this->parent_.preset_mode.empty() + && !this->has_preset_mode() && + this->parent_.get_preset_mode() == nullptr // ...and neither current nor new speed is available... && traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) { // ...set speed to 100% @@ -117,12 +123,13 @@ void FanRestoreState::apply(Fan &fan) { auto traits = fan.get_traits(); if (traits.supports_preset_modes()) { - // Use stored preset index to get preset name + // Use stored preset index to get preset name from traits const auto &preset_modes = traits.supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { - fan.preset_mode = preset_modes[this->preset_mode]; + fan.set_preset_mode_(preset_modes[this->preset_mode]); } } + fan.publish_state(); } @@ -131,6 +138,29 @@ FanCall Fan::turn_off() { return this->make_call().set_state(false); } FanCall Fan::toggle() { return this->make_call().set_state(!this->state); } FanCall Fan::make_call() { return FanCall(*this); } +const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_traits().find_preset_mode(preset_mode); } + +bool Fan::set_preset_mode_(const char *preset_mode) { + if (preset_mode == nullptr) { + // Treat nullptr as clearing the preset mode + if (this->preset_mode_ == nullptr) { + return false; // No change + } + this->clear_preset_mode_(); + return true; + } + const char *validated = this->find_preset_mode_(preset_mode); + if (validated == nullptr || this->preset_mode_ == validated) { + return false; // Preset mode not supported or no change + } + this->preset_mode_ = validated; + return true; +} + +bool Fan::set_preset_mode_(const std::string &preset_mode) { return this->set_preset_mode_(preset_mode.c_str()); } + +void Fan::clear_preset_mode_() { this->preset_mode_ = nullptr; } + void Fan::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } void Fan::publish_state() { auto traits = this->get_traits(); @@ -146,8 +176,9 @@ void Fan::publish_state() { if (traits.supports_direction()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } - if (traits.supports_preset_modes() && !this->preset_mode.empty()) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode.c_str()); + const char *preset = this->get_preset_mode(); + if (preset != nullptr) { + ESP_LOGD(TAG, " Preset Mode: %s", preset); } this->state_callback_.call(); this->save_state_(); @@ -199,16 +230,15 @@ void Fan::save_state_() { state.speed = this->speed; state.direction = this->direction; - if (traits.supports_preset_modes() && !this->preset_mode.empty()) { + const char *preset = this->get_preset_mode(); + if (preset != nullptr) { const auto &preset_modes = traits.supported_preset_modes(); - // Store index of current preset mode - size_t i = 0; - for (const auto &mode : preset_modes) { - if (strcmp(mode, this->preset_mode.c_str()) == 0) { + // Find index of current preset mode (pointer comparison is safe since preset is from traits) + for (size_t i = 0; i < preset_modes.size(); i++) { + if (preset_modes[i] == preset) { state.preset_mode = i; break; } - i++; } } diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 3739de29a23..e38a80dbbe3 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -70,11 +70,10 @@ class FanCall { return *this; } optional get_direction() const { return this->direction_; } - FanCall &set_preset_mode(const std::string &preset_mode) { - this->preset_mode_ = preset_mode; - return *this; - } - std::string get_preset_mode() const { return this->preset_mode_; } + FanCall &set_preset_mode(const std::string &preset_mode); + FanCall &set_preset_mode(const char *preset_mode); + const char *get_preset_mode() const { return this->preset_mode_; } + bool has_preset_mode() const { return this->preset_mode_ != nullptr; } void perform(); @@ -86,7 +85,7 @@ class FanCall { optional oscillating_; optional speed_; optional direction_{}; - std::string preset_mode_{}; + const char *preset_mode_{nullptr}; // Pointer to string in traits (after validation) }; struct FanRestoreState { @@ -112,8 +111,6 @@ class Fan : public EntityBase { int speed{0}; /// The current direction of the fan FanDirection direction{FanDirection::FORWARD}; - // The current preset mode of the fan - std::string preset_mode{}; FanCall turn_on(); FanCall turn_off(); @@ -130,8 +127,15 @@ class Fan : public EntityBase { /// Set the restore mode of this fan. void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + /// Get the current preset mode (returns pointer to string stored in traits, or nullptr if not set) + const char *get_preset_mode() const { return this->preset_mode_; } + + /// Check if a preset mode is currently active + bool has_preset_mode() const { return this->preset_mode_ != nullptr; } + protected: friend FanCall; + friend struct FanRestoreState; virtual void control(const FanCall &call) = 0; @@ -140,9 +144,21 @@ class Fan : public EntityBase { void dump_traits_(const char *tag, const char *prefix); + /// Set the preset mode (finds and stores pointer from traits). Returns true if changed. + bool set_preset_mode_(const char *preset_mode); + /// Set the preset mode (finds and stores pointer from traits). Returns true if changed. + bool set_preset_mode_(const std::string &preset_mode); + /// Clear the preset mode + void clear_preset_mode_(); + /// Find and return the matching preset mode pointer from traits, or nullptr if not found. + const char *find_preset_mode_(const char *preset_mode); + CallbackManager state_callback_{}; ESPPreferenceObject rtc_; FanRestoreMode restore_mode_; + + private: + const char *preset_mode_{nullptr}; }; } // namespace fan diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index df345f9b048..24987fe984b 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -44,6 +45,17 @@ class FanTraits { /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_.empty(); } + /// Find and return the matching preset mode pointer from supported modes, or nullptr if not found. + const char *find_preset_mode(const char *preset_mode) const { + if (preset_mode == nullptr) + return nullptr; + for (const char *mode : this->preset_modes_) { + if (strcmp(mode, preset_mode) == 0) { + return mode; // Return pointer from traits + } + } + return nullptr; + } protected: bool oscillation_{false}; diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 605a9d4ef39..488208b7255 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -57,7 +57,7 @@ void HBridgeFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); - this->preset_mode = call.get_preset_mode(); + this->set_preset_mode_(call.get_preset_mode()); this->write_state_(); this->publish_state(); diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 57bd7954169..801593c2ac7 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -29,7 +29,7 @@ void SpeedFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); - this->preset_mode = call.get_preset_mode(); + this->set_preset_mode_(call.get_preset_mode()); this->write_state_(); this->publish_state(); diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 5f4a2ae8f77..eba4c673b59 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -29,7 +29,7 @@ void TemplateFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value() && this->has_direction_) this->direction = *call.get_direction(); - this->preset_mode = call.get_preset_mode(); + this->set_preset_mode_(call.get_preset_mode()); this->publish_state(); } From c5e5609e926d4ebb9144638aeaef638b78c17105 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 5 Nov 2025 08:00:12 +1000 Subject: [PATCH 074/461] [lvgl] Fix case sensitivity in flex layout (#11717) --- esphome/components/lvgl/layout.py | 39 +++++++++++++++++++------ tests/components/lvgl/lvgl-package.yaml | 6 ++-- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py index 0aed525e169..a6aa816fda6 100644 --- a/esphome/components/lvgl/layout.py +++ b/esphome/components/lvgl/layout.py @@ -1,4 +1,5 @@ import re +import textwrap import esphome.config_validation as cv from esphome.const import CONF_HEIGHT, CONF_TYPE, CONF_WIDTH @@ -122,7 +123,7 @@ class FlexLayout(Layout): def get_layout_schemas(self, config: dict) -> tuple: layout = config.get(CONF_LAYOUT) - if not isinstance(layout, dict) or layout.get(CONF_TYPE) != TYPE_FLEX: + if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_FLEX: return None, {} child_schema = FLEX_OBJ_SCHEMA if grow := layout.get(CONF_FLEX_GROW): @@ -161,6 +162,8 @@ class DirectionalLayout(FlexLayout): return self.direction def get_layout_schemas(self, config: dict) -> tuple: + if not isinstance(config.get(CONF_LAYOUT), str): + return None, {} if config.get(CONF_LAYOUT, "").lower() != self.direction: return None, {} return cv.one_of(self.direction, lower=True), flex_hv_schema(self.direction) @@ -206,7 +209,7 @@ class GridLayout(Layout): # Not a valid grid layout string return None, {} - if not isinstance(layout, dict) or layout.get(CONF_TYPE) != TYPE_GRID: + if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_GRID: return None, {} return ( { @@ -259,7 +262,7 @@ class GridLayout(Layout): ) # should be guaranteed to be a dict at this point assert isinstance(layout, dict) - assert layout.get(CONF_TYPE) == TYPE_GRID + assert layout.get(CONF_TYPE).lower() == TYPE_GRID rows = len(layout[CONF_GRID_ROWS]) columns = len(layout[CONF_GRID_COLUMNS]) used_cells = [[None] * columns for _ in range(rows)] @@ -335,6 +338,17 @@ def append_layout_schema(schema, config: dict): if CONF_LAYOUT not in config: # If no layout is specified, return the schema as is return schema.extend({cv.Optional(CONF_WIDGETS): any_widget_schema()}) + layout = config[CONF_LAYOUT] + # Sanity check the layout to avoid redundant checks in each type + if not isinstance(layout, str) and not isinstance(layout, dict): + raise cv.Invalid( + "The 'layout' option must be a string or a dictionary", [CONF_LAYOUT] + ) + if isinstance(layout, dict) and not isinstance(layout.get(CONF_TYPE), str): + raise cv.Invalid( + "Invalid layout type; must be a string ('flex' or 'grid')", + [CONF_LAYOUT, CONF_TYPE], + ) for layout_class in LAYOUT_CLASSES: layout_schema, child_schema = layout_class.get_layout_schemas(config) @@ -348,10 +362,17 @@ def append_layout_schema(schema, config: dict): layout_schema.add_extra(layout_class.validate) return layout_schema.extend(schema) - # If no layout class matched, return a default schema - return cv.Schema( - { - cv.Optional(CONF_LAYOUT): cv.one_of(*LAYOUT_CHOICES, lower=True), - cv.Optional(CONF_WIDGETS): any_widget_schema(), - } + if isinstance(layout, dict): + raise cv.Invalid( + "Invalid layout type; must be 'flex' or 'grid'", [CONF_LAYOUT, CONF_TYPE] + ) + raise cv.Invalid( + textwrap.dedent( + """ + Invalid 'layout' value + layout choices are 'horizontal', 'vertical', 'x', + or a dictionary with a 'type' key + """ + ), + [CONF_LAYOUT], ) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index ca669c16e4e..8ac9a60e2d7 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -191,7 +191,7 @@ lvgl: args: ['lv_event_code_name_for(event->code).c_str()'] skip: true layout: - type: flex + type: Flex pad_row: 4 pad_column: 4px flex_align_main: center @@ -863,7 +863,7 @@ lvgl: width: 100% pad_all: 8 layout: - type: grid + type: GRid grid_row_align: end grid_rows: [25px, fr(1), content] grid_columns: [40, fr(1), fr(1)] @@ -1014,7 +1014,7 @@ lvgl: r_mod: -20 opa: 0% - id: page3 - layout: horizontal + layout: Horizontal widgets: - keyboard: id: lv_keyboard From c7ae42461331d1fdeb73c9a042fabc1ab6c954a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 16:14:54 -0600 Subject: [PATCH 075/461] [display] Optimize display writers with function pointers for stateless lambdas (#11629) --- esphome/components/display/display.h | 114 +++++++++++++++++- .../components/lcd_gpio/gpio_lcd_display.h | 9 +- .../components/lcd_pcf8574/pcf8574_display.h | 9 +- esphome/components/max7219/max7219.h | 5 +- .../components/max7219digit/max7219digit.h | 4 +- esphome/components/nextion/nextion.h | 5 +- .../pvvx_mithermometer/display/pvvx_display.h | 5 +- esphome/components/st7920/st7920.h | 4 +- esphome/components/tm1621/tm1621.h | 5 +- esphome/components/tm1637/tm1637.h | 5 +- esphome/components/tm1638/tm1638.h | 5 +- tests/components/image/test.host.yaml | 1 + tests/components/sdl/common.yaml | 3 + 13 files changed, 152 insertions(+), 22 deletions(-) diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 14205da8535..97b0a4e8d70 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -176,7 +176,117 @@ class Display; class DisplayPage; class DisplayOnPageChangeTrigger; -using display_writer_t = std::function; +/** Optimized display writer that uses function pointers for stateless lambdas. + * + * Similar to TemplatableValue but specialized for display writer callbacks. + * Saves ~8 bytes per stateless lambda on 32-bit platforms (16 bytes std::function → ~8 bytes discriminator+pointer). + * + * Supports both: + * - Stateless lambdas (from YAML) → function pointer (4 bytes) + * - Stateful lambdas/std::function (from C++ code) → std::function* (heap allocated) + * + * @tparam T The display type (e.g., Display, Nextion, GPIOLCDDisplay) + */ +template class DisplayWriter { + public: + DisplayWriter() : type_(NONE) {} + + // For stateless lambdas (convertible to function pointer): use function pointer (4 bytes) + template + DisplayWriter(F f) requires std::invocable && std::convertible_to + : type_(STATELESS_LAMBDA) { + this->stateless_f_ = f; // Implicit conversion to function pointer + } + + // For stateful lambdas and std::function (not convertible to function pointer): use std::function* (heap allocated) + // This handles backwards compatibility with external components + template + DisplayWriter(F f) requires std::invocable &&(!std::convertible_to) : type_(LAMBDA) { + this->f_ = new std::function(std::move(f)); + } + + // Copy constructor + DisplayWriter(const DisplayWriter &other) : type_(other.type_) { + if (type_ == LAMBDA) { + this->f_ = new std::function(*other.f_); + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; + } + } + + // Move constructor + DisplayWriter(DisplayWriter &&other) noexcept : type_(other.type_) { + if (type_ == LAMBDA) { + this->f_ = other.f_; + other.f_ = nullptr; + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; + } + other.type_ = NONE; + } + + // Assignment operators + DisplayWriter &operator=(const DisplayWriter &other) { + if (this != &other) { + this->~DisplayWriter(); + new (this) DisplayWriter(other); + } + return *this; + } + + DisplayWriter &operator=(DisplayWriter &&other) noexcept { + if (this != &other) { + this->~DisplayWriter(); + new (this) DisplayWriter(std::move(other)); + } + return *this; + } + + ~DisplayWriter() { + if (type_ == LAMBDA) { + delete this->f_; + } + // STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty) + } + + bool has_value() const { return this->type_ != NONE; } + + void call(T &display) const { + switch (this->type_) { + case STATELESS_LAMBDA: + this->stateless_f_(display); // Direct function pointer call + break; + case LAMBDA: + (*this->f_)(display); // std::function call + break; + case NONE: + default: + break; + } + } + + // Operator() for convenience + void operator()(T &display) const { this->call(display); } + + // Operator* for backwards compatibility with (*writer_)(*this) pattern + DisplayWriter &operator*() { return *this; } + const DisplayWriter &operator*() const { return *this; } + + protected: + enum : uint8_t { + NONE, + LAMBDA, + STATELESS_LAMBDA, + } type_; + + union { + std::function *f_; + void (*stateless_f_)(T &); + }; +}; + +// Type alias for Display writer - uses optimized DisplayWriter instead of std::function +using display_writer_t = DisplayWriter; #define LOG_DISPLAY(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -678,7 +788,7 @@ class Display : public PollingComponent { void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3); DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; - optional writer_{}; + display_writer_t writer_{}; DisplayPage *page_{nullptr}; DisplayPage *previous_page_{nullptr}; std::vector on_page_change_triggers_; diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.h b/esphome/components/lcd_gpio/gpio_lcd_display.h index aba254a90a4..81e4dc51a0c 100644 --- a/esphome/components/lcd_gpio/gpio_lcd_display.h +++ b/esphome/components/lcd_gpio/gpio_lcd_display.h @@ -2,13 +2,18 @@ #include "esphome/core/hal.h" #include "esphome/components/lcd_base/lcd_display.h" +#include "esphome/components/display/display.h" namespace esphome { namespace lcd_gpio { +class GPIOLCDDisplay; + +using gpio_lcd_writer_t = display::DisplayWriter; + class GPIOLCDDisplay : public lcd_base::LCDDisplay { public: - void set_writer(std::function &&writer) { this->writer_ = std::move(writer); } + void set_writer(gpio_lcd_writer_t &&writer) { this->writer_ = std::move(writer); } void setup() override; void set_data_pins(GPIOPin *d0, GPIOPin *d1, GPIOPin *d2, GPIOPin *d3) { this->data_pins_[0] = d0; @@ -43,7 +48,7 @@ class GPIOLCDDisplay : public lcd_base::LCDDisplay { GPIOPin *rw_pin_{nullptr}; GPIOPin *enable_pin_{nullptr}; GPIOPin *data_pins_[8]{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; - std::function writer_; + gpio_lcd_writer_t writer_; }; } // namespace lcd_gpio diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.h b/esphome/components/lcd_pcf8574/pcf8574_display.h index 4db3afb9b01..672b6090368 100644 --- a/esphome/components/lcd_pcf8574/pcf8574_display.h +++ b/esphome/components/lcd_pcf8574/pcf8574_display.h @@ -3,13 +3,18 @@ #include "esphome/core/component.h" #include "esphome/components/lcd_base/lcd_display.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/display/display.h" namespace esphome { namespace lcd_pcf8574 { +class PCF8574LCDDisplay; + +using pcf8574_lcd_writer_t = display::DisplayWriter; + class PCF8574LCDDisplay : public lcd_base::LCDDisplay, public i2c::I2CDevice { public: - void set_writer(std::function &&writer) { this->writer_ = std::move(writer); } + void set_writer(pcf8574_lcd_writer_t &&writer) { this->writer_ = std::move(writer); } void setup() override; void dump_config() override; void backlight(); @@ -24,7 +29,7 @@ class PCF8574LCDDisplay : public lcd_base::LCDDisplay, public i2c::I2CDevice { // Stores the current state of the backlight. uint8_t backlight_value_; - std::function writer_; + pcf8574_lcd_writer_t writer_; }; } // namespace lcd_pcf8574 diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h index 270edf3282f..58d871d54c7 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -4,13 +4,14 @@ #include "esphome/core/time.h" #include "esphome/components/spi/spi.h" +#include "esphome/components/display/display.h" namespace esphome { namespace max7219 { class MAX7219Component; -using max7219_writer_t = std::function; +using max7219_writer_t = display::DisplayWriter; class MAX7219Component : public PollingComponent, public spi::SPIDevice writer_{}; + max7219_writer_t writer_{}; }; } // namespace max7219 diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index ead80338035..af419b9b382 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -23,7 +23,7 @@ enum ScrollMode { class MAX7219Component; -using max7219_writer_t = std::function; +using max7219_writer_t = display::DisplayWriter; class MAX7219Component : public display::DisplayBuffer, public spi::SPIDevice writer_local_{}; + max7219_writer_t writer_local_{}; }; } // namespace max7219digit diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c078ab9d568..61068b52fc4 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -9,6 +9,7 @@ #include "esphome/components/uart/uart.h" #include "nextion_base.h" #include "nextion_component.h" +#include "esphome/components/display/display.h" #include "esphome/components/display/display_color_utils.h" #ifdef USE_NEXTION_TFT_UPLOAD @@ -31,7 +32,7 @@ namespace nextion { class Nextion; class NextionComponentBase; -using nextion_writer_t = std::function; +using nextion_writer_t = display::DisplayWriter; static const std::string COMMAND_DELIMITER{static_cast(255), static_cast(255), static_cast(255)}; @@ -1471,7 +1472,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe CallbackManager touch_callback_{}; CallbackManager buffer_overflow_callback_{}; - optional writer_; + nextion_writer_t writer_; optional brightness_; #ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.h b/esphome/components/pvvx_mithermometer/display/pvvx_display.h index c7fc5234206..8637506baef 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.h +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/display/display.h" #include @@ -29,7 +30,7 @@ enum UNIT { UNIT_DEG_E, ///< show "°E" }; -using pvvx_writer_t = std::function; +using pvvx_writer_t = display::DisplayWriter; class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { public: @@ -126,7 +127,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { esp32_ble_tracker::ESPBTUUID char_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw("00001f1f-0000-1000-8000-00805f9b34fb"); - optional writer_{}; + pvvx_writer_t writer_{}; }; } // namespace pvvx_mithermometer diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h index c9fdad454d3..c48fe8cc1c3 100644 --- a/esphome/components/st7920/st7920.h +++ b/esphome/components/st7920/st7920.h @@ -9,7 +9,7 @@ namespace st7920 { class ST7920; -using st7920_writer_t = std::function; +using st7920_writer_t = display::DisplayWriter; class ST7920 : public display::DisplayBuffer, public spi::SPIDevice writer_local_{}; + st7920_writer_t writer_local_{}; }; } // namespace st7920 diff --git a/esphome/components/tm1621/tm1621.h b/esphome/components/tm1621/tm1621.h index b9f330e96e3..fe923417a60 100644 --- a/esphome/components/tm1621/tm1621.h +++ b/esphome/components/tm1621/tm1621.h @@ -3,13 +3,14 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/components/display/display.h" namespace esphome { namespace tm1621 { class TM1621Display; -using tm1621_writer_t = std::function; +using tm1621_writer_t = display::DisplayWriter; class TM1621Display : public PollingComponent { public: @@ -59,7 +60,7 @@ class TM1621Display : public PollingComponent { GPIOPin *cs_pin_; GPIOPin *read_pin_; GPIOPin *write_pin_; - optional writer_{}; + tm1621_writer_t writer_{}; char row_[2][12]; uint8_t state_; uint8_t device_; diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index d44680c6231..b9e96119e9b 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -4,6 +4,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/time.h" +#include "esphome/components/display/display.h" #include @@ -19,7 +20,7 @@ class TM1637Display; class TM1637Key; #endif -using tm1637_writer_t = std::function; +using tm1637_writer_t = display::DisplayWriter; class TM1637Display : public PollingComponent { public: @@ -78,7 +79,7 @@ class TM1637Display : public PollingComponent { uint8_t length_; bool inverted_; bool on_{true}; - optional writer_{}; + tm1637_writer_t writer_{}; uint8_t buffer_[6] = {0}; #ifdef USE_BINARY_SENSOR std::vector tm1637_keys_{}; diff --git a/esphome/components/tm1638/tm1638.h b/esphome/components/tm1638/tm1638.h index add72cfdf38..f6b2922ecfb 100644 --- a/esphome/components/tm1638/tm1638.h +++ b/esphome/components/tm1638/tm1638.h @@ -5,6 +5,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/time.h" +#include "esphome/components/display/display.h" #include @@ -18,7 +19,7 @@ class KeyListener { class TM1638Component; -using tm1638_writer_t = std::function; +using tm1638_writer_t = display::DisplayWriter; class TM1638Component : public PollingComponent { public: @@ -70,7 +71,7 @@ class TM1638Component : public PollingComponent { GPIOPin *stb_pin_; GPIOPin *dio_pin_; uint8_t *buffer_ = new uint8_t[8]; - optional writer_{}; + tm1638_writer_t writer_{}; std::vector listeners_{}; }; diff --git a/tests/components/image/test.host.yaml b/tests/components/image/test.host.yaml index 0411195e2a3..aa454970882 100644 --- a/tests/components/image/test.host.yaml +++ b/tests/components/image/test.host.yaml @@ -1,5 +1,6 @@ display: - platform: sdl + id: image_display auto_clear_enabled: false dimensions: width: 480 diff --git a/tests/components/sdl/common.yaml b/tests/components/sdl/common.yaml index 52991d595c0..66f93915b6f 100644 --- a/tests/components/sdl/common.yaml +++ b/tests/components/sdl/common.yaml @@ -13,11 +13,14 @@ display: binary_sensor: - platform: sdl + sdl_id: sdl_display id: key_up key: SDLK_UP - platform: sdl + sdl_id: sdl_display id: key_down key: SDLK_DOWN - platform: sdl + sdl_id: sdl_display id: key_enter key: SDLK_RETURN From 6f7e54c3f3ba979eee8f4200a8b298a7dfd61b68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 16:33:01 -0600 Subject: [PATCH 076/461] [select] Refactor to index-based operations for immediate and future RAM savings (#11623) --- esphome/components/api/api_connection.cpp | 2 +- .../components/copy/select/copy_select.cpp | 8 +- esphome/components/copy/select/copy_select.h | 2 +- .../display_menu_base/menu_item.cpp | 2 +- esphome/components/ld2410/ld2410.cpp | 14 +- esphome/components/ld2410/ld2410.h | 4 +- .../ld2410/select/baud_rate_select.cpp | 6 +- .../ld2410/select/baud_rate_select.h | 2 +- .../select/distance_resolution_select.cpp | 6 +- .../select/distance_resolution_select.h | 2 +- .../select/light_out_control_select.cpp | 4 +- .../ld2410/select/light_out_control_select.h | 2 +- esphome/components/ld2412/ld2412.cpp | 14 +- esphome/components/ld2412/ld2412.h | 4 +- .../ld2412/select/baud_rate_select.cpp | 6 +- .../ld2412/select/baud_rate_select.h | 2 +- .../select/distance_resolution_select.cpp | 6 +- .../select/distance_resolution_select.h | 2 +- .../select/light_out_control_select.cpp | 4 +- .../ld2412/select/light_out_control_select.h | 2 +- esphome/components/ld2420/ld2420.cpp | 6 +- esphome/components/ld2420/ld2420.h | 2 +- .../ld2420/select/operating_mode_select.cpp | 6 +- .../ld2420/select/operating_mode_select.h | 2 +- esphome/components/ld2450/ld2450.cpp | 12 +- esphome/components/ld2450/ld2450.h | 4 +- .../ld2450/select/baud_rate_select.cpp | 6 +- .../ld2450/select/baud_rate_select.h | 2 +- .../ld2450/select/zone_type_select.cpp | 6 +- .../ld2450/select/zone_type_select.h | 2 +- .../logger/select/logger_level_select.cpp | 13 +- .../logger/select/logger_level_select.h | 2 +- esphome/components/mqtt/mqtt_select.cpp | 5 +- .../seeed_mr24hpc1/seeed_mr24hpc1.cpp | 10 +- .../seeed_mr60fda2/seeed_mr60fda2.cpp | 6 +- esphome/components/select/select.cpp | 49 ++++-- esphome/components/select/select.h | 49 +++++- esphome/components/select/select_call.cpp | 159 +++++++++--------- esphome/components/select/select_call.h | 6 +- .../template/select/template_select.cpp | 14 +- .../template/select/template_select.h | 2 +- .../components/tuya/select/tuya_select.cpp | 3 +- esphome/components/web_server/web_server.cpp | 10 +- esphome/components/web_server/web_server.h | 2 +- 44 files changed, 268 insertions(+), 204 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 3f1a0760076..5ab8a6eb050 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -877,7 +877,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection bool is_single) { auto *select = static_cast(entity); SelectStateResponse resp; - resp.set_state(StringRef(select->state)); + resp.set_state(StringRef(select->current_option())); resp.missing_state = !select->has_state(); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index bdcbd0b42c1..e45338e7857 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -7,19 +7,19 @@ namespace copy { static const char *const TAG = "copy.select"; void CopySelect::setup() { - source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); + source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); }); traits.set_options(source_->traits.get_options()); if (source_->has_state()) - this->publish_state(source_->state); + this->publish_state(source_->active_index().value()); } void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } -void CopySelect::control(const std::string &value) { +void CopySelect::control(size_t index) { auto call = source_->make_call(); - call.set_option(value); + call.set_index(index); call.perform(); } diff --git a/esphome/components/copy/select/copy_select.h b/esphome/components/copy/select/copy_select.h index fb0aee86f62..bd74a93e820 100644 --- a/esphome/components/copy/select/copy_select.h +++ b/esphome/components/copy/select/copy_select.h @@ -13,7 +13,7 @@ class CopySelect : public select::Select, public Component { void dump_config() override; protected: - void control(const std::string &value) override; + void control(size_t index) override; select::Select *source_; }; diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index 2c7f34c493d..8224adf3fe9 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -42,7 +42,7 @@ std::string MenuItemSelect::get_value_text() const { result = this->value_getter_.value()(this); } else { if (this->select_var_ != nullptr) { - result = this->select_var_->state; + result = this->select_var_->current_option(); } } diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 5c3af54ad82..608882565fd 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -121,9 +121,9 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { }; // Helper functions for lookups -template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) { for (const auto &entry : arr) { - if (str == entry.str) + if (strcmp(str, entry.str) == 0) return entry.value; } return 0xFF; // Not found @@ -441,7 +441,7 @@ bool LD2410Component::handle_ack_data_() { ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); + ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); } #endif break; @@ -626,14 +626,14 @@ void LD2410Component::set_bluetooth(bool enable) { this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } -void LD2410Component::set_distance_resolution(const std::string &state) { +void LD2410Component::set_distance_resolution(const char *state) { this->set_config_mode_(true); const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00}; this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } -void LD2410Component::set_baud_rate(const std::string &state) { +void LD2410Component::set_baud_rate(const char *state) { this->set_config_mode_(true); const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); @@ -759,10 +759,10 @@ void LD2410Component::set_light_out_control() { #endif #ifdef USE_SELECT if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { - this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state); + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option()); } if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) { - this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state); + this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()); } #endif this->set_config_mode_(true); diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 54fe1ce14df..52cf76b5b63 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -98,8 +98,8 @@ class LD2410Component : public Component, public uart::UARTDevice { void read_all_info(); void restart_and_read_all_info(); void set_bluetooth(bool enable); - void set_distance_resolution(const std::string &state); - void set_baud_rate(const std::string &state); + void set_distance_resolution(const char *state); + void set_baud_rate(const char *state); void factory_reset(); protected: diff --git a/esphome/components/ld2410/select/baud_rate_select.cpp b/esphome/components/ld2410/select/baud_rate_select.cpp index f4e0b90e2e4..6da7c1d5f53 100644 --- a/esphome/components/ld2410/select/baud_rate_select.cpp +++ b/esphome/components/ld2410/select/baud_rate_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace ld2410 { -void BaudRateSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_baud_rate(state); +void BaudRateSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_baud_rate(this->option_at(index)); } } // namespace ld2410 diff --git a/esphome/components/ld2410/select/baud_rate_select.h b/esphome/components/ld2410/select/baud_rate_select.h index 3827b6a48ae..9385c8cf7e6 100644 --- a/esphome/components/ld2410/select/baud_rate_select.h +++ b/esphome/components/ld2410/select/baud_rate_select.h @@ -11,7 +11,7 @@ class BaudRateSelect : public select::Select, public Parented { BaudRateSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.cpp b/esphome/components/ld2410/select/distance_resolution_select.cpp index eef34bda631..4fc4c5af021 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.cpp +++ b/esphome/components/ld2410/select/distance_resolution_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace ld2410 { -void DistanceResolutionSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_distance_resolution(state); +void DistanceResolutionSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_distance_resolution(this->option_at(index)); } } // namespace ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.h b/esphome/components/ld2410/select/distance_resolution_select.h index d6affb10205..1a04f843a6f 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.h +++ b/esphome/components/ld2410/select/distance_resolution_select.h @@ -11,7 +11,7 @@ class DistanceResolutionSelect : public select::Select, public Parentedpublish_state(value); +void LightOutControlSelect::control(size_t index) { + this->publish_state(index); this->parent_->set_light_out_control(); } diff --git a/esphome/components/ld2410/select/light_out_control_select.h b/esphome/components/ld2410/select/light_out_control_select.h index 5d72e1774e3..e8cd8f1d6ac 100644 --- a/esphome/components/ld2410/select/light_out_control_select.h +++ b/esphome/components/ld2410/select/light_out_control_select.h @@ -11,7 +11,7 @@ class LightOutControlSelect : public select::Select, public Parented uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) { for (const auto &entry : arr) { - if (str == entry.str) { + if (strcmp(str, entry.str) == 0) { return entry.value; } } @@ -485,7 +485,7 @@ bool LD2412Component::handle_ack_data_() { ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); + ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); } #endif break; @@ -699,14 +699,14 @@ void LD2412Component::set_bluetooth(bool enable) { this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } -void LD2412Component::set_distance_resolution(const std::string &state) { +void LD2412Component::set_distance_resolution(const char *state) { this->set_config_mode_(true); const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00, 0x00, 0x00, 0x00, 0x00}; this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } -void LD2412Component::set_baud_rate(const std::string &state) { +void LD2412Component::set_baud_rate(const char *state) { this->set_config_mode_(true); const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); @@ -783,7 +783,7 @@ void LD2412Component::set_basic_config() { 1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0, #endif #ifdef USE_SELECT - find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state), + find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()), #else 0x01, // Default value if not using select #endif @@ -837,7 +837,7 @@ void LD2412Component::set_light_out_control() { #endif #ifdef USE_SELECT if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { - this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state); + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option()); } #endif uint8_t value[2] = {this->light_function_, this->light_threshold_}; diff --git a/esphome/components/ld2412/ld2412.h b/esphome/components/ld2412/ld2412.h index 41f96ab3016..2bed34bdd8d 100644 --- a/esphome/components/ld2412/ld2412.h +++ b/esphome/components/ld2412/ld2412.h @@ -99,8 +99,8 @@ class LD2412Component : public Component, public uart::UARTDevice { void read_all_info(); void restart_and_read_all_info(); void set_bluetooth(bool enable); - void set_distance_resolution(const std::string &state); - void set_baud_rate(const std::string &state); + void set_distance_resolution(const char *state); + void set_baud_rate(const char *state); void factory_reset(); void start_dynamic_background_correction(); diff --git a/esphome/components/ld2412/select/baud_rate_select.cpp b/esphome/components/ld2412/select/baud_rate_select.cpp index 2291a818963..7bc46838532 100644 --- a/esphome/components/ld2412/select/baud_rate_select.cpp +++ b/esphome/components/ld2412/select/baud_rate_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace ld2412 { -void BaudRateSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_baud_rate(state); +void BaudRateSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_baud_rate(this->option_at(index)); } } // namespace ld2412 diff --git a/esphome/components/ld2412/select/baud_rate_select.h b/esphome/components/ld2412/select/baud_rate_select.h index 2ae33551fb5..ffe0329341c 100644 --- a/esphome/components/ld2412/select/baud_rate_select.h +++ b/esphome/components/ld2412/select/baud_rate_select.h @@ -11,7 +11,7 @@ class BaudRateSelect : public select::Select, public Parented { BaudRateSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.cpp b/esphome/components/ld2412/select/distance_resolution_select.cpp index a282215fbde..5a6f46a0713 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.cpp +++ b/esphome/components/ld2412/select/distance_resolution_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace ld2412 { -void DistanceResolutionSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_distance_resolution(state); +void DistanceResolutionSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_distance_resolution(this->option_at(index)); } } // namespace ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.h b/esphome/components/ld2412/select/distance_resolution_select.h index 0658f5d1a76..842f63b7b1c 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.h +++ b/esphome/components/ld2412/select/distance_resolution_select.h @@ -11,7 +11,7 @@ class DistanceResolutionSelect : public select::Select, public Parentedpublish_state(value); +void LightOutControlSelect::control(size_t index) { + this->publish_state(index); this->parent_->set_light_out_control(); } diff --git a/esphome/components/ld2412/select/light_out_control_select.h b/esphome/components/ld2412/select/light_out_control_select.h index a71bab1e14d..7a50970d0de 100644 --- a/esphome/components/ld2412/select/light_out_control_select.h +++ b/esphome/components/ld2412/select/light_out_control_select.h @@ -11,7 +11,7 @@ class LightOutControlSelect : public select::Select, public Parentedtotal_sample_number_counter); } -void LD2420Component::set_operating_mode(const std::string &state) { +void LD2420Component::set_operating_mode(const char *state) { // If unsupported firmware ignore mode select if (ld2420::get_firmware_int(firmware_ver_) >= CALIBRATE_VERSION_MIN) { this->current_operating_mode = find_uint8(OP_MODE_BY_STR, state); diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 812c408cfde..128baab604d 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -107,7 +107,7 @@ class LD2420Component : public Component, public uart::UARTDevice { int send_cmd_from_array(CmdFrameT cmd_frame); void report_gate_data(); void handle_cmd_error(uint8_t error); - void set_operating_mode(const std::string &state); + void set_operating_mode(const char *state); void auto_calibrate_sensitivity(); void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number); uint8_t set_config_mode(bool enable); diff --git a/esphome/components/ld2420/select/operating_mode_select.cpp b/esphome/components/ld2420/select/operating_mode_select.cpp index 2d576e7cc65..5bf80b33c99 100644 --- a/esphome/components/ld2420/select/operating_mode_select.cpp +++ b/esphome/components/ld2420/select/operating_mode_select.cpp @@ -7,9 +7,9 @@ namespace ld2420 { static const char *const TAG = "ld2420.select"; -void LD2420Select::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_operating_mode(value); +void LD2420Select::control(size_t index) { + this->publish_state(index); + this->parent_->set_operating_mode(this->option_at(index)); } } // namespace ld2420 diff --git a/esphome/components/ld2420/select/operating_mode_select.h b/esphome/components/ld2420/select/operating_mode_select.h index 317b2af8c0f..f59eb334326 100644 --- a/esphome/components/ld2420/select/operating_mode_select.h +++ b/esphome/components/ld2420/select/operating_mode_select.h @@ -11,7 +11,7 @@ class LD2420Select : public Component, public select::Select, public Parentedset_config_mode_(false); #ifdef USE_SELECT const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); - if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) { + if (this->baud_rate_select_ != nullptr && strcmp(this->baud_rate_select_->current_option(), baud_rate.c_str()) != 0) { this->baud_rate_select_->publish_state(baud_rate); } this->publish_zone_type(); @@ -635,7 +635,7 @@ bool LD2450Component::handle_ack_data_() { ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); + ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); } #endif break; @@ -716,7 +716,7 @@ bool LD2450Component::handle_ack_data_() { this->publish_zone_type(); #ifdef USE_SELECT if (this->zone_type_select_ != nullptr) { - ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str()); + ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->current_option()); } #endif if (this->buffer_data_[10] == 0x00) { @@ -790,7 +790,7 @@ void LD2450Component::set_bluetooth(bool enable) { } // Set Baud rate -void LD2450Component::set_baud_rate(const std::string &state) { +void LD2450Component::set_baud_rate(const char *state) { this->set_config_mode_(true); const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); @@ -798,8 +798,8 @@ 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()); +void LD2450Component::set_zone_type(const char *state) { + ESP_LOGV(TAG, "Set zone type: %s", state); uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state); this->zone_type_ = zone_type; this->send_set_zone_command_(); diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 9faa1890190..44b63be4442 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -115,8 +115,8 @@ class LD2450Component : public Component, public uart::UARTDevice { void restart_and_read_all_info(); void set_bluetooth(bool enable); void set_multi_target(bool enable); - void set_baud_rate(const std::string &state); - void set_zone_type(const std::string &state); + void set_baud_rate(const char *state); + void set_zone_type(const char *state); void publish_zone_type(); void factory_reset(); #ifdef USE_TEXT_SENSOR diff --git a/esphome/components/ld2450/select/baud_rate_select.cpp b/esphome/components/ld2450/select/baud_rate_select.cpp index 06439aaa751..754972214e4 100644 --- a/esphome/components/ld2450/select/baud_rate_select.cpp +++ b/esphome/components/ld2450/select/baud_rate_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace ld2450 { -void BaudRateSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_baud_rate(state); +void BaudRateSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_baud_rate(this->option_at(index)); } } // namespace ld2450 diff --git a/esphome/components/ld2450/select/baud_rate_select.h b/esphome/components/ld2450/select/baud_rate_select.h index 04fe65b4fd9..22810d5f132 100644 --- a/esphome/components/ld2450/select/baud_rate_select.h +++ b/esphome/components/ld2450/select/baud_rate_select.h @@ -11,7 +11,7 @@ class BaudRateSelect : public select::Select, public Parented { BaudRateSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.cpp b/esphome/components/ld2450/select/zone_type_select.cpp index a9f6155142b..1111428c7c2 100644 --- a/esphome/components/ld2450/select/zone_type_select.cpp +++ b/esphome/components/ld2450/select/zone_type_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace ld2450 { -void ZoneTypeSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_zone_type(state); +void ZoneTypeSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_zone_type(this->option_at(index)); } } // namespace ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.h b/esphome/components/ld2450/select/zone_type_select.h index 8aafeb6bebf..fc95ec10216 100644 --- a/esphome/components/ld2450/select/zone_type_select.h +++ b/esphome/components/ld2450/select/zone_type_select.h @@ -11,7 +11,7 @@ class ZoneTypeSelect : public select::Select, public Parented { ZoneTypeSelect() = default; protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace ld2450 diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index 6d60a3ae471..e2ec28a3908 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -3,10 +3,10 @@ namespace esphome::logger { void LoggerLevelSelect::publish_state(int level) { - const auto &option = this->at(level_to_index(level)); - if (!option) + auto index = level_to_index(level); + if (!this->has_index(index)) return; - Select::publish_state(option.value()); + Select::publish_state(index); } void LoggerLevelSelect::setup() { @@ -14,11 +14,6 @@ void LoggerLevelSelect::setup() { this->publish_state(this->parent_->get_log_level()); } -void LoggerLevelSelect::control(const std::string &value) { - const auto index = this->index_of(value); - if (!index) - return; - this->parent_->set_log_level(index_to_level(index.value())); -} +void LoggerLevelSelect::control(size_t index) { this->parent_->set_log_level(index_to_level(index)); } } // namespace esphome::logger diff --git a/esphome/components/logger/select/logger_level_select.h b/esphome/components/logger/select/logger_level_select.h index 0631eca45d6..950edd29ac4 100644 --- a/esphome/components/logger/select/logger_level_select.h +++ b/esphome/components/logger/select/logger_level_select.h @@ -9,7 +9,7 @@ class LoggerLevelSelect : public Component, public select::Select, public Parent public: void publish_state(int level); void setup() override; - void control(const std::string &value) override; + void control(size_t index) override; protected: // Convert log level to option index (skip CONFIG at level 4) diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index b8513483063..e1660b07eac 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -21,7 +21,8 @@ void MQTTSelectComponent::setup() { call.set_option(state); call.perform(); }); - this->select_->add_on_state_callback([this](const std::string &state, size_t index) { this->publish_state(state); }); + this->select_->add_on_state_callback( + [this](const std::string &state, size_t index) { this->publish_state(this->select_->option_at(index)); }); } void MQTTSelectComponent::dump_config() { @@ -44,7 +45,7 @@ void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon } bool MQTTSelectComponent::send_initial_state() { if (this->select_->has_state()) { - return this->publish_state(this->select_->state); + return this->publish_state(this->select_->current_option()); } else { return true; } diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 76523ce5c00..4c0416d727f 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -435,12 +435,12 @@ void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *da } else if ((this->existence_boundary_select_ != nullptr) && ((data[FRAME_COMMAND_WORD_INDEX] == 0x0a) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8a))) { if (this->existence_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) { - this->existence_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]); + this->existence_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1); } } else if ((this->motion_boundary_select_ != nullptr) && ((data[FRAME_COMMAND_WORD_INDEX] == 0x0b) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8b))) { if (this->motion_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) { - this->motion_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]); + this->motion_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1); } } else if ((this->motion_trigger_number_ != nullptr) && ((data[FRAME_COMMAND_WORD_INDEX] == 0x0c) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8c))) { @@ -515,7 +515,7 @@ void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) { ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]); } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x07) { if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) { - this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]); + this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]); } else { ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]); } @@ -538,7 +538,7 @@ void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) { ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]); } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x87) { if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) { - this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]); + this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]); } else { ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]); } @@ -581,7 +581,7 @@ void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) { ((data[FRAME_COMMAND_WORD_INDEX] == 0x0A) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8A))) { // none:0x00 1s:0x01 30s:0x02 1min:0x03 2min:0x04 5min:0x05 10min:0x06 30min:0x07 1hour:0x08 if (data[FRAME_DATA_INDEX] < 9) { - this->unman_time_select_->publish_state(S_UNMANNED_TIME_STR[data[FRAME_DATA_INDEX]]); + this->unman_time_select_->publish_state(data[FRAME_DATA_INDEX]); } } else if ((this->keep_away_text_sensor_ != nullptr) && ((data[FRAME_COMMAND_WORD_INDEX] == 0x0B) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8B))) { diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index dea7976578e..7f8bd6a43c1 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -292,7 +292,7 @@ void MR60FDA2Component::process_frame_() { install_height_float = bit_cast(current_install_height_int); uint32_t select_index = find_nearest_index(install_height_float, INSTALL_HEIGHT, 7); - this->install_height_select_->publish_state(this->install_height_select_->at(select_index).value()); + this->install_height_select_->publish_state(select_index); } if (this->height_threshold_select_ != nullptr) { @@ -301,7 +301,7 @@ void MR60FDA2Component::process_frame_() { height_threshold_float = bit_cast(current_height_threshold_int); size_t select_index = find_nearest_index(height_threshold_float, HEIGHT_THRESHOLD, 7); - this->height_threshold_select_->publish_state(this->height_threshold_select_->at(select_index).value()); + this->height_threshold_select_->publish_state(select_index); } if (this->sensitivity_select_ != nullptr) { @@ -309,7 +309,7 @@ void MR60FDA2Component::process_frame_() { encode_uint32(current_data_buf_[11], current_data_buf_[10], current_data_buf_[9], current_data_buf_[8]); uint32_t select_index = find_nearest_index(current_sensitivity, SENSITIVITY, 3); - this->sensitivity_select_->publish_state(this->sensitivity_select_->at(select_index).value()); + this->sensitivity_select_->publish_state(select_index); } ESP_LOGD(TAG, "Mounting height: %.2f, Height threshold: %.2f, Sensitivity: %" PRIu32, install_height_float, diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 5e30be3c138..6bb01ba6e25 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -7,24 +7,43 @@ namespace select { static const char *const TAG = "select"; -void Select::publish_state(const std::string &state) { +void Select::publish_state(const std::string &state) { this->publish_state(state.c_str()); } + +void Select::publish_state(const char *state) { auto index = this->index_of(state); - const auto *name = this->get_name().c_str(); if (index.has_value()) { - this->set_has_state(true); - this->state = state; - ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value()); - this->state_callback_.call(state, index.value()); + this->publish_state(index.value()); } else { - ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str()); + ESP_LOGE(TAG, "'%s': Invalid option %s", this->get_name().c_str(), state); } } +void Select::publish_state(size_t index) { + if (!this->has_index(index)) { + ESP_LOGE(TAG, "'%s': Invalid index %zu", this->get_name().c_str(), index); + return; + } + const char *option = this->option_at(index); + this->set_has_state(true); + this->active_index_ = index; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + this->state = option; // Update deprecated member for backward compatibility +#pragma GCC diagnostic pop + ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); + // Callback signature requires std::string, create temporary for compatibility + this->state_callback_.call(std::string(option), index); +} + +const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; } + void Select::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -bool Select::has_option(const std::string &option) const { return this->index_of(option).has_value(); } +bool Select::has_option(const std::string &option) const { return this->index_of(option.c_str()).has_value(); } + +bool Select::has_option(const char *option) const { return this->index_of(option).has_value(); } bool Select::has_index(size_t index) const { return index < this->size(); } @@ -33,10 +52,12 @@ size_t Select::size() const { return options.size(); } -optional Select::index_of(const std::string &option) const { +optional Select::index_of(const std::string &option) const { return this->index_of(option.c_str()); } + +optional Select::index_of(const char *option) const { const auto &options = traits.get_options(); for (size_t i = 0; i < options.size(); i++) { - if (strcmp(options[i], option.c_str()) == 0) { + if (strcmp(options[i], option) == 0) { return i; } } @@ -45,19 +66,17 @@ optional Select::index_of(const std::string &option) const { optional Select::active_index() const { if (this->has_state()) { - return this->index_of(this->state); - } else { - return {}; + return this->active_index_; } + return {}; } optional Select::at(size_t index) const { if (this->has_index(index)) { const auto &options = traits.get_options(); return std::string(options.at(index)); - } else { - return {}; } + return {}; } const char *Select::option_at(size_t index) const { return traits.get_options().at(index); } diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index eabb39898bd..f859594cd18 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -30,16 +30,31 @@ namespace select { */ class Select : public EntityBase { public: - std::string state; SelectTraits traits; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + /// @deprecated Use current_option() instead. This member will be removed in ESPHome 2026.5.0. + __attribute__((deprecated("Use current_option() instead of .state. Will be removed in 2026.5.0"))) + std::string state{}; + + Select() = default; + ~Select() = default; +#pragma GCC diagnostic pop + void publish_state(const std::string &state); + void publish_state(const char *state); + void publish_state(size_t index); + + /// Return the currently selected option (as const char* from flash). + const char *current_option() const; /// Instantiate a SelectCall object to modify this select component's state. SelectCall make_call() { return SelectCall(this); } /// Return whether this select component contains the provided option. bool has_option(const std::string &option) const; + bool has_option(const char *option) const; /// Return whether this select component contains the provided index offset. bool has_index(size_t index) const; @@ -49,6 +64,7 @@ class Select : public EntityBase { /// Find the (optional) index offset of the provided option value. optional index_of(const std::string &option) const; + optional index_of(const char *option) const; /// Return the (optional) index offset of the currently active option. optional active_index() const; @@ -64,13 +80,36 @@ class Select : public EntityBase { protected: friend class SelectCall; - /** Set the value of the select, this is a virtual method that each select integration must implement. + size_t active_index_{0}; + + /** Set the value of the select by index, this is an optional virtual method. * - * This method is called by the SelectCall. + * IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes. + * Overriding this index-based version is PREFERRED as it avoids string conversions. * - * @param value The value as validated by the SelectCall. + * This method is called by the SelectCall when the index is already known. + * Default implementation converts to string and calls control(const std::string&). + * + * @param index The index as validated by the SelectCall. */ - virtual void control(const std::string &value) = 0; + virtual void control(size_t index) { this->control(this->option_at(index)); } + + /** Set the value of the select, this is a virtual method that each select integration can implement. + * + * IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes. + * Overriding control(size_t) is PREFERRED as it avoids string conversions. + * + * This method is called by control(size_t) when not overridden, or directly by external code. + * Default implementation converts to index and calls control(size_t). + * + * @param value The value as validated by the caller. + */ + virtual void control(const std::string &value) { + auto index = this->index_of(value); + if (index.has_value()) { + this->control(index.value()); + } + } CallbackManager state_callback_; }; diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index dd398b4052b..aa7559e24ea 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -7,19 +7,21 @@ namespace select { static const char *const TAG = "select"; -SelectCall &SelectCall::set_option(const std::string &option) { - return with_operation(SELECT_OP_SET).with_option(option); +SelectCall &SelectCall::set_option(const std::string &option) { return this->with_option(option); } + +SelectCall &SelectCall::set_option(const char *option) { return this->with_option(option); } + +SelectCall &SelectCall::set_index(size_t index) { return this->with_index(index); } + +SelectCall &SelectCall::select_next(bool cycle) { return this->with_operation(SELECT_OP_NEXT).with_cycle(cycle); } + +SelectCall &SelectCall::select_previous(bool cycle) { + return this->with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); } -SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); } +SelectCall &SelectCall::select_first() { return this->with_operation(SELECT_OP_FIRST); } -SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); } - -SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); } - -SelectCall &SelectCall::select_first() { return with_operation(SELECT_OP_FIRST); } - -SelectCall &SelectCall::select_last() { return with_operation(SELECT_OP_LAST); } +SelectCall &SelectCall::select_last() { return this->with_operation(SELECT_OP_LAST); } SelectCall &SelectCall::with_operation(SelectOperation operation) { this->operation_ = operation; @@ -31,89 +33,96 @@ SelectCall &SelectCall::with_cycle(bool cycle) { return *this; } -SelectCall &SelectCall::with_option(const std::string &option) { - this->option_ = option; +SelectCall &SelectCall::with_option(const std::string &option) { return this->with_option(option.c_str()); } + +SelectCall &SelectCall::with_option(const char *option) { + this->operation_ = SELECT_OP_SET; + // Find the option index - this validates the option exists + this->index_ = this->parent_->index_of(option); return *this; } SelectCall &SelectCall::with_index(size_t index) { - this->index_ = index; + this->operation_ = SELECT_OP_SET; + if (index >= this->parent_->size()) { + ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", this->parent_->get_name().c_str(), index); + this->index_ = {}; // Store nullopt for invalid index + } else { + this->index_ = index; + } return *this; } +optional SelectCall::calculate_target_index_(const char *name) { + const auto &options = this->parent_->traits.get_options(); + if (options.empty()) { + ESP_LOGW(TAG, "'%s' - Select has no options", name); + return {}; + } + + if (this->operation_ == SELECT_OP_FIRST) { + return 0; + } + + if (this->operation_ == SELECT_OP_LAST) { + return options.size() - 1; + } + + if (this->operation_ == SELECT_OP_SET) { + ESP_LOGD(TAG, "'%s' - Setting", name); + if (!this->index_.has_value()) { + ESP_LOGW(TAG, "'%s' - No option set", name); + return {}; + } + return this->index_.value(); + } + + // SELECT_OP_NEXT or SELECT_OP_PREVIOUS + ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, + this->operation_ == SELECT_OP_NEXT ? LOG_STR_LITERAL("next") : LOG_STR_LITERAL("previous"), + this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); + + const auto size = options.size(); + if (!this->parent_->has_state()) { + return this->operation_ == SELECT_OP_NEXT ? 0 : size - 1; + } + + // Use cached active_index_ instead of index_of() lookup + const auto active_index = this->parent_->active_index_; + if (this->cycle_) { + return (size + active_index + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; + } + + if (this->operation_ == SELECT_OP_PREVIOUS && active_index > 0) { + return active_index - 1; + } + + if (this->operation_ == SELECT_OP_NEXT && active_index < size - 1) { + return active_index + 1; + } + + return {}; // Can't navigate further without cycling +} + void SelectCall::perform() { auto *parent = this->parent_; const auto *name = parent->get_name().c_str(); - const auto &traits = parent->traits; - const auto &options = traits.get_options(); if (this->operation_ == SELECT_OP_NONE) { ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name); return; } - if (options.empty()) { - ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name); + + // Calculate target index (with_index() and with_option() already validate bounds/existence) + auto target_index = this->calculate_target_index_(name); + if (!target_index.has_value()) { return; } - std::string target_value; - - if (this->operation_ == SELECT_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting", name); - if (!this->option_.has_value()) { - ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); - return; - } - target_value = this->option_.value(); - } else if (this->operation_ == SELECT_OP_SET_INDEX) { - if (!this->index_.has_value()) { - ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name); - return; - } - if (this->index_.value() >= options.size()) { - ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, this->index_.value()); - return; - } - target_value = options[this->index_.value()]; - } else if (this->operation_ == SELECT_OP_FIRST) { - target_value = options.front(); - } else if (this->operation_ == SELECT_OP_LAST) { - target_value = options.back(); - } else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) { - auto cycle = this->cycle_; - ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", - cycle ? "" : "out"); - if (!parent->has_state()) { - target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); - } else { - auto index = parent->index_of(parent->state); - if (index.has_value()) { - auto size = options.size(); - if (cycle) { - auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; - target_value = options[use_index]; - } else { - if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) { - target_value = options[index.value() - 1]; - } else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) { - target_value = options[index.value() + 1]; - } else { - return; - } - } - } else { - target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); - } - } - } - - if (!parent->has_option(target_value)) { - ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str()); - return; - } - - ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str()); - parent->control(target_value); + auto idx = target_index.value(); + // All operations use indices, call control() by index to avoid string conversion + ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx)); + parent->control(idx); } } // namespace select diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index efc9a982ec8..eae7d3de1dc 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -10,7 +10,6 @@ class Select; enum SelectOperation { SELECT_OP_NONE, SELECT_OP_SET, - SELECT_OP_SET_INDEX, SELECT_OP_NEXT, SELECT_OP_PREVIOUS, SELECT_OP_FIRST, @@ -23,6 +22,7 @@ class SelectCall { void perform(); SelectCall &set_option(const std::string &option); + SelectCall &set_option(const char *option); SelectCall &set_index(size_t index); SelectCall &select_next(bool cycle); @@ -33,11 +33,13 @@ class SelectCall { SelectCall &with_operation(SelectOperation operation); SelectCall &with_cycle(bool cycle); SelectCall &with_option(const std::string &option); + SelectCall &with_option(const char *option); SelectCall &with_index(size_t index); protected: + __attribute__((always_inline)) inline optional calculate_target_index_(const char *name); + Select *const parent_; - optional option_; optional index_; SelectOperation operation_{SELECT_OP_NONE}; bool cycle_; diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 3ea34c3c7c8..112f24e9190 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -24,7 +24,7 @@ void TemplateSelect::setup() { ESP_LOGD(TAG, "State from initial: %s", this->option_at(index)); } - this->publish_state(this->at(index).value()); + this->publish_state(index); } void TemplateSelect::update() { @@ -41,16 +41,14 @@ void TemplateSelect::update() { } } -void TemplateSelect::control(const std::string &value) { - this->set_trigger_->trigger(value); +void TemplateSelect::control(size_t index) { + this->set_trigger_->trigger(std::string(this->option_at(index))); if (this->optimistic_) - this->publish_state(value); + this->publish_state(index); - if (this->restore_value_) { - auto index = this->index_of(value); - this->pref_.save(&index.value()); - } + if (this->restore_value_) + this->pref_.save(&index); } void TemplateSelect::dump_config() { diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 1c331538723..2dad059ade1 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -24,7 +24,7 @@ class TemplateSelect : public select::Select, public PollingComponent { void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } protected: - void control(const std::string &value) override; + void control(size_t index) override; bool optimistic_ = false; size_t initial_option_index_{0}; bool restore_value_ = false; diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 7c1cd09d062..07e3ce44eed 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -17,8 +17,7 @@ void TuyaSelect::setup() { return; } size_t mapping_idx = std::distance(mappings.cbegin(), it); - auto value = this->at(mapping_idx); - this->publish_state(value.value()); + this->publish_state(mapping_idx); }); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index acc0f33e610..f1d1a75875a 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1188,7 +1188,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); - std::string data = this->select_json(obj, obj->state, detail); + std::string data = this->select_json(obj, obj->has_state() ? obj->current_option() : "", detail); request->send(200, "application/json", data.c_str()); return; } @@ -1208,12 +1208,14 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { - return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->state, DETAIL_STATE); + auto *obj = (select::Select *) (source); + return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE); } std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { - return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->state, DETAIL_ALL); + auto *obj = (select::Select *) (source); + return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL); } -std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) { +std::string WebServer::select_json(select::Select *obj, const char *value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index fb790483dc6..328140cfae9 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -410,7 +410,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { static std::string select_state_json_generator(WebServer *web_server, void *source); static std::string select_all_json_generator(WebServer *web_server, void *source); /// Dump the select state with its value as a JSON string. - std::string select_json(select::Select *obj, const std::string &value, JsonDetail start_config); + std::string select_json(select::Select *obj, const char *value, JsonDetail start_config); #endif #ifdef USE_CLIMATE From 64f8963566092f382f2b8c1556a201d0f48252be Mon Sep 17 00:00:00 2001 From: Gnuspice Date: Wed, 5 Nov 2025 12:46:06 +1300 Subject: [PATCH 077/461] [const] Move `CONF_ENABLED` to const.py (#11719) --- esphome/components/const/__init__.py | 1 + esphome/components/modbus_controller/__init__.py | 2 +- esphome/components/modbus_controller/const.py | 1 - esphome/components/wireguard/binary_sensor.py | 3 +-- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 2b88bb43a8d..0c22b2d27e4 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -8,6 +8,7 @@ BYTE_ORDER_BIG = "big_endian" CONF_COLOR_DEPTH = "color_depth" CONF_DRAW_ROUNDING = "draw_rounding" +CONF_ENABLED = "enabled" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 28f3326c475..1c23783ce37 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -3,6 +3,7 @@ import binascii from esphome import automation import esphome.codegen as cg from esphome.components import modbus +from esphome.components.const import CONF_ENABLED import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -20,7 +21,6 @@ from .const import ( CONF_BYTE_OFFSET, CONF_COMMAND_THROTTLE, CONF_CUSTOM_COMMAND, - CONF_ENABLED, CONF_FORCE_NEW_RANGE, CONF_MAX_CMD_RETRIES, CONF_MODBUS_CONTROLLER_ID, diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py index ee0b5fc633f..c689d845765 100644 --- a/esphome/components/modbus_controller/const.py +++ b/esphome/components/modbus_controller/const.py @@ -2,7 +2,6 @@ CONF_ALLOW_DUPLICATE_COMMANDS = "allow_duplicate_commands" CONF_BITMASK = "bitmask" CONF_BYTE_OFFSET = "byte_offset" CONF_COMMAND_THROTTLE = "command_throttle" -CONF_ENABLED = "enabled" CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates" CONF_CUSTOM_COMMAND = "custom_command" CONF_FORCE_NEW_RANGE = "force_new_range" diff --git a/esphome/components/wireguard/binary_sensor.py b/esphome/components/wireguard/binary_sensor.py index 02c4862e8d9..2ba59d4c39d 100644 --- a/esphome/components/wireguard/binary_sensor.py +++ b/esphome/components/wireguard/binary_sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import binary_sensor +from esphome.components.const import CONF_ENABLED import esphome.config_validation as cv from esphome.const import ( CONF_STATUS, @@ -9,8 +10,6 @@ from esphome.const import ( from . import CONF_WIREGUARD_ID, Wireguard -CONF_ENABLED = "enabled" - DEPENDENCIES = ["wireguard"] CONFIG_SCHEMA = { From 1446e7174ac96a7158d9d51fb21020e965d72873 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 19:23:24 -0600 Subject: [PATCH 078/461] [core] Reduce action framework argument copies by 83% (#11704) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ags10/ags10.h | 4 +- esphome/components/aic3204/automation.h | 2 +- .../alarm_control_panel/automation.h | 14 ++--- esphome/components/animation/animation.h | 6 +- esphome/components/api/api_server.h | 2 +- .../components/api/homeassistant_service.h | 2 +- esphome/components/at581x/automation.h | 4 +- esphome/components/audio_adc/automation.h | 2 +- esphome/components/audio_dac/automation.h | 6 +- esphome/components/binary_sensor/automation.h | 6 +- esphome/components/bl0906/bl0906.h | 2 +- esphome/components/ble_client/automation.h | 18 +++--- esphome/components/button/automation.h | 2 +- esphome/components/canbus/canbus.h | 2 +- esphome/components/climate/automation.h | 2 +- esphome/components/cm1106/cm1106.h | 2 +- esphome/components/cover/automation.h | 16 ++--- esphome/components/cs5460a/cs5460a.h | 2 +- esphome/components/datetime/date_entity.h | 2 +- esphome/components/datetime/datetime_entity.h | 2 +- esphome/components/datetime/time_entity.h | 2 +- .../deep_sleep/deep_sleep_component.h | 6 +- esphome/components/dfplayer/dfplayer.h | 16 ++--- .../components/dfrobot_sen0395/automation.h | 4 +- esphome/components/display/display.h | 8 +-- .../components/display_menu_base/automation.h | 18 +++--- esphome/components/ds1307/ds1307.h | 4 +- .../components/duty_time/duty_time_sensor.h | 8 +-- esphome/components/esp32_ble/ble.h | 6 +- .../esp32_ble_server/ble_server_automations.h | 6 +- .../components/esp32_ble_tracker/automation.h | 4 +- esphome/components/esp8266_pwm/esp8266_pwm.h | 2 +- esphome/components/esp_ldo/esp_ldo.h | 2 +- esphome/components/espnow/automation.h | 10 ++-- esphome/components/event/automation.h | 2 +- esphome/components/ezo/automation.h | 10 ++-- esphome/components/ezo_pmp/ezo_pmp.h | 24 ++++---- esphome/components/fan/automation.h | 12 ++-- .../fingerprint_grow/fingerprint_grow.h | 12 ++-- .../components/globals/globals_component.h | 2 +- .../grove_tb6612fng/grove_tb6612fng.h | 12 ++-- esphome/components/haier/automation.h | 26 ++++---- esphome/components/hbridge/fan/hbridge_fan.h | 2 +- .../components/http_request/http_request.h | 6 +- .../components/http_request/ota/automation.h | 2 +- esphome/components/htu21d/htu21d.h | 4 +- .../integration/integration_sensor.h | 2 +- .../components/key_collector/key_collector.h | 4 +- esphome/components/ld2410/automation.h | 2 +- esphome/components/ledc/ledc_output.h | 2 +- .../components/libretiny_pwm/libretiny_pwm.h | 2 +- esphome/components/light/automation.h | 12 ++-- esphome/components/lightwaverf/lightwaverf.h | 2 +- esphome/components/lock/automation.h | 8 +-- esphome/components/lvgl/lvgl_esphome.h | 6 +- esphome/components/max17043/automation.h | 2 +- esphome/components/max6956/automation.h | 4 +- esphome/components/max7219digit/automation.h | 8 +-- esphome/components/media_player/automation.h | 20 ++++--- esphome/components/mhz19/mhz19.h | 6 +- .../components/micro_wake_word/automation.h | 12 ++-- esphome/components/microphone/automation.h | 12 ++-- esphome/components/midea/ac_automations.h | 16 ++--- esphome/components/mixer/speaker/automation.h | 2 +- esphome/components/mqtt/mqtt_client.h | 10 ++-- esphome/components/nau7802/nau7802.h | 6 +- esphome/components/nextion/automation.h | 8 +-- esphome/components/number/automation.h | 6 +- .../components/online_image/online_image.h | 4 +- esphome/components/output/automation.h | 10 ++-- esphome/components/pcf85063/pcf85063.h | 4 +- esphome/components/pcf8563/pcf8563.h | 4 +- esphome/components/pid/pid_climate.h | 6 +- .../pipsolar/output/pipsolar_output.h | 2 +- esphome/components/pmwcs3/pmwcs3.h | 6 +- esphome/components/pn532/pn532.h | 2 +- esphome/components/pn7150/automation.h | 22 +++---- esphome/components/pn7160/automation.h | 22 +++---- esphome/components/pulse_counter/automation.h | 2 +- esphome/components/pulse_meter/automation.h | 2 +- esphome/components/pzemac/pzemac.h | 2 +- esphome/components/pzemdc/pzemdc.h | 2 +- esphome/components/remote_base/remote_base.h | 2 +- .../remote_transmitter/automation.h | 2 +- esphome/components/rf_bridge/rf_bridge.h | 16 ++--- .../rotary_encoder/rotary_encoder.h | 2 +- esphome/components/rp2040_pwm/rp2040_pwm.h | 2 +- esphome/components/rtttl/rtttl.h | 6 +- esphome/components/scd30/automation.h | 2 +- esphome/components/scd4x/automation.h | 4 +- esphome/components/script/script.h | 10 ++-- esphome/components/select/automation.h | 6 +- esphome/components/sen5x/automation.h | 2 +- esphome/components/senseair/senseair.h | 10 ++-- esphome/components/sensor/automation.h | 4 +- esphome/components/servo/servo.h | 4 +- esphome/components/sim800l/sim800l.h | 10 ++-- esphome/components/sound_level/sound_level.h | 4 +- esphome/components/speaker/automation.h | 16 ++--- .../speaker/media_player/automation.h | 2 +- esphome/components/sprinkler/automation.h | 30 +++++----- esphome/components/sps30/automation.h | 2 +- esphome/components/stepper/stepper.h | 10 ++-- esphome/components/sun/sun.h | 2 +- esphome/components/switch/automation.h | 12 ++-- esphome/components/sx126x/automation.h | 12 ++-- esphome/components/sx127x/automation.h | 12 ++-- esphome/components/template/lock/automation.h | 2 +- .../components/template/valve/automation.h | 2 +- esphome/components/text/automation.h | 2 +- esphome/components/text_sensor/automation.h | 4 +- esphome/components/time/real_time_clock.h | 2 +- esphome/components/tm1651/tm1651.h | 10 ++-- esphome/components/uart/automation.h | 2 +- esphome/components/udp/automation.h | 2 +- esphome/components/ufire_ec/ufire_ec.h | 4 +- esphome/components/ufire_ise/ufire_ise.h | 6 +- esphome/components/update/automation.h | 4 +- esphome/components/valve/automation.h | 14 ++--- .../voice_assistant/voice_assistant.h | 10 ++-- esphome/components/wifi/wifi_component.h | 10 ++-- esphome/components/wireguard/wireguard.h | 8 +-- esphome/core/automation.h | 14 ++--- esphome/core/base_automation.h | 60 +++++++++---------- .../loop_test_component/loop_test_component.h | 4 +- 125 files changed, 448 insertions(+), 446 deletions(-) diff --git a/esphome/components/ags10/ags10.h b/esphome/components/ags10/ags10.h index e0975f14bcc..9e034b20cb2 100644 --- a/esphome/components/ags10/ags10.h +++ b/esphome/components/ags10/ags10.h @@ -105,7 +105,7 @@ template class AGS10NewI2cAddressAction : public Action, public: TEMPLATABLE_VALUE(uint8_t, new_address) - void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } + void play(const Ts &...x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } }; enum AGS10SetZeroPointActionMode { @@ -122,7 +122,7 @@ template class AGS10SetZeroPointAction : public Action, p TEMPLATABLE_VALUE(uint16_t, value) TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode) - void play(Ts... x) override { + void play(const Ts &...x) override { switch (this->mode_.value(x...)) { case FACTORY_DEFAULT: this->parent_->set_zero_point_with_factory_defaults(); diff --git a/esphome/components/aic3204/automation.h b/esphome/components/aic3204/automation.h index 416a88fa127..851ff930f8a 100644 --- a/esphome/components/aic3204/automation.h +++ b/esphome/components/aic3204/automation.h @@ -13,7 +13,7 @@ template class SetAutoMuteAction : public Action { TEMPLATABLE_VALUE(uint8_t, auto_mute_mode) - void play(Ts... x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } + void play(const Ts &...x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } protected: AIC3204 *aic3204_; diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index 2177fb710f0..db2ef781582 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -89,7 +89,7 @@ template class ArmAwayAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->alarm_control_panel_->make_call(); auto code = this->code_.optional_value(x...); if (code.has_value()) { @@ -109,7 +109,7 @@ template class ArmHomeAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->alarm_control_panel_->make_call(); auto code = this->code_.optional_value(x...); if (code.has_value()) { @@ -129,7 +129,7 @@ template class ArmNightAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->alarm_control_panel_->make_call(); auto code = this->code_.optional_value(x...); if (code.has_value()) { @@ -149,7 +149,7 @@ template class DisarmAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(Ts... x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } + void play(const Ts &...x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } protected: AlarmControlPanel *alarm_control_panel_; @@ -159,7 +159,7 @@ template class PendingAction : public Action { public: explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} - void play(Ts... x) override { this->alarm_control_panel_->make_call().pending().perform(); } + void play(const Ts &...x) override { this->alarm_control_panel_->make_call().pending().perform(); } protected: AlarmControlPanel *alarm_control_panel_; @@ -169,7 +169,7 @@ template class TriggeredAction : public Action { public: explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} - void play(Ts... x) override { this->alarm_control_panel_->make_call().triggered().perform(); } + void play(const Ts &...x) override { this->alarm_control_panel_->make_call().triggered().perform(); } protected: AlarmControlPanel *alarm_control_panel_; @@ -178,7 +178,7 @@ template class TriggeredAction : public Action { template class AlarmControlPanelCondition : public Condition { public: AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {} - bool check(Ts... x) override { + bool check(const Ts &...x) override { return this->parent_->is_state_armed(this->parent_->get_state()) || this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED; } diff --git a/esphome/components/animation/animation.h b/esphome/components/animation/animation.h index c44e0060aff..b33254df30f 100644 --- a/esphome/components/animation/animation.h +++ b/esphome/components/animation/animation.h @@ -39,7 +39,7 @@ class Animation : public image::Image { template class AnimationNextFrameAction : public Action { public: AnimationNextFrameAction(Animation *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->next_frame(); } + void play(const Ts &...x) override { this->parent_->next_frame(); } protected: Animation *parent_; @@ -48,7 +48,7 @@ template class AnimationNextFrameAction : public Action { template class AnimationPrevFrameAction : public Action { public: AnimationPrevFrameAction(Animation *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->prev_frame(); } + void play(const Ts &...x) override { this->parent_->prev_frame(); } protected: Animation *parent_; @@ -58,7 +58,7 @@ template class AnimationSetFrameAction : public Action { public: AnimationSetFrameAction(Animation *parent) : parent_(parent) {} TEMPLATABLE_VALUE(uint16_t, frame) - void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_frame(this->frame_.value(x...)); } protected: Animation *parent_; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index d29181250e7..f1f44a266d3 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -237,7 +237,7 @@ extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-cons template class APIConnectedCondition : public Condition { public: - bool check(Ts... x) override { return global_api_server->is_connected(); } + bool check(const Ts &...x) override { return global_api_server->is_connected(); } }; } // namespace esphome::api diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 4343fcd0bba..d00e9e62570 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -133,7 +133,7 @@ template class HomeAssistantServiceCallAction : public Action *get_error_trigger() const { return this->error_trigger_; } #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES - void play(Ts... x) override { + void play(const Ts &...x) override { HomeassistantActionRequest resp; std::string service_value = this->service_.value(x...); resp.set_service(StringRef(service_value)); diff --git a/esphome/components/at581x/automation.h b/esphome/components/at581x/automation.h index 4863a875651..b1611a6758f 100644 --- a/esphome/components/at581x/automation.h +++ b/esphome/components/at581x/automation.h @@ -10,7 +10,7 @@ namespace at581x { template class AT581XResetAction : public Action, public Parented { public: - void play(Ts... x) { this->parent_->reset_hardware_frontend(); } + void play(const Ts &...x) { this->parent_->reset_hardware_frontend(); } }; template class AT581XSettingsAction : public Action, public Parented { @@ -25,7 +25,7 @@ template class AT581XSettingsAction : public Action, publ TEMPLATABLE_VALUE(int, trigger_keep) TEMPLATABLE_VALUE(int, stage_gain) - void play(Ts... x) { + void play(const Ts &...x) { if (this->frequency_.has_value()) { int v = this->frequency_.value(x...); this->parent_->set_frequency(v); diff --git a/esphome/components/audio_adc/automation.h b/esphome/components/audio_adc/automation.h index 1b0bc2a6ad8..0c424684793 100644 --- a/esphome/components/audio_adc/automation.h +++ b/esphome/components/audio_adc/automation.h @@ -13,7 +13,7 @@ template class SetMicGainAction : public Action { TEMPLATABLE_VALUE(float, mic_gain) - void play(Ts... x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); } + void play(const Ts &...x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); } protected: AudioAdc *audio_adc_; diff --git a/esphome/components/audio_dac/automation.h b/esphome/components/audio_dac/automation.h index b6cf2acaf4c..3eb3441f3d3 100644 --- a/esphome/components/audio_dac/automation.h +++ b/esphome/components/audio_dac/automation.h @@ -11,7 +11,7 @@ template class MuteOffAction : public Action { public: explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} - void play(Ts... x) override { this->audio_dac_->set_mute_off(); } + void play(const Ts &...x) override { this->audio_dac_->set_mute_off(); } protected: AudioDac *audio_dac_; @@ -21,7 +21,7 @@ template class MuteOnAction : public Action { public: explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} - void play(Ts... x) override { this->audio_dac_->set_mute_on(); } + void play(const Ts &...x) override { this->audio_dac_->set_mute_on(); } protected: AudioDac *audio_dac_; @@ -33,7 +33,7 @@ template class SetVolumeAction : public Action { TEMPLATABLE_VALUE(float, volume) - void play(Ts... x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); } + void play(const Ts &...x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); } protected: AudioDac *audio_dac_; diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index 0bc7b9acb37..f6971a2fc4a 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -141,7 +141,7 @@ class StateChangeTrigger : public Trigger, optional > { template class BinarySensorCondition : public Condition { public: BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {} - bool check(Ts... x) override { return this->parent_->state == this->state_; } + bool check(const Ts &...x) override { return this->parent_->state == this->state_; } protected: BinarySensor *parent_; @@ -153,7 +153,7 @@ template class BinarySensorPublishAction : public Action explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { auto val = this->state_.value(x...); this->sensor_->publish_state(val); } @@ -166,7 +166,7 @@ template class BinarySensorInvalidateAction : public Actionsensor_->invalidate_state(); } + void play(const Ts &...x) override { this->sensor_->invalidate_state(); } protected: BinarySensor *sensor_; diff --git a/esphome/components/bl0906/bl0906.h b/esphome/components/bl0906/bl0906.h index 5a9ad0f0280..493b645c89d 100644 --- a/esphome/components/bl0906/bl0906.h +++ b/esphome/components/bl0906/bl0906.h @@ -89,7 +89,7 @@ class BL0906 : public PollingComponent, public uart::UARTDevice { template class ResetEnergyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); } + void play(const Ts &...x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); } }; } // namespace bl0906 diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 55f1cb2f460..ce534501f39 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -123,9 +123,9 @@ template class BLEClientWriteAction : public Action, publ this->has_simple_value_ = true; } - void play(Ts... x) override {} + void play(const Ts &...x) override {} - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; this->var_ = std::make_tuple(x...); auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...); @@ -229,7 +229,7 @@ template class BLEClientPasskeyReplyAction : public Actionvalue_.simple; @@ -266,7 +266,7 @@ template class BLEClientNumericComparisonReplyAction : public Ac public: BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; } - void play(Ts... x) override { + void play(const Ts &...x) override { esp_bd_addr_t remote_bda; memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); if (has_simple_value_) { @@ -299,7 +299,7 @@ template class BLEClientRemoveBondAction : public Action public: BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; } - void play(Ts... x) override { + void play(const Ts &...x) override { esp_bd_addr_t remote_bda; memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); esp_ble_remove_bond_device(remote_bda); @@ -334,9 +334,9 @@ template class BLEClientConnectAction : public Action, pu } // not used since we override play_complex_ - void play(Ts... x) override {} + void play(const Ts &...x) override {} - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { // it makes no sense to have multiple instances of this running at the same time. // this would occur only if the same automation was re-triggered while still // running. So just cancel the second chain if this is detected. @@ -379,9 +379,9 @@ template class BLEClientDisconnectAction : public Action, } // not used since we override play_complex_ - void play(Ts... x) override {} + void play(const Ts &...x) override {} - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; if (this->node_state == espbt::ClientState::IDLE) { this->play_next_(x...); diff --git a/esphome/components/button/automation.h b/esphome/components/button/automation.h index a5fb9f35b70..3b792eb5d72 100644 --- a/esphome/components/button/automation.h +++ b/esphome/components/button/automation.h @@ -11,7 +11,7 @@ template class PressAction : public Action { public: explicit PressAction(Button *button) : button_(button) {} - void play(Ts... x) override { this->button_->press(); } + void play(const Ts &...x) override { this->button_->press(); } protected: Button *button_; diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 51d7c0830aa..029eb278c0b 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -129,7 +129,7 @@ template class CanbusSendAction : public Action, public P this->remote_transmission_request_ = remote_transmission_request; } - void play(Ts... x) override { + void play(const Ts &...x) override { auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; auto use_extended_id = this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index a4d13ade58e..36cc8f4f21d 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -22,7 +22,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(std::string, custom_preset) TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->climate_->make_call(); call.set_mode(this->mode_.optional_value(x...)); call.set_target_temperature(this->target_temperature_.optional_value(x...)); diff --git a/esphome/components/cm1106/cm1106.h b/esphome/components/cm1106/cm1106.h index 3b78e17cf4c..ad089bbe7d7 100644 --- a/esphome/components/cm1106/cm1106.h +++ b/esphome/components/cm1106/cm1106.h @@ -30,7 +30,7 @@ template class CM1106CalibrateZeroAction : public Action public: CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {} - void play(Ts... x) override { this->cm1106_->calibrate_zero(400); } + void play(const Ts &...x) override { this->cm1106_->calibrate_zero(400); } protected: CM1106Component *cm1106_; diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index 6406ba52cb9..752e0398c18 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -11,7 +11,7 @@ template class OpenAction : public Action { public: explicit OpenAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->make_call().set_command_open().perform(); } + void play(const Ts &...x) override { this->cover_->make_call().set_command_open().perform(); } protected: Cover *cover_; @@ -21,7 +21,7 @@ template class CloseAction : public Action { public: explicit CloseAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->make_call().set_command_close().perform(); } + void play(const Ts &...x) override { this->cover_->make_call().set_command_close().perform(); } protected: Cover *cover_; @@ -31,7 +31,7 @@ template class StopAction : public Action { public: explicit StopAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->make_call().set_command_stop().perform(); } + void play(const Ts &...x) override { this->cover_->make_call().set_command_stop().perform(); } protected: Cover *cover_; @@ -41,7 +41,7 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->make_call().set_command_toggle().perform(); } + void play(const Ts &...x) override { this->cover_->make_call().set_command_toggle().perform(); } protected: Cover *cover_; @@ -55,7 +55,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(float, position) TEMPLATABLE_VALUE(float, tilt) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->cover_->make_call(); if (this->stop_.has_value()) call.set_stop(this->stop_.value(x...)); @@ -77,7 +77,7 @@ template class CoverPublishAction : public Action { TEMPLATABLE_VALUE(float, tilt) TEMPLATABLE_VALUE(CoverOperation, current_operation) - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->position_.has_value()) this->cover_->position = this->position_.value(x...); if (this->tilt_.has_value()) @@ -94,7 +94,7 @@ template class CoverPublishAction : public Action { template class CoverIsOpenCondition : public Condition { public: CoverIsOpenCondition(Cover *cover) : cover_(cover) {} - bool check(Ts... x) override { return this->cover_->is_fully_open(); } + bool check(const Ts &...x) override { return this->cover_->is_fully_open(); } protected: Cover *cover_; @@ -103,7 +103,7 @@ template class CoverIsOpenCondition : public Condition { template class CoverIsClosedCondition : public Condition { public: CoverIsClosedCondition(Cover *cover) : cover_(cover) {} - bool check(Ts... x) override { return this->cover_->is_fully_closed(); } + bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); } protected: Cover *cover_; diff --git a/esphome/components/cs5460a/cs5460a.h b/esphome/components/cs5460a/cs5460a.h index 15ae04f3c6f..11b13f5851f 100644 --- a/esphome/components/cs5460a/cs5460a.h +++ b/esphome/components/cs5460a/cs5460a.h @@ -114,7 +114,7 @@ template class CS5460ARestartAction : public Action { public: CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {} - void play(Ts... x) override { cs5460a_->restart(); } + void play(const Ts &...x) override { cs5460a_->restart(); } protected: CS5460AComponent *cs5460a_; diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index fcbb46cf176..ba2edb127ad 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -101,7 +101,7 @@ template class DateSetAction : public Action, public Pare public: TEMPLATABLE_VALUE(ESPTime, date) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); if (this->date_.has_value()) { diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index 275eedfd3b5..43bff5a1812 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -124,7 +124,7 @@ template class DateTimeSetAction : public Action, public public: TEMPLATABLE_VALUE(ESPTime, datetime) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); if (this->datetime_.has_value()) { diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h index e79b8c225de..c5cbeb52da1 100644 --- a/esphome/components/datetime/time_entity.h +++ b/esphome/components/datetime/time_entity.h @@ -103,7 +103,7 @@ template class TimeSetAction : public Action, public Pare public: TEMPLATABLE_VALUE(ESPTime, time) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); if (this->time_.has_value()) { diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 38744163c79..80381e767c9 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -148,7 +148,7 @@ template class EnterDeepSleepAction : public Action { void set_time(time::RealTimeClock *time) { this->time_ = time; } #endif - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->sleep_duration_.has_value()) { this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...)); } @@ -207,12 +207,12 @@ template class EnterDeepSleepAction : public Action { template class PreventDeepSleepAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->prevent_deep_sleep(); } + void play(const Ts &...x) override { this->parent_->prevent_deep_sleep(); } }; template class AllowDeepSleepAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->allow_deep_sleep(); } + void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); } }; } // namespace deep_sleep diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index d2ec0a2310f..03d2230ca68 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -77,7 +77,7 @@ class DFPlayer : public uart::UARTDevice, public Component { class ACTION_CLASS : /* NOLINT */ \ public Action, \ public Parented { \ - void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ + void play(const Ts &...x) override { this->parent_->ACTION_METHOD(); } \ }; DFPLAYER_SIMPLE_ACTION(NextAction, next) @@ -87,7 +87,7 @@ template class PlayMp3Action : public Action, public Pare public: TEMPLATABLE_VALUE(uint16_t, file) - void play(Ts... x) override { + void play(const Ts &...x) override { auto file = this->file_.value(x...); this->parent_->play_mp3(file); } @@ -98,7 +98,7 @@ template class PlayFileAction : public Action, public Par TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(bool, loop) - void play(Ts... x) override { + void play(const Ts &...x) override { auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); if (loop) { @@ -115,7 +115,7 @@ template class PlayFolderAction : public Action, public P TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(bool, loop) - void play(Ts... x) override { + void play(const Ts &...x) override { auto folder = this->folder_.value(x...); auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); @@ -131,7 +131,7 @@ template class SetDeviceAction : public Action, public Pa public: TEMPLATABLE_VALUE(Device, device) - void play(Ts... x) override { + void play(const Ts &...x) override { auto device = this->device_.value(x...); this->parent_->set_device(device); } @@ -141,7 +141,7 @@ template class SetVolumeAction : public Action, public Pa public: TEMPLATABLE_VALUE(uint8_t, volume) - void play(Ts... x) override { + void play(const Ts &...x) override { auto volume = this->volume_.value(x...); this->parent_->set_volume(volume); } @@ -151,7 +151,7 @@ template class SetEqAction : public Action, public Parent public: TEMPLATABLE_VALUE(EqPreset, eq) - void play(Ts... x) override { + void play(const Ts &...x) override { auto eq = this->eq_.value(x...); this->parent_->set_eq(eq); } @@ -168,7 +168,7 @@ DFPLAYER_SIMPLE_ACTION(VolumeDownAction, volume_down) template class DFPlayerIsPlayingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_playing(); } + bool check(const Ts &...x) override { return this->parent_->is_playing(); } }; class DFPlayerFinishedPlaybackTrigger : public Trigger<> { diff --git a/esphome/components/dfrobot_sen0395/automation.h b/esphome/components/dfrobot_sen0395/automation.h index 3f69e482b73..422555d6eba 100644 --- a/esphome/components/dfrobot_sen0395/automation.h +++ b/esphome/components/dfrobot_sen0395/automation.h @@ -11,7 +11,7 @@ namespace dfrobot_sen0395 { template class DfrobotSen0395ResetAction : public Action, public Parented { public: - void play(Ts... x) { this->parent_->enqueue(make_unique()); } + void play(const Ts &...x) { this->parent_->enqueue(make_unique()); } }; template @@ -33,7 +33,7 @@ class DfrobotSen0395SettingsAction : public Action, public Parentedparent_->enqueue(make_unique(0)); if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) { this->parent_->enqueue(make_unique()); diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 97b0a4e8d70..47d40915aa7 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -819,7 +819,7 @@ template class DisplayPageShowAction : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) - void play(Ts... x) override { + void play(const Ts &...x) override { auto *page = this->page_.value(x...); if (page != nullptr) { page->show(); @@ -831,7 +831,7 @@ template class DisplayPageShowNextAction : public Action public: DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {} - void play(Ts... x) override { this->buffer_->show_next_page(); } + void play(const Ts &...x) override { this->buffer_->show_next_page(); } Display *buffer_; }; @@ -840,7 +840,7 @@ template class DisplayPageShowPrevAction : public Action public: DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {} - void play(Ts... x) override { this->buffer_->show_prev_page(); } + void play(const Ts &...x) override { this->buffer_->show_prev_page(); } Display *buffer_; }; @@ -850,7 +850,7 @@ template class DisplayIsDisplayingPageCondition : public Conditi DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {} void set_page(DisplayPage *page) { this->page_ = page; } - bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; } + bool check(const Ts &...x) override { return this->parent_->get_active_page() == this->page_; } protected: Display *parent_; diff --git a/esphome/components/display_menu_base/automation.h b/esphome/components/display_menu_base/automation.h index d5394a1e0c6..9c64794cefe 100644 --- a/esphome/components/display_menu_base/automation.h +++ b/esphome/components/display_menu_base/automation.h @@ -10,7 +10,7 @@ template class UpAction : public Action { public: explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->up(); } + void play(const Ts &...x) override { this->menu_->up(); } protected: DisplayMenuComponent *menu_; @@ -20,7 +20,7 @@ template class DownAction : public Action { public: explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->down(); } + void play(const Ts &...x) override { this->menu_->down(); } protected: DisplayMenuComponent *menu_; @@ -30,7 +30,7 @@ template class LeftAction : public Action { public: explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->left(); } + void play(const Ts &...x) override { this->menu_->left(); } protected: DisplayMenuComponent *menu_; @@ -40,7 +40,7 @@ template class RightAction : public Action { public: explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->right(); } + void play(const Ts &...x) override { this->menu_->right(); } protected: DisplayMenuComponent *menu_; @@ -50,7 +50,7 @@ template class EnterAction : public Action { public: explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->enter(); } + void play(const Ts &...x) override { this->menu_->enter(); } protected: DisplayMenuComponent *menu_; @@ -60,7 +60,7 @@ template class ShowAction : public Action { public: explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->show(); } + void play(const Ts &...x) override { this->menu_->show(); } protected: DisplayMenuComponent *menu_; @@ -70,7 +70,7 @@ template class HideAction : public Action { public: explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->hide(); } + void play(const Ts &...x) override { this->menu_->hide(); } protected: DisplayMenuComponent *menu_; @@ -80,7 +80,7 @@ template class ShowMainAction : public Action { public: explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->show_main(); } + void play(const Ts &...x) override { this->menu_->show_main(); } protected: DisplayMenuComponent *menu_; @@ -88,7 +88,7 @@ template class ShowMainAction : public Action { template class IsActiveCondition : public Condition { public: explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {} - bool check(Ts... x) override { return this->menu_->is_active(); } + bool check(const Ts &...x) override { return this->menu_->is_active(); } protected: DisplayMenuComponent *menu_; diff --git a/esphome/components/ds1307/ds1307.h b/esphome/components/ds1307/ds1307.h index 2e9ac2275c2..f7f06253b79 100644 --- a/esphome/components/ds1307/ds1307.h +++ b/esphome/components/ds1307/ds1307.h @@ -59,12 +59,12 @@ class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice { template class WriteAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->write_time(); } + void play(const Ts &...x) override { this->parent_->write_time(); } }; template class ReadAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->read_time(); } + void play(const Ts &...x) override { this->parent_->read_time(); } }; } // namespace ds1307 } // namespace esphome diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index 18280f8e21e..d9fb2a6d604 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -51,15 +51,15 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent { template class BaseAction : public Action, public Parented {}; template class StartAction : public BaseAction { - void play(Ts... x) override { this->parent_->start(); } + void play(const Ts &...x) override { this->parent_->start(); } }; template class StopAction : public BaseAction { - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class ResetAction : public BaseAction { - void play(Ts... x) override { this->parent_->reset(); } + void play(const Ts &...x) override { this->parent_->reset(); } }; template class RunningCondition : public Condition, public Parented { @@ -67,7 +67,7 @@ template class RunningCondition : public Condition, publi explicit RunningCondition(DutyTimeSensor *parent, bool state) : Parented(parent), state_(state) {} protected: - bool check(Ts... x) override { return this->parent_->is_running() == this->state_; } + bool check(const Ts &...x) override { return this->parent_->is_running() == this->state_; } bool state_; }; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index c3e1ec2ce62..2fb60bb8224 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -214,17 +214,17 @@ extern ESP32BLE *global_ble; template class BLEEnabledCondition : public Condition { public: - bool check(Ts... x) override { return global_ble->is_active(); } + bool check(const Ts &...x) override { return global_ble->is_active(); } }; template class BLEEnableAction : public Action { public: - void play(Ts... x) override { global_ble->enable(); } + void play(const Ts &...x) override { global_ble->enable(); } }; template class BLEDisableAction : public Action { public: - void play(Ts... x) override { global_ble->disable(); } + void play(const Ts &...x) override { global_ble->disable(); } }; } // namespace esphome::esp32_ble diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h index 543b1153fce..fe186002803 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.h +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -71,7 +71,7 @@ template class BLECharacteristicSetValueAction : public Action, buffer) void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } - void play(Ts... x) override { + void play(const Ts &...x) override { // If the listener is already set, do nothing if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_)) return; @@ -96,7 +96,7 @@ template class BLECharacteristicSetValueAction : public Action class BLECharacteristicNotifyAction : public Action { public: BLECharacteristicNotifyAction(BLECharacteristic *characteristic) : parent_(characteristic) {} - void play(Ts... x) override { + void play(const Ts &...x) override { #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION // Call the pre-notify event BLECharacteristicSetValueActionManager::get_instance()->emit_pre_notify(this->parent_); @@ -116,7 +116,7 @@ template class BLEDescriptorSetValueAction : public Action, buffer) void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } - void play(Ts... x) override { this->parent_->set_value(this->buffer_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); } protected: BLEDescriptor *parent_; diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index 784f2eaaa23..054cbaa7df3 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -96,7 +96,7 @@ template class ESP32BLEStartScanAction : public Action { public: ESP32BLEStartScanAction(ESP32BLETracker *parent) : parent_(parent) {} TEMPLATABLE_VALUE(bool, continuous) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_scan_continuous(this->continuous_.value(x...)); this->parent_->start_scan(); } @@ -107,7 +107,7 @@ template class ESP32BLEStartScanAction : public Action { template class ESP32BLEStopScanAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop_scan(); } + void play(const Ts &...x) override { this->parent_->stop_scan(); } }; } // namespace esphome::esp32_ble_tracker diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index 79530aacd44..4b021fc4622 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -40,7 +40,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(ESP8266PWM *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + void play(const Ts &...x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/esp_ldo/esp_ldo.h b/esphome/components/esp_ldo/esp_ldo.h index bafa32db6b9..9edd303e164 100644 --- a/esphome/components/esp_ldo/esp_ldo.h +++ b/esphome/components/esp_ldo/esp_ldo.h @@ -34,7 +34,7 @@ template class AdjustAction : public Action { TEMPLATABLE_VALUE(float, voltage) - void play(Ts... x) override { this->ldo_->adjust_voltage(this->voltage_.value(x...)); } + void play(const Ts &...x) override { this->ldo_->adjust_voltage(this->voltage_.value(x...)); } protected: EspLdo *ldo_; diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h index 5415b088fd0..0b266814005 100644 --- a/esphome/components/espnow/automation.h +++ b/esphome/components/espnow/automation.h @@ -36,7 +36,7 @@ template class SendAction : public Action, public Parente void set_wait_for_sent(bool wait_for_sent) { this->flags_.wait_for_sent = wait_for_sent; } void set_continue_on_error(bool continue_on_error) { this->flags_.continue_on_error = continue_on_error; } - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; send_callback_t send_callback = [this, x...](esp_err_t status) { if (status == ESP_OK) { @@ -67,7 +67,7 @@ template class SendAction : public Action, public Parente } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { @@ -90,7 +90,7 @@ template class AddPeerAction : public Action, public Pare TEMPLATABLE_VALUE(peer_address_t, address); public: - void play(Ts... x) override { + void play(const Ts &...x) override { peer_address_t address = this->address_.value(x...); this->parent_->add_peer(address.data()); } @@ -100,7 +100,7 @@ template class DeletePeerAction : public Action, public P TEMPLATABLE_VALUE(peer_address_t, address); public: - void play(Ts... x) override { + void play(const Ts &...x) override { peer_address_t address = this->address_.value(x...); this->parent_->del_peer(address.data()); } @@ -109,7 +109,7 @@ template class DeletePeerAction : public Action, public P template class SetChannelAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint8_t, channel) - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->parent_->is_wifi_enabled()) { return; } diff --git a/esphome/components/event/automation.h b/esphome/components/event/automation.h index 9ebcb654a04..5bdba186871 100644 --- a/esphome/components/event/automation.h +++ b/esphome/components/event/automation.h @@ -11,7 +11,7 @@ template class TriggerEventAction : public Action, public public: TEMPLATABLE_VALUE(std::string, event_type) - void play(Ts... x) override { this->parent_->trigger(this->event_type_.value(x...)); } + void play(const Ts &...x) override { this->parent_->trigger(this->event_type_.value(x...)); } }; class EventTrigger : public Trigger { diff --git a/esphome/components/ezo/automation.h b/esphome/components/ezo/automation.h index 19427b91591..a4a6fa30146 100644 --- a/esphome/components/ezo/automation.h +++ b/esphome/components/ezo/automation.h @@ -17,35 +17,35 @@ class LedTrigger : public Trigger { class CustomTrigger : public Trigger { public: explicit CustomTrigger(EZOSensor *ezo) { - ezo->add_custom_callback([this](std::string value) { this->trigger(std::move(value)); }); + ezo->add_custom_callback([this](const std::string &value) { this->trigger(value); }); } }; class TTrigger : public Trigger { public: explicit TTrigger(EZOSensor *ezo) { - ezo->add_t_callback([this](std::string value) { this->trigger(std::move(value)); }); + ezo->add_t_callback([this](const std::string &value) { this->trigger(value); }); } }; class CalibrationTrigger : public Trigger { public: explicit CalibrationTrigger(EZOSensor *ezo) { - ezo->add_calibration_callback([this](std::string value) { this->trigger(std::move(value)); }); + ezo->add_calibration_callback([this](const std::string &value) { this->trigger(value); }); } }; class SlopeTrigger : public Trigger { public: explicit SlopeTrigger(EZOSensor *ezo) { - ezo->add_slope_callback([this](std::string value) { this->trigger(std::move(value)); }); + ezo->add_slope_callback([this](const std::string &value) { this->trigger(value); }); } }; class DeviceInformationTrigger : public Trigger { public: explicit DeviceInformationTrigger(EZOSensor *ezo) { - ezo->add_device_infomation_callback([this](std::string value) { this->trigger(std::move(value)); }); + ezo->add_device_infomation_callback([this](const std::string &value) { this->trigger(value); }); } }; diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h index 671e1248103..d4917e7f4b4 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.h +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -119,7 +119,7 @@ template class EzoPMPFindAction : public Action { public: EzoPMPFindAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->find(); } + void play(const Ts &...x) override { this->ezopmp_->find(); } protected: EzoPMP *ezopmp_; @@ -129,7 +129,7 @@ template class EzoPMPDoseContinuouslyAction : public Actionezopmp_->dose_continuously(); } + void play(const Ts &...x) override { this->ezopmp_->dose_continuously(); } protected: EzoPMP *ezopmp_; @@ -139,7 +139,7 @@ template class EzoPMPDoseVolumeAction : public Action { public: EzoPMPDoseVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->dose_volume(this->volume_.value(x...)); } + void play(const Ts &...x) override { this->ezopmp_->dose_volume(this->volume_.value(x...)); } TEMPLATABLE_VALUE(double, volume) protected: @@ -150,7 +150,7 @@ template class EzoPMPDoseVolumeOverTimeAction : public Actionezopmp_->dose_volume_over_time(this->volume_.value(x...), this->duration_.value(x...)); } TEMPLATABLE_VALUE(double, volume) @@ -164,7 +164,7 @@ template class EzoPMPDoseWithConstantFlowRateAction : public Act public: EzoPMPDoseWithConstantFlowRateAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { + void play(const Ts &...x) override { this->ezopmp_->dose_with_constant_flow_rate(this->volume_.value(x...), this->duration_.value(x...)); } TEMPLATABLE_VALUE(double, volume) @@ -178,7 +178,7 @@ template class EzoPMPSetCalibrationVolumeAction : public Action< public: EzoPMPSetCalibrationVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->set_calibration_volume(this->volume_.value(x...)); } + void play(const Ts &...x) override { this->ezopmp_->set_calibration_volume(this->volume_.value(x...)); } TEMPLATABLE_VALUE(double, volume) protected: @@ -189,7 +189,7 @@ template class EzoPMPClearTotalVolumeDispensedAction : public Ac public: EzoPMPClearTotalVolumeDispensedAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->clear_total_volume_dosed(); } + void play(const Ts &...x) override { this->ezopmp_->clear_total_volume_dosed(); } protected: EzoPMP *ezopmp_; @@ -199,7 +199,7 @@ template class EzoPMPClearCalibrationAction : public Actionezopmp_->clear_calibration(); } + void play(const Ts &...x) override { this->ezopmp_->clear_calibration(); } protected: EzoPMP *ezopmp_; @@ -209,7 +209,7 @@ template class EzoPMPPauseDosingAction : public Action { public: EzoPMPPauseDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->pause_dosing(); } + void play(const Ts &...x) override { this->ezopmp_->pause_dosing(); } protected: EzoPMP *ezopmp_; @@ -219,7 +219,7 @@ template class EzoPMPStopDosingAction : public Action { public: EzoPMPStopDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->stop_dosing(); } + void play(const Ts &...x) override { this->ezopmp_->stop_dosing(); } protected: EzoPMP *ezopmp_; @@ -229,7 +229,7 @@ template class EzoPMPChangeI2CAddressAction : public Actionezopmp_->change_i2c_address(this->address_.value(x...)); } + void play(const Ts &...x) override { this->ezopmp_->change_i2c_address(this->address_.value(x...)); } TEMPLATABLE_VALUE(int, address) protected: @@ -240,7 +240,7 @@ template class EzoPMPArbitraryCommandAction : public Actionezopmp_->exec_arbitrary_command(this->command_.value(x...)); } + void play(const Ts &...x) override { this->ezopmp_->exec_arbitrary_command(this->command_.value(x...)); } TEMPLATABLE_VALUE(std::string, command) protected: diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index ae0af1a9bd3..ce1db6fc645 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -15,7 +15,7 @@ template class TurnOnAction : public Action { TEMPLATABLE_VALUE(int, speed) TEMPLATABLE_VALUE(FanDirection, direction) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->state_->turn_on(); if (this->oscillating_.has_value()) { call.set_oscillating(this->oscillating_.value(x...)); @@ -36,7 +36,7 @@ template class TurnOffAction : public Action { public: explicit TurnOffAction(Fan *state) : state_(state) {} - void play(Ts... x) override { this->state_->turn_off().perform(); } + void play(const Ts &...x) override { this->state_->turn_off().perform(); } Fan *state_; }; @@ -45,7 +45,7 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Fan *state) : state_(state) {} - void play(Ts... x) override { this->state_->toggle().perform(); } + void play(const Ts &...x) override { this->state_->toggle().perform(); } Fan *state_; }; @@ -56,7 +56,7 @@ template class CycleSpeedAction : public Action { TEMPLATABLE_VALUE(bool, no_off_cycle) - void play(Ts... x) override { + void play(const Ts &...x) override { // check to see if fan supports speeds and is on if (this->state_->get_traits().supported_speed_count()) { if (this->state_->state) { @@ -97,7 +97,7 @@ template class CycleSpeedAction : public Action { template class FanIsOnCondition : public Condition { public: explicit FanIsOnCondition(Fan *state) : state_(state) {} - bool check(Ts... x) override { return this->state_->state; } + bool check(const Ts &...x) override { return this->state_->state; } protected: Fan *state_; @@ -105,7 +105,7 @@ template class FanIsOnCondition : public Condition { template class FanIsOffCondition : public Condition { public: explicit FanIsOffCondition(Fan *state) : state_(state) {} - bool check(Ts... x) override { return !this->state_->state; } + bool check(const Ts &...x) override { return !this->state_->state; } protected: Fan *state_; diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 590c709c224..370b26f56a3 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -273,7 +273,7 @@ template class EnrollmentAction : public Action, public P TEMPLATABLE_VALUE(uint16_t, finger_id) TEMPLATABLE_VALUE(uint8_t, num_scans) - void play(Ts... x) override { + void play(const Ts &...x) override { auto finger_id = this->finger_id_.value(x...); auto num_scans = this->num_scans_.value(x...); if (num_scans) { @@ -287,14 +287,14 @@ template class EnrollmentAction : public Action, public P template class CancelEnrollmentAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->finish_enrollment(1); } + void play(const Ts &...x) override { this->parent_->finish_enrollment(1); } }; template class DeleteAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint16_t, finger_id) - void play(Ts... x) override { + void play(const Ts &...x) override { auto finger_id = this->finger_id_.value(x...); this->parent_->delete_fingerprint(finger_id); } @@ -302,14 +302,14 @@ template class DeleteAction : public Action, public Paren template class DeleteAllAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->delete_all_fingerprints(); } + void play(const Ts &...x) override { this->parent_->delete_all_fingerprints(); } }; template class LEDControlAction : public Action, public Parented { public: TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { auto state = this->state_.value(x...); this->parent_->led_control(state); } @@ -322,7 +322,7 @@ template class AuraLEDControlAction : public Action, publ TEMPLATABLE_VALUE(uint8_t, color) TEMPLATABLE_VALUE(uint8_t, count) - void play(Ts... x) override { + void play(const Ts &...x) override { auto state = this->state_.value(x...); auto speed = this->speed_.value(x...); auto color = this->color_.value(x...); diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 4c6a12aa720..1d2a08937ee 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -134,7 +134,7 @@ template class GlobalVarSetAction : public Actionparent_->value() = this->value_.value(x...); } + void play(const Ts &...x) override { this->parent_->value() = this->value_.value(x...); } protected: C *parent_; diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.h b/esphome/components/grove_tb6612fng/grove_tb6612fng.h index 68281117e76..a36cb85cffc 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.h +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.h @@ -168,7 +168,7 @@ class GROVETB6612FNGMotorRunAction : public Action, public Parentedchannel_.value(x...); auto speed = this->speed_.value(x...); this->parent_->dc_motor_run(channel, speed); @@ -180,7 +180,7 @@ class GROVETB6612FNGMotorBrakeAction : public Action, public Parentedparent_->dc_motor_brake(this->channel_.value(x...)); } + void play(const Ts &...x) override { this->parent_->dc_motor_brake(this->channel_.value(x...)); } }; template @@ -188,19 +188,19 @@ class GROVETB6612FNGMotorStopAction : public Action, public Parentedparent_->dc_motor_stop(this->channel_.value(x...)); } + void play(const Ts &...x) override { this->parent_->dc_motor_stop(this->channel_.value(x...)); } }; template class GROVETB6612FNGMotorStandbyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->standby(); } + void play(const Ts &...x) override { this->parent_->standby(); } }; template class GROVETB6612FNGMotorNoStandbyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->not_standby(); } + void play(const Ts &...x) override { this->parent_->not_standby(); } }; template @@ -208,7 +208,7 @@ class GROVETB6612FNGMotorChangeAddressAction : public Action, public Pare public: TEMPLATABLE_VALUE(uint8_t, address) - void play(Ts... x) override { this->parent_->set_i2c_addr(this->address_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_i2c_addr(this->address_.value(x...)); } }; } // namespace grove_tb6612fng diff --git a/esphome/components/haier/automation.h b/esphome/components/haier/automation.h index 55df7ecc1d6..c1ce7c01ea4 100644 --- a/esphome/components/haier/automation.h +++ b/esphome/components/haier/automation.h @@ -10,7 +10,7 @@ namespace haier { template class DisplayOnAction : public Action { public: DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_display_state(true); } + void play(const Ts &...x) { this->parent_->set_display_state(true); } protected: HaierClimateBase *parent_; @@ -19,7 +19,7 @@ template class DisplayOnAction : public Action { template class DisplayOffAction : public Action { public: DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_display_state(false); } + void play(const Ts &...x) { this->parent_->set_display_state(false); } protected: HaierClimateBase *parent_; @@ -28,7 +28,7 @@ template class DisplayOffAction : public Action { template class BeeperOnAction : public Action { public: BeeperOnAction(HonClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_beeper_state(true); } + void play(const Ts &...x) { this->parent_->set_beeper_state(true); } protected: HonClimate *parent_; @@ -37,7 +37,7 @@ template class BeeperOnAction : public Action { template class BeeperOffAction : public Action { public: BeeperOffAction(HonClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_beeper_state(false); } + void play(const Ts &...x) { this->parent_->set_beeper_state(false); } protected: HonClimate *parent_; @@ -47,7 +47,7 @@ template class VerticalAirflowAction : public Action { public: VerticalAirflowAction(HonClimate *parent) : parent_(parent) {} TEMPLATABLE_VALUE(hon_protocol::VerticalSwingMode, direction) - void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); } + void play(const Ts &...x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); } protected: HonClimate *parent_; @@ -57,7 +57,7 @@ template class HorizontalAirflowAction : public Action { public: HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {} TEMPLATABLE_VALUE(hon_protocol::HorizontalSwingMode, direction) - void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); } + void play(const Ts &...x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); } protected: HonClimate *parent_; @@ -66,7 +66,7 @@ template class HorizontalAirflowAction : public Action { template class HealthOnAction : public Action { public: HealthOnAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_health_mode(true); } + void play(const Ts &...x) { this->parent_->set_health_mode(true); } protected: HaierClimateBase *parent_; @@ -75,7 +75,7 @@ template class HealthOnAction : public Action { template class HealthOffAction : public Action { public: HealthOffAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_health_mode(false); } + void play(const Ts &...x) { this->parent_->set_health_mode(false); } protected: HaierClimateBase *parent_; @@ -84,7 +84,7 @@ template class HealthOffAction : public Action { template class StartSelfCleaningAction : public Action { public: StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->start_self_cleaning(); } + void play(const Ts &...x) { this->parent_->start_self_cleaning(); } protected: HonClimate *parent_; @@ -93,7 +93,7 @@ template class StartSelfCleaningAction : public Action { template class StartSteriCleaningAction : public Action { public: StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->start_steri_cleaning(); } + void play(const Ts &...x) { this->parent_->start_steri_cleaning(); } protected: HonClimate *parent_; @@ -102,7 +102,7 @@ template class StartSteriCleaningAction : public Action { template class PowerOnAction : public Action { public: PowerOnAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->send_power_on_command(); } + void play(const Ts &...x) { this->parent_->send_power_on_command(); } protected: HaierClimateBase *parent_; @@ -111,7 +111,7 @@ template class PowerOnAction : public Action { template class PowerOffAction : public Action { public: PowerOffAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->send_power_off_command(); } + void play(const Ts &...x) { this->parent_->send_power_off_command(); } protected: HaierClimateBase *parent_; @@ -120,7 +120,7 @@ template class PowerOffAction : public Action { template class PowerToggleAction : public Action { public: PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->toggle_power(); } + void play(const Ts &...x) { this->parent_->toggle_power(); } protected: HaierClimateBase *parent_; diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 143c7c18530..ec1e8ada0e1 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -49,7 +49,7 @@ template class BrakeAction : public Action { public: explicit BrakeAction(HBridgeFan *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->brake(); } + void play(const Ts &...x) override { this->parent_->brake(); } HBridgeFan *parent_; }; diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 482cd2da44e..8a82a44d7da 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -113,8 +113,8 @@ class HttpContainer : public Parented { class HttpRequestResponseTrigger : public Trigger, std::string &> { public: - void process(std::shared_ptr container, std::string &response_body) { - this->trigger(std::move(container), response_body); + void process(const std::shared_ptr &container, std::string &response_body) { + this->trigger(container, response_body); } }; @@ -210,7 +210,7 @@ template class HttpRequestSendAction : public Action { this->max_response_buffer_size_ = max_response_buffer_size; } - void play(Ts... x) override { + void play(const Ts &...x) override { std::string body; if (this->body_.has_value()) { body = this->body_.value(x...); diff --git a/esphome/components/http_request/ota/automation.h b/esphome/components/http_request/ota/automation.h index d4c21f1c721..6c50bb9b0d9 100644 --- a/esphome/components/http_request/ota/automation.h +++ b/esphome/components/http_request/ota/automation.h @@ -15,7 +15,7 @@ template class OtaHttpRequestComponentFlashAction : public Actio TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(std::string, username) - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->md5_url_.has_value()) { this->parent_->set_md5_url(this->md5_url_.value(x...)); } diff --git a/esphome/components/htu21d/htu21d.h b/esphome/components/htu21d/htu21d.h index 9b3831b784c..277c6ca3e5f 100644 --- a/esphome/components/htu21d/htu21d.h +++ b/esphome/components/htu21d/htu21d.h @@ -41,7 +41,7 @@ template class SetHeaterLevelAction : public Action, publ public: TEMPLATABLE_VALUE(uint8_t, level) - void play(Ts... x) override { + void play(const Ts &...x) override { auto level = this->level_.value(x...); this->parent_->set_heater_level(level); @@ -52,7 +52,7 @@ template class SetHeaterAction : public Action, public Pa public: TEMPLATABLE_VALUE(bool, status) - void play(Ts... x) override { + void play(const Ts &...x) override { auto status = this->status_.value(x...); this->parent_->set_heater(status); diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index d9f2f5e50f0..f075d163fec 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -75,7 +75,7 @@ template class ResetAction : public Action { public: explicit ResetAction(IntegrationSensor *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->reset(); } + void play(const Ts &...x) override { this->parent_->reset(); } protected: IntegrationSensor *parent_; diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h index 35e8141ce54..735f3968097 100644 --- a/esphome/components/key_collector/key_collector.h +++ b/esphome/components/key_collector/key_collector.h @@ -52,11 +52,11 @@ class KeyCollector : public Component { }; template class EnableAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_enabled(true); } + void play(const Ts &...x) override { this->parent_->set_enabled(true); } }; template class DisableAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_enabled(false); } + void play(const Ts &...x) override { this->parent_->set_enabled(false); } }; } // namespace key_collector diff --git a/esphome/components/ld2410/automation.h b/esphome/components/ld2410/automation.h index 7cb9855f841..f4f1c197b2b 100644 --- a/esphome/components/ld2410/automation.h +++ b/esphome/components/ld2410/automation.h @@ -12,7 +12,7 @@ template class BluetoothPasswordSetAction : public Action explicit BluetoothPasswordSetAction(LD2410Component *ld2410_comp) : ld2410_comp_(ld2410_comp) {} TEMPLATABLE_VALUE(std::string, password) - void play(Ts... x) override { this->ld2410_comp_->set_bluetooth_password(this->password_.value(x...)); } + void play(const Ts &...x) override { this->ld2410_comp_->set_bluetooth_password(this->password_.value(x...)); } protected: LD2410Component *ld2410_comp_; diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index f04543bc5be..b24e3cfdb23 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -47,7 +47,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(LEDCOutput *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + void play(const Ts &...x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/libretiny_pwm/libretiny_pwm.h b/esphome/components/libretiny_pwm/libretiny_pwm.h index 42ce40ca392..f9117090548 100644 --- a/esphome/components/libretiny_pwm/libretiny_pwm.h +++ b/esphome/components/libretiny_pwm/libretiny_pwm.h @@ -40,7 +40,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(LibreTinyPWM *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + void play(const Ts &...x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 6e055741dad..8899db8bba5 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -15,7 +15,7 @@ template class ToggleAction : public Action { TEMPLATABLE_VALUE(uint32_t, transition_length) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->state_->toggle(); call.set_transition_length(this->transition_length_.optional_value(x...)); call.perform(); @@ -44,7 +44,7 @@ template class LightControlAction : public Action { TEMPLATABLE_VALUE(float, warm_white) TEMPLATABLE_VALUE(std::string, effect) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); call.set_color_mode(this->color_mode_.optional_value(x...)); call.set_state(this->state_.optional_value(x...)); @@ -74,7 +74,7 @@ template class DimRelativeAction : public Action { TEMPLATABLE_VALUE(float, relative_brightness) TEMPLATABLE_VALUE(uint32_t, transition_length) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); float rel = this->relative_brightness_.value(x...); float cur; @@ -107,7 +107,7 @@ template class DimRelativeAction : public Action { template class LightIsOnCondition : public Condition { public: explicit LightIsOnCondition(LightState *state) : state_(state) {} - bool check(Ts... x) override { return this->state_->current_values.is_on(); } + bool check(const Ts &...x) override { return this->state_->current_values.is_on(); } protected: LightState *state_; @@ -115,7 +115,7 @@ template class LightIsOnCondition : public Condition { template class LightIsOffCondition : public Condition { public: explicit LightIsOffCondition(LightState *state) : state_(state) {} - bool check(Ts... x) override { return !this->state_->current_values.is_on(); } + bool check(const Ts &...x) override { return !this->state_->current_values.is_on(); } protected: LightState *state_; @@ -179,7 +179,7 @@ template class AddressableSet : public Action { TEMPLATABLE_VALUE(float, blue) TEMPLATABLE_VALUE(float, white) - void play(Ts... x) override { + void play(const Ts &...x) override { auto *out = (AddressableLight *) this->parent_->get_output(); int32_t range_from = interpret_index(this->range_from_.value_or(x..., 0), out->size()); if (range_from < 0 || range_from >= out->size()) diff --git a/esphome/components/lightwaverf/lightwaverf.h b/esphome/components/lightwaverf/lightwaverf.h index b9f2abfcb3c..ee4e91e9d1d 100644 --- a/esphome/components/lightwaverf/lightwaverf.h +++ b/esphome/components/lightwaverf/lightwaverf.h @@ -51,7 +51,7 @@ template class SendRawAction : public Action { void set_pulse_length(const int &data) { pulse_length_ = data; } void set_data(const std::vector &data) { code_ = data; } - void play(Ts... x) { + void play(const Ts &...x) { int repeats = this->repeat_.value(x...); int inverted = this->inverted_.value(x...); int pulse_length = this->pulse_length_.value(x...); diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 8cb3b64ffe6..0f596ef5e69 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -11,7 +11,7 @@ template class LockAction : public Action { public: explicit LockAction(Lock *a_lock) : lock_(a_lock) {} - void play(Ts... x) override { this->lock_->lock(); } + void play(const Ts &...x) override { this->lock_->lock(); } protected: Lock *lock_; @@ -21,7 +21,7 @@ template class UnlockAction : public Action { public: explicit UnlockAction(Lock *a_lock) : lock_(a_lock) {} - void play(Ts... x) override { this->lock_->unlock(); } + void play(const Ts &...x) override { this->lock_->unlock(); } protected: Lock *lock_; @@ -31,7 +31,7 @@ template class OpenAction : public Action { public: explicit OpenAction(Lock *a_lock) : lock_(a_lock) {} - void play(Ts... x) override { this->lock_->open(); } + void play(const Ts &...x) override { this->lock_->open(); } protected: Lock *lock_; @@ -40,7 +40,7 @@ template class OpenAction : public Action { template class LockCondition : public Condition { public: LockCondition(Lock *parent, bool state) : parent_(parent), state_(state) {} - bool check(Ts... x) override { + bool check(const Ts &...x) override { auto check_state = this->state_ ? LockState::LOCK_STATE_LOCKED : LockState::LOCK_STATE_UNLOCKED; return this->parent_->state == check_state; } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 50d192fde34..1ae05f933f1 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -129,7 +129,7 @@ template class ObjUpdateAction : public Action { public: explicit ObjUpdateAction(std::function &&lamb) : lamb_(std::move(lamb)) {} - void play(Ts... x) override { this->lamb_(x...); } + void play(const Ts &...x) override { this->lamb_(x...); } protected: std::function lamb_; @@ -263,7 +263,7 @@ class IdleTrigger : public Trigger<> { template class LvglAction : public Action, public Parented { public: explicit LvglAction(std::function &&lamb) : action_(std::move(lamb)) {} - void play(Ts... x) override { this->action_(this->parent_); } + void play(const Ts &...x) override { this->action_(this->parent_); } protected: std::function action_{}; @@ -272,7 +272,7 @@ template class LvglAction : public Action, public Parente template class LvglCondition : public Condition, public Parented { public: LvglCondition(std::function &&condition_lambda) : condition_lambda_(std::move(condition_lambda)) {} - bool check(Ts... x) override { return this->condition_lambda_(this->parent_); } + bool check(const Ts &...x) override { return this->condition_lambda_(this->parent_); } protected: std::function condition_lambda_{}; diff --git a/esphome/components/max17043/automation.h b/esphome/components/max17043/automation.h index 44729d119b1..ac201a73096 100644 --- a/esphome/components/max17043/automation.h +++ b/esphome/components/max17043/automation.h @@ -10,7 +10,7 @@ template class SleepAction : public Action { public: explicit SleepAction(MAX17043Component *max17043) : max17043_(max17043) {} - void play(Ts... x) override { this->max17043_->sleep_mode(); } + void play(const Ts &...x) override { this->max17043_->sleep_mode(); } protected: MAX17043Component *max17043_; diff --git a/esphome/components/max6956/automation.h b/esphome/components/max6956/automation.h index c0b491dc7f9..ca2c3e3ce4d 100644 --- a/esphome/components/max6956/automation.h +++ b/esphome/components/max6956/automation.h @@ -13,7 +13,7 @@ template class SetCurrentGlobalAction : public Action { TEMPLATABLE_VALUE(uint8_t, brightness_global) - void play(Ts... x) override { + void play(const Ts &...x) override { this->max6956_->set_brightness_global(this->brightness_global_.value(x...)); this->max6956_->write_brightness_global(); } @@ -28,7 +28,7 @@ template class SetCurrentModeAction : public Action { TEMPLATABLE_VALUE(max6956::MAX6956CURRENTMODE, brightness_mode) - void play(Ts... x) override { + void play(const Ts &...x) override { this->max6956_->set_brightness_mode(this->brightness_mode_.value(x...)); this->max6956_->write_brightness_mode(); } diff --git a/esphome/components/max7219digit/automation.h b/esphome/components/max7219digit/automation.h index 02acebb1091..be8245d14d3 100644 --- a/esphome/components/max7219digit/automation.h +++ b/esphome/components/max7219digit/automation.h @@ -12,7 +12,7 @@ template class DisplayInvertAction : public Action, publi public: TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { bool state = this->state_.value(x...); this->parent_->invert_on_off(state); } @@ -22,7 +22,7 @@ template class DisplayVisibilityAction : public Action, p public: TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { bool state = this->state_.value(x...); this->parent_->turn_on_off(state); } @@ -32,7 +32,7 @@ template class DisplayReverseAction : public Action, publ public: TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { bool state = this->state_.value(x...); this->parent_->set_reverse(state); } @@ -42,7 +42,7 @@ template class DisplayIntensityAction : public Action, pu public: TEMPLATABLE_VALUE(uint8_t, state) - void play(Ts... x) override { + void play(const Ts &...x) override { uint8_t state = this->state_.value(x...); this->parent_->set_intensity(state); } diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 3af5959f321..50e7693cb51 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -11,7 +11,7 @@ template class MediaPlayerCommandAction : public Action, public Parented { public: TEMPLATABLE_VALUE(bool, announcement); - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->make_call().set_command(Command).set_announcement(this->announcement_.value(x...)).perform(); } }; @@ -36,7 +36,7 @@ using TurnOffAction = MediaPlayerCommandAction class PlayMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) TEMPLATABLE_VALUE(bool, announcement) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->make_call() .set_media_url(this->media_url_.value(x...)) .set_announcement(this->announcement_.value(x...)) @@ -46,7 +46,7 @@ template class PlayMediaAction : public Action, public Pa template class VolumeSetAction : public Action, public Parented { TEMPLATABLE_VALUE(float, volume) - void play(Ts... x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } + void play(const Ts &...x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } }; class StateTrigger : public Trigger<> { @@ -75,32 +75,34 @@ using OffTrigger = MediaPlayerStateTrigger class IsIdleCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_IDLE; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_IDLE; } }; template class IsPlayingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; } }; template class IsPausedCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PAUSED; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PAUSED; } }; template class IsAnnouncingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; } + bool check(const Ts &...x) override { + return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + } }; template class IsOnCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ON; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ON; } }; template class IsOffCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; } }; } // namespace media_player diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index ec38f2cd2fc..be36886d622 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -40,7 +40,7 @@ template class MHZ19CalibrateZeroAction : public Action { public: MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - void play(Ts... x) override { this->mhz19_->calibrate_zero(); } + void play(const Ts &...x) override { this->mhz19_->calibrate_zero(); } protected: MHZ19Component *mhz19_; @@ -50,7 +50,7 @@ template class MHZ19ABCEnableAction : public Action { public: MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - void play(Ts... x) override { this->mhz19_->abc_enable(); } + void play(const Ts &...x) override { this->mhz19_->abc_enable(); } protected: MHZ19Component *mhz19_; @@ -60,7 +60,7 @@ template class MHZ19ABCDisableAction : public Action { public: MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - void play(Ts... x) override { this->mhz19_->abc_disable(); } + void play(const Ts &...x) override { this->mhz19_->abc_disable(); } protected: MHZ19Component *mhz19_; diff --git a/esphome/components/micro_wake_word/automation.h b/esphome/components/micro_wake_word/automation.h index f10a4ed3470..e1795a7e644 100644 --- a/esphome/components/micro_wake_word/automation.h +++ b/esphome/components/micro_wake_word/automation.h @@ -9,23 +9,23 @@ namespace micro_wake_word { template class StartAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->start(); } + void play(const Ts &...x) override { this->parent_->start(); } }; template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class IsRunningCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_running(); } + bool check(const Ts &...x) override { return this->parent_->is_running(); } }; template class EnableModelAction : public Action { public: explicit EnableModelAction(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} - void play(Ts... x) override { this->wake_word_model_->enable(); } + void play(const Ts &...x) override { this->wake_word_model_->enable(); } protected: WakeWordModel *wake_word_model_; @@ -34,7 +34,7 @@ template class EnableModelAction : public Action { template class DisableModelAction : public Action { public: explicit DisableModelAction(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} - void play(Ts... x) override { this->wake_word_model_->disable(); } + void play(const Ts &...x) override { this->wake_word_model_->disable(); } protected: WakeWordModel *wake_word_model_; @@ -43,7 +43,7 @@ template class DisableModelAction : public Action { template class ModelIsEnabledCondition : public Condition { public: explicit ModelIsEnabledCondition(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} - bool check(Ts... x) override { return this->wake_word_model_->is_enabled(); } + bool check(const Ts &...x) override { return this->wake_word_model_->is_enabled(); } protected: WakeWordModel *wake_word_model_; diff --git a/esphome/components/microphone/automation.h b/esphome/components/microphone/automation.h index 5745909c46f..a6c4bdae663 100644 --- a/esphome/components/microphone/automation.h +++ b/esphome/components/microphone/automation.h @@ -9,18 +9,18 @@ namespace esphome { namespace microphone { template class CaptureAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->start(); } + void play(const Ts &...x) override { this->parent_->start(); } }; template class StopCaptureAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class MuteAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_mute_state(true); } + void play(const Ts &...x) override { this->parent_->set_mute_state(true); } }; template class UnmuteAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_mute_state(false); } + void play(const Ts &...x) override { this->parent_->set_mute_state(false); } }; class DataTrigger : public Trigger &> { @@ -32,12 +32,12 @@ class DataTrigger : public Trigger &> { template class IsCapturingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_running(); } + bool check(const Ts &...x) override { return this->parent_->is_running(); } }; template class IsMutedCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->get_mute_state(); } + bool check(const Ts &...x) override { return this->parent_->get_mute_state(); } }; } // namespace microphone diff --git a/esphome/components/midea/ac_automations.h b/esphome/components/midea/ac_automations.h index e6fffa2511c..760737be87e 100644 --- a/esphome/components/midea/ac_automations.h +++ b/esphome/components/midea/ac_automations.h @@ -22,7 +22,7 @@ template class FollowMeAction : public MideaActionBase { TEMPLATABLE_VALUE(bool, use_fahrenheit) TEMPLATABLE_VALUE(bool, beeper) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->do_follow_me(this->temperature_.value(x...), this->use_fahrenheit_.value(x...), this->beeper_.value(x...)); } @@ -30,37 +30,37 @@ template class FollowMeAction : public MideaActionBase { template class SwingStepAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_swing_step(); } + void play(const Ts &...x) override { this->parent_->do_swing_step(); } }; template class DisplayToggleAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_display_toggle(); } + void play(const Ts &...x) override { this->parent_->do_display_toggle(); } }; template class BeeperOnAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_beeper_on(); } + void play(const Ts &...x) override { this->parent_->do_beeper_on(); } }; template class BeeperOffAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_beeper_off(); } + void play(const Ts &...x) override { this->parent_->do_beeper_off(); } }; template class PowerOnAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_power_on(); } + void play(const Ts &...x) override { this->parent_->do_power_on(); } }; template class PowerOffAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_power_off(); } + void play(const Ts &...x) override { this->parent_->do_power_off(); } }; template class PowerToggleAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_power_toggle(); } + void play(const Ts &...x) override { this->parent_->do_power_toggle(); } }; } // namespace ac diff --git a/esphome/components/mixer/speaker/automation.h b/esphome/components/mixer/speaker/automation.h index b688fa2c1ec..22349366289 100644 --- a/esphome/components/mixer/speaker/automation.h +++ b/esphome/components/mixer/speaker/automation.h @@ -9,7 +9,7 @@ namespace mixer_speaker { template class DuckingApplyAction : public Action, public Parented { TEMPLATABLE_VALUE(uint8_t, decibel_reduction) TEMPLATABLE_VALUE(uint32_t, duration) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->apply_ducking(this->decibel_reduction_.value(x...), this->duration_.value(x...)); } }; diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 325ca56f4b6..79383ee857b 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -389,7 +389,7 @@ template class MQTTPublishAction : public Action { TEMPLATABLE_VALUE(uint8_t, qos) TEMPLATABLE_VALUE(bool, retain) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->publish(this->topic_.value(x...), this->payload_.value(x...), this->qos_.value(x...), this->retain_.value(x...)); } @@ -407,7 +407,7 @@ template class MQTTPublishJsonAction : public Action { void set_payload(std::function payload) { this->payload_ = payload; } - void play(Ts... x) override { + void play(const Ts &...x) override { auto f = std::bind(&MQTTPublishJsonAction::encode_, this, x..., std::placeholders::_1); auto topic = this->topic_.value(x...); auto qos = this->qos_.value(x...); @@ -424,7 +424,7 @@ template class MQTTPublishJsonAction : public Action { template class MQTTConnectedCondition : public Condition { public: MQTTConnectedCondition(MQTTClientComponent *parent) : parent_(parent) {} - bool check(Ts... x) override { return this->parent_->is_connected(); } + bool check(const Ts &...x) override { return this->parent_->is_connected(); } protected: MQTTClientComponent *parent_; @@ -434,7 +434,7 @@ template class MQTTEnableAction : public Action { public: MQTTEnableAction(MQTTClientComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->enable(); } + void play(const Ts &...x) override { this->parent_->enable(); } protected: MQTTClientComponent *parent_; @@ -444,7 +444,7 @@ template class MQTTDisableAction : public Action { public: MQTTDisableAction(MQTTClientComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->disable(); } + void play(const Ts &...x) override { this->parent_->disable(); } protected: MQTTClientComponent *parent_; diff --git a/esphome/components/nau7802/nau7802.h b/esphome/components/nau7802/nau7802.h index 17e426ccc6c..05452851caf 100644 --- a/esphome/components/nau7802/nau7802.h +++ b/esphome/components/nau7802/nau7802.h @@ -101,18 +101,18 @@ class NAU7802Sensor : public sensor::Sensor, public PollingComponent, public i2c template class NAU7802CalbrateExternalOffsetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->calibrate_external_offset(); } + void play(const Ts &...x) override { this->parent_->calibrate_external_offset(); } }; template class NAU7802CalbrateInternalOffsetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->calibrate_internal_offset(); } + void play(const Ts &...x) override { this->parent_->calibrate_internal_offset(); } }; template class NAU7802CalbrateGainAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->calibrate_gain(); } + void play(const Ts &...x) override { this->parent_->calibrate_gain(); } }; } // namespace nau7802 diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index c718355af85..8e85e15823e 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -55,7 +55,7 @@ template class NextionSetBrightnessAction : public Action TEMPLATABLE_VALUE(float, brightness) - void play(Ts... x) override { + void play(const Ts &...x) override { this->component_->set_brightness(this->brightness_.value(x...)); this->component_->set_backlight_brightness(this->brightness_.value(x...)); } @@ -74,7 +74,7 @@ template class NextionPublishFloatAction : public Action TEMPLATABLE_VALUE(bool, publish_state) TEMPLATABLE_VALUE(bool, send_to_nextion) - void play(Ts... x) override { + void play(const Ts &...x) override { this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...), this->send_to_nextion_.value(x...)); } @@ -97,7 +97,7 @@ template class NextionPublishTextAction : public Action { TEMPLATABLE_VALUE(bool, publish_state) TEMPLATABLE_VALUE(bool, send_to_nextion) - void play(Ts... x) override { + void play(const Ts &...x) override { this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...), this->send_to_nextion_.value(x...)); } @@ -120,7 +120,7 @@ template class NextionPublishBoolAction : public Action { TEMPLATABLE_VALUE(bool, publish_state) TEMPLATABLE_VALUE(bool, send_to_nextion) - void play(Ts... x) override { + void play(const Ts &...x) override { this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...), this->send_to_nextion_.value(x...)); } diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index 33f0f9727e5..79eba883c47 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -19,7 +19,7 @@ template class NumberSetAction : public Action { NumberSetAction(Number *number) : number_(number) {} TEMPLATABLE_VALUE(float, value) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->number_->make_call(); call.set_value(this->value_.value(x...)); call.perform(); @@ -35,7 +35,7 @@ template class NumberOperationAction : public Action { TEMPLATABLE_VALUE(NumberOperation, operation) TEMPLATABLE_VALUE(bool, cycle) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->number_->make_call(); call.with_operation(this->operation_.value(x...)); if (this->cycle_.has_value()) { @@ -74,7 +74,7 @@ template class NumberInRangeCondition : public Condition void set_min(float min) { this->min_ = min; } void set_max(float max) { this->max_ = max; } - bool check(Ts... x) override { + bool check(const Ts &...x) override { const float state = this->parent_->state; if (std::isnan(this->min_)) { return state <= this->max_; diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 3326cbe8d6b..12d409ca29b 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -207,7 +207,7 @@ template class OnlineImageSetUrlAction : public Action { OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(bool, update) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_url(this->url_.value(x...)); if (this->update_.value(x...)) { this->parent_->update(); @@ -221,7 +221,7 @@ template class OnlineImageSetUrlAction : public Action { template class OnlineImageReleaseAction : public Action { public: OnlineImageReleaseAction(OnlineImage *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->release(); } + void play(const Ts &...x) override { this->parent_->release(); } protected: OnlineImage *parent_; diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index de84bb91ca2..32793781292 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -12,7 +12,7 @@ template class TurnOffAction : public Action { public: TurnOffAction(BinaryOutput *output) : output_(output) {} - void play(Ts... x) override { this->output_->turn_off(); } + void play(const Ts &...x) override { this->output_->turn_off(); } protected: BinaryOutput *output_; @@ -22,7 +22,7 @@ template class TurnOnAction : public Action { public: TurnOnAction(BinaryOutput *output) : output_(output) {} - void play(Ts... x) override { this->output_->turn_on(); } + void play(const Ts &...x) override { this->output_->turn_on(); } protected: BinaryOutput *output_; @@ -34,7 +34,7 @@ template class SetLevelAction : public Action { TEMPLATABLE_VALUE(float, level) - void play(Ts... x) override { this->output_->set_level(this->level_.value(x...)); } + void play(const Ts &...x) override { this->output_->set_level(this->level_.value(x...)); } protected: FloatOutput *output_; @@ -46,7 +46,7 @@ template class SetMinPowerAction : public Action { TEMPLATABLE_VALUE(float, min_power) - void play(Ts... x) override { this->output_->set_min_power(this->min_power_.value(x...)); } + void play(const Ts &...x) override { this->output_->set_min_power(this->min_power_.value(x...)); } protected: FloatOutput *output_; @@ -58,7 +58,7 @@ template class SetMaxPowerAction : public Action { TEMPLATABLE_VALUE(float, max_power) - void play(Ts... x) override { this->output_->set_max_power(this->max_power_.value(x...)); } + void play(const Ts &...x) override { this->output_->set_max_power(this->max_power_.value(x...)); } protected: FloatOutput *output_; diff --git a/esphome/components/pcf85063/pcf85063.h b/esphome/components/pcf85063/pcf85063.h index 1a3fd704e57..b7034d4f00f 100644 --- a/esphome/components/pcf85063/pcf85063.h +++ b/esphome/components/pcf85063/pcf85063.h @@ -85,12 +85,12 @@ class PCF85063Component : public time::RealTimeClock, public i2c::I2CDevice { template class WriteAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->write_time(); } + void play(const Ts &...x) override { this->parent_->write_time(); } }; template class ReadAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->read_time(); } + void play(const Ts &...x) override { this->parent_->read_time(); } }; } // namespace pcf85063 } // namespace esphome diff --git a/esphome/components/pcf8563/pcf8563.h b/esphome/components/pcf8563/pcf8563.h index b6832efe72d..81aa816b42d 100644 --- a/esphome/components/pcf8563/pcf8563.h +++ b/esphome/components/pcf8563/pcf8563.h @@ -113,12 +113,12 @@ class PCF8563Component : public time::RealTimeClock, public i2c::I2CDevice { template class WriteAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->write_time(); } + void play(const Ts &...x) override { this->parent_->write_time(); } }; template class ReadAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->read_time(); } + void play(const Ts &...x) override { this->parent_->read_time(); } }; } // namespace pcf8563 } // namespace esphome diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 1a09ffdd20f..dc0a92efed5 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -109,7 +109,7 @@ template class PIDAutotuneAction : public Action { void set_positive_output(float positive_output) { positive_output_ = positive_output; } void set_negative_output(float negative_output) { negative_output_ = negative_output; } - void play(Ts... x) { + void play(const Ts &...x) { auto tuner = make_unique(); tuner->set_noiseband(this->noiseband_); tuner->set_output_negative(this->negative_output_); @@ -128,7 +128,7 @@ template class PIDResetIntegralTermAction : public Action public: PIDResetIntegralTermAction(PIDClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->reset_integral_term(); } + void play(const Ts &...x) { this->parent_->reset_integral_term(); } protected: PIDClimate *parent_; @@ -138,7 +138,7 @@ template class PIDSetControlParametersAction : public Actionkp_.value(x...); auto ki = this->ki_.value(x...); auto kd = this->kd_.value(x...); diff --git a/esphome/components/pipsolar/output/pipsolar_output.h b/esphome/components/pipsolar/output/pipsolar_output.h index 29b2d116f2c..b4b8000962c 100644 --- a/esphome/components/pipsolar/output/pipsolar_output.h +++ b/esphome/components/pipsolar/output/pipsolar_output.h @@ -32,7 +32,7 @@ template class SetOutputAction : public Action { TEMPLATABLE_VALUE(float, level) - void play(Ts... x) override { this->output_->set_value(this->level_.value(x...)); } + void play(const Ts &...x) override { this->output_->set_value(this->level_.value(x...)); } protected: PipsolarOutput *output_; diff --git a/esphome/components/pmwcs3/pmwcs3.h b/esphome/components/pmwcs3/pmwcs3.h index d60f9d1f61c..d63c5165864 100644 --- a/esphome/components/pmwcs3/pmwcs3.h +++ b/esphome/components/pmwcs3/pmwcs3.h @@ -38,7 +38,7 @@ template class PMWCS3AirCalibrationAction : public Action public: PMWCS3AirCalibrationAction(PMWCS3Component *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->air_calibration(); } + void play(const Ts &...x) override { this->parent_->air_calibration(); } protected: PMWCS3Component *parent_; @@ -48,7 +48,7 @@ template class PMWCS3WaterCalibrationAction : public Actionparent_->water_calibration(); } + void play(const Ts &...x) override { this->parent_->water_calibration(); } protected: PMWCS3Component *parent_; @@ -59,7 +59,7 @@ template class PMWCS3NewI2cAddressAction : public Action PMWCS3NewI2cAddressAction(PMWCS3Component *parent) : parent_(parent) {} TEMPLATABLE_VALUE(int, new_address) - void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } + void play(const Ts &...x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } protected: PMWCS3Component *parent_; diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index c8e9a400086..eeb15648fbf 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -143,7 +143,7 @@ class PN532OnFinishedWriteTrigger : public Trigger<> { template class PN532IsWritingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_writing(); } + bool check(const Ts &...x) override { return this->parent_->is_writing(); } }; } // namespace pn532 diff --git a/esphome/components/pn7150/automation.h b/esphome/components/pn7150/automation.h index aebb1b7573e..21329a998a2 100644 --- a/esphome/components/pn7150/automation.h +++ b/esphome/components/pn7150/automation.h @@ -23,42 +23,42 @@ class PN7150OnFinishedWriteTrigger : public Trigger<> { template class PN7150IsWritingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_writing(); } + bool check(const Ts &...x) override { return this->parent_->is_writing(); } }; template class EmulationOffAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } + void play(const Ts &...x) override { this->parent_->set_tag_emulation_off(); } }; template class EmulationOnAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } + void play(const Ts &...x) override { this->parent_->set_tag_emulation_on(); } }; template class PollingOffAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_polling_off(); } + void play(const Ts &...x) override { this->parent_->set_polling_off(); } }; template class PollingOnAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_polling_on(); } + void play(const Ts &...x) override { this->parent_->set_polling_on(); } }; template class SetCleanModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->clean_mode(); } + void play(const Ts &...x) override { this->parent_->clean_mode(); } }; template class SetFormatModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->format_mode(); } + void play(const Ts &...x) override { this->parent_->format_mode(); } }; template class SetReadModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->read_mode(); } + void play(const Ts &...x) override { this->parent_->read_mode(); } }; template class SetEmulationMessageAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, message) TEMPLATABLE_VALUE(bool, include_android_app_record) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), this->include_android_app_record_.optional_value(x...)); } @@ -68,14 +68,14 @@ template class SetWriteMessageAction : public Action, pub TEMPLATABLE_VALUE(std::string, message) TEMPLATABLE_VALUE(bool, include_android_app_record) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_tag_write_message(this->message_.optional_value(x...), this->include_android_app_record_.optional_value(x...)); } }; template class SetWriteModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->write_mode(); } + void play(const Ts &...x) override { this->parent_->write_mode(); } }; } // namespace pn7150 diff --git a/esphome/components/pn7160/automation.h b/esphome/components/pn7160/automation.h index 854fb116848..08148c23114 100644 --- a/esphome/components/pn7160/automation.h +++ b/esphome/components/pn7160/automation.h @@ -23,42 +23,42 @@ class PN7160OnFinishedWriteTrigger : public Trigger<> { template class PN7160IsWritingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_writing(); } + bool check(const Ts &...x) override { return this->parent_->is_writing(); } }; template class EmulationOffAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } + void play(const Ts &...x) override { this->parent_->set_tag_emulation_off(); } }; template class EmulationOnAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } + void play(const Ts &...x) override { this->parent_->set_tag_emulation_on(); } }; template class PollingOffAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_polling_off(); } + void play(const Ts &...x) override { this->parent_->set_polling_off(); } }; template class PollingOnAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_polling_on(); } + void play(const Ts &...x) override { this->parent_->set_polling_on(); } }; template class SetCleanModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->clean_mode(); } + void play(const Ts &...x) override { this->parent_->clean_mode(); } }; template class SetFormatModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->format_mode(); } + void play(const Ts &...x) override { this->parent_->format_mode(); } }; template class SetReadModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->read_mode(); } + void play(const Ts &...x) override { this->parent_->read_mode(); } }; template class SetEmulationMessageAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, message) TEMPLATABLE_VALUE(bool, include_android_app_record) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), this->include_android_app_record_.optional_value(x...)); } @@ -68,14 +68,14 @@ template class SetWriteMessageAction : public Action, pub TEMPLATABLE_VALUE(std::string, message) TEMPLATABLE_VALUE(bool, include_android_app_record) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_tag_write_message(this->message_.optional_value(x...), this->include_android_app_record_.optional_value(x...)); } }; template class SetWriteModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->write_mode(); } + void play(const Ts &...x) override { this->parent_->write_mode(); } }; } // namespace pn7160 diff --git a/esphome/components/pulse_counter/automation.h b/esphome/components/pulse_counter/automation.h index d749540a954..0c0dc2552d1 100644 --- a/esphome/components/pulse_counter/automation.h +++ b/esphome/components/pulse_counter/automation.h @@ -14,7 +14,7 @@ template class SetTotalPulsesAction : public Action { TEMPLATABLE_VALUE(uint32_t, total_pulses) - void play(Ts... x) override { this->pulse_counter_->set_total_pulses(this->total_pulses_.value(x...)); } + void play(const Ts &...x) override { this->pulse_counter_->set_total_pulses(this->total_pulses_.value(x...)); } protected: PulseCounterSensor *pulse_counter_; diff --git a/esphome/components/pulse_meter/automation.h b/esphome/components/pulse_meter/automation.h index 3112ded6802..bf0768b7af5 100644 --- a/esphome/components/pulse_meter/automation.h +++ b/esphome/components/pulse_meter/automation.h @@ -14,7 +14,7 @@ template class SetTotalPulsesAction : public Action { TEMPLATABLE_VALUE(uint32_t, total_pulses) - void play(Ts... x) override { this->pulse_meter_->set_total_pulses(this->total_pulses_.value(x...)); } + void play(const Ts &...x) override { this->pulse_meter_->set_total_pulses(this->total_pulses_.value(x...)); } protected: PulseMeterSensor *pulse_meter_; diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h index 7a229b49ce6..e5b96115f93 100644 --- a/esphome/components/pzemac/pzemac.h +++ b/esphome/components/pzemac/pzemac.h @@ -43,7 +43,7 @@ template class ResetEnergyAction : public Action { public: ResetEnergyAction(PZEMAC *pzemac) : pzemac_(pzemac) {} - void play(Ts... x) override { this->pzemac_->reset_energy_(); } + void play(const Ts &...x) override { this->pzemac_->reset_energy_(); } protected: PZEMAC *pzemac_; diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h index b91ab4c0a56..2e6c26a10cf 100644 --- a/esphome/components/pzemdc/pzemdc.h +++ b/esphome/components/pzemdc/pzemdc.h @@ -36,7 +36,7 @@ template class ResetEnergyAction : public Action { public: ResetEnergyAction(PZEMDC *pzemdc) : pzemdc_(pzemdc) {} - void play(Ts... x) override { this->pzemdc_->reset_energy(); } + void play(const Ts &...x) override { this->pzemdc_->reset_energy(); } protected: PZEMDC *pzemdc_; diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index b740ba80850..2cb79bf5715 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -276,7 +276,7 @@ template class RemoteTransmitterActionBase : public RemoteTransm TEMPLATABLE_VALUE(uint32_t, send_wait) protected: - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->transmitter_->transmit(); this->encode(call.get_data(), x...); call.set_send_times(this->send_times_.value_or(x..., 1)); diff --git a/esphome/components/remote_transmitter/automation.h b/esphome/components/remote_transmitter/automation.h index 75b017ec61b..bee1d0be8ac 100644 --- a/esphome/components/remote_transmitter/automation.h +++ b/esphome/components/remote_transmitter/automation.h @@ -11,7 +11,7 @@ namespace remote_transmitter { template class DigitalWriteAction : public Action, public Parented { public: TEMPLATABLE_VALUE(bool, value) - void play(Ts... x) override { this->parent_->digital_write(this->value_.value(x...)); } + void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); } }; } // namespace remote_transmitter diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index fe6dd96b38b..d2f75c819dc 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -98,7 +98,7 @@ template class RFBridgeSendCodeAction : public Action { TEMPLATABLE_VALUE(uint16_t, high) TEMPLATABLE_VALUE(uint32_t, code) - void play(Ts... x) { + void play(const Ts &...x) { RFBridgeData data{}; data.sync = this->sync_.value(x...); data.low = this->low_.value(x...); @@ -118,7 +118,7 @@ template class RFBridgeSendAdvancedCodeAction : public Actionlength_.value(x...); data.protocol = this->protocol_.value(x...); @@ -134,7 +134,7 @@ template class RFBridgeLearnAction : public Action { public: RFBridgeLearnAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->learn(); } + void play(const Ts &...x) { this->parent_->learn(); } protected: RFBridgeComponent *parent_; @@ -144,7 +144,7 @@ template class RFBridgeStartAdvancedSniffingAction : public Acti public: RFBridgeStartAdvancedSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->start_advanced_sniffing(); } + void play(const Ts &...x) { this->parent_->start_advanced_sniffing(); } protected: RFBridgeComponent *parent_; @@ -154,7 +154,7 @@ template class RFBridgeStopAdvancedSniffingAction : public Actio public: RFBridgeStopAdvancedSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->stop_advanced_sniffing(); } + void play(const Ts &...x) { this->parent_->stop_advanced_sniffing(); } protected: RFBridgeComponent *parent_; @@ -164,7 +164,7 @@ template class RFBridgeStartBucketSniffingAction : public Action public: RFBridgeStartBucketSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->start_bucket_sniffing(); } + void play(const Ts &...x) { this->parent_->start_bucket_sniffing(); } protected: RFBridgeComponent *parent_; @@ -175,7 +175,7 @@ template class RFBridgeSendRawAction : public Action { RFBridgeSendRawAction(RFBridgeComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, raw) - void play(Ts... x) { this->parent_->send_raw(this->raw_.value(x...)); } + void play(const Ts &...x) { this->parent_->send_raw(this->raw_.value(x...)); } protected: RFBridgeComponent *parent_; @@ -186,7 +186,7 @@ template class RFBridgeBeepAction : public Action { RFBridgeBeepAction(RFBridgeComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(uint16_t, duration) - void play(Ts... x) { this->parent_->beep(this->duration_.value(x...)); } + void play(const Ts &...x) { this->parent_->beep(this->duration_.value(x...)); } protected: RFBridgeComponent *parent_; diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index e88ee9152af..14442f0565f 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -114,7 +114,7 @@ template class RotaryEncoderSetValueAction : public Actionencoder_->set_value(this->value_.value(x...)); } + void play(const Ts &...x) override { this->encoder_->set_value(this->value_.value(x...)); } protected: RotaryEncoderSensor *encoder_; diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.h b/esphome/components/rp2040_pwm/rp2040_pwm.h index e499e72b061..b82765b1c01 100644 --- a/esphome/components/rp2040_pwm/rp2040_pwm.h +++ b/esphome/components/rp2040_pwm/rp2040_pwm.h @@ -45,7 +45,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(RP2040PWM *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + void play(const Ts &...x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index d536c6c08e1..1e924a897c2 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -122,7 +122,7 @@ template class PlayAction : public Action { PlayAction(Rtttl *rtttl) : rtttl_(rtttl) {} TEMPLATABLE_VALUE(std::string, value) - void play(Ts... x) override { this->rtttl_->play(this->value_.value(x...)); } + void play(const Ts &...x) override { this->rtttl_->play(this->value_.value(x...)); } protected: Rtttl *rtttl_; @@ -130,12 +130,12 @@ template class PlayAction : public Action { template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class IsPlayingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_playing(); } + bool check(const Ts &...x) override { return this->parent_->is_playing(); } }; class FinishedPlaybackTrigger : public Trigger<> { diff --git a/esphome/components/scd30/automation.h b/esphome/components/scd30/automation.h index 37b3bc16747..1f89e7c8159 100644 --- a/esphome/components/scd30/automation.h +++ b/esphome/components/scd30/automation.h @@ -9,7 +9,7 @@ namespace scd30 { template class ForceRecalibrationWithReference : public Action, public Parented { public: - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->value_.has_value()) { this->parent_->force_recalibration_with_reference(this->value_.value(x...)); } diff --git a/esphome/components/scd4x/automation.h b/esphome/components/scd4x/automation.h index dc43e9eb560..6ce14685770 100644 --- a/esphome/components/scd4x/automation.h +++ b/esphome/components/scd4x/automation.h @@ -9,7 +9,7 @@ namespace scd4x { template class PerformForcedCalibrationAction : public Action, public Parented { public: - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->value_.has_value()) { this->parent_->perform_forced_calibration(this->value_.value(x...)); } @@ -21,7 +21,7 @@ template class PerformForcedCalibrationAction : public Action class FactoryResetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->factory_reset(); } + void play(const Ts &...x) override { this->parent_->factory_reset(); } }; } // namespace scd4x diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 870a623f32c..51cece01e41 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -215,7 +215,7 @@ template class ScriptExecuteAction, T template void set_args(F... x) { args_ = Args{x...}; } - void play(Ts... x) override { this->script_->execute_tuple(this->eval_args_(x...)); } + void play(const Ts &...x) override { this->script_->execute_tuple(this->eval_args_(x...)); } protected: // NOTE: @@ -249,7 +249,7 @@ template class ScriptStopAction : public Action public: ScriptStopAction(C *script) : script_(script) {} - void play(Ts... x) override { this->script_->stop(); } + void play(const Ts &...x) override { this->script_->stop(); } protected: C *script_; @@ -259,7 +259,7 @@ template class IsRunningCondition : public Conditionparent_->is_running(); } + bool check(const Ts &...x) override { return this->parent_->is_running(); } protected: C *parent_; @@ -281,7 +281,7 @@ template class ScriptWaitAction : public Action, this->disable_loop(); } - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; // Check if we can continue immediately. if (!this->script_->is_running()) { @@ -312,7 +312,7 @@ template class ScriptWaitAction : public Action, this->disable_loop(); } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h index 12506651883..3e42eaf98a7 100644 --- a/esphome/components/select/automation.h +++ b/esphome/components/select/automation.h @@ -19,7 +19,7 @@ template class SelectSetAction : public Action { explicit SelectSetAction(Select *select) : select_(select) {} TEMPLATABLE_VALUE(std::string, option) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->select_->make_call(); call.set_option(this->option_.value(x...)); call.perform(); @@ -34,7 +34,7 @@ template class SelectSetIndexAction : public Action { explicit SelectSetIndexAction(Select *select) : select_(select) {} TEMPLATABLE_VALUE(size_t, index) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->select_->make_call(); call.set_index(this->index_.value(x...)); call.perform(); @@ -50,7 +50,7 @@ template class SelectOperationAction : public Action { TEMPLATABLE_VALUE(bool, cycle) TEMPLATABLE_VALUE(SelectOperation, operation) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->select_->make_call(); call.with_operation(this->operation_.value(x...)); if (this->cycle_.has_value()) { diff --git a/esphome/components/sen5x/automation.h b/esphome/components/sen5x/automation.h index 423b9420005..558ea46e47f 100644 --- a/esphome/components/sen5x/automation.h +++ b/esphome/components/sen5x/automation.h @@ -11,7 +11,7 @@ template class StartFanAction : public Action { public: explicit StartFanAction(SEN5XComponent *sen5x) : sen5x_(sen5x) {} - void play(Ts... x) override { this->sen5x_->start_fan_cleaning(); } + void play(const Ts &...x) override { this->sen5x_->start_fan_cleaning(); } protected: SEN5XComponent *sen5x_; diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h index 5b66860f1a4..9db849075d9 100644 --- a/esphome/components/senseair/senseair.h +++ b/esphome/components/senseair/senseair.h @@ -42,7 +42,7 @@ template class SenseAirBackgroundCalibrationAction : public Acti public: SenseAirBackgroundCalibrationAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->background_calibration(); } + void play(const Ts &...x) override { this->senseair_->background_calibration(); } protected: SenseAirComponent *senseair_; @@ -52,7 +52,7 @@ template class SenseAirBackgroundCalibrationResultAction : publi public: SenseAirBackgroundCalibrationResultAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->background_calibration_result(); } + void play(const Ts &...x) override { this->senseair_->background_calibration_result(); } protected: SenseAirComponent *senseair_; @@ -62,7 +62,7 @@ template class SenseAirABCEnableAction : public Action { public: SenseAirABCEnableAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->abc_enable(); } + void play(const Ts &...x) override { this->senseair_->abc_enable(); } protected: SenseAirComponent *senseair_; @@ -72,7 +72,7 @@ template class SenseAirABCDisableAction : public Action { public: SenseAirABCDisableAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->abc_disable(); } + void play(const Ts &...x) override { this->senseair_->abc_disable(); } protected: SenseAirComponent *senseair_; @@ -82,7 +82,7 @@ template class SenseAirABCGetPeriodAction : public Action public: SenseAirABCGetPeriodAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->abc_get_period(); } + void play(const Ts &...x) override { this->senseair_->abc_get_period(); } protected: SenseAirComponent *senseair_; diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 4f34c35023a..df7d31a0c92 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -26,7 +26,7 @@ template class SensorPublishAction : public Action { SensorPublishAction(Sensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(float, state) - void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } + void play(const Ts &...x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: Sensor *sensor_; @@ -90,7 +90,7 @@ template class SensorInRangeCondition : public Condition void set_min(float min) { this->min_ = min; } void set_max(float max) { this->max_ = max; } - bool check(Ts... x) override { + bool check(const Ts &...x) override { const float state = this->parent_->state; if (std::isnan(this->min_)) { return state <= this->max_; diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index ff1708dc534..3d15aefefec 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -57,7 +57,7 @@ template class ServoWriteAction : public Action { ServoWriteAction(Servo *servo) : servo_(servo) {} TEMPLATABLE_VALUE(float, value) - void play(Ts... x) override { this->servo_->write(this->value_.value(x...)); } + void play(const Ts &...x) override { this->servo_->write(this->value_.value(x...)); } protected: Servo *servo_; @@ -67,7 +67,7 @@ template class ServoDetachAction : public Action { public: ServoDetachAction(Servo *servo) : servo_(servo) {} - void play(Ts... x) override { this->servo_->detach(); } + void play(const Ts &...x) override { this->servo_->detach(); } protected: Servo *servo_; diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index bf7efd69154..a2da686ce1f 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -162,7 +162,7 @@ template class Sim800LSendSmsAction : public Action { TEMPLATABLE_VALUE(std::string, recipient) TEMPLATABLE_VALUE(std::string, message) - void play(Ts... x) { + void play(const Ts &...x) { auto recipient = this->recipient_.value(x...); auto message = this->message_.value(x...); this->parent_->send_sms(recipient, message); @@ -177,7 +177,7 @@ template class Sim800LSendUssdAction : public Action { Sim800LSendUssdAction(Sim800LComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, ussd) - void play(Ts... x) { + void play(const Ts &...x) { auto ussd_code = this->ussd_.value(x...); this->parent_->send_ussd(ussd_code); } @@ -191,7 +191,7 @@ template class Sim800LDialAction : public Action { Sim800LDialAction(Sim800LComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, recipient) - void play(Ts... x) { + void play(const Ts &...x) { auto recipient = this->recipient_.value(x...); this->parent_->dial(recipient); } @@ -203,7 +203,7 @@ template class Sim800LConnectAction : public Action { public: Sim800LConnectAction(Sim800LComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->connect(); } + void play(const Ts &...x) { this->parent_->connect(); } protected: Sim800LComponent *parent_; @@ -213,7 +213,7 @@ template class Sim800LDisconnectAction : public Action { public: Sim800LDisconnectAction(Sim800LComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->disconnect(); } + void play(const Ts &...x) { this->parent_->disconnect(); } protected: Sim800LComponent *parent_; diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index 6a80a60ac71..dc35f69fe25 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -60,12 +60,12 @@ class SoundLevelComponent : public Component { template class StartAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->start(); } + void play(const Ts &...x) override { this->parent_->start(); } }; template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; } // namespace sound_level diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index c083796eea9..80bba250300 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -19,7 +19,7 @@ template class PlayAction : public Action, public Parente this->static_ = true; } - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->static_) { this->parent_->play(this->data_static_); } else { @@ -36,14 +36,14 @@ template class PlayAction : public Action, public Parente template class VolumeSetAction : public Action, public Parented { TEMPLATABLE_VALUE(float, volume) - void play(Ts... x) override { this->parent_->set_volume(this->volume_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_volume(this->volume_.value(x...)); } }; template class MuteOnAction : public Action { public: explicit MuteOnAction(Speaker *speaker) : speaker_(speaker) {} - void play(Ts... x) override { this->speaker_->set_mute_state(true); } + void play(const Ts &...x) override { this->speaker_->set_mute_state(true); } protected: Speaker *speaker_; @@ -53,7 +53,7 @@ template class MuteOffAction : public Action { public: explicit MuteOffAction(Speaker *speaker) : speaker_(speaker) {} - void play(Ts... x) override { this->speaker_->set_mute_state(false); } + void play(const Ts &...x) override { this->speaker_->set_mute_state(false); } protected: Speaker *speaker_; @@ -61,22 +61,22 @@ template class MuteOffAction : public Action { template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class FinishAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->finish(); } + void play(const Ts &...x) override { this->parent_->finish(); } }; template class IsPlayingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_running(); } + bool check(const Ts &...x) override { return this->parent_->is_running(); } }; template class IsStoppedCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_stopped(); } + bool check(const Ts &...x) override { return this->parent_->is_stopped(); } }; } // namespace speaker diff --git a/esphome/components/speaker/media_player/automation.h b/esphome/components/speaker/media_player/automation.h index d1a01aabc48..fdf3db07f9f 100644 --- a/esphome/components/speaker/media_player/automation.h +++ b/esphome/components/speaker/media_player/automation.h @@ -14,7 +14,7 @@ template class PlayOnDeviceMediaAction : public Action, p TEMPLATABLE_VALUE(audio::AudioFile *, audio_file) TEMPLATABLE_VALUE(bool, announcement) TEMPLATABLE_VALUE(bool, enqueue) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->play_file(this->audio_file_.value(x...), this->announcement_.value(x...), this->enqueue_.value(x...)); } diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h index 59c6cd50e1f..d6c877ae903 100644 --- a/esphome/components/sprinkler/automation.h +++ b/esphome/components/sprinkler/automation.h @@ -13,7 +13,7 @@ template class SetDividerAction : public Action { TEMPLATABLE_VALUE(uint32_t, divider) - void play(Ts... x) override { this->sprinkler_->set_divider(this->divider_.optional_value(x...)); } + void play(const Ts &...x) override { this->sprinkler_->set_divider(this->divider_.optional_value(x...)); } protected: Sprinkler *sprinkler_; @@ -25,7 +25,7 @@ template class SetMultiplierAction : public Action { TEMPLATABLE_VALUE(float, multiplier) - void play(Ts... x) override { this->sprinkler_->set_multiplier(this->multiplier_.optional_value(x...)); } + void play(const Ts &...x) override { this->sprinkler_->set_multiplier(this->multiplier_.optional_value(x...)); } protected: Sprinkler *sprinkler_; @@ -38,7 +38,7 @@ template class QueueValveAction : public Action { TEMPLATABLE_VALUE(size_t, valve_number) TEMPLATABLE_VALUE(uint32_t, valve_run_duration) - void play(Ts... x) override { + void play(const Ts &...x) override { this->sprinkler_->queue_valve(this->valve_number_.optional_value(x...), this->valve_run_duration_.optional_value(x...)); } @@ -51,7 +51,7 @@ template class ClearQueuedValvesAction : public Action { public: explicit ClearQueuedValvesAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->clear_queued_valves(); } + void play(const Ts &...x) override { this->sprinkler_->clear_queued_valves(); } protected: Sprinkler *sprinkler_; @@ -63,7 +63,7 @@ template class SetRepeatAction : public Action { TEMPLATABLE_VALUE(uint32_t, repeat) - void play(Ts... x) override { this->sprinkler_->set_repeat(this->repeat_.optional_value(x...)); } + void play(const Ts &...x) override { this->sprinkler_->set_repeat(this->repeat_.optional_value(x...)); } protected: Sprinkler *sprinkler_; @@ -76,7 +76,7 @@ template class SetRunDurationAction : public Action { TEMPLATABLE_VALUE(size_t, valve_number) TEMPLATABLE_VALUE(uint32_t, valve_run_duration) - void play(Ts... x) override { + void play(const Ts &...x) override { this->sprinkler_->set_valve_run_duration(this->valve_number_.optional_value(x...), this->valve_run_duration_.optional_value(x...)); } @@ -89,7 +89,7 @@ template class StartFromQueueAction : public Action { public: explicit StartFromQueueAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->start_from_queue(); } + void play(const Ts &...x) override { this->sprinkler_->start_from_queue(); } protected: Sprinkler *sprinkler_; @@ -99,7 +99,7 @@ template class StartFullCycleAction : public Action { public: explicit StartFullCycleAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->start_full_cycle(); } + void play(const Ts &...x) override { this->sprinkler_->start_full_cycle(); } protected: Sprinkler *sprinkler_; @@ -112,7 +112,7 @@ template class StartSingleValveAction : public Action { TEMPLATABLE_VALUE(size_t, valve_to_start) TEMPLATABLE_VALUE(uint32_t, valve_run_duration) - void play(Ts... x) override { + void play(const Ts &...x) override { this->sprinkler_->start_single_valve(this->valve_to_start_.optional_value(x...), this->valve_run_duration_.optional_value(x...)); } @@ -125,7 +125,7 @@ template class ShutdownAction : public Action { public: explicit ShutdownAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->shutdown(); } + void play(const Ts &...x) override { this->sprinkler_->shutdown(); } protected: Sprinkler *sprinkler_; @@ -135,7 +135,7 @@ template class NextValveAction : public Action { public: explicit NextValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->next_valve(); } + void play(const Ts &...x) override { this->sprinkler_->next_valve(); } protected: Sprinkler *sprinkler_; @@ -145,7 +145,7 @@ template class PreviousValveAction : public Action { public: explicit PreviousValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->previous_valve(); } + void play(const Ts &...x) override { this->sprinkler_->previous_valve(); } protected: Sprinkler *sprinkler_; @@ -155,7 +155,7 @@ template class PauseAction : public Action { public: explicit PauseAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->pause(); } + void play(const Ts &...x) override { this->sprinkler_->pause(); } protected: Sprinkler *sprinkler_; @@ -165,7 +165,7 @@ template class ResumeAction : public Action { public: explicit ResumeAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->resume(); } + void play(const Ts &...x) override { this->sprinkler_->resume(); } protected: Sprinkler *sprinkler_; @@ -175,7 +175,7 @@ template class ResumeOrStartAction : public Action { public: explicit ResumeOrStartAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->resume_or_start_full_cycle(); } + void play(const Ts &...x) override { this->sprinkler_->resume_or_start_full_cycle(); } protected: Sprinkler *sprinkler_; diff --git a/esphome/components/sps30/automation.h b/esphome/components/sps30/automation.h index 443aafb5756..67af8136872 100644 --- a/esphome/components/sps30/automation.h +++ b/esphome/components/sps30/automation.h @@ -11,7 +11,7 @@ template class StartFanAction : public Action { public: explicit StartFanAction(SPS30Component *sps30) : sps30_(sps30) {} - void play(Ts... x) override { this->sps30_->start_fan_cleaning(); } + void play(const Ts &...x) override { this->sps30_->start_fan_cleaning(); } protected: SPS30Component *sps30_; diff --git a/esphome/components/stepper/stepper.h b/esphome/components/stepper/stepper.h index 1cf4830b1f1..2bad672494f 100644 --- a/esphome/components/stepper/stepper.h +++ b/esphome/components/stepper/stepper.h @@ -44,7 +44,7 @@ template class SetTargetAction : public Action { TEMPLATABLE_VALUE(int32_t, target) - void play(Ts... x) override { this->parent_->set_target(this->target_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_target(this->target_.value(x...)); } protected: Stepper *parent_; @@ -56,7 +56,7 @@ template class ReportPositionAction : public Action { TEMPLATABLE_VALUE(int32_t, position) - void play(Ts... x) override { this->parent_->report_position(this->position_.value(x...)); } + void play(const Ts &...x) override { this->parent_->report_position(this->position_.value(x...)); } protected: Stepper *parent_; @@ -68,7 +68,7 @@ template class SetSpeedAction : public Action { TEMPLATABLE_VALUE(float, speed); - void play(Ts... x) override { + void play(const Ts &...x) override { float speed = this->speed_.value(x...); this->parent_->set_max_speed(speed); this->parent_->on_update_speed(); @@ -84,7 +84,7 @@ template class SetAccelerationAction : public Action { TEMPLATABLE_VALUE(float, acceleration); - void play(Ts... x) override { + void play(const Ts &...x) override { float acceleration = this->acceleration_.value(x...); this->parent_->set_acceleration(acceleration); } @@ -99,7 +99,7 @@ template class SetDecelerationAction : public Action { TEMPLATABLE_VALUE(float, deceleration); - void play(Ts... x) override { + void play(const Ts &...x) override { float deceleration = this->deceleration_.value(x...); this->parent_->set_deceleration(deceleration); } diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 77d62d34c38..67a0306a37b 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -115,7 +115,7 @@ template class SunCondition : public Condition, public Pa TEMPLATABLE_VALUE(double, elevation); void set_above(bool above) { above_ = above; } - bool check(Ts... x) override { + bool check(const Ts &...x) override { double elevation = this->elevation_.value(x...); double current = this->parent_->elevation(); if (this->above_) { diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index b8cbc9b9769..27d3474c971 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -11,7 +11,7 @@ template class TurnOnAction : public Action { public: explicit TurnOnAction(Switch *a_switch) : switch_(a_switch) {} - void play(Ts... x) override { this->switch_->turn_on(); } + void play(const Ts &...x) override { this->switch_->turn_on(); } protected: Switch *switch_; @@ -21,7 +21,7 @@ template class TurnOffAction : public Action { public: explicit TurnOffAction(Switch *a_switch) : switch_(a_switch) {} - void play(Ts... x) override { this->switch_->turn_off(); } + void play(const Ts &...x) override { this->switch_->turn_off(); } protected: Switch *switch_; @@ -31,7 +31,7 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Switch *a_switch) : switch_(a_switch) {} - void play(Ts... x) override { this->switch_->toggle(); } + void play(const Ts &...x) override { this->switch_->toggle(); } protected: Switch *switch_; @@ -43,7 +43,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { auto state = this->state_.optional_value(x...); if (state.has_value()) { this->switch_->control(*state); @@ -57,7 +57,7 @@ template class ControlAction : public Action { template class SwitchCondition : public Condition { public: SwitchCondition(Switch *parent, bool state) : parent_(parent), state_(state) {} - bool check(Ts... x) override { return this->parent_->state == this->state_; } + bool check(const Ts &...x) override { return this->parent_->state == this->state_; } protected: Switch *parent_; @@ -98,7 +98,7 @@ template class SwitchPublishAction : public Action { SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {} TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { this->switch_->publish_state(this->state_.value(x...)); } + void play(const Ts &...x) override { this->switch_->publish_state(this->state_.value(x...)); } protected: Switch *switch_; diff --git a/esphome/components/sx126x/automation.h b/esphome/components/sx126x/automation.h index 520ef997188..6b2371e2539 100644 --- a/esphome/components/sx126x/automation.h +++ b/esphome/components/sx126x/automation.h @@ -9,7 +9,7 @@ namespace sx126x { template class RunImageCalAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->run_image_cal(); } + void play(const Ts &...x) override { this->parent_->run_image_cal(); } }; template class SendPacketAction : public Action, public Parented { @@ -24,7 +24,7 @@ template class SendPacketAction : public Action, public P this->static_ = true; } - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->static_) { this->parent_->transmit_packet(this->data_static_); } else { @@ -40,22 +40,22 @@ template class SendPacketAction : public Action, public P template class SetModeTxAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_tx(); } + void play(const Ts &...x) override { this->parent_->set_mode_tx(); } }; template class SetModeRxAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_rx(); } + void play(const Ts &...x) override { this->parent_->set_mode_rx(); } }; template class SetModeSleepAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_sleep(); } + void play(const Ts &...x) override { this->parent_->set_mode_sleep(); } }; template class SetModeStandbyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_standby(STDBY_XOSC); } + void play(const Ts &...x) override { this->parent_->set_mode_standby(STDBY_XOSC); } }; } // namespace sx126x diff --git a/esphome/components/sx127x/automation.h b/esphome/components/sx127x/automation.h index 2b9c261de11..eae16c11fa6 100644 --- a/esphome/components/sx127x/automation.h +++ b/esphome/components/sx127x/automation.h @@ -9,7 +9,7 @@ namespace sx127x { template class RunImageCalAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->run_image_cal(); } + void play(const Ts &...x) override { this->parent_->run_image_cal(); } }; template class SendPacketAction : public Action, public Parented { @@ -24,7 +24,7 @@ template class SendPacketAction : public Action, public P this->static_ = true; } - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->static_) { this->parent_->transmit_packet(this->data_static_); } else { @@ -40,22 +40,22 @@ template class SendPacketAction : public Action, public P template class SetModeTxAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_tx(); } + void play(const Ts &...x) override { this->parent_->set_mode_tx(); } }; template class SetModeRxAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_rx(); } + void play(const Ts &...x) override { this->parent_->set_mode_rx(); } }; template class SetModeSleepAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_sleep(); } + void play(const Ts &...x) override { this->parent_->set_mode_sleep(); } }; template class SetModeStandbyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_standby(); } + void play(const Ts &...x) override { this->parent_->set_mode_standby(); } }; } // namespace sx127x diff --git a/esphome/components/template/lock/automation.h b/esphome/components/template/lock/automation.h index 61245465929..bd110b7b0c2 100644 --- a/esphome/components/template/lock/automation.h +++ b/esphome/components/template/lock/automation.h @@ -11,7 +11,7 @@ template class TemplateLockPublishAction : public Action, public: TEMPLATABLE_VALUE(lock::LockState, state) - void play(Ts... x) override { this->parent_->publish_state(this->state_.value(x...)); } + void play(const Ts &...x) override { this->parent_->publish_state(this->state_.value(x...)); } }; } // namespace template_ diff --git a/esphome/components/template/valve/automation.h b/esphome/components/template/valve/automation.h index af9b070c607..e3f394ac7c8 100644 --- a/esphome/components/template/valve/automation.h +++ b/esphome/components/template/valve/automation.h @@ -11,7 +11,7 @@ template class TemplateValvePublishAction : public Action TEMPLATABLE_VALUE(float, position) TEMPLATABLE_VALUE(valve::ValveOperation, current_operation) - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->position_.has_value()) this->parent_->position = this->position_.value(x...); if (this->current_operation_.has_value()) diff --git a/esphome/components/text/automation.h b/esphome/components/text/automation.h index f20a4f433b6..e7667fe491f 100644 --- a/esphome/components/text/automation.h +++ b/esphome/components/text/automation.h @@ -19,7 +19,7 @@ template class TextSetAction : public Action { explicit TextSetAction(Text *text) : text_(text) {} TEMPLATABLE_VALUE(std::string, value) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->text_->make_call(); call.set_value(this->value_.value(x...)); call.perform(); diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index d7286845e09..709c54c1409 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -29,7 +29,7 @@ template class TextSensorStateCondition : public Conditionparent_->state == this->state_.value(x...); } + bool check(const Ts &...x) override { return this->parent_->state == this->state_.value(x...); } protected: TextSensor *parent_; @@ -40,7 +40,7 @@ template class TextSensorPublishAction : public Action { TextSensorPublishAction(TextSensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(std::string, state) - void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } + void play(const Ts &...x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: TextSensor *sensor_; diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 7e60bbd234a..bbcecaa6284 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -67,7 +67,7 @@ class RealTimeClock : public PollingComponent { template class TimeHasTimeCondition : public Condition { public: TimeHasTimeCondition(RealTimeClock *parent) : parent_(parent) {} - bool check(Ts... x) override { return this->parent_->now().is_valid(); } + bool check(const Ts &...x) override { return this->parent_->now().is_valid(); } protected: RealTimeClock *parent_; diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h index 7079910adf5..83e74c5f330 100644 --- a/esphome/components/tm1651/tm1651.h +++ b/esphome/components/tm1651/tm1651.h @@ -61,7 +61,7 @@ template class SetBrightnessAction : public Action, publi public: TEMPLATABLE_VALUE(uint8_t, brightness) - void play(Ts... x) override { + void play(const Ts &...x) override { auto brightness = this->brightness_.value(x...); this->parent_->set_brightness(brightness); } @@ -71,7 +71,7 @@ template class SetLevelAction : public Action, public Par public: TEMPLATABLE_VALUE(uint8_t, level) - void play(Ts... x) override { + void play(const Ts &...x) override { auto level = this->level_.value(x...); this->parent_->set_level(level); } @@ -81,7 +81,7 @@ template class SetLevelPercentAction : public Action, pub public: TEMPLATABLE_VALUE(uint8_t, level_percent) - void play(Ts... x) override { + void play(const Ts &...x) override { auto level_percent = this->level_percent_.value(x...); this->parent_->set_level_percent(level_percent); } @@ -89,12 +89,12 @@ template class SetLevelPercentAction : public Action, pub template class TurnOnAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->turn_on(); } + void play(const Ts &...x) override { this->parent_->turn_on(); } }; template class TurnOffAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->turn_off(); } + void play(const Ts &...x) override { this->parent_->turn_off(); } }; } // namespace tm1651 diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index 9c599253de1..ad2c4d2bf1d 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -23,7 +23,7 @@ template class UARTWriteAction : public Action, public Pa this->static_ = true; } - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->static_) { this->parent_->write_array(this->data_static_); } else { diff --git a/esphome/components/udp/automation.h b/esphome/components/udp/automation.h index f75e6d35bf2..c5e5e2eae8c 100644 --- a/esphome/components/udp/automation.h +++ b/esphome/components/udp/automation.h @@ -20,7 +20,7 @@ template class UDPWriteAction : public Action, public Par this->static_ = true; } - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->static_) { this->parent_->send_packet(this->data_static_); } else { diff --git a/esphome/components/ufire_ec/ufire_ec.h b/esphome/components/ufire_ec/ufire_ec.h index 3d436555a2b..bfbed1b43e0 100644 --- a/esphome/components/ufire_ec/ufire_ec.h +++ b/esphome/components/ufire_ec/ufire_ec.h @@ -65,7 +65,7 @@ template class UFireECCalibrateProbeAction : public Actionparent_->calibrate_probe(this->solution_.value(x...), this->temperature_.value(x...)); } @@ -77,7 +77,7 @@ template class UFireECResetAction : public Action { public: UFireECResetAction(UFireECComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->reset_board(); } + void play(const Ts &...x) override { this->parent_->reset_board(); } protected: UFireECComponent *parent_; diff --git a/esphome/components/ufire_ise/ufire_ise.h b/esphome/components/ufire_ise/ufire_ise.h index 01efdcdb557..fe9a6dfb9c8 100644 --- a/esphome/components/ufire_ise/ufire_ise.h +++ b/esphome/components/ufire_ise/ufire_ise.h @@ -64,7 +64,7 @@ template class UFireISECalibrateProbeLowAction : public Actionparent_->calibrate_probe_low(this->solution_.value(x...)); } + void play(const Ts &...x) override { this->parent_->calibrate_probe_low(this->solution_.value(x...)); } protected: UFireISEComponent *parent_; @@ -75,7 +75,7 @@ template class UFireISECalibrateProbeHighAction : public Action< UFireISECalibrateProbeHighAction(UFireISEComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, solution) - void play(Ts... x) override { this->parent_->calibrate_probe_high(this->solution_.value(x...)); } + void play(const Ts &...x) override { this->parent_->calibrate_probe_high(this->solution_.value(x...)); } protected: UFireISEComponent *parent_; @@ -85,7 +85,7 @@ template class UFireISEResetAction : public Action { public: UFireISEResetAction(UFireISEComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->reset_board(); } + void play(const Ts &...x) override { this->parent_->reset_board(); } protected: UFireISEComponent *parent_; diff --git a/esphome/components/update/automation.h b/esphome/components/update/automation.h index df50f86a0cd..8563b855fe4 100644 --- a/esphome/components/update/automation.h +++ b/esphome/components/update/automation.h @@ -11,12 +11,12 @@ template class PerformAction : public Action, public Pare TEMPLATABLE_VALUE(bool, force) public: - void play(Ts... x) override { this->parent_->perform(this->force_.value(x...)); } + void play(const Ts &...x) override { this->parent_->perform(this->force_.value(x...)); } }; template class IsAvailableCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } + bool check(const Ts &...x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } }; } // namespace update diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index f2c06270c07..87e9cde0885 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -11,7 +11,7 @@ template class OpenAction : public Action { public: explicit OpenAction(Valve *valve) : valve_(valve) {} - void play(Ts... x) override { this->valve_->make_call().set_command_open().perform(); } + void play(const Ts &...x) override { this->valve_->make_call().set_command_open().perform(); } protected: Valve *valve_; @@ -21,7 +21,7 @@ template class CloseAction : public Action { public: explicit CloseAction(Valve *valve) : valve_(valve) {} - void play(Ts... x) override { this->valve_->make_call().set_command_close().perform(); } + void play(const Ts &...x) override { this->valve_->make_call().set_command_close().perform(); } protected: Valve *valve_; @@ -31,7 +31,7 @@ template class StopAction : public Action { public: explicit StopAction(Valve *valve) : valve_(valve) {} - void play(Ts... x) override { this->valve_->make_call().set_command_stop().perform(); } + void play(const Ts &...x) override { this->valve_->make_call().set_command_stop().perform(); } protected: Valve *valve_; @@ -41,7 +41,7 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Valve *valve) : valve_(valve) {} - void play(Ts... x) override { this->valve_->make_call().set_command_toggle().perform(); } + void play(const Ts &...x) override { this->valve_->make_call().set_command_toggle().perform(); } protected: Valve *valve_; @@ -54,7 +54,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(bool, stop) TEMPLATABLE_VALUE(float, position) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->valve_->make_call(); if (this->stop_.has_value()) call.set_stop(this->stop_.value(x...)); @@ -70,7 +70,7 @@ template class ControlAction : public Action { template class ValveIsOpenCondition : public Condition { public: ValveIsOpenCondition(Valve *valve) : valve_(valve) {} - bool check(Ts... x) override { return this->valve_->is_fully_open(); } + bool check(const Ts &...x) override { return this->valve_->is_fully_open(); } protected: Valve *valve_; @@ -79,7 +79,7 @@ template class ValveIsOpenCondition : public Condition { template class ValveIsClosedCondition : public Condition { public: ValveIsClosedCondition(Valve *valve) : valve_(valve) {} - bool check(Ts... x) override { return this->valve_->is_fully_closed(); } + bool check(const Ts &...x) override { return this->valve_->is_fully_closed(); } protected: Valve *valve_; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 95f77dbf092..8d3d3497ec5 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -324,7 +324,7 @@ template class StartAction : public Action, public Parent TEMPLATABLE_VALUE(std::string, wake_word); public: - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_wake_word(this->wake_word_.value(x...)); this->parent_->request_start(false, this->silence_detection_); } @@ -337,22 +337,22 @@ template class StartAction : public Action, public Parent template class StartContinuousAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->request_start(true, true); } + void play(const Ts &...x) override { this->parent_->request_start(true, true); } }; template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->request_stop(); } + void play(const Ts &...x) override { this->parent_->request_stop(); } }; template class IsRunningCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } + bool check(const Ts &...x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } }; template class ConnectedCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->get_api_connection() != nullptr; } + bool check(const Ts &...x) override { return this->parent_->get_api_connection() != nullptr; } }; extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 89b7b1fa41c..ac63e0eb0c5 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -455,22 +455,22 @@ extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid- template class WiFiConnectedCondition : public Condition { public: - bool check(Ts... x) override { return global_wifi_component->is_connected(); } + bool check(const Ts &...x) override { return global_wifi_component->is_connected(); } }; template class WiFiEnabledCondition : public Condition { public: - bool check(Ts... x) override { return !global_wifi_component->is_disabled(); } + bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); } }; template class WiFiEnableAction : public Action { public: - void play(Ts... x) override { global_wifi_component->enable(); } + void play(const Ts &...x) override { global_wifi_component->enable(); } }; template class WiFiDisableAction : public Action { public: - void play(Ts... x) override { global_wifi_component->disable(); } + void play(const Ts &...x) override { global_wifi_component->disable(); } }; template class WiFiConfigureAction : public Action, public Component { @@ -480,7 +480,7 @@ template class WiFiConfigureAction : public Action, publi TEMPLATABLE_VALUE(bool, save) TEMPLATABLE_VALUE(uint32_t, connection_timeout) - void play(Ts... x) override { + void play(const Ts &...x) override { auto ssid = this->ssid_.value(x...); auto password = this->password_.value(x...); // Avoid multiple calls diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index 5db9a48c909..f8f79b835d8 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -148,25 +148,25 @@ std::string mask_key(const std::string &key); /// Condition to check if remote peer is online. template class WireguardPeerOnlineCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_peer_up(); } + bool check(const Ts &...x) override { return this->parent_->is_peer_up(); } }; /// Condition to check if Wireguard component is enabled. template class WireguardEnabledCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_enabled(); } + bool check(const Ts &...x) override { return this->parent_->is_enabled(); } }; /// Action to enable Wireguard component. template class WireguardEnableAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->enable(); } + void play(const Ts &...x) override { this->parent_->enable(); } }; /// Action to disable Wireguard component. template class WireguardDisableAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->disable(); } + void play(const Ts &...x) override { this->parent_->disable(); } }; } // namespace wireguard diff --git a/esphome/core/automation.h b/esphome/core/automation.h index c22b3ca0e35..33e08c9c1c6 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -148,7 +148,7 @@ template class TemplatableValue { template class Condition { public: /// Check whether this condition passes. This condition check must be instant, and not cause any delays. - virtual bool check(Ts... x) = 0; + virtual bool check(const Ts &...x) = 0; /// Call check with a tuple of values as parameter. bool check_tuple(const std::tuple &tuple) { @@ -166,7 +166,7 @@ template class Automation; template class Trigger { public: /// Inform the parent automation that the event has triggered. - void trigger(Ts... x) { + void trigger(const Ts &...x) { if (this->automation_parent_ == nullptr) return; this->automation_parent_->trigger(x...); @@ -194,7 +194,7 @@ template class ActionList; template class Action { public: - virtual void play_complex(Ts... x) { + virtual void play_complex(const Ts &...x) { this->num_running_++; this->play(x...); this->play_next_(x...); @@ -222,8 +222,8 @@ template class Action { friend ActionList; template friend class ContinuationAction; - virtual void play(Ts... x) = 0; - void play_next_(Ts... x) { + virtual void play(const Ts &...x) = 0; + void play_next_(const Ts &...x) { if (this->num_running_ > 0) { this->num_running_--; if (this->next_ != nullptr) { @@ -273,7 +273,7 @@ template class ActionList { this->add_action(action); } } - void play(Ts... x) { + void play(const Ts &...x) { if (this->actions_begin_ != nullptr) this->actions_begin_->play_complex(x...); } @@ -315,7 +315,7 @@ template class Automation { void stop() { this->actions_.stop(); } - void trigger(Ts... x) { this->actions_.play(x...); } + void trigger(const Ts &...x) { this->actions_.play(x...); } bool is_running() { return this->actions_.is_running(); } diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 083bb3ae311..128a2d4b086 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -17,7 +17,7 @@ namespace esphome { template class AndCondition : public Condition { public: explicit AndCondition(std::initializer_list *> conditions) : conditions_(conditions) {} - bool check(Ts... x) override { + bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (!condition->check(x...)) return false; @@ -33,7 +33,7 @@ template class AndCondition : public Condition { template class OrCondition : public Condition { public: explicit OrCondition(std::initializer_list *> conditions) : conditions_(conditions) {} - bool check(Ts... x) override { + bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (condition->check(x...)) return true; @@ -49,7 +49,7 @@ template class OrCondition : public Condition { template class NotCondition : public Condition { public: explicit NotCondition(Condition *condition) : condition_(condition) {} - bool check(Ts... x) override { return !this->condition_->check(x...); } + bool check(const Ts &...x) override { return !this->condition_->check(x...); } protected: Condition *condition_; @@ -58,7 +58,7 @@ template class NotCondition : public Condition { template class XorCondition : public Condition { public: explicit XorCondition(std::initializer_list *> conditions) : conditions_(conditions) {} - bool check(Ts... x) override { + bool check(const Ts &...x) override { size_t result = 0; for (auto *condition : this->conditions_) { result += condition->check(x...); @@ -74,7 +74,7 @@ template class XorCondition : public Condition { template class LambdaCondition : public Condition { public: explicit LambdaCondition(std::function &&f) : f_(std::move(f)) {} - bool check(Ts... x) override { return this->f_(x...); } + bool check(const Ts &...x) override { return this->f_(x...); } protected: std::function f_; @@ -86,7 +86,7 @@ template class LambdaCondition : public Condition { template class StatelessLambdaCondition : public Condition { public: explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {} - bool check(Ts... x) override { return this->f_(x...); } + bool check(const Ts &...x) override { return this->f_(x...); } protected: bool (*f_)(Ts...); @@ -107,7 +107,7 @@ template class ForCondition : public Condition, public Co return cond; } - bool check(Ts... x) override { + bool check(const Ts &...x) override { if (!this->check_internal()) return false; return millis() - this->last_inactive_ >= this->time_.value(x...); @@ -171,7 +171,7 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; @@ -187,7 +187,7 @@ template class DelayAction : public Action, public Compon } float get_setup_priority() const override { return setup_priority::HARDWARE; } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->cancel_timeout("delay"); } @@ -197,7 +197,7 @@ template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} - void play(Ts... x) override { this->f_(x...); } + void play(const Ts &...x) override { this->f_(x...); } protected: std::function f_; @@ -210,7 +210,7 @@ template class StatelessLambdaAction : public Action { public: explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} - void play(Ts... x) override { this->f_(x...); } + void play(const Ts &...x) override { this->f_(x...); } protected: void (*f_)(Ts...); @@ -223,7 +223,7 @@ template class ContinuationAction : public Action { public: explicit ContinuationAction(Action *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->play_next_(x...); } + void play(const Ts &...x) override { this->parent_->play_next_(x...); } protected: Action *parent_; @@ -238,7 +238,7 @@ template class WhileLoopContinuation : public Action { public: explicit WhileLoopContinuation(WhileAction *parent) : parent_(parent) {} - void play(Ts... x) override; + void play(const Ts &...x) override; protected: WhileAction *parent_; @@ -258,7 +258,7 @@ template class IfAction : public Action { this->else_.add_action(new ContinuationAction(this)); } - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; bool res = this->condition_->check(x...); if (res) { @@ -276,7 +276,7 @@ template class IfAction : public Action { } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { @@ -301,7 +301,7 @@ template class WhileAction : public Action { friend class WhileLoopContinuation; - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; // Initial condition check if (!this->condition_->check(x...)) { @@ -316,7 +316,7 @@ template class WhileAction : public Action { } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->then_.stop(); } @@ -327,7 +327,7 @@ template class WhileAction : public Action { }; // Implementation of WhileLoopContinuation::play -template void WhileLoopContinuation::play(Ts... x) { +template void WhileLoopContinuation::play(const Ts &...x) { if (this->parent_->num_running_ > 0 && this->parent_->condition_->check(x...)) { // play again this->parent_->then_.play(x...); @@ -346,7 +346,7 @@ template class RepeatLoopContinuation : public Action *parent) : parent_(parent) {} - void play(uint32_t iteration, Ts... x) override; + void play(const uint32_t &iteration, const Ts &...x) override; protected: RepeatAction *parent_; @@ -363,7 +363,7 @@ template class RepeatAction : public Action { friend class RepeatLoopContinuation; - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; if (this->count_.value(x...) > 0) { this->then_.play(0, x...); @@ -372,7 +372,7 @@ template class RepeatAction : public Action { } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->then_.stop(); } @@ -382,12 +382,12 @@ template class RepeatAction : public Action { }; // Implementation of RepeatLoopContinuation::play -template void RepeatLoopContinuation::play(uint32_t iteration, Ts... x) { - iteration++; - if (iteration >= this->parent_->count_.value(x...)) { +template void RepeatLoopContinuation::play(const uint32_t &iteration, const Ts &...x) { + uint32_t next_iteration = iteration + 1; + if (next_iteration >= this->parent_->count_.value(x...)) { this->parent_->play_next_(x...); } else { - this->parent_->then_.play(iteration, x...); + this->parent_->then_.play(next_iteration, x...); } } @@ -409,7 +409,7 @@ template class WaitUntilAction : public Action, public Co this->disable_loop(); } - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; // Check if we can continue immediately. if (this->condition_->check(x...)) { @@ -463,7 +463,7 @@ template class WaitUntilAction : public Action, public Co float get_setup_priority() const override { return setup_priority::DATA; } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } protected: @@ -475,7 +475,7 @@ template class UpdateComponentAction : public Action { public: UpdateComponentAction(PollingComponent *component) : component_(component) {} - void play(Ts... x) override { + void play(const Ts &...x) override { if (!this->component_->is_ready()) return; this->component_->update(); @@ -489,7 +489,7 @@ template class SuspendComponentAction : public Action { public: SuspendComponentAction(PollingComponent *component) : component_(component) {} - void play(Ts... x) override { + void play(const Ts &...x) override { if (!this->component_->is_ready()) return; this->component_->stop_poller(); @@ -504,7 +504,7 @@ template class ResumeComponentAction : public Action { ResumeComponentAction(PollingComponent *component) : component_(component) {} TEMPLATABLE_VALUE(uint32_t, update_interval) - void play(Ts... x) override { + void play(const Ts &...x) override { if (!this->component_->is_ready()) { return; } diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h index cdc04d491b4..3dca2da2e9f 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h @@ -39,7 +39,7 @@ template class EnableAction : public Action { public: EnableAction(LoopTestComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->service_enable(); } + void play(const Ts &...x) override { this->parent_->service_enable(); } protected: LoopTestComponent *parent_; @@ -49,7 +49,7 @@ template class DisableAction : public Action { public: DisableAction(LoopTestComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->service_disable(); } + void play(const Ts &...x) override { this->parent_->service_disable(); } protected: LoopTestComponent *parent_; From 32975c9d8be215e06795e1f7cd64d59b22a44386 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 19:49:27 -0600 Subject: [PATCH 079/461] [select][lvgl] Fix FixedVector size() returning 0 when using operator[] after init() (#11721) --- esphome/components/lvgl/select/lvgl_select.h | 4 ++-- esphome/components/select/select_traits.cpp | 4 ++-- esphome/core/helpers.h | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index 3b1fd67d68f..d4c9631073a 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -59,8 +59,8 @@ class LVGLSelect : public select::Select, public Component { const auto &opts = this->widget_->get_options(); FixedVector opt_ptrs; opt_ptrs.init(opts.size()); - for (size_t i = 0; i < opts.size(); i++) { - opt_ptrs[i] = opts[i].c_str(); + for (const auto &opt : opts) { + opt_ptrs.push_back(opt.c_str()); } this->traits.set_options(opt_ptrs); } diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index c6ded98ebf7..e5e12bdc7a4 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -7,8 +7,8 @@ void SelectTraits::set_options(const std::initializer_list &option void SelectTraits::set_options(const FixedVector &options) { this->options_.init(options.size()); - for (size_t i = 0; i < options.size(); i++) { - this->options_[i] = options[i]; + for (const auto &opt : options) { + this->options_.push_back(opt); } } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 91ddc70afaa..660874ed1a8 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -248,6 +248,8 @@ template class FixedVector { } // Allocate capacity - can be called multiple times to reinit + // IMPORTANT: After calling init(), you MUST use push_back() to add elements. + // Direct assignment via operator[] does NOT update the size counter. void init(size_t n) { cleanup_(); reset_(); From d70fe126f66c1b0890fbc27bd5394e001838c176 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 21:13:46 -0600 Subject: [PATCH 080/461] preen --- esphome/components/select/select.h | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index d7811251bd1..a4dd5a15da0 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -92,18 +92,6 @@ class Select : public EntityBase { size_t active_index_{0}; - /** Set the value of the select by index, this is an optional virtual method. - * - * IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes. - * Overriding this index-based version is PREFERRED as it avoids string conversions. - * - * This method is called by the SelectCall when the index is already known. - * Default implementation converts to string and calls control(const std::string&). - * - * @param index The index as validated by the SelectCall. - */ - virtual void control(size_t index) { this->control(this->option_at(index)); } - /** Set the value of the select, this is a virtual method that each select integration can implement. * * IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes. From bf83b70a189203d44582be720e4e067bb7787efb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 21:45:00 -0600 Subject: [PATCH 081/461] [rtttl] Reduce flash usage by eliminating substr() allocations --- esphome/components/rtttl/rtttl.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index b79f27e2e5c..46b672f67dc 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -35,9 +35,7 @@ void Rtttl::dump_config() { void Rtttl::play(std::string rtttl) { if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { - int pos = this->rtttl_.find(':'); - auto name = this->rtttl_.substr(0, pos); - ESP_LOGW(TAG, "Already playing: %s", name.c_str()); + ESP_LOGW(TAG, "Already playing: %.*s", (int) this->rtttl_.find(':'), this->rtttl_.c_str()); return; } @@ -59,8 +57,7 @@ void Rtttl::play(std::string rtttl) { return; } - auto name = this->rtttl_.substr(0, this->position_); - ESP_LOGD(TAG, "Playing song %s", name.c_str()); + ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str()); // get default duration this->position_ = this->rtttl_.find("d=", this->position_); From 009d6a15f66b0de9f67df811a78e3d90b715fa97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 21:58:44 -0600 Subject: [PATCH 082/461] [wifi_info] Reduce heap usage by up to 1.7KB in scan_results sensor --- esphome/components/wifi_info/wifi_info_text_sensor.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 2cb96123a0f..04889d6bb3c 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -10,6 +10,8 @@ namespace esphome { namespace wifi_info { +static constexpr size_t MAX_STATE_LENGTH = 255; + class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSensor { public: void update() override { @@ -71,11 +73,14 @@ class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSen scan_results += "dB\n"; } + // There's a limit of 255 characters per state. + // Longer states just don't get sent so we truncate it. + if (scan_results.length() > MAX_STATE_LENGTH) { + scan_results.resize(MAX_STATE_LENGTH); + } if (this->last_scan_results_ != scan_results) { this->last_scan_results_ = scan_results; - // There's a limit of 255 characters per state. - // Longer states just don't get sent so we truncate it. - this->publish_state(scan_results.substr(0, 255)); + this->publish_state(scan_results); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } From f420a8f32dc1cc6a6c637a70a630629159f46850 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 22:11:46 -0600 Subject: [PATCH 083/461] [ld2420] Eliminate substr() allocation in firmware version parsing --- esphome/components/ld2420/ld2420.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index b48c336d4ec..f544acc1124 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -174,7 +174,7 @@ static uint8_t calc_checksum(void *data, size_t size) { static int get_firmware_int(const char *version_string) { std::string version_str = version_string; if (version_str[0] == 'v') { - version_str = version_str.substr(1); + version_str.erase(0, 1); } version_str.erase(remove(version_str.begin(), version_str.end(), '.'), version_str.end()); int version_integer = stoi(version_str); From 34208138c147bc9f68feee2c4b77422d5ef8563c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 22:20:55 -0600 Subject: [PATCH 084/461] [voice_assistant] Eliminate substr() allocations in text truncation --- esphome/components/voice_assistant/voice_assistant.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 7ece73994f3..fd35dc7d09e 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -657,7 +657,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGW(TAG, "No text in STT_END event"); return; } else if (text.length() > 500) { - text = text.substr(0, 497) + "..."; + text.resize(497); + text += "..."; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); @@ -714,7 +715,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } if (text.length() > 500) { - text = text.substr(0, 497) + "..."; + text.resize(497); + text += "..."; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); this->defer([this, text]() { From 358296a57e3bea89951ad1ffa2ec363b481c581e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Nov 2025 22:32:20 -0600 Subject: [PATCH 085/461] [remote_base] Eliminate substr() allocations in Pronto dump logging --- .../components/remote_base/pronto_protocol.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 35fd7822487..1bc532dc7c4 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -71,6 +71,7 @@ static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0 static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; static const uint16_t PRONTO_DEFAULT_GAP = 45000; static const uint16_t MARK_EXCESS_MICROS = 20; +static constexpr size_t PRONTO_LOG_CHUNK_SIZE = 230; static uint16_t to_frequency_k_hz(uint16_t code) { if (code == 0) @@ -225,18 +226,17 @@ optional ProntoProtocol::decode(RemoteReceiveData src) { } void ProntoProtocol::dump(const ProntoData &data) { - std::string rest; - - rest = data.data; + std::string rest = data.data; ESP_LOGI(TAG, "Received Pronto: data="); - while (true) { - ESP_LOGI(TAG, "%s", rest.substr(0, 230).c_str()); - if (rest.size() > 230) { - rest = rest.substr(230); + do { + size_t chunk_size = rest.size() > PRONTO_LOG_CHUNK_SIZE ? PRONTO_LOG_CHUNK_SIZE : rest.size(); + ESP_LOGI(TAG, "%.*s", (int) chunk_size, rest.c_str()); + if (rest.size() > PRONTO_LOG_CHUNK_SIZE) { + rest.erase(0, PRONTO_LOG_CHUNK_SIZE); } else { break; } - } + } while (true); } } // namespace remote_base From d77f63eff5f8682c2c7dccde0d8f2ed34cd6be18 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 09:02:36 -0600 Subject: [PATCH 086/461] add some safety for future refactoring --- esphome/components/rtttl/rtttl.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 46b672f67dc..65fcc207d48 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -35,7 +35,9 @@ void Rtttl::dump_config() { void Rtttl::play(std::string rtttl) { if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { - ESP_LOGW(TAG, "Already playing: %.*s", (int) this->rtttl_.find(':'), this->rtttl_.c_str()); + size_t pos = this->rtttl_.find(':'); + size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length(); + ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str()); return; } From 6b522dfee6880df49d6826734310ca6ab7f3d25c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 09:14:21 -0600 Subject: [PATCH 087/461] [wifi_info] Reduce heap usage by up to 1.7KB in scan_results sensor (#11723) --- esphome/components/wifi_info/wifi_info_text_sensor.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 2cb96123a0f..04889d6bb3c 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -10,6 +10,8 @@ namespace esphome { namespace wifi_info { +static constexpr size_t MAX_STATE_LENGTH = 255; + class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSensor { public: void update() override { @@ -71,11 +73,14 @@ class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSen scan_results += "dB\n"; } + // There's a limit of 255 characters per state. + // Longer states just don't get sent so we truncate it. + if (scan_results.length() > MAX_STATE_LENGTH) { + scan_results.resize(MAX_STATE_LENGTH); + } if (this->last_scan_results_ != scan_results) { this->last_scan_results_ = scan_results; - // There's a limit of 255 characters per state. - // Longer states just don't get sent so we truncate it. - this->publish_state(scan_results.substr(0, 255)); + this->publish_state(scan_results); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } From 6e2dbbf636dbe98665904618553caddb0eb46315 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 09:15:05 -0600 Subject: [PATCH 088/461] [voice_assistant] Eliminate substr() allocations in text truncation (#11725) --- esphome/components/voice_assistant/voice_assistant.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 7ece73994f3..fd35dc7d09e 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -657,7 +657,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGW(TAG, "No text in STT_END event"); return; } else if (text.length() > 500) { - text = text.substr(0, 497) + "..."; + text.resize(497); + text += "..."; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); @@ -714,7 +715,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } if (text.length() > 500) { - text = text.substr(0, 497) + "..."; + text.resize(497); + text += "..."; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); this->defer([this, text]() { From 479f8dd85c75b7ca961a7c031330eb5ca4ea41a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 09:17:28 -0600 Subject: [PATCH 089/461] [rtttl] Reduce flash usage by eliminating substr() allocations (#11722) --- esphome/components/rtttl/rtttl.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index b79f27e2e5c..65fcc207d48 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -35,9 +35,9 @@ void Rtttl::dump_config() { void Rtttl::play(std::string rtttl) { if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { - int pos = this->rtttl_.find(':'); - auto name = this->rtttl_.substr(0, pos); - ESP_LOGW(TAG, "Already playing: %s", name.c_str()); + size_t pos = this->rtttl_.find(':'); + size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length(); + ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str()); return; } @@ -59,8 +59,7 @@ void Rtttl::play(std::string rtttl) { return; } - auto name = this->rtttl_.substr(0, this->position_); - ESP_LOGD(TAG, "Playing song %s", name.c_str()); + ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str()); // get default duration this->position_ = this->rtttl_.find("d=", this->position_); From 5372eca46e3c2338c2007f4ab56167379accea03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 10:43:02 -0600 Subject: [PATCH 090/461] [mqtt] Use StringRef to avoid string copies in discovery --- esphome/components/mqtt/mqtt_binary_sensor.cpp | 5 +++-- esphome/components/mqtt/mqtt_button.cpp | 5 +++-- esphome/components/mqtt/mqtt_component.cpp | 7 ++++--- esphome/components/mqtt/mqtt_component.h | 8 ++++---- esphome/components/mqtt/mqtt_cover.cpp | 5 +++-- esphome/components/mqtt/mqtt_event.cpp | 5 +++-- esphome/components/mqtt/mqtt_number.cpp | 10 ++++++---- esphome/components/mqtt/mqtt_sensor.cpp | 10 ++++++---- esphome/components/mqtt/mqtt_text_sensor.cpp | 5 +++-- esphome/components/mqtt/mqtt_valve.cpp | 5 +++-- 10 files changed, 38 insertions(+), 27 deletions(-) diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 2ce4928574f..8388e1cd7df 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -31,8 +31,9 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->binary_sensor_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); + const auto device_class = this->binary_sensor_->get_device_class_ref(); + if (!device_class.empty()) + root[MQTT_DEVICE_CLASS] = device_class; if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available; if (this->binary_sensor_->is_status_binary_sensor()) diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index b3435edf381..3b91fe7297e 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -33,8 +33,9 @@ void MQTTButtonComponent::dump_config() { void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson config.state_topic = false; - if (!this->button_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); + const auto device_class = this->button_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index eb6114008a6..f57aa75a836 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -89,8 +89,9 @@ bool MQTTComponent::send_discovery_() { if (this->is_disabled_by_default()) root[MQTT_ENABLED_BY_DEFAULT] = false; - if (!this->get_icon().empty()) - root[MQTT_ICON] = this->get_icon(); + const auto icon_ref = this->get_icon_ref(); + if (!icon_ref.empty()) + root[MQTT_ICON] = icon_ref; const auto entity_category = this->get_entity()->get_entity_category(); switch (entity_category) { @@ -269,7 +270,7 @@ bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connec // Pull these properties from EntityBase if not overridden std::string MQTTComponent::friendly_name() const { return this->get_entity()->get_name(); } -std::string MQTTComponent::get_icon() const { return this->get_entity()->get_icon(); } +StringRef MQTTComponent::get_icon_ref() const { return this->get_entity()->get_icon_ref(); } bool MQTTComponent::is_disabled_by_default() const { return this->get_entity()->is_disabled_by_default(); } bool MQTTComponent::is_internal() { if (this->has_custom_state_topic_) { diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 851fdd842c1..fbe84bc5f7a 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -165,13 +165,13 @@ class MQTTComponent : public Component { virtual const EntityBase *get_entity() const = 0; /// Get the friendly name of this MQTT component. - virtual std::string friendly_name() const; + std::string friendly_name() const; - /// Get the icon field of this component - virtual std::string get_icon() const; + /// Get the icon field of this component as StringRef + StringRef get_icon_ref() const; /// Get whether the underlying Entity is disabled by default - virtual bool is_disabled_by_default() const; + bool is_disabled_by_default() const; /// Get the MQTT topic that new states will be shared to. std::string get_state_topic_() const; diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 6fb61ee4693..429c45b23b9 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -68,8 +68,9 @@ void MQTTCoverComponent::dump_config() { } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->cover_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); + const auto device_class = this->cover_->get_device_class_ref(); + if (!device_class.empty()) + root[MQTT_DEVICE_CLASS] = device_class; auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index f972d545c6a..73b13bba052 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -21,8 +21,9 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf for (const auto &event_type : this->event_->get_event_types()) event_types.add(event_type); - if (!this->event_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->event_->get_device_class(); + const auto device_class = this->event_->get_device_class_ref(); + if (!device_class.empty()) + root[MQTT_DEVICE_CLASS] = device_class; config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index a44632ff308..1dac188478d 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -44,8 +44,9 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MIN] = traits.get_min_value(); root[MQTT_MAX] = traits.get_max_value(); root[MQTT_STEP] = traits.get_step(); - if (!this->number_->traits.get_unit_of_measurement().empty()) - root[MQTT_UNIT_OF_MEASUREMENT] = this->number_->traits.get_unit_of_measurement(); + const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref(); + if (!unit_of_measurement.empty()) + root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; switch (this->number_->traits.get_mode()) { case NUMBER_MODE_AUTO: break; @@ -56,8 +57,9 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MODE] = "slider"; break; } - if (!this->number_->traits.get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->number_->traits.get_device_class(); + const auto device_class = this->number_->traits.get_device_class_ref(); + if (!device_class.empty()) + root[MQTT_DEVICE_CLASS] = device_class; config.command_topic = true; } diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 9e61f6ef3b6..b436e48f20a 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -45,12 +45,14 @@ void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->sensor_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + const auto device_class = this->sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } - if (!this->sensor_->get_unit_of_measurement().empty()) - root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); + const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref(); + if (!unit_of_measurement.empty()) + root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; if (this->get_expire_after() > 0) root[MQTT_EXPIRE_AFTER] = this->get_expire_after() / 1000; diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index 42260ed2a8c..f649586845d 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -16,8 +16,9 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->sensor_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + const auto device_class = this->sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 551398cf42a..d594ee381ae 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -50,8 +50,9 @@ void MQTTValveComponent::dump_config() { } void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->valve_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); + const auto device_class = this->valve_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } auto traits = this->valve_->get_traits(); From 2c9fdb33e64bd73132cacf3012049d24752dc17c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 10:52:58 -0600 Subject: [PATCH 091/461] [core] Deprecate get_icon(), get_device_class(), get_unit_of_measurement() and fix remaining non-MQTT usages --- esphome/components/graph/graph.cpp | 4 ++-- esphome/components/prometheus/prometheus_handler.cpp | 2 +- esphome/components/sprinkler/sprinkler.cpp | 4 ++-- esphome/core/entity_base.h | 12 +++++++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index ac6ace96ee3..c701f4f73fc 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -235,7 +235,7 @@ void GraphLegend::init(Graph *g) { std::string valstr = value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (this->units_) { - valstr += trace->sensor_->get_unit_of_measurement(); + valstr += trace->sensor_->get_unit_of_measurement_ref().c_str(); } this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh); if (fw > valw) @@ -371,7 +371,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of std::string valstr = value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (legend_->units_) { - valstr += trace->sensor_->get_unit_of_measurement(); + valstr += trace->sensor_->get_unit_of_measurement_ref().c_str(); } buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str()); ESP_LOGV(TAG, " value: %s", valstr.c_str()); diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 6e7ed6f79fc..5cfcacf0cbb 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -158,7 +158,7 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(ESPHOME_F("\",unit=\"")); - stream->print(obj->get_unit_of_measurement().c_str()); + stream->print(obj->get_unit_of_measurement_ref().c_str()); stream->print(ESPHOME_F("\"} ")); stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); stream->print(ESPHOME_F("\n")); diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 7676e174688..8edb240a415 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -650,7 +650,7 @@ void Sprinkler::set_valve_run_duration(const optional valve_number, cons return; } auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); - if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { + if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { call.set_value(run_duration.value() / 60.0); } else { call.set_value(run_duration.value()); @@ -732,7 +732,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { return 0; } if (this->valve_[valve_number].run_duration_number != nullptr) { - if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { + if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); } else { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 80cd6b8e770..6e5362464f0 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -61,7 +61,9 @@ class EntityBase { } // Get/set this entity's icon - std::string get_icon() const; + [[deprecated("Use get_icon_ref() instead for better performance (avoids string copy). Will stop working in ESPHome " + "2026.5.0")]] std::string + get_icon() const; void set_icon(const char *icon); StringRef get_icon_ref() const { static constexpr auto EMPTY_STRING = StringRef::from_lit(""); @@ -158,7 +160,9 @@ class EntityBase { class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) public: /// Get the device class, using the manual override if set. - std::string get_device_class(); + [[deprecated("Use get_device_class_ref() instead for better performance (avoids string copy). Will stop working in " + "ESPHome 2026.5.0")]] std::string + get_device_class(); /// Manually set the device class. void set_device_class(const char *device_class); /// Get the device class as StringRef @@ -174,7 +178,9 @@ class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) public: /// Get the unit of measurement, using the manual override if set. - std::string get_unit_of_measurement(); + [[deprecated("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will stop " + "working in ESPHome 2026.5.0")]] std::string + get_unit_of_measurement(); /// Manually set the unit of measurement. void set_unit_of_measurement(const char *unit_of_measurement); /// Get the unit of measurement as StringRef From b7838671ae53b501d829a5fc7d50e04f7dc7790d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 10:57:20 -0600 Subject: [PATCH 092/461] [ld2420] Eliminate substr() allocation in firmware version parsing (#11724) --- esphome/components/ld2420/ld2420.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index b48c336d4ec..f544acc1124 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -174,7 +174,7 @@ static uint8_t calc_checksum(void *data, size_t size) { static int get_firmware_int(const char *version_string) { std::string version_str = version_string; if (version_str[0] == 'v') { - version_str = version_str.substr(1); + version_str.erase(0, 1); } version_str.erase(remove(version_str.begin(), version_str.end(), '.'), version_str.end()); int version_integer = stoi(version_str); From d663ea56b013dff4c66a1bad2c572a62f3441a48 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 11:00:02 -0600 Subject: [PATCH 093/461] tidy --- .../mqtt/mqtt_alarm_control_panel.cpp | 2 +- esphome/components/mqtt/mqtt_button.cpp | 2 +- esphome/components/mqtt/mqtt_component.cpp | 20 +++++++++---------- esphome/components/mqtt/mqtt_component.h | 6 +++--- esphome/components/mqtt/mqtt_fan.cpp | 14 ++++++------- esphome/components/mqtt/mqtt_lock.cpp | 2 +- esphome/components/mqtt/mqtt_switch.cpp | 2 +- esphome/components/mqtt/mqtt_update.cpp | 2 +- 8 files changed, 25 insertions(+), 25 deletions(-) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 94460c31a75..dd3df5f8aa6 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -36,7 +36,7 @@ void MQTTAlarmControlPanelComponent::setup() { } else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) { call.triggered(); } else { - ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str()); } call.perform(); }); diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index 3b91fe7297e..f8eb0eab2d7 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -20,7 +20,7 @@ void MQTTButtonComponent::setup() { if (payload == "PRESS") { this->button_->press(); } else { - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index f57aa75a836..8c6d0e6e3ef 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -64,11 +64,11 @@ bool MQTTComponent::send_discovery_() { const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); if (discovery_info.clean) { - ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name().c_str()); + ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str()); return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true); } - ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str()); + ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str()); // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return global_mqtt_client->publish_json( @@ -85,11 +85,11 @@ bool MQTTComponent::send_discovery_() { } // Fields from EntityBase - root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name() : ""; + root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : ""; - if (this->is_disabled_by_default()) + if (this->is_disabled_by_default_()) root[MQTT_ENABLED_BY_DEFAULT] = false; - const auto icon_ref = this->get_icon_ref(); + const auto icon_ref = this->get_icon_ref_(); if (!icon_ref.empty()) root[MQTT_ICON] = icon_ref; @@ -123,7 +123,7 @@ bool MQTTComponent::send_discovery_() { const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { char friendly_name_hash[9]; - sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name())); + sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_())); friendly_name_hash[8] = 0; // ensure the hash-string ends with null root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; } else { @@ -185,7 +185,7 @@ bool MQTTComponent::is_discovery_enabled() const { } std::string MQTTComponent::get_default_object_id_() const { - return str_sanitize(str_snake_case(this->friendly_name())); + return str_sanitize(str_snake_case(this->friendly_name_())); } void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { @@ -269,9 +269,9 @@ void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } // Pull these properties from EntityBase if not overridden -std::string MQTTComponent::friendly_name() const { return this->get_entity()->get_name(); } -StringRef MQTTComponent::get_icon_ref() const { return this->get_entity()->get_icon_ref(); } -bool MQTTComponent::is_disabled_by_default() const { return this->get_entity()->is_disabled_by_default(); } +std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); } +StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); } +bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); } bool MQTTComponent::is_internal() { if (this->has_custom_state_topic_) { // If the custom state_topic is null, return true as it is internal and should not publish diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index fbe84bc5f7a..2f8dfcf64ed 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -165,13 +165,13 @@ class MQTTComponent : public Component { virtual const EntityBase *get_entity() const = 0; /// Get the friendly name of this MQTT component. - std::string friendly_name() const; + std::string friendly_name_() const; /// Get the icon field of this component as StringRef - StringRef get_icon_ref() const; + StringRef get_icon_ref_() const; /// Get whether the underlying Entity is disabled by default - bool is_disabled_by_default() const; + bool is_disabled_by_default_() const; /// Get the MQTT topic that new states will be shared to. std::string get_state_topic_() const; diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 70e1ae3b4ad..2aefc3a4db5 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -24,15 +24,15 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str()); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name_().c_str()); this->state_->turn_on().perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name_().c_str()); this->state_->turn_off().perform(); break; case PARSE_TOGGLE: - ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name_().c_str()); this->state_->toggle().perform(); break; case PARSE_NONE: @@ -48,11 +48,11 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str(), "forward", "reverse"); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name_().c_str()); this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name_().c_str()); this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform(); break; case PARSE_TOGGLE: @@ -75,11 +75,11 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str(), "oscillate_on", "oscillate_off"); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name_().c_str()); this->state_->make_call().set_oscillating(true).perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name_().c_str()); this->state_->make_call().set_oscillating(false).perform(); break; case PARSE_TOGGLE: diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 04126249834..0e15377ba45 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -24,7 +24,7 @@ void MQTTLockComponent::setup() { } else if (strcasecmp(payload.c_str(), "OPEN") == 0) { this->lock_->open(); } else { - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index 8b1323bdb24..b3a35420b92 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -29,7 +29,7 @@ void MQTTSwitchComponent::setup() { break; case PARSE_NONE: default: - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); break; } diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp index 5d4807c7f37..20f3a69a9e0 100644 --- a/esphome/components/mqtt/mqtt_update.cpp +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -20,7 +20,7 @@ void MQTTUpdateComponent::setup() { if (payload == "INSTALL") { this->update_->perform(); } else { - ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); From 5dc8bfcf13cdadc7cd21f772a8256204fd4eeffb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 11:08:10 -0600 Subject: [PATCH 094/461] [template] Mark all component classes as final --- .../alarm_control_panel/template_alarm_control_panel.h | 2 +- .../template/binary_sensor/template_binary_sensor.h | 2 +- esphome/components/template/button/template_button.h | 2 +- esphome/components/template/cover/template_cover.h | 2 +- esphome/components/template/datetime/template_date.h | 2 +- esphome/components/template/datetime/template_datetime.h | 2 +- esphome/components/template/datetime/template_time.h | 2 +- esphome/components/template/event/template_event.h | 2 +- esphome/components/template/fan/template_fan.h | 2 +- esphome/components/template/lock/template_lock.h | 2 +- esphome/components/template/number/template_number.h | 2 +- esphome/components/template/output/template_output.h | 4 ++-- esphome/components/template/select/template_select.h | 2 +- esphome/components/template/sensor/template_sensor.h | 2 +- esphome/components/template/switch/template_switch.h | 2 +- esphome/components/template/text/template_text.h | 2 +- .../components/template/text_sensor/template_text_sensor.h | 2 +- esphome/components/template/valve/template_valve.h | 2 +- 18 files changed, 19 insertions(+), 19 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 40a79004da0..202dc7c13fb 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -49,7 +49,7 @@ struct SensorInfo { uint8_t store_index; }; -class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, public Component { +class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControlPanel, public Component { public: TemplateAlarmControlPanel(); void dump_config() override; diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index bc591391b98..0af709b097f 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { +class TemplateBinarySensor final : public Component, public binary_sensor::BinarySensor { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/button/template_button.h b/esphome/components/template/button/template_button.h index 68e976f64b3..5bda82c58f9 100644 --- a/esphome/components/template/button/template_button.h +++ b/esphome/components/template/button/template_button.h @@ -5,7 +5,7 @@ namespace esphome { namespace template_ { -class TemplateButton : public button::Button { +class TemplateButton final : public button::Button { public: // Implements the abstract `press_action` but the `on_press` trigger already handles the press. void press_action() override{}; diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index faff69f867b..125c67bb861 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -14,7 +14,7 @@ enum TemplateCoverRestoreMode { COVER_RESTORE_AND_CALL, }; -class TemplateCover : public cover::Cover, public Component { +class TemplateCover final : public cover::Cover, public Component { public: TemplateCover(); diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 7fed704d0ee..fe64b0ba14e 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -14,7 +14,7 @@ namespace esphome { namespace template_ { -class TemplateDate : public datetime::DateEntity, public PollingComponent { +class TemplateDate final : public datetime::DateEntity, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index ec45bf0326e..c44bd852652 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -14,7 +14,7 @@ namespace esphome { namespace template_ { -class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { +class TemplateDateTime final : public datetime::DateTimeEntity, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index ea7474c0bad..0c95330d273 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -14,7 +14,7 @@ namespace esphome { namespace template_ { -class TemplateTime : public datetime::TimeEntity, public PollingComponent { +class TemplateTime final : public datetime::TimeEntity, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/event/template_event.h b/esphome/components/template/event/template_event.h index 251ae9299b6..5467a641418 100644 --- a/esphome/components/template/event/template_event.h +++ b/esphome/components/template/event/template_event.h @@ -6,7 +6,7 @@ namespace esphome { namespace template_ { -class TemplateEvent : public Component, public event::Event {}; +class TemplateEvent final : public Component, public event::Event {}; } // namespace template_ } // namespace esphome diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index b09352f4d47..052b385b93a 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -6,7 +6,7 @@ namespace esphome { namespace template_ { -class TemplateFan : public Component, public fan::Fan { +class TemplateFan final : public Component, public fan::Fan { public: TemplateFan() {} void setup() override; diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 14fca4635ec..ac10794e4d1 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -8,7 +8,7 @@ namespace esphome { namespace template_ { -class TemplateLock : public lock::Lock, public Component { +class TemplateLock final : public lock::Lock, public Component { public: TemplateLock(); diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index a9307e9246e..876ec96b3b8 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -9,7 +9,7 @@ namespace esphome { namespace template_ { -class TemplateNumber : public number::Number, public PollingComponent { +class TemplateNumber final : public number::Number, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/output/template_output.h b/esphome/components/template/output/template_output.h index 90de801a5c9..9ecfc446b97 100644 --- a/esphome/components/template/output/template_output.h +++ b/esphome/components/template/output/template_output.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateBinaryOutput : public output::BinaryOutput { +class TemplateBinaryOutput final : public output::BinaryOutput { public: Trigger *get_trigger() const { return trigger_; } @@ -17,7 +17,7 @@ class TemplateBinaryOutput : public output::BinaryOutput { Trigger *trigger_ = new Trigger(); }; -class TemplateFloatOutput : public output::FloatOutput { +class TemplateFloatOutput final : public output::FloatOutput { public: Trigger *get_trigger() const { return trigger_; } diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 2dad059ade1..cb5b5469767 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -9,7 +9,7 @@ namespace esphome { namespace template_ { -class TemplateSelect : public select::Select, public PollingComponent { +class TemplateSelect final : public select::Select, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 793d754a0f8..3ca965dde37 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateSensor : public sensor::Sensor, public PollingComponent { +class TemplateSensor final : public sensor::Sensor, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 18a374df359..35c18af448d 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -8,7 +8,7 @@ namespace esphome { namespace template_ { -class TemplateSwitch : public switch_::Switch, public Component { +class TemplateSwitch final : public switch_::Switch, public Component { public: TemplateSwitch(); diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index c12021f80eb..1a0a66ed5b5 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -60,7 +60,7 @@ template class TextSaver : public TemplateTextSaverBase { } }; -class TemplateText : public text::Text, public PollingComponent { +class TemplateText final : public text::Text, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 0d01c72023f..da5c518c7f2 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -8,7 +8,7 @@ namespace esphome { namespace template_ { -class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { +class TemplateTextSensor final : public text_sensor::TextSensor, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index d6235f8e5cc..c452648193e 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -14,7 +14,7 @@ enum TemplateValveRestoreMode { VALVE_RESTORE_AND_CALL, }; -class TemplateValve : public valve::Valve, public Component { +class TemplateValve final : public valve::Valve, public Component { public: TemplateValve(); From 4c097616ae862ef5255d45663b91e58ff6161951 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 11:10:13 -0600 Subject: [PATCH 095/461] move comments --- esphome/components/mqtt/mqtt_binary_sensor.cpp | 2 +- esphome/components/mqtt/mqtt_component.cpp | 1 + esphome/components/mqtt/mqtt_cover.cpp | 2 +- esphome/components/mqtt/mqtt_event.cpp | 1 + esphome/components/mqtt/mqtt_number.cpp | 2 ++ esphome/components/mqtt/mqtt_sensor.cpp | 3 ++- esphome/components/mqtt/mqtt_text_sensor.cpp | 2 +- esphome/components/mqtt/mqtt_valve.cpp | 2 +- 8 files changed, 10 insertions(+), 5 deletions(-) diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 8388e1cd7df..37705477a38 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -30,9 +30,9 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor } void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson const auto device_class = this->binary_sensor_->get_device_class_ref(); if (!device_class.empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_DEVICE_CLASS] = device_class; if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available; diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 8c6d0e6e3ef..9e563012ead 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -91,6 +91,7 @@ bool MQTTComponent::send_discovery_() { root[MQTT_ENABLED_BY_DEFAULT] = false; const auto icon_ref = this->get_icon_ref_(); if (!icon_ref.empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_ICON] = icon_ref; const auto entity_category = this->get_entity()->get_entity_category(); diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 429c45b23b9..58fa272a551 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -67,9 +67,9 @@ void MQTTCoverComponent::dump_config() { } } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson const auto device_class = this->cover_->get_device_class_ref(); if (!device_class.empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_DEVICE_CLASS] = device_class; auto traits = this->cover_->get_traits(); diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index 73b13bba052..46f6b7f479f 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -23,6 +23,7 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf const auto device_class = this->event_->get_device_class_ref(); if (!device_class.empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_DEVICE_CLASS] = device_class; config.command_topic = false; diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 1dac188478d..358e45368ad 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -46,6 +46,7 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_STEP] = traits.get_step(); const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref(); if (!unit_of_measurement.empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; switch (this->number_->traits.get_mode()) { case NUMBER_MODE_AUTO: @@ -59,6 +60,7 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon } const auto device_class = this->number_->traits.get_device_class_ref(); if (!device_class.empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_DEVICE_CLASS] = device_class; config.command_topic = true; diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index b436e48f20a..3d0a656bbb2 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -44,14 +44,15 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson const auto device_class = this->sensor_->get_device_class_ref(); if (!device_class.empty()) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_DEVICE_CLASS] = device_class; } const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref(); if (!unit_of_measurement.empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; if (this->get_expire_after() > 0) diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index f649586845d..1e727fed885 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -15,9 +15,9 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson const auto device_class = this->sensor_->get_device_class_ref(); if (!device_class.empty()) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_DEVICE_CLASS] = device_class; } config.command_topic = false; diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index d594ee381ae..32740235c5c 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -49,9 +49,9 @@ void MQTTValveComponent::dump_config() { } } void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson const auto device_class = this->valve_->get_device_class_ref(); if (!device_class.empty()) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_DEVICE_CLASS] = device_class; } From 4c5533b2ead3aead28d231289cede7f5e0a8c7d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 11:16:43 -0600 Subject: [PATCH 096/461] move comments --- esphome/components/mqtt/mqtt_binary_sensor.cpp | 3 ++- esphome/components/mqtt/mqtt_component.cpp | 3 ++- esphome/components/mqtt/mqtt_cover.cpp | 3 ++- esphome/components/mqtt/mqtt_event.cpp | 3 ++- esphome/components/mqtt/mqtt_number.cpp | 6 ++++-- esphome/components/mqtt/mqtt_sensor.cpp | 3 ++- 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 37705477a38..3f9089bef81 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -31,9 +31,10 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { const auto device_class = this->binary_sensor_->get_device_class_ref(); - if (!device_class.empty()) + if (!device_class.empty()) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_DEVICE_CLASS] = device_class; + } if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available; if (this->binary_sensor_->is_status_binary_sensor()) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 9e563012ead..6bcb179f721 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -90,9 +90,10 @@ bool MQTTComponent::send_discovery_() { if (this->is_disabled_by_default_()) root[MQTT_ENABLED_BY_DEFAULT] = false; const auto icon_ref = this->get_icon_ref_(); - if (!icon_ref.empty()) + if (!icon_ref.empty()) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_ICON] = icon_ref; + } const auto entity_category = this->get_entity()->get_entity_category(); switch (entity_category) { diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 58fa272a551..4a5a70a1c7c 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -68,9 +68,10 @@ void MQTTCoverComponent::dump_config() { } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { const auto device_class = this->cover_->get_device_class_ref(); - if (!device_class.empty()) + if (!device_class.empty()) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_DEVICE_CLASS] = device_class; + } auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index 46f6b7f479f..ab4e095c06f 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -22,9 +22,10 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf event_types.add(event_type); const auto device_class = this->event_->get_device_class_ref(); - if (!device_class.empty()) + if (!device_class.empty()) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_DEVICE_CLASS] = device_class; + } config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 358e45368ad..c50853a66c1 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -45,9 +45,10 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MAX] = traits.get_max_value(); root[MQTT_STEP] = traits.get_step(); const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref(); - if (!unit_of_measurement.empty()) + if (!unit_of_measurement.empty()) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; + } switch (this->number_->traits.get_mode()) { case NUMBER_MODE_AUTO: break; @@ -59,9 +60,10 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon break; } const auto device_class = this->number_->traits.get_device_class_ref(); - if (!device_class.empty()) + if (!device_class.empty()) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_DEVICE_CLASS] = device_class; + } config.command_topic = true; } diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 3d0a656bbb2..cc135935bfc 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -51,9 +51,10 @@ void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon } const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref(); - if (!unit_of_measurement.empty()) + if (!unit_of_measurement.empty()) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; + } if (this->get_expire_after() > 0) root[MQTT_EXPIRE_AFTER] = this->get_expire_after() / 1000; From ed0d9e60b85bd0927c1a6e3e74b45ef6583cf1fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 11:19:06 -0600 Subject: [PATCH 097/461] [mdns] Eliminate redundant hostname copy to save heap memory --- esphome/components/mdns/mdns_component.cpp | 4 +--- esphome/components/mdns/mdns_component.h | 1 - esphome/components/mdns/mdns_esp32.cpp | 6 ++++-- esphome/components/mdns/mdns_esp8266.cpp | 3 ++- esphome/components/mdns/mdns_libretiny.cpp | 3 ++- esphome/components/mdns/mdns_rp2040.cpp | 3 ++- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index d476136554d..2c3150ff5dd 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -37,8 +37,6 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION); void MDNSComponent::compile_records_(StaticVector &services) { - this->hostname_ = App.get_name(); - // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES // in mdns/__init__.py. If you add a new service here, update both locations. @@ -179,7 +177,7 @@ void MDNSComponent::dump_config() { ESP_LOGCONFIG(TAG, "mDNS:\n" " Hostname: %s", - this->hostname_.c_str()); + App.get_name().c_str()); #ifdef USE_MDNS_STORE_SERVICES ESP_LOGV(TAG, " Services:"); for (const auto &service : this->services_) { diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 35371fd7391..f4237d5a690 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -76,7 +76,6 @@ class MDNSComponent : public Component { #ifdef USE_MDNS_STORE_SERVICES StaticVector services_{}; #endif - std::string hostname_; void compile_records_(StaticVector &services); }; diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index c02bfcbadb8..ecdc926cc96 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -2,6 +2,7 @@ #if defined(USE_ESP32) && defined(USE_MDNS) #include +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -27,8 +28,9 @@ void MDNSComponent::setup() { return; } - mdns_hostname_set(this->hostname_.c_str()); - mdns_instance_name_set(this->hostname_.c_str()); + const char *hostname = App.get_name().c_str(); + mdns_hostname_set(hostname); + mdns_instance_name_set(hostname); for (const auto &service : services) { auto txt_records = std::make_unique(service.txt_records.size()); diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 25a3defa7ba..9bbb4060700 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -4,6 +4,7 @@ #include #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -20,7 +21,7 @@ void MDNSComponent::setup() { this->compile_records_(services); #endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index a3e317a2bfa..fb2088f7194 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -3,6 +3,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -20,7 +21,7 @@ void MDNSComponent::setup() { this->compile_records_(services); #endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 791fa3934d3..a9f5349f14b 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -3,6 +3,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -20,7 +21,7 @@ void MDNSComponent::setup() { this->compile_records_(services); #endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is From cf209e369444c580db75c2c0f8b01414fbd44357 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 11:42:13 -0600 Subject: [PATCH 098/461] touch ups --- esphome/components/graph/graph.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index c701f4f73fc..88bb306408f 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -235,7 +235,7 @@ void GraphLegend::init(Graph *g) { std::string valstr = value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (this->units_) { - valstr += trace->sensor_->get_unit_of_measurement_ref().c_str(); + valstr += trace->sensor_->get_unit_of_measurement_ref(); } this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh); if (fw > valw) @@ -371,7 +371,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of std::string valstr = value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (legend_->units_) { - valstr += trace->sensor_->get_unit_of_measurement_ref().c_str(); + valstr += trace->sensor_->get_unit_of_measurement_ref(); } buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str()); ESP_LOGV(TAG, " value: %s", valstr.c_str()); From 2352114757a6af107000fbf8f144e4fe4bd5a1c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 11:42:52 -0600 Subject: [PATCH 099/461] [graph] Remove unnecessary .c_str() calls when appending StringRef to std::string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StringRef has an operator+= overload that allows direct appending to std::string. No need to call .c_str() first - this is even more efficient. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ...device_class_get_unit_of_measurement_pr.md | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 deprecate_get_icon_get_device_class_get_unit_of_measurement_pr.md diff --git a/deprecate_get_icon_get_device_class_get_unit_of_measurement_pr.md b/deprecate_get_icon_get_device_class_get_unit_of_measurement_pr.md new file mode 100644 index 00000000000..3acfdccdb20 --- /dev/null +++ b/deprecate_get_icon_get_device_class_get_unit_of_measurement_pr.md @@ -0,0 +1,125 @@ +# PR Title Suggestion + +``` +[core] Deprecate get_icon(), get_device_class(), get_unit_of_measurement() and fix remaining non-MQTT usages +``` + +# What does this implement/fix? + +This PR deprecates three inefficient methods in `entity_base.h` that return `std::string` copies and updates the remaining non-MQTT usages in the core codebase to use the more efficient `StringRef` alternatives. + +## Background + +PR #11731 eliminates MQTT component usages of these methods, demonstrating significant performance improvements by avoiding unnecessary string copies. This PR completes the migration by: + +1. Adding deprecation warnings to the old methods +2. Fixing the remaining non-MQTT usages in prometheus, graph, and sprinkler components +3. Setting a clear removal timeline (ESPHome 2026.5.0) + +**Note:** This PR should be merged **after** PR #11731 is merged to ensure all internal usages are updated before deprecation warnings are enabled. + +## Changes + +### 1. Deprecate inefficient methods (entity_base.h) + +Added `[[deprecated]]` attributes to: +- `EntityBase::get_icon()` → Use `get_icon_ref()` instead +- `EntityBase_DeviceClass::get_device_class()` → Use `get_device_class_ref()` instead +- `EntityBase_UnitOfMeasurement::get_unit_of_measurement()` → Use `get_unit_of_measurement_ref()` instead + +**Deprecation message:** "Use _ref() instead for better performance (avoids string copy). Will stop working in ESPHome 2026.5.0" + +### 2. Fix remaining components (prometheus, graph, sprinkler) + +Updated 3 components to use `get_unit_of_measurement_ref()` instead of `get_unit_of_measurement()`: + +- **prometheus_handler.cpp** (line 161): Eliminates temporary string allocation when printing metrics +- **graph.cpp** (lines 238, 374): Direct `StringRef` appending to `std::string` (uses `StringRef` operator `+=` overload) +- **sprinkler.cpp** (lines 653, 735): Zero-allocation string comparisons with `MIN_STR` + +## Why deprecate instead of remove immediately? + +This is a **breaking change** for external components, but we're using a gradual deprecation approach: + +1. **External components:** Many third-party components may use these methods - immediate removal would break them without notice +2. **Migration path:** Gives external component authors 6 months to update with clear compiler warnings +3. **Clear timeline:** ESPHome 2026.5.0 gives a concrete removal date that can be communicated in release notes +4. **Compiler warnings:** External components will see deprecation warnings during compilation, making the migration explicit and traceable +5. **Simple migration:** The fix is straightforward - replace `get_X()` with `get_X_ref()` (add `.c_str()` if passing to functions expecting `const char*`) + +## Benefits + +### Performance improvements: +- **Eliminates string copies** in hot code paths (prometheus metrics, graph rendering, sprinkler logic) +- **Reduces heap fragmentation** on ESP8266 where memory is constrained +- **Faster string comparisons** (sprinkler component) +- **Safer code** (prometheus: no more `.c_str()` on temporaries) + +### Code quality improvements: +- **Consistent API usage:** All core code now uses `*_ref()` methods +- **Better practices:** Encourages zero-copy string handling +- **Clear migration path:** External components have time and guidance to update + +### Flash/RAM impact: +- Minimal: Deprecation attributes add no runtime overhead +- Once methods are removed in 2026.5.0: ~200-500 bytes flash savings (removes unused vtable entries and std::string conversions) + +## Timeline + +1. **Today:** Merge this PR (deprecates methods, fixes core usage) + - Deprecation warnings appear during compilation for external components + - All core components updated to use `*_ref()` methods +2. **Next 6 months:** External components see deprecation warnings, maintainers update their code + - Component authors have time to migrate to `*_ref()` methods + - Warnings provide clear guidance on what to change +3. **ESPHome 2026.5.0:** **BREAKING CHANGE** - Remove the three deprecated methods entirely + - External components still using old methods will fail to compile + - Migration is simple: replace `get_X()` with `get_X_ref()` (or `.c_str()` if needed) + +## Dependencies + +**This PR depends on PR #11731 being merged first.** + +PR #11731 eliminates MQTT component usages (8 components × 2 calls = 16 string copies per discovery cycle). Once that's merged, this PR: +1. Adds deprecation warnings to guide external components +2. Fixes the remaining 3 components (prometheus, graph, sprinkler) +3. Completes the migration for all core components + +## Merge Order + +1. **First:** Merge PR #11731 (fixes MQTT components) +2. **Then:** Merge this PR (adds deprecation warnings + fixes remaining components) + +## Types of changes + +- [x] Code quality improvements to existing code +- [x] Deprecation of inefficient APIs +- [x] **Breaking change** (External components using these methods will break in ESPHome 2026.5.0) + +## Test Environment + +- [ ] ESP32 IDF +- [ ] ESP32 Arduino +- [ ] ESP8266 Arduino +- [ ] RP2040 + +## Example entry for `config.yaml`: + +No configuration changes required. This is a transparent optimization for all components. + +External component authors using the deprecated methods will see compiler warnings: + +``` +warning: 'get_unit_of_measurement' is deprecated: Use get_unit_of_measurement_ref() instead +for better performance (avoids string copy). Will stop working in ESPHome 2026.5.0 +[-Wdeprecated-declarations] +``` + +## Checklist: + - [ ] The code change is tested and works locally. + - [ ] Tests have been added to verify that the new code works (under `tests/` folder). + - Existing component tests cover the updated code paths + +If user exposed functionality or configuration variables are added/changed: + - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs). + - N/A - PR summary will be automatically added to release notes From 6ccea58ee26fc6ce7a27fa01e50e891e1c299c41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 11:45:27 -0600 Subject: [PATCH 100/461] merge --- ...device_class_get_unit_of_measurement_pr.md | 125 ------------------ 1 file changed, 125 deletions(-) delete mode 100644 deprecate_get_icon_get_device_class_get_unit_of_measurement_pr.md diff --git a/deprecate_get_icon_get_device_class_get_unit_of_measurement_pr.md b/deprecate_get_icon_get_device_class_get_unit_of_measurement_pr.md deleted file mode 100644 index 3acfdccdb20..00000000000 --- a/deprecate_get_icon_get_device_class_get_unit_of_measurement_pr.md +++ /dev/null @@ -1,125 +0,0 @@ -# PR Title Suggestion - -``` -[core] Deprecate get_icon(), get_device_class(), get_unit_of_measurement() and fix remaining non-MQTT usages -``` - -# What does this implement/fix? - -This PR deprecates three inefficient methods in `entity_base.h` that return `std::string` copies and updates the remaining non-MQTT usages in the core codebase to use the more efficient `StringRef` alternatives. - -## Background - -PR #11731 eliminates MQTT component usages of these methods, demonstrating significant performance improvements by avoiding unnecessary string copies. This PR completes the migration by: - -1. Adding deprecation warnings to the old methods -2. Fixing the remaining non-MQTT usages in prometheus, graph, and sprinkler components -3. Setting a clear removal timeline (ESPHome 2026.5.0) - -**Note:** This PR should be merged **after** PR #11731 is merged to ensure all internal usages are updated before deprecation warnings are enabled. - -## Changes - -### 1. Deprecate inefficient methods (entity_base.h) - -Added `[[deprecated]]` attributes to: -- `EntityBase::get_icon()` → Use `get_icon_ref()` instead -- `EntityBase_DeviceClass::get_device_class()` → Use `get_device_class_ref()` instead -- `EntityBase_UnitOfMeasurement::get_unit_of_measurement()` → Use `get_unit_of_measurement_ref()` instead - -**Deprecation message:** "Use _ref() instead for better performance (avoids string copy). Will stop working in ESPHome 2026.5.0" - -### 2. Fix remaining components (prometheus, graph, sprinkler) - -Updated 3 components to use `get_unit_of_measurement_ref()` instead of `get_unit_of_measurement()`: - -- **prometheus_handler.cpp** (line 161): Eliminates temporary string allocation when printing metrics -- **graph.cpp** (lines 238, 374): Direct `StringRef` appending to `std::string` (uses `StringRef` operator `+=` overload) -- **sprinkler.cpp** (lines 653, 735): Zero-allocation string comparisons with `MIN_STR` - -## Why deprecate instead of remove immediately? - -This is a **breaking change** for external components, but we're using a gradual deprecation approach: - -1. **External components:** Many third-party components may use these methods - immediate removal would break them without notice -2. **Migration path:** Gives external component authors 6 months to update with clear compiler warnings -3. **Clear timeline:** ESPHome 2026.5.0 gives a concrete removal date that can be communicated in release notes -4. **Compiler warnings:** External components will see deprecation warnings during compilation, making the migration explicit and traceable -5. **Simple migration:** The fix is straightforward - replace `get_X()` with `get_X_ref()` (add `.c_str()` if passing to functions expecting `const char*`) - -## Benefits - -### Performance improvements: -- **Eliminates string copies** in hot code paths (prometheus metrics, graph rendering, sprinkler logic) -- **Reduces heap fragmentation** on ESP8266 where memory is constrained -- **Faster string comparisons** (sprinkler component) -- **Safer code** (prometheus: no more `.c_str()` on temporaries) - -### Code quality improvements: -- **Consistent API usage:** All core code now uses `*_ref()` methods -- **Better practices:** Encourages zero-copy string handling -- **Clear migration path:** External components have time and guidance to update - -### Flash/RAM impact: -- Minimal: Deprecation attributes add no runtime overhead -- Once methods are removed in 2026.5.0: ~200-500 bytes flash savings (removes unused vtable entries and std::string conversions) - -## Timeline - -1. **Today:** Merge this PR (deprecates methods, fixes core usage) - - Deprecation warnings appear during compilation for external components - - All core components updated to use `*_ref()` methods -2. **Next 6 months:** External components see deprecation warnings, maintainers update their code - - Component authors have time to migrate to `*_ref()` methods - - Warnings provide clear guidance on what to change -3. **ESPHome 2026.5.0:** **BREAKING CHANGE** - Remove the three deprecated methods entirely - - External components still using old methods will fail to compile - - Migration is simple: replace `get_X()` with `get_X_ref()` (or `.c_str()` if needed) - -## Dependencies - -**This PR depends on PR #11731 being merged first.** - -PR #11731 eliminates MQTT component usages (8 components × 2 calls = 16 string copies per discovery cycle). Once that's merged, this PR: -1. Adds deprecation warnings to guide external components -2. Fixes the remaining 3 components (prometheus, graph, sprinkler) -3. Completes the migration for all core components - -## Merge Order - -1. **First:** Merge PR #11731 (fixes MQTT components) -2. **Then:** Merge this PR (adds deprecation warnings + fixes remaining components) - -## Types of changes - -- [x] Code quality improvements to existing code -- [x] Deprecation of inefficient APIs -- [x] **Breaking change** (External components using these methods will break in ESPHome 2026.5.0) - -## Test Environment - -- [ ] ESP32 IDF -- [ ] ESP32 Arduino -- [ ] ESP8266 Arduino -- [ ] RP2040 - -## Example entry for `config.yaml`: - -No configuration changes required. This is a transparent optimization for all components. - -External component authors using the deprecated methods will see compiler warnings: - -``` -warning: 'get_unit_of_measurement' is deprecated: Use get_unit_of_measurement_ref() instead -for better performance (avoids string copy). Will stop working in ESPHome 2026.5.0 -[-Wdeprecated-declarations] -``` - -## Checklist: - - [ ] The code change is tested and works locally. - - [ ] Tests have been added to verify that the new code works (under `tests/` folder). - - Existing component tests cover the updated code paths - -If user exposed functionality or configuration variables are added/changed: - - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs). - - N/A - PR summary will be automatically added to release notes From df53ff7afed11a1fc18ad03484d77c9c64bf1024 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:13:12 -0600 Subject: [PATCH 101/461] [scheduler] Extract helper functions to improve code readability (#11730) --- esphome/core/scheduler.cpp | 45 ++++++++++++++++++++------------------ esphome/core/scheduler.h | 34 ++++++++++++++++++++-------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 11d59c24993..d285af2d0e1 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -117,12 +117,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->set_name(name_cstr, !is_static_string); item->type = type; item->callback = std::move(func); - // Initialize remove to false (though it should already be from constructor) -#ifdef ESPHOME_THREAD_MULTI_ATOMICS - item->remove.store(false, std::memory_order_relaxed); -#else - item->remove = false; -#endif + // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use + this->set_item_removed_(item.get(), false); item->is_retry = is_retry; #ifndef ESPHOME_THREAD_SINGLE @@ -153,21 +149,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #ifdef ESPHOME_DEBUG_SCHEDULER - // Validate static strings in debug mode - if (is_static_string && name_cstr != nullptr) { - validate_static_string(name_cstr); - } - - // Debug logging - const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; - if (type == SchedulerItem::TIMEOUT) { - ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), - name_cstr ? name_cstr : "(null)", type_str, delay); - } else { - ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), - name_cstr ? name_cstr : "(null)", type_str, delay, - static_cast(item->get_next_execution() - now)); - } + this->debug_log_timer_(item.get(), is_static_string, name_cstr, type, delay, now); #endif /* ESPHOME_DEBUG_SCHEDULER */ // For retries, check if there's a cancelled timeout first @@ -787,4 +769,25 @@ void Scheduler::recycle_item_(std::unique_ptr item) { // else: unique_ptr will delete the item when it goes out of scope } +#ifdef ESPHOME_DEBUG_SCHEDULER +void Scheduler::debug_log_timer_(const SchedulerItem *item, bool is_static_string, const char *name_cstr, + SchedulerItem::Type type, uint32_t delay, uint64_t now) { + // Validate static strings in debug mode + if (is_static_string && name_cstr != nullptr) { + validate_static_string(name_cstr); + } + + // Debug logging + const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; + if (type == SchedulerItem::TIMEOUT) { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), + name_cstr ? name_cstr : "(null)", type_str, delay); + } else { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), + name_cstr ? name_cstr : "(null)", type_str, delay, + static_cast(item->get_next_execution() - now)); + } +} +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } // namespace esphome diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index f6ec07294d2..fd16840240a 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -266,6 +266,12 @@ class Scheduler { // Helper to perform full cleanup when too many items are cancelled void full_cleanup_removed_items_(); +#ifdef ESPHOME_DEBUG_SCHEDULER + // Helper for debug logging in set_timer_common_ - extracted to reduce code size + void debug_log_timer_(const SchedulerItem *item, bool is_static_string, const char *name_cstr, + SchedulerItem::Type type, uint32_t delay, uint64_t now); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + #ifndef ESPHOME_THREAD_SINGLE // Helper to process defer queue - inline for performance in hot path inline void process_defer_queue_(uint32_t &now) { @@ -367,6 +373,24 @@ class Scheduler { #endif } + // Helper to set item removal flag (platform-specific) + // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this + // function. Uses memory_order_release when setting to true (for cancellation synchronization), + // and memory_order_relaxed when setting to false (for initialization). + void set_item_removed_(SchedulerItem *item, bool removed) { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Multi-threaded with atomics: use atomic store with appropriate ordering + // Release ordering when setting to true ensures cancellation is visible to other threads + // Relaxed ordering when setting to false is sufficient for initialization + item->remove.store(removed, removed ? std::memory_order_release : std::memory_order_relaxed); +#else + // Single-threaded (ESPHOME_THREAD_SINGLE) or + // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write + // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! + item->remove = removed; +#endif + } + // Helper to mark matching items in a container as removed // Returns the number of items marked for removal // IMPORTANT: Caller must hold the scheduler lock before calling this function. @@ -383,15 +407,7 @@ class Scheduler { continue; if (this->matches_item_(item, component, name_cstr, type, match_retry)) { // Mark item for removal (platform-specific) -#ifdef ESPHOME_THREAD_MULTI_ATOMICS - // Multi-threaded with atomics: use atomic store - item->remove.store(true, std::memory_order_release); -#else - // Single-threaded (ESPHOME_THREAD_SINGLE) or - // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write - // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! - item->remove = true; -#endif + this->set_item_removed_(item.get(), true); count++; } } From 90e4d15fd96b120010bed005e40ac53cc488c9f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:14:09 -0600 Subject: [PATCH 102/461] [api] Release excess buffer capacity after initial sync --- esphome/components/api/api_connection.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5ab8a6eb050..47dc829b641 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -193,6 +193,11 @@ void APIConnection::loop() { if (!this->deferred_batch_.empty()) { this->process_batch_(); } + // Release excess capacity from initial entity flood + // deferred_batch_ grew up to MAX_INITIAL_PER_BATCH (24) items, now shrink to free up to ~384 bytes + this->deferred_batch_.items.shrink_to_fit(); + // shared_write_buffer_ grew up to ~1.5KB during initial state, now shrink to free up to ~1.4KB + this->parent_->get_shared_buffer_ref().shrink_to_fit(); // Now that everything is sent, enable immediate sending for future state changes this->flags_.should_try_send_immediately = true; } From d36ef050a90d09a87e22b2cc5866b52007e9c875 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:15:50 -0600 Subject: [PATCH 103/461] [template] Mark all component classes as final (#11733) --- .../alarm_control_panel/template_alarm_control_panel.h | 2 +- .../template/binary_sensor/template_binary_sensor.h | 2 +- esphome/components/template/button/template_button.h | 2 +- esphome/components/template/cover/template_cover.h | 2 +- esphome/components/template/datetime/template_date.h | 2 +- esphome/components/template/datetime/template_datetime.h | 2 +- esphome/components/template/datetime/template_time.h | 2 +- esphome/components/template/event/template_event.h | 2 +- esphome/components/template/fan/template_fan.h | 2 +- esphome/components/template/lock/template_lock.h | 2 +- esphome/components/template/number/template_number.h | 2 +- esphome/components/template/output/template_output.h | 4 ++-- esphome/components/template/select/template_select.h | 2 +- esphome/components/template/sensor/template_sensor.h | 2 +- esphome/components/template/switch/template_switch.h | 2 +- esphome/components/template/text/template_text.h | 2 +- .../components/template/text_sensor/template_text_sensor.h | 2 +- esphome/components/template/valve/template_valve.h | 2 +- 18 files changed, 19 insertions(+), 19 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 40a79004da0..202dc7c13fb 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -49,7 +49,7 @@ struct SensorInfo { uint8_t store_index; }; -class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, public Component { +class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControlPanel, public Component { public: TemplateAlarmControlPanel(); void dump_config() override; diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index bc591391b98..0af709b097f 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { +class TemplateBinarySensor final : public Component, public binary_sensor::BinarySensor { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/button/template_button.h b/esphome/components/template/button/template_button.h index 68e976f64b3..5bda82c58f9 100644 --- a/esphome/components/template/button/template_button.h +++ b/esphome/components/template/button/template_button.h @@ -5,7 +5,7 @@ namespace esphome { namespace template_ { -class TemplateButton : public button::Button { +class TemplateButton final : public button::Button { public: // Implements the abstract `press_action` but the `on_press` trigger already handles the press. void press_action() override{}; diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index faff69f867b..125c67bb861 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -14,7 +14,7 @@ enum TemplateCoverRestoreMode { COVER_RESTORE_AND_CALL, }; -class TemplateCover : public cover::Cover, public Component { +class TemplateCover final : public cover::Cover, public Component { public: TemplateCover(); diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 7fed704d0ee..fe64b0ba14e 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -14,7 +14,7 @@ namespace esphome { namespace template_ { -class TemplateDate : public datetime::DateEntity, public PollingComponent { +class TemplateDate final : public datetime::DateEntity, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index ec45bf0326e..c44bd852652 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -14,7 +14,7 @@ namespace esphome { namespace template_ { -class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { +class TemplateDateTime final : public datetime::DateTimeEntity, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index ea7474c0bad..0c95330d273 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -14,7 +14,7 @@ namespace esphome { namespace template_ { -class TemplateTime : public datetime::TimeEntity, public PollingComponent { +class TemplateTime final : public datetime::TimeEntity, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/event/template_event.h b/esphome/components/template/event/template_event.h index 251ae9299b6..5467a641418 100644 --- a/esphome/components/template/event/template_event.h +++ b/esphome/components/template/event/template_event.h @@ -6,7 +6,7 @@ namespace esphome { namespace template_ { -class TemplateEvent : public Component, public event::Event {}; +class TemplateEvent final : public Component, public event::Event {}; } // namespace template_ } // namespace esphome diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index b09352f4d47..052b385b93a 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -6,7 +6,7 @@ namespace esphome { namespace template_ { -class TemplateFan : public Component, public fan::Fan { +class TemplateFan final : public Component, public fan::Fan { public: TemplateFan() {} void setup() override; diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 14fca4635ec..ac10794e4d1 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -8,7 +8,7 @@ namespace esphome { namespace template_ { -class TemplateLock : public lock::Lock, public Component { +class TemplateLock final : public lock::Lock, public Component { public: TemplateLock(); diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index a9307e9246e..876ec96b3b8 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -9,7 +9,7 @@ namespace esphome { namespace template_ { -class TemplateNumber : public number::Number, public PollingComponent { +class TemplateNumber final : public number::Number, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/output/template_output.h b/esphome/components/template/output/template_output.h index 90de801a5c9..9ecfc446b97 100644 --- a/esphome/components/template/output/template_output.h +++ b/esphome/components/template/output/template_output.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateBinaryOutput : public output::BinaryOutput { +class TemplateBinaryOutput final : public output::BinaryOutput { public: Trigger *get_trigger() const { return trigger_; } @@ -17,7 +17,7 @@ class TemplateBinaryOutput : public output::BinaryOutput { Trigger *trigger_ = new Trigger(); }; -class TemplateFloatOutput : public output::FloatOutput { +class TemplateFloatOutput final : public output::FloatOutput { public: Trigger *get_trigger() const { return trigger_; } diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 2dad059ade1..cb5b5469767 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -9,7 +9,7 @@ namespace esphome { namespace template_ { -class TemplateSelect : public select::Select, public PollingComponent { +class TemplateSelect final : public select::Select, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 793d754a0f8..3ca965dde37 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateSensor : public sensor::Sensor, public PollingComponent { +class TemplateSensor final : public sensor::Sensor, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 18a374df359..35c18af448d 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -8,7 +8,7 @@ namespace esphome { namespace template_ { -class TemplateSwitch : public switch_::Switch, public Component { +class TemplateSwitch final : public switch_::Switch, public Component { public: TemplateSwitch(); diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index c12021f80eb..1a0a66ed5b5 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -60,7 +60,7 @@ template class TextSaver : public TemplateTextSaverBase { } }; -class TemplateText : public text::Text, public PollingComponent { +class TemplateText final : public text::Text, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 0d01c72023f..da5c518c7f2 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -8,7 +8,7 @@ namespace esphome { namespace template_ { -class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { +class TemplateTextSensor final : public text_sensor::TextSensor, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index d6235f8e5cc..c452648193e 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -14,7 +14,7 @@ enum TemplateValveRestoreMode { VALVE_RESTORE_AND_CALL, }; -class TemplateValve : public valve::Valve, public Component { +class TemplateValve final : public valve::Valve, public Component { public: TemplateValve(); From 1fce2918fb2d82fee74d1e0641aebc75eda78e51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:22:13 -0600 Subject: [PATCH 104/461] Revert "[api] Release excess buffer capacity after initial sync" This reverts commit 90e4d15fd96b120010bed005e40ac53cc488c9f9. --- esphome/components/api/api_connection.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 47dc829b641..5ab8a6eb050 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -193,11 +193,6 @@ void APIConnection::loop() { if (!this->deferred_batch_.empty()) { this->process_batch_(); } - // Release excess capacity from initial entity flood - // deferred_batch_ grew up to MAX_INITIAL_PER_BATCH (24) items, now shrink to free up to ~384 bytes - this->deferred_batch_.items.shrink_to_fit(); - // shared_write_buffer_ grew up to ~1.5KB during initial state, now shrink to free up to ~1.4KB - this->parent_->get_shared_buffer_ref().shrink_to_fit(); // Now that everything is sent, enable immediate sending for future state changes this->flags_.should_try_send_immediately = true; } From e8c7f74abd271674316f94737ec7083ebbd19fb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:25:16 -0600 Subject: [PATCH 105/461] Revert "Revert "[api] Release excess buffer capacity after initial sync"" This reverts commit 1fce2918fb2d82fee74d1e0641aebc75eda78e51. --- esphome/components/api/api_connection.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5ab8a6eb050..47dc829b641 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -193,6 +193,11 @@ void APIConnection::loop() { if (!this->deferred_batch_.empty()) { this->process_batch_(); } + // Release excess capacity from initial entity flood + // deferred_batch_ grew up to MAX_INITIAL_PER_BATCH (24) items, now shrink to free up to ~384 bytes + this->deferred_batch_.items.shrink_to_fit(); + // shared_write_buffer_ grew up to ~1.5KB during initial state, now shrink to free up to ~1.4KB + this->parent_->get_shared_buffer_ref().shrink_to_fit(); // Now that everything is sent, enable immediate sending for future state changes this->flags_.should_try_send_immediately = true; } From b08419fa473533e28a7cd86f98b2b3f98f1b7f27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:30:45 -0600 Subject: [PATCH 106/461] [mqtt] Use StringRef to avoid string copies in discovery (#11731) --- .../mqtt/mqtt_alarm_control_panel.cpp | 2 +- .../components/mqtt/mqtt_binary_sensor.cpp | 9 ++++--- esphome/components/mqtt/mqtt_button.cpp | 7 ++--- esphome/components/mqtt/mqtt_component.cpp | 26 +++++++++++-------- esphome/components/mqtt/mqtt_component.h | 8 +++--- esphome/components/mqtt/mqtt_cover.cpp | 9 ++++--- esphome/components/mqtt/mqtt_event.cpp | 8 ++++-- esphome/components/mqtt/mqtt_fan.cpp | 14 +++++----- esphome/components/mqtt/mqtt_lock.cpp | 2 +- esphome/components/mqtt/mqtt_number.cpp | 14 +++++++--- esphome/components/mqtt/mqtt_sensor.cpp | 14 ++++++---- esphome/components/mqtt/mqtt_switch.cpp | 2 +- esphome/components/mqtt/mqtt_text_sensor.cpp | 8 +++--- esphome/components/mqtt/mqtt_update.cpp | 2 +- esphome/components/mqtt/mqtt_valve.cpp | 8 +++--- 15 files changed, 81 insertions(+), 52 deletions(-) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 94460c31a75..dd3df5f8aa6 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -36,7 +36,7 @@ void MQTTAlarmControlPanelComponent::setup() { } else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) { call.triggered(); } else { - ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str()); } call.perform(); }); diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 2ce4928574f..479cee205ad 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -30,9 +30,12 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor } void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->binary_sensor_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->binary_sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available; if (this->binary_sensor_->is_status_binary_sensor()) diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index b3435edf381..f8eb0eab2d7 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -20,7 +20,7 @@ void MQTTButtonComponent::setup() { if (payload == "PRESS") { this->button_->press(); } else { - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); @@ -33,8 +33,9 @@ void MQTTButtonComponent::dump_config() { void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson config.state_topic = false; - if (!this->button_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); + const auto device_class = this->button_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index eb6114008a6..1cd818964eb 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -64,11 +64,11 @@ bool MQTTComponent::send_discovery_() { const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); if (discovery_info.clean) { - ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name().c_str()); + ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str()); return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true); } - ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str()); + ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str()); // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return global_mqtt_client->publish_json( @@ -85,12 +85,16 @@ bool MQTTComponent::send_discovery_() { } // Fields from EntityBase - root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name() : ""; + root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : ""; - if (this->is_disabled_by_default()) + if (this->is_disabled_by_default_()) root[MQTT_ENABLED_BY_DEFAULT] = false; - if (!this->get_icon().empty()) - root[MQTT_ICON] = this->get_icon(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto icon_ref = this->get_icon_ref_(); + if (!icon_ref.empty()) { + root[MQTT_ICON] = icon_ref; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) const auto entity_category = this->get_entity()->get_entity_category(); switch (entity_category) { @@ -122,7 +126,7 @@ bool MQTTComponent::send_discovery_() { const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { char friendly_name_hash[9]; - sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name())); + sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_())); friendly_name_hash[8] = 0; // ensure the hash-string ends with null root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; } else { @@ -184,7 +188,7 @@ bool MQTTComponent::is_discovery_enabled() const { } std::string MQTTComponent::get_default_object_id_() const { - return str_sanitize(str_snake_case(this->friendly_name())); + return str_sanitize(str_snake_case(this->friendly_name_())); } void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { @@ -268,9 +272,9 @@ void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } // Pull these properties from EntityBase if not overridden -std::string MQTTComponent::friendly_name() const { return this->get_entity()->get_name(); } -std::string MQTTComponent::get_icon() const { return this->get_entity()->get_icon(); } -bool MQTTComponent::is_disabled_by_default() const { return this->get_entity()->is_disabled_by_default(); } +std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); } +StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); } +bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); } bool MQTTComponent::is_internal() { if (this->has_custom_state_topic_) { // If the custom state_topic is null, return true as it is internal and should not publish diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 851fdd842c1..2f8dfcf64ed 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -165,13 +165,13 @@ class MQTTComponent : public Component { virtual const EntityBase *get_entity() const = 0; /// Get the friendly name of this MQTT component. - virtual std::string friendly_name() const; + std::string friendly_name_() const; - /// Get the icon field of this component - virtual std::string get_icon() const; + /// Get the icon field of this component as StringRef + StringRef get_icon_ref_() const; /// Get whether the underlying Entity is disabled by default - virtual bool is_disabled_by_default() const; + bool is_disabled_by_default_() const; /// Get the MQTT topic that new states will be shared to. std::string get_state_topic_() const; diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 6fb61ee4693..b63aa66d295 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -67,9 +67,12 @@ void MQTTCoverComponent::dump_config() { } } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->cover_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->cover_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index f972d545c6a..e2063354464 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -21,8 +21,12 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf for (const auto &event_type : this->event_->get_event_types()) event_types.add(event_type); - if (!this->event_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->event_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->event_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 70e1ae3b4ad..2aefc3a4db5 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -24,15 +24,15 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str()); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name_().c_str()); this->state_->turn_on().perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name_().c_str()); this->state_->turn_off().perform(); break; case PARSE_TOGGLE: - ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name_().c_str()); this->state_->toggle().perform(); break; case PARSE_NONE: @@ -48,11 +48,11 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str(), "forward", "reverse"); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name_().c_str()); this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name_().c_str()); this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform(); break; case PARSE_TOGGLE: @@ -75,11 +75,11 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str(), "oscillate_on", "oscillate_off"); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name_().c_str()); this->state_->make_call().set_oscillating(true).perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name_().c_str()); this->state_->make_call().set_oscillating(false).perform(); break; case PARSE_TOGGLE: diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 04126249834..0e15377ba45 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -24,7 +24,7 @@ void MQTTLockComponent::setup() { } else if (strcasecmp(payload.c_str(), "OPEN") == 0) { this->lock_->open(); } else { - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index a44632ff308..f419eac130f 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -44,8 +44,11 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MIN] = traits.get_min_value(); root[MQTT_MAX] = traits.get_max_value(); root[MQTT_STEP] = traits.get_step(); - if (!this->number_->traits.get_unit_of_measurement().empty()) - root[MQTT_UNIT_OF_MEASUREMENT] = this->number_->traits.get_unit_of_measurement(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref(); + if (!unit_of_measurement.empty()) { + root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; + } switch (this->number_->traits.get_mode()) { case NUMBER_MODE_AUTO: break; @@ -56,8 +59,11 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MODE] = "slider"; break; } - if (!this->number_->traits.get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->number_->traits.get_device_class(); + const auto device_class = this->number_->traits.get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = true; } diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 9e61f6ef3b6..010ac3013ef 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -44,13 +44,17 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->sensor_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } - if (!this->sensor_->get_unit_of_measurement().empty()) - root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); + const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref(); + if (!unit_of_measurement.empty()) { + root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (this->get_expire_after() > 0) root[MQTT_EXPIRE_AFTER] = this->get_expire_after() / 1000; diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index 8b1323bdb24..b3a35420b92 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -29,7 +29,7 @@ void MQTTSwitchComponent::setup() { break; case PARSE_NONE: default: - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); break; } diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index 42260ed2a8c..e6e7cf04e8a 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -15,10 +15,12 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->sensor_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } void MQTTTextSensor::setup() { diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp index 5d4807c7f37..20f3a69a9e0 100644 --- a/esphome/components/mqtt/mqtt_update.cpp +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -20,7 +20,7 @@ void MQTTUpdateComponent::setup() { if (payload == "INSTALL") { this->update_->perform(); } else { - ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 551398cf42a..ae606707482 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -49,10 +49,12 @@ void MQTTValveComponent::dump_config() { } } void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->valve_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->valve_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) auto traits = this->valve_->get_traits(); if (traits.get_is_assumed_state()) { From be006ecaddad5df7c2be5cfa3ffa2499dda5c203 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:31:19 -0600 Subject: [PATCH 107/461] [mdns] Eliminate redundant hostname copy to save heap memory (#11734) --- esphome/components/mdns/mdns_component.cpp | 4 +--- esphome/components/mdns/mdns_component.h | 1 - esphome/components/mdns/mdns_esp32.cpp | 6 ++++-- esphome/components/mdns/mdns_esp8266.cpp | 3 ++- esphome/components/mdns/mdns_libretiny.cpp | 3 ++- esphome/components/mdns/mdns_rp2040.cpp | 3 ++- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index d476136554d..2c3150ff5dd 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -37,8 +37,6 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION); void MDNSComponent::compile_records_(StaticVector &services) { - this->hostname_ = App.get_name(); - // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES // in mdns/__init__.py. If you add a new service here, update both locations. @@ -179,7 +177,7 @@ void MDNSComponent::dump_config() { ESP_LOGCONFIG(TAG, "mDNS:\n" " Hostname: %s", - this->hostname_.c_str()); + App.get_name().c_str()); #ifdef USE_MDNS_STORE_SERVICES ESP_LOGV(TAG, " Services:"); for (const auto &service : this->services_) { diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 35371fd7391..f4237d5a690 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -76,7 +76,6 @@ class MDNSComponent : public Component { #ifdef USE_MDNS_STORE_SERVICES StaticVector services_{}; #endif - std::string hostname_; void compile_records_(StaticVector &services); }; diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index c02bfcbadb8..ecdc926cc96 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -2,6 +2,7 @@ #if defined(USE_ESP32) && defined(USE_MDNS) #include +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -27,8 +28,9 @@ void MDNSComponent::setup() { return; } - mdns_hostname_set(this->hostname_.c_str()); - mdns_instance_name_set(this->hostname_.c_str()); + const char *hostname = App.get_name().c_str(); + mdns_hostname_set(hostname); + mdns_instance_name_set(hostname); for (const auto &service : services) { auto txt_records = std::make_unique(service.txt_records.size()); diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 25a3defa7ba..9bbb4060700 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -4,6 +4,7 @@ #include #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -20,7 +21,7 @@ void MDNSComponent::setup() { this->compile_records_(services); #endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index a3e317a2bfa..fb2088f7194 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -3,6 +3,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -20,7 +21,7 @@ void MDNSComponent::setup() { this->compile_records_(services); #endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 791fa3934d3..a9f5349f14b 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -3,6 +3,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -20,7 +21,7 @@ void MDNSComponent::setup() { this->compile_records_(services); #endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is From 8514fbcf7192084f618283e70d77e6e1659c60ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:36:27 -0600 Subject: [PATCH 108/461] Revert "Revert "Revert "[api] Release excess buffer capacity after initial sync""" This reverts commit e8c7f74abd271674316f94737ec7083ebbd19fb4. --- esphome/components/api/api_connection.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 47dc829b641..5ab8a6eb050 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -193,11 +193,6 @@ void APIConnection::loop() { if (!this->deferred_batch_.empty()) { this->process_batch_(); } - // Release excess capacity from initial entity flood - // deferred_batch_ grew up to MAX_INITIAL_PER_BATCH (24) items, now shrink to free up to ~384 bytes - this->deferred_batch_.items.shrink_to_fit(); - // shared_write_buffer_ grew up to ~1.5KB during initial state, now shrink to free up to ~1.4KB - this->parent_->get_shared_buffer_ref().shrink_to_fit(); // Now that everything is sent, enable immediate sending for future state changes this->flags_.should_try_send_immediately = true; } From 00c0854323a79663a27243f05db911be9ad956e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:50:35 -0600 Subject: [PATCH 109/461] [core] Deprecate get_icon(), get_device_class(), get_unit_of_measurement() and fix remaining non-MQTT usages (#11732) --- esphome/components/graph/graph.cpp | 4 ++-- esphome/components/prometheus/prometheus_handler.cpp | 2 +- esphome/components/sprinkler/sprinkler.cpp | 4 ++-- esphome/core/entity_base.h | 12 +++++++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index ac6ace96ee3..88bb306408f 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -235,7 +235,7 @@ void GraphLegend::init(Graph *g) { std::string valstr = value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (this->units_) { - valstr += trace->sensor_->get_unit_of_measurement(); + valstr += trace->sensor_->get_unit_of_measurement_ref(); } this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh); if (fw > valw) @@ -371,7 +371,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of std::string valstr = value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (legend_->units_) { - valstr += trace->sensor_->get_unit_of_measurement(); + valstr += trace->sensor_->get_unit_of_measurement_ref(); } buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str()); ESP_LOGV(TAG, " value: %s", valstr.c_str()); diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 6e7ed6f79fc..5cfcacf0cbb 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -158,7 +158,7 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(ESPHOME_F("\",unit=\"")); - stream->print(obj->get_unit_of_measurement().c_str()); + stream->print(obj->get_unit_of_measurement_ref().c_str()); stream->print(ESPHOME_F("\"} ")); stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); stream->print(ESPHOME_F("\n")); diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 7676e174688..8edb240a415 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -650,7 +650,7 @@ void Sprinkler::set_valve_run_duration(const optional valve_number, cons return; } auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); - if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { + if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { call.set_value(run_duration.value() / 60.0); } else { call.set_value(run_duration.value()); @@ -732,7 +732,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { return 0; } if (this->valve_[valve_number].run_duration_number != nullptr) { - if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { + if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); } else { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 80cd6b8e770..6e5362464f0 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -61,7 +61,9 @@ class EntityBase { } // Get/set this entity's icon - std::string get_icon() const; + [[deprecated("Use get_icon_ref() instead for better performance (avoids string copy). Will stop working in ESPHome " + "2026.5.0")]] std::string + get_icon() const; void set_icon(const char *icon); StringRef get_icon_ref() const { static constexpr auto EMPTY_STRING = StringRef::from_lit(""); @@ -158,7 +160,9 @@ class EntityBase { class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) public: /// Get the device class, using the manual override if set. - std::string get_device_class(); + [[deprecated("Use get_device_class_ref() instead for better performance (avoids string copy). Will stop working in " + "ESPHome 2026.5.0")]] std::string + get_device_class(); /// Manually set the device class. void set_device_class(const char *device_class); /// Get the device class as StringRef @@ -174,7 +178,9 @@ class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) public: /// Get the unit of measurement, using the manual override if set. - std::string get_unit_of_measurement(); + [[deprecated("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will stop " + "working in ESPHome 2026.5.0")]] std::string + get_unit_of_measurement(); /// Manually set the unit of measurement. void set_unit_of_measurement(const char *unit_of_measurement); /// Get the unit of measurement as StringRef From aa5795c019c055e08807f9943633d3a8582582e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 13:17:34 -0600 Subject: [PATCH 110/461] [tests] Fix ID collision between bl0940 and nau7802 component tests (#11739) --- tests/components/bl0940/common.yaml | 6 +++--- tests/components/nau7802/common.yaml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/components/bl0940/common.yaml b/tests/components/bl0940/common.yaml index 0b73fd6d552..e476ba10c0d 100644 --- a/tests/components/bl0940/common.yaml +++ b/tests/components/bl0940/common.yaml @@ -1,11 +1,11 @@ button: - platform: bl0940 - bl0940_id: test_id + bl0940_id: bl0940_test_id name: Cal Reset sensor: - platform: bl0940 - id: test_id + id: bl0940_test_id voltage: name: BL0940 Voltage current: @@ -22,7 +22,7 @@ sensor: number: - platform: bl0940 id: bl0940_number_id - bl0940_id: test_id + bl0940_id: bl0940_test_id current_calibration: name: Cal Current min_value: -5 diff --git a/tests/components/nau7802/common.yaml b/tests/components/nau7802/common.yaml index 5c52c33dadb..5251910df9f 100644 --- a/tests/components/nau7802/common.yaml +++ b/tests/components/nau7802/common.yaml @@ -1,13 +1,13 @@ sensor: - platform: nau7802 i2c_id: i2c_bus - id: test_id + id: nau7802_test_id name: weight gain: 32 ldo_voltage: "3.0v" samples_per_second: 10 on_value: then: - - nau7802.calibrate_external_offset: test_id - - nau7802.calibrate_internal_offset: test_id - - nau7802.calibrate_gain: test_id + - nau7802.calibrate_external_offset: nau7802_test_id + - nau7802.calibrate_internal_offset: nau7802_test_id + - nau7802.calibrate_gain: nau7802_test_id From ce5e6088638d6b5cdc2becc34196205d3e03b2dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 14:32:45 -0600 Subject: [PATCH 111/461] [ci] Skip memory impact analysis for release and beta branches (#11740) --- script/determine-jobs.py | 15 +++++++ script/helpers.py | 45 ++++++++++++++++--- tests/script/test_determine_jobs.py | 70 +++++++++++++++++++++++++++++ tests/script/test_helpers.py | 7 +++ 4 files changed, 130 insertions(+), 7 deletions(-) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 6f908b7150b..e9d17d8fe5b 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -63,6 +63,7 @@ from helpers import ( get_components_from_integration_fixtures, get_components_with_dependencies, get_cpp_changed_components, + get_target_branch, git_ls_files, parse_test_filename, root_path, @@ -471,6 +472,20 @@ def detect_memory_impact_config( - platform: platform name for the merged build - use_merged_config: "true" (always use merged config) """ + # Skip memory impact analysis for release* or beta* branches + # These branches typically contain many merged changes from dev, and building + # all components at once would produce nonsensical memory impact results. + # Memory impact analysis is most useful for focused PRs targeting dev. + target_branch = get_target_branch() + if target_branch and ( + target_branch.startswith("release") or target_branch.startswith("beta") + ): + print( + f"Memory impact: Skipping analysis for target branch {target_branch} " + f"(would try to build all components at once, giving nonsensical results)", + file=sys.stderr, + ) + return {"should_run": "false"} # Get actually changed files (not dependencies) files = changed_files(branch) diff --git a/script/helpers.py b/script/helpers.py index 5b2fe6cd06a..1039ef39ac2 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -196,6 +196,20 @@ def splitlines_no_ends(string: str) -> list[str]: return [s.strip() for s in string.splitlines()] +@cache +def _get_github_event_data() -> dict | None: + """Read and parse GitHub event file (cached). + + Returns: + Parsed event data dictionary, or None if not available + """ + github_event_path = os.environ.get("GITHUB_EVENT_PATH") + if github_event_path and os.path.exists(github_event_path): + with open(github_event_path) as f: + return json.load(f) + return None + + def _get_pr_number_from_github_env() -> str | None: """Extract PR number from GitHub environment variables. @@ -208,13 +222,30 @@ def _get_pr_number_from_github_env() -> str | None: return github_ref.split("/pull/")[1].split("/")[0] # Fallback to GitHub event file - github_event_path = os.environ.get("GITHUB_EVENT_PATH") - if github_event_path and os.path.exists(github_event_path): - with open(github_event_path) as f: - event_data = json.load(f) - pr_data = event_data.get("pull_request", {}) - if pr_number := pr_data.get("number"): - return str(pr_number) + if event_data := _get_github_event_data(): + pr_data = event_data.get("pull_request", {}) + if pr_number := pr_data.get("number"): + return str(pr_number) + + return None + + +def get_target_branch() -> str | None: + """Get the target branch from GitHub environment variables. + + Returns: + Target branch name (e.g., "dev", "release", "beta"), or None if not in PR context + """ + # First try GITHUB_BASE_REF (set for pull_request events) + if base_ref := os.environ.get("GITHUB_BASE_REF"): + return base_ref + + # Fallback to GitHub event file + if event_data := _get_github_event_data(): + pr_data = event_data.get("pull_request", {}) + base_data = pr_data.get("base", {}) + if ref := base_data.get("ref"): + return ref return None diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index e084e2e3982..9f12d7ffcff 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -1240,3 +1240,73 @@ def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32( ) assert result["use_merged_config"] == "true" + + +def test_detect_memory_impact_config_skips_release_branch(tmp_path: Path) -> None: + """Test that memory impact analysis is skipped for release* branches.""" + # Create test directory structure with components that have tests + tests_dir = tmp_path / "tests" / "components" + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="release"), + ): + mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should be skipped for release branch + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_skips_beta_branch(tmp_path: Path) -> None: + """Test that memory impact analysis is skipped for beta* branches.""" + # Create test directory structure with components that have tests + tests_dir = tmp_path / "tests" / "components" + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="beta"), + ): + mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should be skipped for beta branch + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_runs_for_dev_branch(tmp_path: Path) -> None: + """Test that memory impact analysis runs for dev branch.""" + # Create test directory structure with components that have tests + tests_dir = tmp_path / "tests" / "components" + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="dev"), + ): + mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run for dev branch + assert result["should_run"] == "true" + assert result["components"] == ["wifi"] diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 1bfffef51cb..c51273f2980 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -31,6 +31,13 @@ print_file_list = helpers.print_file_list get_all_dependencies = helpers.get_all_dependencies +@pytest.fixture(autouse=True) +def clear_helpers_cache() -> None: + """Clear cached functions before each test.""" + helpers._get_github_event_data.cache_clear() + helpers._get_changed_files_github_actions.cache_clear() + + @pytest.mark.parametrize( ("github_ref", "expected_pr_number"), [ From e3310565008cdf224fd9d6daf387bf08e900413b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 16:35:01 -0600 Subject: [PATCH 112/461] Update AI instructions with C++ style guidelines from developers documentation --- .ai/instructions.md | 62 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/.ai/instructions.md b/.ai/instructions.md index 5f314a0dc99..fbff419efa1 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -51,7 +51,67 @@ This document provides essential context for AI models interacting with this pro * **Naming Conventions:** * **Python:** Follows PEP 8. Use clear, descriptive names following snake_case. - * **C++:** Follows the Google C++ Style Guide. + * **C++:** Follows the Google C++ Style Guide with these specifics (following clang-tidy conventions): + - Function, method, and variable names: `lower_snake_case` + - Class/struct/enum names: `UpperCamelCase` + - Top-level constants (global/namespace scope): `UPPER_SNAKE_CASE` + - Function-local constants: `lower_snake_case` + - Protected/private fields: `lower_snake_case_with_trailing_underscore_` + - Favor descriptive names over abbreviations + +* **C++ Field Visibility:** + * **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`. + * **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants: + 1. **Pointer lifetime issues:** When setters validate pointers against known lists to prevent dangling references. + ```cpp + class SelectComponent { + public: + void set_options(const std::vector &options) { + this->options_ = options; + this->current_option_ = nullptr; // Reset to prevent dangling pointer + } + void set_selected_option(const std::string *option) { + // Validate that option points to an entry in options_ + if (std::find(this->options_.begin(), this->options_.end(), *option) != this->options_.end()) { + this->current_option_ = option; + } + } + private: + std::vector options_; + const std::string *current_option_{nullptr}; // Must point to entry in options_ + }; + ``` + 2. **Invariant coupling:** When multiple fields must remain synchronized to prevent buffer overflows or data corruption. + ```cpp + class Buffer { + public: + void resize(size_t new_size) { + auto new_data = std::make_unique(new_size); + if (this->data_) { + std::memcpy(new_data.get(), this->data_.get(), std::min(this->size_, new_size)); + } + this->data_ = std::move(new_data); + this->size_ = new_size; // Must stay in sync with data_ + } + private: + std::unique_ptr data_; + size_t size_{0}; // Must match allocated size of data_ + }; + ``` + 3. **Resource management:** When setters perform cleanup or registration operations that derived classes might skip. + * **Provide `protected` accessor methods:** When derived classes need controlled access to `private` members. + +* **C++ Preprocessor Directives:** + * **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums. + * **Use `#define` only for:** + - Conditional compilation (`#ifdef`, `#ifndef`) + - Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`) + +* **C++ Additional Conventions:** + * **Member access:** Prefix all class member access with `this->` (e.g., `this->value_` not `value_`) + * **Indentation:** Use spaces (two per indentation level), not tabs + * **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;` + * **Line length:** Wrap lines at no more than 120 characters * **Component Structure:** * **Standard Files:** From c83e5e076be04398c209fc0854246f9dea0e055c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 16:41:26 -0600 Subject: [PATCH 113/461] cleanup --- .ai/instructions.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.ai/instructions.md b/.ai/instructions.md index fbff419efa1..bb87eb3050a 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -71,10 +71,17 @@ This document provides essential context for AI models interacting with this pro this->current_option_ = nullptr; // Reset to prevent dangling pointer } void set_selected_option(const std::string *option) { - // Validate that option points to an entry in options_ - if (std::find(this->options_.begin(), this->options_.end(), *option) != this->options_.end()) { - this->current_option_ = option; + // Validate that option points to an entry in options_ by checking address range + if (option == nullptr) { + return; } + for (const auto &opt : this->options_) { + if (&opt == option) { + this->current_option_ = option; + return; + } + } + // Invalid pointer - doesn't point to an element in options_ } private: std::vector options_; From f8aee13a3ab9fa7773549d19dfba553a73e4eb85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 16:44:06 -0600 Subject: [PATCH 114/461] use actual pattern --- .ai/instructions.md | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/.ai/instructions.md b/.ai/instructions.md index bb87eb3050a..9309c67c656 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -62,30 +62,35 @@ This document provides essential context for AI models interacting with this pro * **C++ Field Visibility:** * **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`. * **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants: - 1. **Pointer lifetime issues:** When setters validate pointers against known lists to prevent dangling references. + 1. **Pointer lifetime issues:** When setters validate and store pointers from known lists to prevent dangling references. ```cpp - class SelectComponent { - public: - void set_options(const std::vector &options) { - this->options_ = options; - this->current_option_ = nullptr; // Reset to prevent dangling pointer + // Helper to find matching string in vector and return its pointer + inline const char *vector_find(const std::vector &vec, const char *value) { + for (const char *item : vec) { + if (strcmp(item, value) == 0) + return item; } - void set_selected_option(const std::string *option) { - // Validate that option points to an entry in options_ by checking address range - if (option == nullptr) { - return; + return nullptr; + } + + class ClimateDevice { + public: + void set_custom_fan_modes(std::initializer_list modes) { + this->custom_fan_modes_ = modes; + this->active_custom_fan_mode_ = nullptr; // Reset when modes change + } + bool set_custom_fan_mode(const char *mode) { + // Find mode in supported list and store that pointer (not the input pointer) + const char *validated_mode = vector_find(this->custom_fan_modes_, mode); + if (validated_mode != nullptr) { + this->active_custom_fan_mode_ = validated_mode; + return true; } - for (const auto &opt : this->options_) { - if (&opt == option) { - this->current_option_ = option; - return; - } - } - // Invalid pointer - doesn't point to an element in options_ + return false; } private: - std::vector options_; - const std::string *current_option_{nullptr}; // Must point to entry in options_ + std::vector custom_fan_modes_; // Pointers to string literals in flash + const char *active_custom_fan_mode_{nullptr}; // Must point to entry in custom_fan_modes_ }; ``` 2. **Invariant coupling:** When multiple fields must remain synchronized to prevent buffer overflows or data corruption. From 20b6e0d5c239d949a89f61a528733772f5c3166b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 6 Nov 2025 09:37:38 +1000 Subject: [PATCH 115/461] [lvgl] Allow text substitution for NaN (#11712) --- esphome/components/lvgl/helpers.py | 39 ++++++++++++++++++++---- esphome/components/lvgl/lv_validation.py | 16 ++++++++-- esphome/components/lvgl/schemas.py | 3 +- tests/components/lvgl/lvgl-package.yaml | 6 ++++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index 8d5b6354bb2..c2bd58f71c3 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -3,6 +3,8 @@ import re from esphome import config_validation as cv from esphome.const import CONF_ARGS, CONF_FORMAT +CONF_IF_NAN = "if_nan" + lv_uses = { "USER_DATA", "LOG", @@ -21,23 +23,48 @@ lv_fonts_used = set() esphome_fonts_used = set() lvgl_components_required = set() - -def validate_printf(value): - cfmt = r""" +# noqa +f_regex = re.compile( + r""" ( # start of capture group 1 % # literal "%" - (?:[-+0 #]{0,5}) # optional flags + [-+0 #]{0,5} # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:h|l|ll|w|I|I32|I64)? # size + f # type + ) + """, + flags=re.VERBOSE, +) +# noqa +c_regex = re.compile( + r""" + ( # start of capture group 1 + % # literal "%" + [-+0 #]{0,5} # optional flags (?:\d+|\*)? # width (?:\.(?:\d+|\*))? # precision (?:h|l|ll|w|I|I32|I64)? # size [cCdiouxXeEfgGaAnpsSZ] # type ) - """ # noqa - matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE) + """, + flags=re.VERBOSE, +) + + +def validate_printf(value): + format_string = value[CONF_FORMAT] + matches = c_regex.findall(format_string) if len(matches) != len(value[CONF_ARGS]): raise cv.Invalid( f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!" ) + + if value.get(CONF_IF_NAN) and len(f_regex.findall(format_string)) != 1: + raise cv.Invalid( + "Use of 'if_nan' requires a single valid printf-pattern of type %f" + ) return value diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 9fe72128cee..045258555c6 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -33,7 +33,13 @@ from .defines import ( call_lambda, literal, ) -from .helpers import add_lv_use, esphome_fonts_used, lv_fonts_used, requires_component +from .helpers import ( + CONF_IF_NAN, + add_lv_use, + esphome_fonts_used, + lv_fonts_used, + requires_component, +) from .types import lv_font_t, lv_gradient_t opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") @@ -412,7 +418,13 @@ class TextValidator(LValidator): str_args = [str(x) for x in value[CONF_ARGS]] arg_expr = cg.RawExpression(",".join(str_args)) format_str = cpp_string_escape(format_str) - return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") + sprintf_str = f"str_sprintf({format_str}, {arg_expr}).c_str()" + if nanval := value.get(CONF_IF_NAN): + nanval = cpp_string_escape(nanval) + return literal( + f"(std::isfinite({arg_expr}) ? {sprintf_str} : {nanval})" + ) + return literal(sprintf_str) if time_format := value.get(CONF_TIME_FORMAT): source = value[CONF_TIME] if isinstance(source, Lambda): diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index dd248d0b947..0dcf420f249 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -20,7 +20,7 @@ from esphome.core.config import StartupTrigger from . import defines as df, lv_validation as lvalid from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR -from .helpers import requires_component, validate_printf +from .helpers import CONF_IF_NAN, requires_component, validate_printf from .layout import ( FLEX_OBJ_SCHEMA, GRID_CELL_SCHEMA, @@ -54,6 +54,7 @@ PRINTF_TEXT_SCHEMA = cv.All( { cv.Required(CONF_FORMAT): cv.string, cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_), + cv.Optional(CONF_IF_NAN): cv.string, }, ), validate_printf, diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 8ac9a60e2d7..b122d10f041 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -726,6 +726,12 @@ lvgl: - logger.log: format: "Spinbox value is %f" args: [x] + - lvgl.label.update: + id: hello_label + text: + format: "value is %.1f now" + args: [x] + if_nan: "Value unknown" - button: styles: spin_button id: spin_down From bd0705cdc0fc94fe3d9ce9ba38f237d9a7d75e68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 18:49:58 -0600 Subject: [PATCH 116/461] [api] Store YAML service names in flash instead of heap Reduces memory usage for YAML-defined API services by storing service names and argument names as pointers to string literals in flash instead of heap-allocated std::string objects. Implementation: - Created UserServiceBase for YAML services (const char* storage) - Created UserServiceDynamic for custom_api_device (std::string storage) - Updated CustomAPIDeviceService to inherit from UserServiceDynamic - UserServiceTrigger uses UserServiceBase (YAML-only) Memory savings per YAML service: - 0 args: 32 bytes (57% reduction) - 2 args: 48 bytes (60% reduction) - 5 args: 96 bytes (63% reduction) Custom API device services maintain same memory footprint (no regression). Typical ESPHome device (2-5 services): 100-240 bytes saved High-service device (10+ services): 400-800 bytes saved --- esphome/components/api/custom_api_device.h | 4 +- esphome/components/api/user_services.h | 60 +++++++++++++++++-- .../fixtures/api_user_services_union.yaml | 59 ++++++++++++++++++ 3 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 tests/integration/fixtures/api_user_services_union.yaml diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index d34ccfa0cef..43ea644f0c3 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -9,11 +9,11 @@ namespace esphome::api { #ifdef USE_API_SERVICES -template class CustomAPIDeviceService : public UserServiceBase { +template class CustomAPIDeviceService : public UserServiceDynamic { public: CustomAPIDeviceService(const std::string &name, const std::array &arg_names, T *obj, void (T::*callback)(Ts...)) - : UserServiceBase(name, arg_names), obj_(obj), callback_(callback) {} + : UserServiceDynamic(name, arg_names), obj_(obj), callback_(callback) {} protected: void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 9ca5e1093e0..4d980a148d5 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -23,11 +23,13 @@ template T get_execute_arg_value(const ExecuteServiceArgument &arg); template enums::ServiceArgType to_service_arg_type(); +// Base class for YAML-defined services (most common case) +// Stores only pointers to string literals in flash - no heap allocation template class UserServiceBase : public UserServiceDescriptor { public: - UserServiceBase(std::string name, const std::array &arg_names) - : name_(std::move(name)), arg_names_(arg_names) { - this->key_ = fnv1_hash(this->name_); + UserServiceBase(const char *name, const std::array &arg_names) + : name_(name), arg_names_(arg_names) { + this->key_ = fnv1_hash(name); } ListEntitiesServicesResponse encode_list_service_response() override { @@ -47,7 +49,7 @@ template class UserServiceBase : public UserServiceDescriptor { bool execute_service(const ExecuteServiceRequest &req) override { if (req.key != this->key_) return false; - if (req.args.size() != this->arg_names_.size()) + if (req.args.size() != sizeof...(Ts)) return false; this->execute_(req.args, typename gens::type()); return true; @@ -59,14 +61,60 @@ template class UserServiceBase : public UserServiceDescriptor { this->execute((get_execute_arg_value(args[S]))...); } - std::string name_; + // Pointers to string literals in flash - no heap allocation + const char *name_; + std::array arg_names_; uint32_t key_{0}; +}; + +// Derived class for custom_api_device services (rare case) +// Stores copies of runtime-generated names +template class UserServiceDynamic : public UserServiceDescriptor { + public: + UserServiceDynamic(const std::string &name, const std::array &arg_names) + : name_(name), arg_names_(arg_names) { + this->key_ = fnv1_hash(this->name_.c_str()); + } + + ListEntitiesServicesResponse encode_list_service_response() override { + ListEntitiesServicesResponse msg; + msg.set_name(StringRef(this->name_)); + msg.key = this->key_; + std::array arg_types = {to_service_arg_type()...}; + msg.args.init(sizeof...(Ts)); + for (size_t i = 0; i < sizeof...(Ts); i++) { + auto &arg = msg.args.emplace_back(); + arg.type = arg_types[i]; + arg.set_name(StringRef(this->arg_names_[i])); + } + return msg; + } + + bool execute_service(const ExecuteServiceRequest &req) override { + if (req.key != this->key_) + return false; + if (req.args.size() != sizeof...(Ts)) + return false; + this->execute_(req.args, typename gens::type()); + return true; + } + + protected: + virtual void execute(Ts... x) = 0; + template void execute_(const ArgsContainer &args, seq type) { + this->execute((get_execute_arg_value(args[S]))...); + } + + // Heap-allocated strings for runtime-generated names + std::string name_; std::array arg_names_; + uint32_t key_{0}; }; template class UserServiceTrigger : public UserServiceBase, public Trigger { public: - UserServiceTrigger(const std::string &name, const std::array &arg_names) + // Constructor for static names (YAML-defined services - used by code generator) + UserServiceTrigger(const char *name, const std::array &arg_names) : UserServiceBase(name, arg_names) {} protected: diff --git a/tests/integration/fixtures/api_user_services_union.yaml b/tests/integration/fixtures/api_user_services_union.yaml new file mode 100644 index 00000000000..4fd93437722 --- /dev/null +++ b/tests/integration/fixtures/api_user_services_union.yaml @@ -0,0 +1,59 @@ +esphome: + name: test-user-services-union + friendly_name: Test User Services Union Storage + +esp32: + board: esp32dev + framework: + type: esp-idf + +logger: + level: DEBUG + +wifi: + ssid: "test" + password: "password" + +api: + actions: + # Test service with no arguments + - action: test_no_args + then: + - logger.log: "No args service called" + + # Test service with one argument + - action: test_one_arg + variables: + value: int + then: + - logger.log: + format: "One arg service: %d" + args: [value] + + # Test service with multiple arguments of different types + - action: test_multi_args + variables: + int_val: int + float_val: float + str_val: string + bool_val: bool + then: + - logger.log: + format: "Multi args: %d, %.2f, %s, %d" + args: [int_val, float_val, str_val.c_str(), bool_val] + + # Test service with max typical arguments + - action: test_many_args + variables: + arg1: int + arg2: int + arg3: int + arg4: string + arg5: float + then: + - logger.log: "Many args service called" + +binary_sensor: + - platform: template + name: "Test Binary Sensor" + id: test_sensor From ab6cb2dee6211d890ee5d87ba351924a42748565 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 18:51:38 -0600 Subject: [PATCH 117/461] remove extra test --- .../fixtures/api_user_services_union.yaml | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 tests/integration/fixtures/api_user_services_union.yaml diff --git a/tests/integration/fixtures/api_user_services_union.yaml b/tests/integration/fixtures/api_user_services_union.yaml deleted file mode 100644 index 4fd93437722..00000000000 --- a/tests/integration/fixtures/api_user_services_union.yaml +++ /dev/null @@ -1,59 +0,0 @@ -esphome: - name: test-user-services-union - friendly_name: Test User Services Union Storage - -esp32: - board: esp32dev - framework: - type: esp-idf - -logger: - level: DEBUG - -wifi: - ssid: "test" - password: "password" - -api: - actions: - # Test service with no arguments - - action: test_no_args - then: - - logger.log: "No args service called" - - # Test service with one argument - - action: test_one_arg - variables: - value: int - then: - - logger.log: - format: "One arg service: %d" - args: [value] - - # Test service with multiple arguments of different types - - action: test_multi_args - variables: - int_val: int - float_val: float - str_val: string - bool_val: bool - then: - - logger.log: - format: "Multi args: %d, %.2f, %s, %d" - args: [int_val, float_val, str_val.c_str(), bool_val] - - # Test service with max typical arguments - - action: test_many_args - variables: - arg1: int - arg2: int - arg3: int - arg4: string - arg5: float - then: - - logger.log: "Many args service called" - -binary_sensor: - - platform: template - name: "Test Binary Sensor" - id: test_sensor From 15c167b5cea4fe45d0849a0859672bb67f324263 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 18:55:04 -0600 Subject: [PATCH 118/461] adjust --- .../fixtures/api_custom_services.yaml | 22 +++++++ tests/integration/test_api_custom_services.py | 60 ++++++++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/tests/integration/fixtures/api_custom_services.yaml b/tests/integration/fixtures/api_custom_services.yaml index 41efc95b854..a597c741267 100644 --- a/tests/integration/fixtures/api_custom_services.yaml +++ b/tests/integration/fixtures/api_custom_services.yaml @@ -11,6 +11,28 @@ api: then: - logger.log: "YAML service called" + # Test YAML service with arguments (tests UserServiceBase with const char* array) + - action: test_yaml_service_with_args + variables: + my_int: int + my_string: string + then: + - logger.log: + format: "YAML service with args: %d, %s" + args: [my_int, my_string.c_str()] + + # Test YAML service with multiple arguments + - action: test_yaml_service_many_args + variables: + arg1: int + arg2: float + arg3: bool + arg4: string + then: + - logger.log: + format: "YAML service many args: %d, %.2f, %d, %s" + args: [arg1, arg2, arg3, arg4.c_str()] + logger: level: DEBUG diff --git a/tests/integration/test_api_custom_services.py b/tests/integration/test_api_custom_services.py index 9ae4cdcb5d6..967c5041123 100644 --- a/tests/integration/test_api_custom_services.py +++ b/tests/integration/test_api_custom_services.py @@ -33,12 +33,16 @@ async def test_api_custom_services( # Track log messages yaml_service_future = loop.create_future() + yaml_args_future = loop.create_future() + yaml_many_args_future = loop.create_future() custom_service_future = loop.create_future() custom_args_future = loop.create_future() custom_arrays_future = loop.create_future() # Patterns to match in logs yaml_service_pattern = re.compile(r"YAML service called") + yaml_args_pattern = re.compile(r"YAML service with args: 123, test_value") + yaml_many_args_pattern = re.compile(r"YAML service many args: 42, 3\.14, 1, hello") custom_service_pattern = re.compile(r"Custom test service called!") custom_args_pattern = re.compile( r"Custom service called with: test_string, 456, 1, 78\.90" @@ -51,6 +55,10 @@ async def test_api_custom_services( """Check log output for expected messages.""" if not yaml_service_future.done() and yaml_service_pattern.search(line): yaml_service_future.set_result(True) + elif not yaml_args_future.done() and yaml_args_pattern.search(line): + yaml_args_future.set_result(True) + elif not yaml_many_args_future.done() and yaml_many_args_pattern.search(line): + yaml_many_args_future.set_result(True) elif not custom_service_future.done() and custom_service_pattern.search(line): custom_service_future.set_result(True) elif not custom_args_future.done() and custom_args_pattern.search(line): @@ -71,11 +79,13 @@ async def test_api_custom_services( # List services _, services = await client.list_entities_services() - # Should have 4 services: 1 YAML + 3 CustomAPIDevice - assert len(services) == 4, f"Expected 4 services, found {len(services)}" + # Should have 6 services: 3 YAML + 3 CustomAPIDevice + assert len(services) == 6, f"Expected 6 services, found {len(services)}" # Find our services yaml_service: UserService | None = None + yaml_args_service: UserService | None = None + yaml_many_args_service: UserService | None = None custom_service: UserService | None = None custom_args_service: UserService | None = None custom_arrays_service: UserService | None = None @@ -83,6 +93,10 @@ async def test_api_custom_services( for service in services: if service.name == "test_yaml_service": yaml_service = service + elif service.name == "test_yaml_service_with_args": + yaml_args_service = service + elif service.name == "test_yaml_service_many_args": + yaml_many_args_service = service elif service.name == "custom_test_service": custom_service = service elif service.name == "custom_service_with_args": @@ -91,6 +105,10 @@ async def test_api_custom_services( custom_arrays_service = service assert yaml_service is not None, "test_yaml_service not found" + assert yaml_args_service is not None, "test_yaml_service_with_args not found" + assert yaml_many_args_service is not None, ( + "test_yaml_service_many_args not found" + ) assert custom_service is not None, "custom_test_service not found" assert custom_args_service is not None, "custom_service_with_args not found" assert custom_arrays_service is not None, "custom_service_with_arrays not found" @@ -99,6 +117,44 @@ async def test_api_custom_services( client.execute_service(yaml_service, {}) await asyncio.wait_for(yaml_service_future, timeout=5.0) + # Verify YAML service with args arguments + assert len(yaml_args_service.args) == 2 + yaml_args_types = {arg.name: arg.type for arg in yaml_args_service.args} + assert yaml_args_types["my_int"] == UserServiceArgType.INT + assert yaml_args_types["my_string"] == UserServiceArgType.STRING + + # Test YAML service with arguments + client.execute_service( + yaml_args_service, + { + "my_int": 123, + "my_string": "test_value", + }, + ) + await asyncio.wait_for(yaml_args_future, timeout=5.0) + + # Verify YAML service with many args arguments + assert len(yaml_many_args_service.args) == 4 + yaml_many_args_types = { + arg.name: arg.type for arg in yaml_many_args_service.args + } + assert yaml_many_args_types["arg1"] == UserServiceArgType.INT + assert yaml_many_args_types["arg2"] == UserServiceArgType.FLOAT + assert yaml_many_args_types["arg3"] == UserServiceArgType.BOOL + assert yaml_many_args_types["arg4"] == UserServiceArgType.STRING + + # Test YAML service with many arguments + client.execute_service( + yaml_many_args_service, + { + "arg1": 42, + "arg2": 3.14, + "arg3": True, + "arg4": "hello", + }, + ) + await asyncio.wait_for(yaml_many_args_future, timeout=5.0) + # Test simple CustomAPIDevice service client.execute_service(custom_service, {}) await asyncio.wait_for(custom_service_future, timeout=5.0) From ce4f9db778777b09429e71882f5bbc8de3cb06ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 18:55:04 -0600 Subject: [PATCH 119/461] adjust --- esphome/components/api/user_services.h | 4 +- .../fixtures/api_custom_services.yaml | 22 +++++++ tests/integration/test_api_custom_services.py | 60 ++++++++++++++++++- 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 4d980a148d5..c755e14c025 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -71,8 +71,8 @@ template class UserServiceBase : public UserServiceDescriptor { // Stores copies of runtime-generated names template class UserServiceDynamic : public UserServiceDescriptor { public: - UserServiceDynamic(const std::string &name, const std::array &arg_names) - : name_(name), arg_names_(arg_names) { + UserServiceDynamic(std::string name, const std::array &arg_names) + : name_(std::move(name)), arg_names_(arg_names) { this->key_ = fnv1_hash(this->name_.c_str()); } diff --git a/tests/integration/fixtures/api_custom_services.yaml b/tests/integration/fixtures/api_custom_services.yaml index 41efc95b854..a597c741267 100644 --- a/tests/integration/fixtures/api_custom_services.yaml +++ b/tests/integration/fixtures/api_custom_services.yaml @@ -11,6 +11,28 @@ api: then: - logger.log: "YAML service called" + # Test YAML service with arguments (tests UserServiceBase with const char* array) + - action: test_yaml_service_with_args + variables: + my_int: int + my_string: string + then: + - logger.log: + format: "YAML service with args: %d, %s" + args: [my_int, my_string.c_str()] + + # Test YAML service with multiple arguments + - action: test_yaml_service_many_args + variables: + arg1: int + arg2: float + arg3: bool + arg4: string + then: + - logger.log: + format: "YAML service many args: %d, %.2f, %d, %s" + args: [arg1, arg2, arg3, arg4.c_str()] + logger: level: DEBUG diff --git a/tests/integration/test_api_custom_services.py b/tests/integration/test_api_custom_services.py index 9ae4cdcb5d6..967c5041123 100644 --- a/tests/integration/test_api_custom_services.py +++ b/tests/integration/test_api_custom_services.py @@ -33,12 +33,16 @@ async def test_api_custom_services( # Track log messages yaml_service_future = loop.create_future() + yaml_args_future = loop.create_future() + yaml_many_args_future = loop.create_future() custom_service_future = loop.create_future() custom_args_future = loop.create_future() custom_arrays_future = loop.create_future() # Patterns to match in logs yaml_service_pattern = re.compile(r"YAML service called") + yaml_args_pattern = re.compile(r"YAML service with args: 123, test_value") + yaml_many_args_pattern = re.compile(r"YAML service many args: 42, 3\.14, 1, hello") custom_service_pattern = re.compile(r"Custom test service called!") custom_args_pattern = re.compile( r"Custom service called with: test_string, 456, 1, 78\.90" @@ -51,6 +55,10 @@ async def test_api_custom_services( """Check log output for expected messages.""" if not yaml_service_future.done() and yaml_service_pattern.search(line): yaml_service_future.set_result(True) + elif not yaml_args_future.done() and yaml_args_pattern.search(line): + yaml_args_future.set_result(True) + elif not yaml_many_args_future.done() and yaml_many_args_pattern.search(line): + yaml_many_args_future.set_result(True) elif not custom_service_future.done() and custom_service_pattern.search(line): custom_service_future.set_result(True) elif not custom_args_future.done() and custom_args_pattern.search(line): @@ -71,11 +79,13 @@ async def test_api_custom_services( # List services _, services = await client.list_entities_services() - # Should have 4 services: 1 YAML + 3 CustomAPIDevice - assert len(services) == 4, f"Expected 4 services, found {len(services)}" + # Should have 6 services: 3 YAML + 3 CustomAPIDevice + assert len(services) == 6, f"Expected 6 services, found {len(services)}" # Find our services yaml_service: UserService | None = None + yaml_args_service: UserService | None = None + yaml_many_args_service: UserService | None = None custom_service: UserService | None = None custom_args_service: UserService | None = None custom_arrays_service: UserService | None = None @@ -83,6 +93,10 @@ async def test_api_custom_services( for service in services: if service.name == "test_yaml_service": yaml_service = service + elif service.name == "test_yaml_service_with_args": + yaml_args_service = service + elif service.name == "test_yaml_service_many_args": + yaml_many_args_service = service elif service.name == "custom_test_service": custom_service = service elif service.name == "custom_service_with_args": @@ -91,6 +105,10 @@ async def test_api_custom_services( custom_arrays_service = service assert yaml_service is not None, "test_yaml_service not found" + assert yaml_args_service is not None, "test_yaml_service_with_args not found" + assert yaml_many_args_service is not None, ( + "test_yaml_service_many_args not found" + ) assert custom_service is not None, "custom_test_service not found" assert custom_args_service is not None, "custom_service_with_args not found" assert custom_arrays_service is not None, "custom_service_with_arrays not found" @@ -99,6 +117,44 @@ async def test_api_custom_services( client.execute_service(yaml_service, {}) await asyncio.wait_for(yaml_service_future, timeout=5.0) + # Verify YAML service with args arguments + assert len(yaml_args_service.args) == 2 + yaml_args_types = {arg.name: arg.type for arg in yaml_args_service.args} + assert yaml_args_types["my_int"] == UserServiceArgType.INT + assert yaml_args_types["my_string"] == UserServiceArgType.STRING + + # Test YAML service with arguments + client.execute_service( + yaml_args_service, + { + "my_int": 123, + "my_string": "test_value", + }, + ) + await asyncio.wait_for(yaml_args_future, timeout=5.0) + + # Verify YAML service with many args arguments + assert len(yaml_many_args_service.args) == 4 + yaml_many_args_types = { + arg.name: arg.type for arg in yaml_many_args_service.args + } + assert yaml_many_args_types["arg1"] == UserServiceArgType.INT + assert yaml_many_args_types["arg2"] == UserServiceArgType.FLOAT + assert yaml_many_args_types["arg3"] == UserServiceArgType.BOOL + assert yaml_many_args_types["arg4"] == UserServiceArgType.STRING + + # Test YAML service with many arguments + client.execute_service( + yaml_many_args_service, + { + "arg1": 42, + "arg2": 3.14, + "arg3": True, + "arg4": "hello", + }, + ) + await asyncio.wait_for(yaml_many_args_future, timeout=5.0) + # Test simple CustomAPIDevice service client.execute_service(custom_service, {}) await asyncio.wait_for(custom_service_future, timeout=5.0) From 8fded918b7c3431258dc35c15476bd37290ef240 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 19:16:37 -0600 Subject: [PATCH 120/461] adjust --- esphome/components/api/user_services.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index c755e14c025..2a887fc52da 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -67,7 +67,7 @@ template class UserServiceBase : public UserServiceDescriptor { uint32_t key_{0}; }; -// Derived class for custom_api_device services (rare case) +// Separate class for custom_api_device services (rare case) // Stores copies of runtime-generated names template class UserServiceDynamic : public UserServiceDescriptor { public: From bdfd88441a3666ce6f89773bb9bbfbc384eee4af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 19:31:23 -0600 Subject: [PATCH 121/461] [ci] Skip memory impact analysis when more than 40 components changed (#11741) --- script/determine-jobs.py | 12 +++++ tests/script/test_determine_jobs.py | 77 ++++++++++++++++++++++------- 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index e9d17d8fe5b..39a7571fbe2 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -94,6 +94,7 @@ class Platform(StrEnum): # Memory impact analysis constants MEMORY_IMPACT_FALLBACK_COMPONENT = "api" # Representative component for core changes MEMORY_IMPACT_FALLBACK_PLATFORM = Platform.ESP32_IDF # Most representative platform +MEMORY_IMPACT_MAX_COMPONENTS = 40 # Max components before results become nonsensical # Platform-specific components that can only be built on their respective platforms # These components contain platform-specific code and cannot be cross-compiled @@ -555,6 +556,17 @@ def detect_memory_impact_config( if not components_with_tests: return {"should_run": "false"} + # Skip memory impact analysis if too many components changed + # Building 40+ components at once produces nonsensical memory impact results + # This typically happens with large refactorings or batch updates + if len(components_with_tests) > MEMORY_IMPACT_MAX_COMPONENTS: + print( + f"Memory impact: Skipping analysis for {len(components_with_tests)} components " + f"(limit is {MEMORY_IMPACT_MAX_COMPONENTS}, would give nonsensical results)", + file=sys.stderr, + ) + return {"should_run": "false"} + # Find common platforms supported by ALL components # This ensures we can build all components together in a merged config common_platforms = set(MEMORY_IMPACT_PLATFORM_PREFERENCE) diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 9f12d7ffcff..4894a5e28af 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -71,9 +71,10 @@ def mock_changed_files() -> Generator[Mock, None, None]: @pytest.fixture(autouse=True) -def clear_clang_tidy_cache() -> None: - """Clear the clang-tidy full scan cache before each test.""" +def clear_determine_jobs_caches() -> None: + """Clear all cached functions before each test.""" determine_jobs._is_clang_tidy_full_scan.cache_clear() + determine_jobs._component_has_tests.cache_clear() def test_main_all_tests_should_run( @@ -565,7 +566,6 @@ def test_main_filters_components_without_tests( patch.object(determine_jobs, "changed_files", return_value=[]), ): # Clear the cache since we're mocking root_path - determine_jobs._component_has_tests.cache_clear() determine_jobs.main() # Check output @@ -665,7 +665,6 @@ def test_main_detects_components_with_variant_tests( patch.object(determine_jobs, "changed_files", return_value=[]), ): # Clear the cache since we're mocking root_path - determine_jobs._component_has_tests.cache_clear() determine_jobs.main() # Check output @@ -714,7 +713,6 @@ def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> Non "esphome/components/wifi/wifi.cpp", "esphome/components/api/api.cpp", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -744,7 +742,6 @@ def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None: "esphome/core/application.cpp", "esphome/core/component.h", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -775,7 +772,6 @@ def test_detect_memory_impact_config_core_python_only_changes(tmp_path: Path) -> "esphome/config.py", "esphome/core/config.py", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -808,7 +804,6 @@ def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None: "esphome/components/wifi/wifi.cpp", "esphome/components/logger/logger.cpp", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -830,7 +825,6 @@ def test_detect_memory_impact_config_no_changes(tmp_path: Path) -> None: patch.object(determine_jobs, "changed_files") as mock_changed_files, ): mock_changed_files.return_value = [] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -855,7 +849,6 @@ def test_detect_memory_impact_config_no_components_with_tests(tmp_path: Path) -> mock_changed_files.return_value = [ "esphome/components/my_custom_component/component.cpp", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -895,7 +888,6 @@ def test_detect_memory_impact_config_includes_base_bus_components( "esphome/components/uart/automation.h", # Header file with inline code "esphome/components/wifi/wifi.cpp", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -938,7 +930,6 @@ def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None: "esphome/components/improv_serial/improv_serial.cpp", "esphome/components/ethernet/ethernet.cpp", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -1168,7 +1159,6 @@ def test_detect_memory_impact_config_filters_incompatible_esp32_on_esp8266( "tests/components/esp8266/test.esp8266-ard.yaml", "esphome/core/helpers_esp8266.h", # ESP8266-specific file to hint platform ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -1222,7 +1212,6 @@ def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32( "esphome/components/wifi/wifi_component_esp_idf.cpp", # ESP-IDF hint "esphome/components/ethernet/ethernet_esp32.cpp", # ESP32 hint ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -1257,7 +1246,6 @@ def test_detect_memory_impact_config_skips_release_branch(tmp_path: Path) -> Non patch.object(determine_jobs, "get_target_branch", return_value="release"), ): mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -1280,7 +1268,6 @@ def test_detect_memory_impact_config_skips_beta_branch(tmp_path: Path) -> None: patch.object(determine_jobs, "get_target_branch", return_value="beta"), ): mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -1303,10 +1290,66 @@ def test_detect_memory_impact_config_runs_for_dev_branch(tmp_path: Path) -> None patch.object(determine_jobs, "get_target_branch", return_value="dev"), ): mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() # Memory impact should run for dev branch assert result["should_run"] == "true" assert result["components"] == ["wifi"] + + +def test_detect_memory_impact_config_skips_too_many_components( + tmp_path: Path, +) -> None: + """Test that memory impact analysis is skipped when more than 40 components changed.""" + # Create test directory structure with 41 components + tests_dir = tmp_path / "tests" / "components" + component_names = [f"component_{i}" for i in range(41)] + + for component_name in component_names: + comp_dir = tests_dir / component_name + comp_dir.mkdir(parents=True) + (comp_dir / "test.esp32-idf.yaml").write_text(f"test: {component_name}") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="dev"), + ): + mock_changed_files.return_value = [ + f"esphome/components/{name}/{name}.cpp" for name in component_names + ] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should be skipped for too many components (41 > 40) + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) -> None: + """Test that memory impact analysis runs with exactly 40 components (at limit).""" + # Create test directory structure with exactly 40 components + tests_dir = tmp_path / "tests" / "components" + component_names = [f"component_{i}" for i in range(40)] + + for component_name in component_names: + comp_dir = tests_dir / component_name + comp_dir.mkdir(parents=True) + (comp_dir / "test.esp32-idf.yaml").write_text(f"test: {component_name}") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="dev"), + ): + mock_changed_files.return_value = [ + f"esphome/components/{name}/{name}.cpp" for name in component_names + ] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run at exactly 40 components (at limit but not over) + assert result["should_run"] == "true" + assert len(result["components"]) == 40 From 5eea7bdb44fcd58b99f6738264122e9528ebcbb0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 19:45:48 -0600 Subject: [PATCH 122/461] Update AI instructions with C++ style guidelines from developers docs (#11743) --- .ai/instructions.md | 74 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/.ai/instructions.md b/.ai/instructions.md index 5f314a0dc99..9309c67c656 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -51,7 +51,79 @@ This document provides essential context for AI models interacting with this pro * **Naming Conventions:** * **Python:** Follows PEP 8. Use clear, descriptive names following snake_case. - * **C++:** Follows the Google C++ Style Guide. + * **C++:** Follows the Google C++ Style Guide with these specifics (following clang-tidy conventions): + - Function, method, and variable names: `lower_snake_case` + - Class/struct/enum names: `UpperCamelCase` + - Top-level constants (global/namespace scope): `UPPER_SNAKE_CASE` + - Function-local constants: `lower_snake_case` + - Protected/private fields: `lower_snake_case_with_trailing_underscore_` + - Favor descriptive names over abbreviations + +* **C++ Field Visibility:** + * **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`. + * **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants: + 1. **Pointer lifetime issues:** When setters validate and store pointers from known lists to prevent dangling references. + ```cpp + // Helper to find matching string in vector and return its pointer + inline const char *vector_find(const std::vector &vec, const char *value) { + for (const char *item : vec) { + if (strcmp(item, value) == 0) + return item; + } + return nullptr; + } + + class ClimateDevice { + public: + void set_custom_fan_modes(std::initializer_list modes) { + this->custom_fan_modes_ = modes; + this->active_custom_fan_mode_ = nullptr; // Reset when modes change + } + bool set_custom_fan_mode(const char *mode) { + // Find mode in supported list and store that pointer (not the input pointer) + const char *validated_mode = vector_find(this->custom_fan_modes_, mode); + if (validated_mode != nullptr) { + this->active_custom_fan_mode_ = validated_mode; + return true; + } + return false; + } + private: + std::vector custom_fan_modes_; // Pointers to string literals in flash + const char *active_custom_fan_mode_{nullptr}; // Must point to entry in custom_fan_modes_ + }; + ``` + 2. **Invariant coupling:** When multiple fields must remain synchronized to prevent buffer overflows or data corruption. + ```cpp + class Buffer { + public: + void resize(size_t new_size) { + auto new_data = std::make_unique(new_size); + if (this->data_) { + std::memcpy(new_data.get(), this->data_.get(), std::min(this->size_, new_size)); + } + this->data_ = std::move(new_data); + this->size_ = new_size; // Must stay in sync with data_ + } + private: + std::unique_ptr data_; + size_t size_{0}; // Must match allocated size of data_ + }; + ``` + 3. **Resource management:** When setters perform cleanup or registration operations that derived classes might skip. + * **Provide `protected` accessor methods:** When derived classes need controlled access to `private` members. + +* **C++ Preprocessor Directives:** + * **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums. + * **Use `#define` only for:** + - Conditional compilation (`#ifdef`, `#ifndef`) + - Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`) + +* **C++ Additional Conventions:** + * **Member access:** Prefix all class member access with `this->` (e.g., `this->value_` not `value_`) + * **Indentation:** Use spaces (two per indentation level), not tabs + * **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;` + * **Line length:** Wrap lines at no more than 120 characters * **Component Structure:** * **Standard Files:** From 83f30a64ed54f9285b0d68ba22b8654cdc75c5be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 20:31:59 -0600 Subject: [PATCH 123/461] [api] Store YAML service names in flash instead of heap (#11744) --- esphome/components/api/custom_api_device.h | 4 +- esphome/components/api/user_services.h | 60 +++++++++++++++++-- .../fixtures/api_custom_services.yaml | 22 +++++++ tests/integration/test_api_custom_services.py | 60 ++++++++++++++++++- 4 files changed, 136 insertions(+), 10 deletions(-) diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index d34ccfa0cef..43ea644f0c3 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -9,11 +9,11 @@ namespace esphome::api { #ifdef USE_API_SERVICES -template class CustomAPIDeviceService : public UserServiceBase { +template class CustomAPIDeviceService : public UserServiceDynamic { public: CustomAPIDeviceService(const std::string &name, const std::array &arg_names, T *obj, void (T::*callback)(Ts...)) - : UserServiceBase(name, arg_names), obj_(obj), callback_(callback) {} + : UserServiceDynamic(name, arg_names), obj_(obj), callback_(callback) {} protected: void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 9ca5e1093e0..2a887fc52da 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -23,11 +23,13 @@ template T get_execute_arg_value(const ExecuteServiceArgument &arg); template enums::ServiceArgType to_service_arg_type(); +// Base class for YAML-defined services (most common case) +// Stores only pointers to string literals in flash - no heap allocation template class UserServiceBase : public UserServiceDescriptor { public: - UserServiceBase(std::string name, const std::array &arg_names) - : name_(std::move(name)), arg_names_(arg_names) { - this->key_ = fnv1_hash(this->name_); + UserServiceBase(const char *name, const std::array &arg_names) + : name_(name), arg_names_(arg_names) { + this->key_ = fnv1_hash(name); } ListEntitiesServicesResponse encode_list_service_response() override { @@ -47,7 +49,7 @@ template class UserServiceBase : public UserServiceDescriptor { bool execute_service(const ExecuteServiceRequest &req) override { if (req.key != this->key_) return false; - if (req.args.size() != this->arg_names_.size()) + if (req.args.size() != sizeof...(Ts)) return false; this->execute_(req.args, typename gens::type()); return true; @@ -59,14 +61,60 @@ template class UserServiceBase : public UserServiceDescriptor { this->execute((get_execute_arg_value(args[S]))...); } - std::string name_; + // Pointers to string literals in flash - no heap allocation + const char *name_; + std::array arg_names_; uint32_t key_{0}; +}; + +// Separate class for custom_api_device services (rare case) +// Stores copies of runtime-generated names +template class UserServiceDynamic : public UserServiceDescriptor { + public: + UserServiceDynamic(std::string name, const std::array &arg_names) + : name_(std::move(name)), arg_names_(arg_names) { + this->key_ = fnv1_hash(this->name_.c_str()); + } + + ListEntitiesServicesResponse encode_list_service_response() override { + ListEntitiesServicesResponse msg; + msg.set_name(StringRef(this->name_)); + msg.key = this->key_; + std::array arg_types = {to_service_arg_type()...}; + msg.args.init(sizeof...(Ts)); + for (size_t i = 0; i < sizeof...(Ts); i++) { + auto &arg = msg.args.emplace_back(); + arg.type = arg_types[i]; + arg.set_name(StringRef(this->arg_names_[i])); + } + return msg; + } + + bool execute_service(const ExecuteServiceRequest &req) override { + if (req.key != this->key_) + return false; + if (req.args.size() != sizeof...(Ts)) + return false; + this->execute_(req.args, typename gens::type()); + return true; + } + + protected: + virtual void execute(Ts... x) = 0; + template void execute_(const ArgsContainer &args, seq type) { + this->execute((get_execute_arg_value(args[S]))...); + } + + // Heap-allocated strings for runtime-generated names + std::string name_; std::array arg_names_; + uint32_t key_{0}; }; template class UserServiceTrigger : public UserServiceBase, public Trigger { public: - UserServiceTrigger(const std::string &name, const std::array &arg_names) + // Constructor for static names (YAML-defined services - used by code generator) + UserServiceTrigger(const char *name, const std::array &arg_names) : UserServiceBase(name, arg_names) {} protected: diff --git a/tests/integration/fixtures/api_custom_services.yaml b/tests/integration/fixtures/api_custom_services.yaml index 41efc95b854..a597c741267 100644 --- a/tests/integration/fixtures/api_custom_services.yaml +++ b/tests/integration/fixtures/api_custom_services.yaml @@ -11,6 +11,28 @@ api: then: - logger.log: "YAML service called" + # Test YAML service with arguments (tests UserServiceBase with const char* array) + - action: test_yaml_service_with_args + variables: + my_int: int + my_string: string + then: + - logger.log: + format: "YAML service with args: %d, %s" + args: [my_int, my_string.c_str()] + + # Test YAML service with multiple arguments + - action: test_yaml_service_many_args + variables: + arg1: int + arg2: float + arg3: bool + arg4: string + then: + - logger.log: + format: "YAML service many args: %d, %.2f, %d, %s" + args: [arg1, arg2, arg3, arg4.c_str()] + logger: level: DEBUG diff --git a/tests/integration/test_api_custom_services.py b/tests/integration/test_api_custom_services.py index 9ae4cdcb5d6..967c5041123 100644 --- a/tests/integration/test_api_custom_services.py +++ b/tests/integration/test_api_custom_services.py @@ -33,12 +33,16 @@ async def test_api_custom_services( # Track log messages yaml_service_future = loop.create_future() + yaml_args_future = loop.create_future() + yaml_many_args_future = loop.create_future() custom_service_future = loop.create_future() custom_args_future = loop.create_future() custom_arrays_future = loop.create_future() # Patterns to match in logs yaml_service_pattern = re.compile(r"YAML service called") + yaml_args_pattern = re.compile(r"YAML service with args: 123, test_value") + yaml_many_args_pattern = re.compile(r"YAML service many args: 42, 3\.14, 1, hello") custom_service_pattern = re.compile(r"Custom test service called!") custom_args_pattern = re.compile( r"Custom service called with: test_string, 456, 1, 78\.90" @@ -51,6 +55,10 @@ async def test_api_custom_services( """Check log output for expected messages.""" if not yaml_service_future.done() and yaml_service_pattern.search(line): yaml_service_future.set_result(True) + elif not yaml_args_future.done() and yaml_args_pattern.search(line): + yaml_args_future.set_result(True) + elif not yaml_many_args_future.done() and yaml_many_args_pattern.search(line): + yaml_many_args_future.set_result(True) elif not custom_service_future.done() and custom_service_pattern.search(line): custom_service_future.set_result(True) elif not custom_args_future.done() and custom_args_pattern.search(line): @@ -71,11 +79,13 @@ async def test_api_custom_services( # List services _, services = await client.list_entities_services() - # Should have 4 services: 1 YAML + 3 CustomAPIDevice - assert len(services) == 4, f"Expected 4 services, found {len(services)}" + # Should have 6 services: 3 YAML + 3 CustomAPIDevice + assert len(services) == 6, f"Expected 6 services, found {len(services)}" # Find our services yaml_service: UserService | None = None + yaml_args_service: UserService | None = None + yaml_many_args_service: UserService | None = None custom_service: UserService | None = None custom_args_service: UserService | None = None custom_arrays_service: UserService | None = None @@ -83,6 +93,10 @@ async def test_api_custom_services( for service in services: if service.name == "test_yaml_service": yaml_service = service + elif service.name == "test_yaml_service_with_args": + yaml_args_service = service + elif service.name == "test_yaml_service_many_args": + yaml_many_args_service = service elif service.name == "custom_test_service": custom_service = service elif service.name == "custom_service_with_args": @@ -91,6 +105,10 @@ async def test_api_custom_services( custom_arrays_service = service assert yaml_service is not None, "test_yaml_service not found" + assert yaml_args_service is not None, "test_yaml_service_with_args not found" + assert yaml_many_args_service is not None, ( + "test_yaml_service_many_args not found" + ) assert custom_service is not None, "custom_test_service not found" assert custom_args_service is not None, "custom_service_with_args not found" assert custom_arrays_service is not None, "custom_service_with_arrays not found" @@ -99,6 +117,44 @@ async def test_api_custom_services( client.execute_service(yaml_service, {}) await asyncio.wait_for(yaml_service_future, timeout=5.0) + # Verify YAML service with args arguments + assert len(yaml_args_service.args) == 2 + yaml_args_types = {arg.name: arg.type for arg in yaml_args_service.args} + assert yaml_args_types["my_int"] == UserServiceArgType.INT + assert yaml_args_types["my_string"] == UserServiceArgType.STRING + + # Test YAML service with arguments + client.execute_service( + yaml_args_service, + { + "my_int": 123, + "my_string": "test_value", + }, + ) + await asyncio.wait_for(yaml_args_future, timeout=5.0) + + # Verify YAML service with many args arguments + assert len(yaml_many_args_service.args) == 4 + yaml_many_args_types = { + arg.name: arg.type for arg in yaml_many_args_service.args + } + assert yaml_many_args_types["arg1"] == UserServiceArgType.INT + assert yaml_many_args_types["arg2"] == UserServiceArgType.FLOAT + assert yaml_many_args_types["arg3"] == UserServiceArgType.BOOL + assert yaml_many_args_types["arg4"] == UserServiceArgType.STRING + + # Test YAML service with many arguments + client.execute_service( + yaml_many_args_service, + { + "arg1": 42, + "arg2": 3.14, + "arg3": True, + "arg4": "hello", + }, + ) + await asyncio.wait_for(yaml_many_args_future, timeout=5.0) + # Test simple CustomAPIDevice service client.execute_service(custom_service, {}) await asyncio.wait_for(custom_service_future, timeout=5.0) From ab5d8f67aee0c631b3fdf7d6cae94aa71fb4b60c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 6 Nov 2025 12:48:02 +1000 Subject: [PATCH 124/461] [core] Add helper functions for clamp_at_... (#10387) --- esphome/core/helpers.h | 16 +++++++++++++++- tests/components/esp32/common.yaml | 13 +++++++++++++ tests/components/esp8266/test.esp8266-ard.yaml | 13 +++++++++++++ tests/components/host/common.yaml | 7 +++++++ tests/components/libretiny/test.bk72xx-ard.yaml | 13 +++++++++++++ tests/components/nrf52/test.nrf52-adafruit.yaml | 10 ++++++++++ tests/components/rp2040/test.rp2040-ard.yaml | 13 +++++++++++++ 7 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests/components/esp32/common.yaml create mode 100644 tests/components/esp8266/test.esp8266-ard.yaml create mode 100644 tests/components/libretiny/test.bk72xx-ard.yaml create mode 100644 tests/components/rp2040/test.rp2040-ard.yaml diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 660874ed1a8..48af7f674a4 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -12,6 +12,7 @@ #include #include #include +#include #include "esphome/core/optional.h" @@ -1169,7 +1170,20 @@ template class RAMAllocator { template using ExternalRAMAllocator = RAMAllocator; -/// @} +/** + * Functions to constrain the range of arithmetic values. + */ + +template T clamp_at_least(T value, T min) { + if (value < min) + return min; + return value; +} +template T clamp_at_most(T value, T max) { + if (value > max) + return max; + return value; +} /// @name Internal functions ///@{ diff --git a/tests/components/esp32/common.yaml b/tests/components/esp32/common.yaml new file mode 100644 index 00000000000..039a2610160 --- /dev/null +++ b/tests/components/esp32/common.yaml @@ -0,0 +1,13 @@ +logger: + level: VERBOSE + +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/esp8266/test.esp8266-ard.yaml b/tests/components/esp8266/test.esp8266-ard.yaml new file mode 100644 index 00000000000..039a2610160 --- /dev/null +++ b/tests/components/esp8266/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +logger: + level: VERBOSE + +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/host/common.yaml b/tests/components/host/common.yaml index 5c329c8245c..d5c8446ae8a 100644 --- a/tests/components/host/common.yaml +++ b/tests/components/host/common.yaml @@ -15,3 +15,10 @@ esphome: static const uint8_t my_addr[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (!mac_address_is_valid(my_addr)) ESP_LOGD("test", "Invalid mac address %X", my_addr[0]); // etc. + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/libretiny/test.bk72xx-ard.yaml b/tests/components/libretiny/test.bk72xx-ard.yaml new file mode 100644 index 00000000000..039a2610160 --- /dev/null +++ b/tests/components/libretiny/test.bk72xx-ard.yaml @@ -0,0 +1,13 @@ +logger: + level: VERBOSE + +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/nrf52/test.nrf52-adafruit.yaml b/tests/components/nrf52/test.nrf52-adafruit.yaml index 3fe80209b62..cf704ecceb2 100644 --- a/tests/components/nrf52/test.nrf52-adafruit.yaml +++ b/tests/components/nrf52/test.nrf52-adafruit.yaml @@ -1,3 +1,13 @@ +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); nrf52: dfu: reset_pin: diff --git a/tests/components/rp2040/test.rp2040-ard.yaml b/tests/components/rp2040/test.rp2040-ard.yaml new file mode 100644 index 00000000000..039a2610160 --- /dev/null +++ b/tests/components/rp2040/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +logger: + level: VERBOSE + +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); From 822eacfd77878067484601ec2fe58edde7fae9e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 20:49:24 -0600 Subject: [PATCH 125/461] [core] Fix wait_until and for_condition timing regression in automation chains (#11716) --- esphome/core/base_automation.h | 82 +++++++------ .../fixtures/wait_until_mid_loop_timing.yaml | 109 +++++++++++++++++ .../test_wait_until_mid_loop_timing.py | 112 ++++++++++++++++++ 3 files changed, 269 insertions(+), 34 deletions(-) create mode 100644 tests/integration/fixtures/wait_until_mid_loop_timing.yaml create mode 100644 tests/integration/test_wait_until_mid_loop_timing.py diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 128a2d4b086..6f392c8959e 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -98,22 +98,28 @@ template class ForCondition : public Condition, public Co TEMPLATABLE_VALUE(uint32_t, time); - void loop() override { this->check_internal(); } - float get_setup_priority() const override { return setup_priority::DATA; } - bool check_internal() { - bool cond = this->condition_->check(); - if (!cond) - this->last_inactive_ = App.get_loop_component_start_time(); - return cond; + void loop() override { + // Safe to use cached time - only called from Application::loop() + this->check_internal_(App.get_loop_component_start_time()); } + float get_setup_priority() const override { return setup_priority::DATA; } + bool check(const Ts &...x) override { - if (!this->check_internal()) + auto now = millis(); + if (!this->check_internal_(now)) return false; - return millis() - this->last_inactive_ >= this->time_.value(x...); + return now - this->last_inactive_ >= this->time_.value(x...); } protected: + bool check_internal_(uint32_t now) { + bool cond = this->condition_->check(); + if (!cond) + this->last_inactive_ = now; + return cond; + } + Condition<> *condition_; uint32_t last_inactive_{0}; }; @@ -424,34 +430,17 @@ template class WaitUntilAction : public Action, public Co auto timeout = this->timeout_value_.optional_value(x...); this->var_queue_.emplace_front(now, timeout, std::make_tuple(x...)); - // Enable loop now that we have work to do - this->enable_loop(); - this->loop(); + // Do immediate check with fresh timestamp + if (this->process_queue_(now)) { + // Only enable loop if we still have pending items + this->enable_loop(); + } } void loop() override { - if (this->num_running_ == 0) - return; - - auto now = App.get_loop_component_start_time(); - - this->var_queue_.remove_if([&](auto &queued) { - auto start = std::get(queued); - auto timeout = std::get>(queued); - auto &var = std::get>(queued); - - auto expired = timeout && (now - start) >= *timeout; - - if (!expired && !this->condition_->check_tuple(var)) { - return false; - } - - this->play_next_tuple_(var); - return true; - }); - - // If queue is now empty, disable loop until next play_complex - if (this->var_queue_.empty()) { + // Safe to use cached time - only called from Application::loop() + if (this->num_running_ > 0 && !this->process_queue_(App.get_loop_component_start_time())) { + // If queue is now empty, disable loop until next play_complex this->disable_loop(); } } @@ -467,6 +456,31 @@ template class WaitUntilAction : public Action, public Co } protected: + // Helper: Process queue, triggering completed items and removing them + // Returns true if queue still has pending items + bool process_queue_(uint32_t now) { + // Process each queued wait_until and remove completed ones + this->var_queue_.remove_if([&](auto &queued) { + auto start = std::get(queued); + auto timeout = std::get>(queued); + auto &var = std::get>(queued); + + // Check if timeout has expired + auto expired = timeout && (now - start) >= *timeout; + + // Keep waiting if not expired and condition not met + if (!expired && !this->condition_->check_tuple(var)) { + return false; + } + + // Condition met or timed out - trigger next action + this->play_next_tuple_(var); + return true; + }); + + return !this->var_queue_.empty(); + } + Condition *condition_; std::forward_list, std::tuple>> var_queue_{}; }; diff --git a/tests/integration/fixtures/wait_until_mid_loop_timing.yaml b/tests/integration/fixtures/wait_until_mid_loop_timing.yaml new file mode 100644 index 00000000000..32f59e81a1a --- /dev/null +++ b/tests/integration/fixtures/wait_until_mid_loop_timing.yaml @@ -0,0 +1,109 @@ +# Test for PR #11676 bug: wait_until timeout when triggered mid-component-loop +# This demonstrates that App.get_loop_component_start_time() is stale when +# wait_until is triggered partway through a component's loop execution + +esphome: + name: wait-mid-loop + +host: + +api: + actions: + - action: test_mid_loop_timeout + then: + - logger.log: "=== Test: wait_until triggered mid-loop should timeout correctly ===" + + # Reset test state + - globals.set: + id: test_complete + value: 'false' + + # Trigger the slow script that will call wait_until mid-execution + - script.execute: slow_script + + # Wait for test to complete (should take ~300ms: 100ms delay + 200ms timeout) + - wait_until: + condition: + lambda: return id(test_complete); + timeout: 2s + + - if: + condition: + lambda: return id(test_complete); + then: + - logger.log: "✓ Test PASSED: wait_until timed out correctly" + else: + - logger.log: "✗ Test FAILED: wait_until did not complete properly" + +logger: + level: DEBUG + +globals: + - id: test_complete + type: bool + restore_value: false + initial_value: 'false' + + - id: test_condition + type: bool + restore_value: false + initial_value: 'false' + + - id: timeout_start_time + type: uint32_t + restore_value: false + initial_value: '0' + + - id: timeout_end_time + type: uint32_t + restore_value: false + initial_value: '0' + +script: + # This script simulates a component that takes time during its execution + # When wait_until is triggered mid-script, the loop_component_start_time + # will be stale (from when the script's component loop started) + - id: slow_script + then: + - logger.log: "Script: Starting, about to do some work..." + + # Simulate component doing work for 100ms + # This represents time spent in a component's loop() before triggering wait_until + - delay: 100ms + + - logger.log: "Script: 100ms elapsed, now starting wait_until with 200ms timeout" + - lambda: |- + // Record when timeout starts + id(timeout_start_time) = millis(); + id(test_condition) = false; + + # At this point: + # - Script component's loop started 100ms ago + # - App.loop_component_start_time_ = time from 100ms ago (stale!) + # - wait_until will capture millis() NOW (fresh) + # - BUG: loop() will use stale loop_component_start_time, causing immediate timeout + + - wait_until: + condition: + lambda: return id(test_condition); + timeout: 200ms + + - lambda: |- + // Record when timeout completes + id(timeout_end_time) = millis(); + uint32_t elapsed = id(timeout_end_time) - id(timeout_start_time); + + ESP_LOGD("TEST", "wait_until completed after %u ms (expected ~200ms)", elapsed); + + // Check if timeout took approximately correct time + // Should be ~200ms, not <50ms (immediate timeout) + if (elapsed >= 150 && elapsed <= 250) { + ESP_LOGD("TEST", "✓ Timeout duration correct: %u ms", elapsed); + id(test_complete) = true; + } else { + ESP_LOGE("TEST", "✗ Timeout duration WRONG: %u ms (expected 150-250ms)", elapsed); + if (elapsed < 50) { + ESP_LOGE("TEST", " → Likely BUG: Immediate timeout due to stale loop_component_start_time"); + } + id(test_complete) = false; + } diff --git a/tests/integration/test_wait_until_mid_loop_timing.py b/tests/integration/test_wait_until_mid_loop_timing.py new file mode 100644 index 00000000000..01cad747aeb --- /dev/null +++ b/tests/integration/test_wait_until_mid_loop_timing.py @@ -0,0 +1,112 @@ +"""Integration test for PR #11676 mid-loop timing bug. + +This test validates that wait_until timeouts work correctly when triggered +mid-component-loop, where App.get_loop_component_start_time() is stale. + +The bug: When wait_until is triggered partway through a component's loop execution +(e.g., from a script or automation), the cached loop_component_start_time_ is stale +relative to when the action was actually triggered. This causes timeout calculations +to underflow and timeout immediately instead of waiting the specified duration. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_mid_loop_timing( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that wait_until timeout works when triggered mid-component-loop. + + This test: + 1. Executes a script that delays 100ms (simulating component work) + 2. Then starts wait_until with 200ms timeout + 3. Verifies timeout takes ~200ms, not <50ms (immediate timeout bug) + """ + loop = asyncio.get_running_loop() + + # Track test results + test_results = { + "timeout_duration": None, + "passed": False, + "failed": False, + "bug_detected": False, + } + + # Patterns for log messages + timeout_duration = re.compile(r"wait_until completed after (\d+) ms") + test_pass = re.compile(r"✓ Timeout duration correct") + test_fail = re.compile(r"✗ Timeout duration WRONG") + bug_pattern = re.compile(r"Likely BUG: Immediate timeout") + test_passed = re.compile(r"✓ Test PASSED") + test_failed = re.compile(r"✗ Test FAILED") + + test_complete = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for test results.""" + # Extract timeout duration + match = timeout_duration.search(line) + if match: + test_results["timeout_duration"] = int(match.group(1)) + + if test_pass.search(line): + test_results["passed"] = True + if test_fail.search(line): + test_results["failed"] = True + if bug_pattern.search(line): + test_results["bug_detected"] = True + + # Final test result + if ( + test_passed.search(line) + or test_failed.search(line) + and not test_complete.done() + ): + test_complete.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get the test service + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_mid_loop_timeout"), None + ) + assert test_service is not None, "test_mid_loop_timeout service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test to complete (100ms delay + 200ms timeout + margins = ~500ms) + await asyncio.wait_for(test_complete, timeout=5.0) + + # Verify results + assert test_results["timeout_duration"] is not None, ( + "Timeout duration not reported" + ) + assert test_results["passed"], ( + f"Test failed: wait_until took {test_results['timeout_duration']}ms, expected ~200ms. " + f"Bug detected: {test_results['bug_detected']}" + ) + assert not test_results["bug_detected"], ( + f"BUG DETECTED: wait_until timed out immediately ({test_results['timeout_duration']}ms) " + "instead of waiting 200ms. This indicates stale loop_component_start_time." + ) + + # Additional validation: timeout should be ~200ms (150-250ms range) + duration = test_results["timeout_duration"] + assert 150 <= duration <= 250, ( + f"Timeout duration {duration}ms outside expected range (150-250ms). " + f"This suggests timing regression from PR #11676." + ) From 74187845b7625efba82b1adf753335eb1713022a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 20:55:26 -0600 Subject: [PATCH 126/461] [select] Convert remaining components to use index-based control() (#11693) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/demo/demo_select.h | 2 +- .../es8388/select/adc_input_mic_select.cpp | 6 ++--- .../es8388/select/adc_input_mic_select.h | 2 +- .../es8388/select/dac_output_select.cpp | 6 ++--- .../es8388/select/dac_output_select.h | 2 +- esphome/components/lvgl/select/lvgl_select.h | 8 +++---- .../select/modbus_select.cpp | 22 +++++++++---------- .../modbus_controller/select/modbus_select.h | 2 +- .../select/existence_boundary_select.cpp | 9 +++----- .../select/existence_boundary_select.h | 2 +- .../select/motion_boundary_select.cpp | 9 +++----- .../select/motion_boundary_select.h | 2 +- .../select/scene_mode_select.cpp | 9 +++----- .../seeed_mr24hpc1/select/scene_mode_select.h | 2 +- .../select/unman_time_select.cpp | 9 +++----- .../seeed_mr24hpc1/select/unman_time_select.h | 2 +- .../select/height_threshold_select.cpp | 9 +++----- .../select/height_threshold_select.h | 2 +- .../select/install_height_select.cpp | 9 +++----- .../select/install_height_select.h | 2 +- .../select/sensitivity_select.cpp | 9 +++----- .../select/sensitivity_select.h | 2 +- .../components/tuya/select/tuya_select.cpp | 22 +++++++------------ esphome/components/tuya/select/tuya_select.h | 2 +- 24 files changed, 61 insertions(+), 90 deletions(-) diff --git a/esphome/components/demo/demo_select.h b/esphome/components/demo/demo_select.h index 1951a684a2d..1a5df13eda7 100644 --- a/esphome/components/demo/demo_select.h +++ b/esphome/components/demo/demo_select.h @@ -8,7 +8,7 @@ namespace demo { class DemoSelect : public select::Select, public Component { protected: - void control(const std::string &value) override { this->publish_state(value); } + void control(size_t index) override { this->publish_state(index); } }; } // namespace demo diff --git a/esphome/components/es8388/select/adc_input_mic_select.cpp b/esphome/components/es8388/select/adc_input_mic_select.cpp index 5fab5b8a92f..2e475342961 100644 --- a/esphome/components/es8388/select/adc_input_mic_select.cpp +++ b/esphome/components/es8388/select/adc_input_mic_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace es8388 { -void ADCInputMicSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_adc_input_mic(static_cast(this->index_of(value).value())); +void ADCInputMicSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_adc_input_mic(static_cast(index)); } } // namespace es8388 diff --git a/esphome/components/es8388/select/adc_input_mic_select.h b/esphome/components/es8388/select/adc_input_mic_select.h index 8d035525ef5..f0fa840d006 100644 --- a/esphome/components/es8388/select/adc_input_mic_select.h +++ b/esphome/components/es8388/select/adc_input_mic_select.h @@ -8,7 +8,7 @@ namespace es8388 { class ADCInputMicSelect : public select::Select, public Parented { protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace es8388 diff --git a/esphome/components/es8388/select/dac_output_select.cpp b/esphome/components/es8388/select/dac_output_select.cpp index 268b5f290cb..9af288a721d 100644 --- a/esphome/components/es8388/select/dac_output_select.cpp +++ b/esphome/components/es8388/select/dac_output_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace es8388 { -void DacOutputSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_dac_output(static_cast(this->index_of(value).value())); +void DacOutputSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_dac_output(static_cast(index)); } } // namespace es8388 diff --git a/esphome/components/es8388/select/dac_output_select.h b/esphome/components/es8388/select/dac_output_select.h index fccae9fc191..40d8a665535 100644 --- a/esphome/components/es8388/select/dac_output_select.h +++ b/esphome/components/es8388/select/dac_output_select.h @@ -8,7 +8,7 @@ namespace es8388 { class DacOutputSelect : public select::Select, public Parented { protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace es8388 diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index d4c9631073a..70bb3e7bcbe 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -41,16 +41,16 @@ class LVGLSelect : public select::Select, public Component { } void publish() { - this->publish_state(this->widget_->get_selected_text()); + auto index = this->widget_->get_selected_index(); + this->publish_state(index); if (this->restore_) { - auto index = this->widget_->get_selected_index(); this->pref_.save(&index); } } protected: - void control(const std::string &value) override { - this->widget_->set_selected_text(value, this->anim_); + void control(size_t index) override { + this->widget_->set_selected_index(index, this->anim_); this->publish(); } void set_options_() { diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 48bf2835f2c..853f4215c35 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -28,8 +28,9 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { if (map_it != this->mapping_.cend()) { size_t idx = std::distance(this->mapping_.cbegin(), map_it); - new_state = std::string(this->option_at(idx)); - ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value); + ESP_LOGV(TAG, "Found option %s for value %lld", this->option_at(idx), value); + this->publish_state(idx); + return; } else { ESP_LOGE(TAG, "No option found for mapping %lld", value); } @@ -40,19 +41,16 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { } } -void ModbusSelect::control(const std::string &value) { - auto idx = this->index_of(value); - if (!idx.has_value()) { - ESP_LOGW(TAG, "Invalid option '%s'", value.c_str()); - return; - } - optional mapval = this->mapping_[idx.value()]; - ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str()); +void ModbusSelect::control(size_t index) { + optional mapval = this->mapping_[index]; + const char *option = this->option_at(index); + ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, option); std::vector data; if (this->write_transform_func_.has_value()) { - auto val = (*this->write_transform_func_)(this, value, *mapval, data); + // Transform func requires string parameter for backward compatibility + auto val = (*this->write_transform_func_)(this, std::string(option), *mapval, data); if (val.has_value()) { mapval = *val; ESP_LOGV(TAG, "write_lambda returned mapping value %lld", *mapval); @@ -85,7 +83,7 @@ void ModbusSelect::control(const std::string &value) { this->parent_->queue_command(write_cmd); if (this->optimistic_) - this->publish_state(value); + this->publish_state(index); } } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h index e6b98aead28..fde441f2bcf 100644 --- a/esphome/components/modbus_controller/select/modbus_select.h +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -38,7 +38,7 @@ class ModbusSelect : public Component, public select::Select, public SensorItem void dump_config() override; void parse_and_publish(const std::vector &data) override; - void control(const std::string &value) override; + void control(size_t index) override; protected: std::vector mapping_{}; diff --git a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp index 03c2ec47458..81543055a48 100644 --- a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp +++ b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp @@ -3,12 +3,9 @@ namespace esphome { namespace seeed_mr24hpc1 { -void ExistenceBoundarySelect::control(const std::string &value) { - this->publish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_existence_boundary(index.value()); - } +void ExistenceBoundarySelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_existence_boundary(index); } } // namespace seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h index ad770a72964..933279dd13e 100644 --- a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h +++ b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h @@ -11,7 +11,7 @@ class ExistenceBoundarySelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_motion_boundary(index.value()); - } +void MotionBoundarySelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_motion_boundary(index); } } // namespace seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h index 9058e3130b7..b0051ae6b15 100644 --- a/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h +++ b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h @@ -11,7 +11,7 @@ class MotionBoundarySelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_scene_mode(index.value()); - } +void SceneModeSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_scene_mode(index); } } // namespace seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h index 95508d49b04..f478ea5b669 100644 --- a/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h +++ b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h @@ -11,7 +11,7 @@ class SceneModeSelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_unman_time(index.value()); - } +void UnmanTimeSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_unman_time(index); } } // namespace seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/unman_time_select.h b/esphome/components/seeed_mr24hpc1/select/unman_time_select.h index 7131988cdaf..a64ff4b8406 100644 --- a/esphome/components/seeed_mr24hpc1/select/unman_time_select.h +++ b/esphome/components/seeed_mr24hpc1/select/unman_time_select.h @@ -11,7 +11,7 @@ class UnmanTimeSelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_height_threshold(index.value()); - } +void HeightThresholdSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_height_threshold(index); } } // namespace seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/height_threshold_select.h b/esphome/components/seeed_mr60fda2/select/height_threshold_select.h index b856dbc89a6..f5707c7a880 100644 --- a/esphome/components/seeed_mr60fda2/select/height_threshold_select.h +++ b/esphome/components/seeed_mr60fda2/select/height_threshold_select.h @@ -11,7 +11,7 @@ class HeightThresholdSelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_install_height(index.value()); - } +void InstallHeightSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_install_height(index); } } // namespace seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/install_height_select.h b/esphome/components/seeed_mr60fda2/select/install_height_select.h index 7430da3493d..470d96c50cd 100644 --- a/esphome/components/seeed_mr60fda2/select/install_height_select.h +++ b/esphome/components/seeed_mr60fda2/select/install_height_select.h @@ -11,7 +11,7 @@ class InstallHeightSelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_sensitivity(index.value()); - } +void SensitivitySelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_sensitivity(index); } } // namespace seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/sensitivity_select.h b/esphome/components/seeed_mr60fda2/select/sensitivity_select.h index d1accc1b5b6..82ed4c5d79b 100644 --- a/esphome/components/seeed_mr60fda2/select/sensitivity_select.h +++ b/esphome/components/seeed_mr60fda2/select/sensitivity_select.h @@ -11,7 +11,7 @@ class SensitivitySelect : public select::Select, public Parentedoptimistic_) - this->publish_state(value); + this->publish_state(index); - auto idx = this->index_of(value); - if (idx.has_value()) { - uint8_t mapping = this->mappings_.at(idx.value()); - ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, value.c_str()); - if (this->is_int_) { - this->parent_->set_integer_datapoint_value(this->select_id_, mapping); - } else { - this->parent_->set_enum_datapoint_value(this->select_id_, mapping); - } - return; + uint8_t mapping = this->mappings_.at(index); + ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, this->option_at(index)); + if (this->is_int_) { + this->parent_->set_integer_datapoint_value(this->select_id_, mapping); + } else { + this->parent_->set_enum_datapoint_value(this->select_id_, mapping); } - - ESP_LOGW(TAG, "Invalid value %s", value.c_str()); } void TuyaSelect::dump_config() { diff --git a/esphome/components/tuya/select/tuya_select.h b/esphome/components/tuya/select/tuya_select.h index 12d7b507d4b..24505c99104 100644 --- a/esphome/components/tuya/select/tuya_select.h +++ b/esphome/components/tuya/select/tuya_select.h @@ -23,7 +23,7 @@ class TuyaSelect : public select::Select, public Component { void set_select_mappings(std::vector mappings) { this->mappings_ = std::move(mappings); } protected: - void control(const std::string &value) override; + void control(size_t index) override; Tuya *parent_; bool optimistic_ = false; From 895d76ca030927210f5736161e19e1d7cfd23a54 Mon Sep 17 00:00:00 2001 From: Szewcson Date: Thu, 6 Nov 2025 04:19:29 +0100 Subject: [PATCH 127/461] [gdk101] Fix fw version reporting (#11029) Signed-off-by: szewcu Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/gdk101/gdk101.cpp | 15 +++++++++------ esphome/components/gdk101/gdk101.h | 7 ++++++- esphome/components/gdk101/sensor.py | 9 ++------- esphome/components/gdk101/text_sensor.py | 23 +++++++++++++++++++++++ tests/components/gdk101/common.yaml | 7 +++++-- 5 files changed, 45 insertions(+), 16 deletions(-) create mode 100644 esphome/components/gdk101/text_sensor.py diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 4c156ab24b8..6c218f03d9c 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -62,7 +62,6 @@ void GDK101Component::dump_config() { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } #ifdef USE_SENSOR - LOG_SENSOR(" ", "Firmware Version", this->fw_version_sensor_); LOG_SENSOR(" ", "Average Radaition Dose per 1 minute", this->rad_1m_sensor_); LOG_SENSOR(" ", "Average Radaition Dose per 10 minutes", this->rad_10m_sensor_); LOG_SENSOR(" ", "Status", this->status_sensor_); @@ -72,6 +71,10 @@ void GDK101Component::dump_config() { #ifdef USE_BINARY_SENSOR LOG_BINARY_SENSOR(" ", "Vibration Status", this->vibration_binary_sensor_); #endif // USE_BINARY_SENSOR + +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR(" ", "Firmware Version", this->fw_version_text_sensor_); +#endif // USE_TEXT_SENSOR } float GDK101Component::get_setup_priority() const { return setup_priority::DATA; } @@ -153,18 +156,18 @@ bool GDK101Component::read_status_(uint8_t *data) { } bool GDK101Component::read_fw_version_(uint8_t *data) { -#ifdef USE_SENSOR - if (this->fw_version_sensor_ != nullptr) { +#ifdef USE_TEXT_SENSOR + if (this->fw_version_text_sensor_ != nullptr) { if (!this->read_bytes(GDK101_REG_READ_FIRMWARE, data, 2)) { ESP_LOGE(TAG, "Updating GDK101 failed!"); return false; } - const float fw_version = data[0] + (data[1] / 10.0f); + const std::string fw_version_str = str_sprintf("%d.%d", data[0], data[1]); - this->fw_version_sensor_->publish_state(fw_version); + this->fw_version_text_sensor_->publish_state(fw_version_str); } -#endif // USE_SENSOR +#endif // USE_TEXT_SENSOR return true; } diff --git a/esphome/components/gdk101/gdk101.h b/esphome/components/gdk101/gdk101.h index 460e72ac89e..f250a42a540 100644 --- a/esphome/components/gdk101/gdk101.h +++ b/esphome/components/gdk101/gdk101.h @@ -8,6 +8,9 @@ #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif // USE_BINARY_SENSOR +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif // USE_TEXT_SENSOR #include "esphome/components/i2c/i2c.h" namespace esphome { @@ -25,12 +28,14 @@ class GDK101Component : public PollingComponent, public i2c::I2CDevice { SUB_SENSOR(rad_1m) SUB_SENSOR(rad_10m) SUB_SENSOR(status) - SUB_SENSOR(fw_version) SUB_SENSOR(measurement_duration) #endif // USE_SENSOR #ifdef USE_BINARY_SENSOR SUB_BINARY_SENSOR(vibration) #endif // USE_BINARY_SENSOR +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(fw_version) +#endif // USE_TEXT_SENSOR public: void setup() override; diff --git a/esphome/components/gdk101/sensor.py b/esphome/components/gdk101/sensor.py index d04e0b8367b..6cf89e0fd4c 100644 --- a/esphome/components/gdk101/sensor.py +++ b/esphome/components/gdk101/sensor.py @@ -40,9 +40,8 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_EMPTY, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_VERSION): sensor.sensor_schema( - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - accuracy_decimals=1, + cv.Optional(CONF_VERSION): cv.invalid( + "The 'version' option has been moved to the `text_sensor` component." ), cv.Optional(CONF_STATUS): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, @@ -71,10 +70,6 @@ async def to_code(config): sens = await sensor.new_sensor(radiation_dose_per_10m) cg.add(hub.set_rad_10m_sensor(sens)) - if version_config := config.get(CONF_VERSION): - sens = await sensor.new_sensor(version_config) - cg.add(hub.set_fw_version_sensor(sens)) - if status_config := config.get(CONF_STATUS): sens = await sensor.new_sensor(status_config) cg.add(hub.set_status_sensor(sens)) diff --git a/esphome/components/gdk101/text_sensor.py b/esphome/components/gdk101/text_sensor.py new file mode 100644 index 00000000000..703e68493af --- /dev/null +++ b/esphome/components/gdk101/text_sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP + +from . import CONF_GDK101_ID, GDK101Component + +DEPENDENCIES = ["gdk101"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_GDK101_ID): cv.use_id(GDK101Component), + cv.Required(CONF_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_GDK101_ID]) + var = await text_sensor.new_text_sensor(config[CONF_VERSION]) + cg.add(hub.set_fw_version_text_sensor(var)) diff --git a/tests/components/gdk101/common.yaml b/tests/components/gdk101/common.yaml index 4eb5586ade5..b9b93d760f3 100644 --- a/tests/components/gdk101/common.yaml +++ b/tests/components/gdk101/common.yaml @@ -11,8 +11,6 @@ sensor: name: Radiation Dose @ 10 min status: name: Status - version: - name: FW Version measurement_duration: name: Measuring Time @@ -21,3 +19,8 @@ binary_sensor: gdk101_id: my_gdk101 vibrations: name: Vibrations + +text_sensor: + - platform: gdk101 + version: + name: FW Version From 70d947fab9cdb6a599d4b84844341a8be19a215e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 21:20:27 -0600 Subject: [PATCH 128/461] [esp32_ble] Store custom GAP device name in flash --- esphome/components/esp32_ble/ble.cpp | 4 ++-- esphome/components/esp32_ble/ble.h | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 8bbb21e3ca2..ce0aa8b2f51 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -257,8 +257,8 @@ bool ESP32BLE::ble_setup_() { #endif std::string name; - if (this->name_.has_value()) { - name = this->name_.value(); + if (this->name_ != nullptr) { + name = this->name_; if (App.is_name_add_mac_suffix_enabled()) { // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 2fb60bb8224..b6b6eb1fbcb 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -112,7 +112,7 @@ class ESP32BLE : public Component { void loop() override; void dump_config() override; float get_setup_priority() const override; - void set_name(const std::string &name) { this->name_ = name; } + void set_name(const char *name) { this->name_ = name; } #ifdef USE_ESP32_BLE_ADVERTISING void advertising_start(); @@ -191,8 +191,8 @@ class ESP32BLE : public Component { esphome::LockFreeQueue ble_events_; esphome::EventPool ble_event_pool_; - // optional (typically 16+ bytes on 32-bit, aligned to 4 bytes) - optional name_; + // Pointer to compile-time string literal or nullptr (4 bytes on 32-bit) + const char *name_{nullptr}; // 4-byte aligned members #ifdef USE_ESP32_BLE_ADVERTISING From 6c09b16b38883c40a1aad10b0bff0e540728e59b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 21:28:17 -0600 Subject: [PATCH 129/461] Revert "[esp32_ble] Store custom GAP device name in flash" This reverts commit 70d947fab9cdb6a599d4b84844341a8be19a215e. --- esphome/components/esp32_ble/ble.cpp | 4 ++-- esphome/components/esp32_ble/ble.h | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index ebf65305229..ef7cb6b055c 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -261,8 +261,8 @@ bool ESP32BLE::ble_setup_() { #endif std::string name; - if (this->name_ != nullptr) { - name = this->name_; + if (this->name_.has_value()) { + name = this->name_.value(); if (App.is_name_add_mac_suffix_enabled()) { // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index e3d4beacdc3..a2c2b00d142 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -112,7 +112,7 @@ class ESP32BLE : public Component { void loop() override; void dump_config() override; float get_setup_priority() const override; - void set_name(const char *name) { this->name_ = name; } + void set_name(const std::string &name) { this->name_ = name; } #ifdef USE_ESP32_BLE_ADVERTISING void advertising_start(); @@ -192,8 +192,8 @@ class ESP32BLE : public Component { esphome::LockFreeQueue ble_events_; esphome::EventPool ble_event_pool_; - // Pointer to compile-time string literal or nullptr (4 bytes on 32-bit) - const char *name_{nullptr}; + // optional (typically 16+ bytes on 32-bit, aligned to 4 bytes) + optional name_; // 4-byte aligned members #ifdef USE_ESP32_BLE_ADVERTISING From 26607713bb1ba0848100384b9ddecd6d4345ba4d Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Wed, 5 Nov 2025 22:57:31 -0500 Subject: [PATCH 130/461] [openthread] add poll period for mtd devices (#11374) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/openthread/__init__.py | 19 ++++++++++++-- esphome/components/openthread/const.py | 1 + esphome/components/openthread/openthread.cpp | 17 ++++++++++++ esphome/components/openthread/openthread.h | 7 +++++ .../components/openthread/openthread_esp.cpp | 26 +++++++++++++++++++ .../openthread/test.esp32-c6-idf.yaml | 4 ++- 6 files changed, 71 insertions(+), 3 deletions(-) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 572ec144d49..e3ad3ed76ce 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -9,7 +9,7 @@ from esphome.components.esp32 import ( from esphome.components.mdns import MDNSComponent, enable_mdns_storage import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS -from esphome.core import CORE +from esphome.core import CORE, TimePeriodMilliseconds import esphome.final_validate as fv from esphome.types import ConfigType @@ -22,6 +22,7 @@ from .const import ( CONF_NETWORK_KEY, CONF_NETWORK_NAME, CONF_PAN_ID, + CONF_POLL_PERIOD, CONF_PSKC, CONF_SRP_ID, CONF_TLV, @@ -89,7 +90,7 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5) - # TODO: Add suport for sleepy end devices + # TODO: Add suport for synchronized sleepy end devices (SSED) add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True) @@ -113,6 +114,17 @@ _CONNECTION_SCHEMA = cv.Schema( def _validate(config: ConfigType) -> ConfigType: if CONF_USE_ADDRESS not in config: config[CONF_USE_ADDRESS] = f"{CORE.name}.local" + device_type = config.get(CONF_DEVICE_TYPE) + poll_period = config.get(CONF_POLL_PERIOD) + if ( + device_type == "FTD" + and poll_period + and poll_period > TimePeriodMilliseconds(milliseconds=0) + ): + raise cv.Invalid( + f"{CONF_POLL_PERIOD} can only be used with {CONF_DEVICE_TYPE}: MTD" + ) + return config @@ -135,6 +147,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FORCE_DATASET): cv.boolean, cv.Optional(CONF_TLV): cv.string_strict, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, + cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds, } ).extend(_CONNECTION_SCHEMA), cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), @@ -167,6 +180,8 @@ async def to_code(config): ot = cg.new_Pvariable(config[CONF_ID]) cg.add(ot.set_use_address(config[CONF_USE_ADDRESS])) await cg.register_component(ot, config) + if (poll_period := config.get(CONF_POLL_PERIOD)) is not None: + cg.add(ot.set_poll_period(poll_period)) srp = cg.new_Pvariable(config[CONF_SRP_ID]) mdns_component = await cg.get_variable(config[CONF_MDNS_ID]) diff --git a/esphome/components/openthread/const.py b/esphome/components/openthread/const.py index 7a6ffb2df4b..f0274a8c9e5 100644 --- a/esphome/components/openthread/const.py +++ b/esphome/components/openthread/const.py @@ -6,6 +6,7 @@ CONF_MESH_LOCAL_PREFIX = "mesh_local_prefix" CONF_NETWORK_NAME = "network_name" CONF_NETWORK_KEY = "network_key" CONF_PAN_ID = "pan_id" +CONF_POLL_PERIOD = "poll_period" CONF_PSKC = "pskc" CONF_SRP_ID = "srp_id" CONF_TLV = "tlv" diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index d7fb1e1d42d..721ab89326c 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -29,6 +29,23 @@ OpenThreadComponent *global_openthread_component = // NOLINT(cppcoreguidelines- OpenThreadComponent::OpenThreadComponent() { global_openthread_component = this; } +void OpenThreadComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Open Thread:"); +#if CONFIG_OPENTHREAD_FTD + ESP_LOGCONFIG(TAG, " Device Type: FTD"); +#elif CONFIG_OPENTHREAD_MTD + ESP_LOGCONFIG(TAG, " Device Type: MTD"); + // TBD: Synchronized Sleepy End Device + if (this->poll_period > 0) { + ESP_LOGCONFIG(TAG, " Device is configured as Sleepy End Device (SED)"); + uint32_t duration = this->poll_period / 1000; + ESP_LOGCONFIG(TAG, " Poll Period: %" PRIu32 "s", duration); + } else { + ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)"); + } +#endif +} + bool OpenThreadComponent::is_connected() { auto lock = InstanceLock::try_acquire(100); if (!lock) { diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 3132e416962..546128b3666 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -22,6 +22,7 @@ class OpenThreadComponent : public Component { public: OpenThreadComponent(); ~OpenThreadComponent(); + void dump_config() override; void setup() override; bool teardown() override; float get_setup_priority() const override { return setup_priority::WIFI; } @@ -35,6 +36,9 @@ class OpenThreadComponent : public Component { const char *get_use_address() const; void set_use_address(const char *use_address); +#if CONFIG_OPENTHREAD_MTD + void set_poll_period(uint32_t poll_period) { this->poll_period = poll_period; } +#endif protected: std::optional get_omr_address_(InstanceLock &lock); @@ -46,6 +50,9 @@ class OpenThreadComponent : public Component { // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. const char *use_address_{""}; +#if CONFIG_OPENTHREAD_MTD + uint32_t poll_period{0}; +#endif }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index b11b7ad34a3..72dc521091c 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -105,6 +105,32 @@ void OpenThreadComponent::ot_main() { esp_cli_custom_command_init(); #endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION + otLinkModeConfig link_mode_config = {0}; +#if CONFIG_OPENTHREAD_FTD + link_mode_config.mRxOnWhenIdle = true; + link_mode_config.mDeviceType = true; + link_mode_config.mNetworkData = true; +#elif CONFIG_OPENTHREAD_MTD + if (this->poll_period > 0) { + if (otLinkSetPollPeriod(esp_openthread_get_instance(), this->poll_period) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); + } + uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); + ESP_LOGD(TAG, "Link Polling Period: %d", link_polling_period); + } + link_mode_config.mRxOnWhenIdle = this->poll_period == 0; + link_mode_config.mDeviceType = false; + link_mode_config.mNetworkData = false; +#endif + + if (otThreadSetLinkMode(esp_openthread_get_instance(), link_mode_config) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set OpenThread linkmode."); + } + link_mode_config = otThreadGetLinkMode(esp_openthread_get_instance()); + ESP_LOGD(TAG, "Link Mode Device Type: %s", link_mode_config.mDeviceType ? "true" : "false"); + ESP_LOGD(TAG, "Link Mode Network Data: %s", link_mode_config.mNetworkData ? "true" : "false"); + ESP_LOGD(TAG, "Link Mode RX On When Idle: %s", link_mode_config.mRxOnWhenIdle ? "true" : "false"); + // Run the main loop #if CONFIG_OPENTHREAD_CLI esp_openthread_cli_create_task(); diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index da5339fb39d..9df63b2f29b 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -2,7 +2,7 @@ network: enable_ipv6: true openthread: - device_type: FTD + device_type: MTD channel: 13 network_name: OpenThread-8f28 network_key: 0xdfd34f0f05cad978ec4e32b0413038ff @@ -11,3 +11,5 @@ openthread: pskc: 0xc23a76e98f1a6483639b1ac1271e2e27 mesh_local_prefix: fd53:145f:ed22:ad81::/64 force_dataset: true + use_address: open-thread-test.local + poll_period: 20sec From 2ddfabe09e4521781d5614bc2a508cee578e8b03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 22:49:13 -0600 Subject: [PATCH 131/461] [core] Deduplicate entity icon and device class logging --- .../binary_sensor/binary_sensor.cpp | 11 ++---- esphome/components/button/button.cpp | 11 ++---- esphome/components/cover/cover.h | 4 +-- esphome/components/datetime/date_entity.h | 4 +-- esphome/components/datetime/datetime_entity.h | 4 +-- esphome/components/datetime/time_entity.h | 4 +-- esphome/components/event/event.h | 8 ++--- esphome/components/lock/lock.h | 4 +-- esphome/components/number/number.cpp | 21 ++++------- esphome/components/select/select.h | 4 +-- esphome/components/sensor/sensor.cpp | 35 ++++++++----------- esphome/components/switch/switch.cpp | 8 ++--- esphome/components/text/text.h | 4 +-- .../components/text_sensor/text_sensor.cpp | 16 +++------ esphome/components/valve/valve.h | 4 +-- esphome/core/entity_base.cpp | 14 ++++++++ esphome/core/entity_base.h | 7 ++++ 17 files changed, 64 insertions(+), 99 deletions(-) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 33b3de6d72b..2fc8d9d01fd 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -9,14 +9,9 @@ static const char *const TAG = "binary_sensor"; // Function implementation of LOG_BINARY_SENSOR macro to reduce code size void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj) { - if (obj == nullptr) { - return; - } - - ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - - if (!obj->get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); + if (obj != nullptr) { + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + obj->log_device_class(tag, prefix); } } diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index c968d310888..3a2624a6bf0 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -8,14 +8,9 @@ static const char *const TAG = "button"; // Function implementation of LOG_BUTTON macro to reduce code size void log_button(const char *tag, const char *prefix, const char *type, Button *obj) { - if (obj == nullptr) { - return; - } - - ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - - if (!obj->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); + if (obj != nullptr) { + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + obj->log_icon(tag, prefix); } } diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index d5db6cfb4f7..3308255f9a4 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -20,9 +20,7 @@ const extern float COVER_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - if (!(obj)->get_device_class_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ - } \ + (obj)->log_device_class(TAG, prefix); \ } class Cover; diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index ba2edb127ad..ba2a64062dc 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -16,9 +16,7 @@ namespace datetime { #define LOG_DATETIME_DATE(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ + (obj)->log_icon(TAG, prefix); \ } class DateCall; diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index 43bff5a1812..9955686d8d7 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -16,9 +16,7 @@ namespace datetime { #define LOG_DATETIME_DATETIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ + (obj)->log_icon(TAG, prefix); \ } class DateTimeCall; diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h index c5cbeb52da1..30f73f3be2e 100644 --- a/esphome/components/datetime/time_entity.h +++ b/esphome/components/datetime/time_entity.h @@ -16,9 +16,7 @@ namespace datetime { #define LOG_DATETIME_TIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ + (obj)->log_icon(TAG, prefix); \ } class TimeCall; diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 2f6267a2006..f2a619eb380 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -12,12 +12,8 @@ namespace event { #define LOG_EVENT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ - if (!(obj)->get_device_class_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ - } \ + (obj)->log_icon(TAG, prefix); \ + (obj)->log_device_class(TAG, prefix); \ } class Event : public EntityBase, public EntityBase_DeviceClass { diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 97375699213..842c4e732bf 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -15,9 +15,7 @@ class Lock; #define LOG_LOCK(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ + (obj)->log_icon(TAG, prefix); \ if ((obj)->traits.get_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index da08faf6558..85e0d41b9c3 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -8,22 +8,15 @@ static const char *const TAG = "number"; // Function implementation of LOG_NUMBER macro to reduce code size void log_number(const char *tag, const char *prefix, const char *type, Number *obj) { - if (obj == nullptr) { - return; - } + if (obj != nullptr) { + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + obj->log_icon(tag, prefix); - ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + if (!obj->traits.get_unit_of_measurement_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement_ref().c_str()); + } - if (!obj->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); - } - - if (!obj->traits.get_unit_of_measurement_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement_ref().c_str()); - } - - if (!obj->traits.get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->traits.get_device_class_ref().c_str()); + obj->traits.log_device_class(tag, prefix); } } diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index f859594cd18..bc3d35fa3d0 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -12,9 +12,7 @@ namespace select { #define LOG_SELECT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ + (obj)->log_icon(TAG, prefix); \ } #define SUB_SELECT(name) \ diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 92da4345b70..a472f9bec6a 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -8,29 +8,22 @@ static const char *const TAG = "sensor"; // Function implementation of LOG_SENSOR macro to reduce code size void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *obj) { - if (obj == nullptr) { - return; - } + if (obj != nullptr) { + ESP_LOGCONFIG(tag, + "%s%s '%s'\n" + "%s State Class: '%s'\n" + "%s Unit of Measurement: '%s'\n" + "%s Accuracy Decimals: %d", + prefix, type, obj->get_name().c_str(), prefix, + LOG_STR_ARG(state_class_to_string(obj->get_state_class())), prefix, + obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals()); - ESP_LOGCONFIG(tag, - "%s%s '%s'\n" - "%s State Class: '%s'\n" - "%s Unit of Measurement: '%s'\n" - "%s Accuracy Decimals: %d", - prefix, type, obj->get_name().c_str(), prefix, - LOG_STR_ARG(state_class_to_string(obj->get_state_class())), prefix, - obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals()); + obj->log_device_class(tag, prefix); + obj->log_icon(tag, prefix); - if (!obj->get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); - } - - if (!obj->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); - } - - if (obj->get_force_update()) { - ESP_LOGV(tag, "%s Force Update: YES", prefix); + if (obj->get_force_update()) { + ESP_LOGV(tag, "%s Force Update: YES", prefix); + } } } diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 02cee91a768..659d15d8833 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -91,18 +91,14 @@ void log_switch(const char *tag, const char *prefix, const char *type, Switch *o LOG_STR_ARG(onoff)); // Add optional fields separately - if (!obj->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); - } + obj->log_icon(tag, prefix); if (obj->assumed_state()) { ESP_LOGCONFIG(tag, "%s Assumed State: YES", prefix); } if (obj->is_inverted()) { ESP_LOGCONFIG(tag, "%s Inverted: YES", prefix); } - if (!obj->get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); - } + obj->log_device_class(tag, prefix); } } diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index 74d08eda8a7..18d5ba3f507 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -12,9 +12,7 @@ namespace text { #define LOG_TEXT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ + (obj)->log_icon(TAG, prefix); \ } /** Base-class for all text inputs. diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 0294d65861c..e5eea3c7af9 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -7,18 +7,10 @@ namespace text_sensor { static const char *const TAG = "text_sensor"; void log_text_sensor(const char *tag, const char *prefix, const char *type, TextSensor *obj) { - if (obj == nullptr) { - return; - } - - ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - - if (!obj->get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); - } - - if (!obj->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); + if (obj != nullptr) { + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + obj->log_device_class(tag, prefix); + obj->log_icon(tag, prefix); } } diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index ab7ff5abe1e..cd41c6d34a2 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -19,9 +19,7 @@ const extern float VALVE_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - if (!(obj)->get_device_class_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ - } \ + (obj)->log_device_class(TAG, prefix); \ } class Valve; diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 4883c72cf13..404297cbfd3 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -45,6 +45,14 @@ void EntityBase::set_icon(const char *icon) { #endif } +void EntityBase::log_icon(const char *tag, const char *prefix) const { +#ifdef USE_ENTITY_ICON + if (!this->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, this->get_icon_ref().c_str()); + } +#endif +} + // Check if the object_id is dynamic (changes with MAC suffix) bool EntityBase::is_object_id_dynamic_() const { return !this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled(); @@ -91,6 +99,12 @@ std::string EntityBase_DeviceClass::get_device_class() { void EntityBase_DeviceClass::set_device_class(const char *device_class) { this->device_class_ = device_class; } +void EntityBase_DeviceClass::log_device_class(const char *tag, const char *prefix) const { + if (!this->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, this->get_device_class_ref().c_str()); + } +} + std::string EntityBase_UnitOfMeasurement::get_unit_of_measurement() { if (this->unit_of_measurement_ == nullptr) return ""; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 6e5362464f0..94436c0d479 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -74,6 +74,9 @@ class EntityBase { #endif } + /// Log entity icon if present (guarded by USE_ENTITY_ICON) + void log_icon(const char *tag, const char *prefix) const; + #ifdef USE_DEVICES // Get/set this entity's device id uint32_t get_device_id() const { @@ -171,6 +174,9 @@ class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) return this->device_class_ == nullptr ? EMPTY_STRING : StringRef(this->device_class_); } + /// Log entity device class if present + void log_device_class(const char *tag, const char *prefix) const; + protected: const char *device_class_{nullptr}; ///< Device class override }; @@ -247,4 +253,5 @@ template class StatefulEntityBase : public EntityBase { CallbackManager previous, optional current)> *full_state_callbacks_{}; CallbackManager *state_callbacks_{}; }; + } // namespace esphome From e17b69c20d413cdb8399c1f355dfdb6ff8e0c5be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 23:46:49 -0600 Subject: [PATCH 132/461] Revert "[core] Deduplicate entity icon and device class logging" This reverts commit 2ddfabe09e4521781d5614bc2a508cee578e8b03. --- .../binary_sensor/binary_sensor.cpp | 11 ++++-- esphome/components/button/button.cpp | 11 ++++-- esphome/components/cover/cover.h | 4 ++- esphome/components/datetime/date_entity.h | 4 ++- esphome/components/datetime/datetime_entity.h | 4 ++- esphome/components/datetime/time_entity.h | 4 ++- esphome/components/event/event.h | 8 +++-- esphome/components/lock/lock.h | 4 ++- esphome/components/number/number.cpp | 21 +++++++---- esphome/components/select/select.h | 4 ++- esphome/components/sensor/sensor.cpp | 35 +++++++++++-------- esphome/components/switch/switch.cpp | 8 +++-- esphome/components/text/text.h | 4 ++- .../components/text_sensor/text_sensor.cpp | 16 ++++++--- esphome/components/valve/valve.h | 4 ++- esphome/core/entity_base.cpp | 14 -------- esphome/core/entity_base.h | 7 ---- 17 files changed, 99 insertions(+), 64 deletions(-) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 2fc8d9d01fd..33b3de6d72b 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -9,9 +9,14 @@ static const char *const TAG = "binary_sensor"; // Function implementation of LOG_BINARY_SENSOR macro to reduce code size void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj) { - if (obj != nullptr) { - ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - obj->log_device_class(tag, prefix); + if (obj == nullptr) { + return; + } + + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + + if (!obj->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); } } diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index 3a2624a6bf0..c968d310888 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -8,9 +8,14 @@ static const char *const TAG = "button"; // Function implementation of LOG_BUTTON macro to reduce code size void log_button(const char *tag, const char *prefix, const char *type, Button *obj) { - if (obj != nullptr) { - ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - obj->log_icon(tag, prefix); + if (obj == nullptr) { + return; + } + + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); } } diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 3308255f9a4..d5db6cfb4f7 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -20,7 +20,9 @@ const extern float COVER_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - (obj)->log_device_class(TAG, prefix); \ + if (!(obj)->get_device_class_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ + } \ } class Cover; diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index ba2a64062dc..ba2edb127ad 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -16,7 +16,9 @@ namespace datetime { #define LOG_DATETIME_DATE(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ + } \ } class DateCall; diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index 9955686d8d7..43bff5a1812 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -16,7 +16,9 @@ namespace datetime { #define LOG_DATETIME_DATETIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ + } \ } class DateTimeCall; diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h index 30f73f3be2e..c5cbeb52da1 100644 --- a/esphome/components/datetime/time_entity.h +++ b/esphome/components/datetime/time_entity.h @@ -16,7 +16,9 @@ namespace datetime { #define LOG_DATETIME_TIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ + } \ } class TimeCall; diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index f2a619eb380..2f6267a2006 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -12,8 +12,12 @@ namespace event { #define LOG_EVENT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ - (obj)->log_device_class(TAG, prefix); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ + } \ + if (!(obj)->get_device_class_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ + } \ } class Event : public EntityBase, public EntityBase_DeviceClass { diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 842c4e732bf..97375699213 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -15,7 +15,9 @@ class Lock; #define LOG_LOCK(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ + } \ if ((obj)->traits.get_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 85e0d41b9c3..da08faf6558 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -8,15 +8,22 @@ static const char *const TAG = "number"; // Function implementation of LOG_NUMBER macro to reduce code size void log_number(const char *tag, const char *prefix, const char *type, Number *obj) { - if (obj != nullptr) { - ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - obj->log_icon(tag, prefix); + if (obj == nullptr) { + return; + } - if (!obj->traits.get_unit_of_measurement_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement_ref().c_str()); - } + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - obj->traits.log_device_class(tag, prefix); + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); + } + + if (!obj->traits.get_unit_of_measurement_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement_ref().c_str()); + } + + if (!obj->traits.get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->traits.get_device_class_ref().c_str()); } } diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index f1e0db6c290..a4dd5a15da0 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -12,7 +12,9 @@ namespace select { #define LOG_SELECT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ + } \ } #define SUB_SELECT(name) \ diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index a472f9bec6a..92da4345b70 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -8,22 +8,29 @@ static const char *const TAG = "sensor"; // Function implementation of LOG_SENSOR macro to reduce code size void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *obj) { - if (obj != nullptr) { - ESP_LOGCONFIG(tag, - "%s%s '%s'\n" - "%s State Class: '%s'\n" - "%s Unit of Measurement: '%s'\n" - "%s Accuracy Decimals: %d", - prefix, type, obj->get_name().c_str(), prefix, - LOG_STR_ARG(state_class_to_string(obj->get_state_class())), prefix, - obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals()); + if (obj == nullptr) { + return; + } - obj->log_device_class(tag, prefix); - obj->log_icon(tag, prefix); + ESP_LOGCONFIG(tag, + "%s%s '%s'\n" + "%s State Class: '%s'\n" + "%s Unit of Measurement: '%s'\n" + "%s Accuracy Decimals: %d", + prefix, type, obj->get_name().c_str(), prefix, + LOG_STR_ARG(state_class_to_string(obj->get_state_class())), prefix, + obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals()); - if (obj->get_force_update()) { - ESP_LOGV(tag, "%s Force Update: YES", prefix); - } + if (!obj->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); + } + + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); + } + + if (obj->get_force_update()) { + ESP_LOGV(tag, "%s Force Update: YES", prefix); } } diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 659d15d8833..02cee91a768 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -91,14 +91,18 @@ void log_switch(const char *tag, const char *prefix, const char *type, Switch *o LOG_STR_ARG(onoff)); // Add optional fields separately - obj->log_icon(tag, prefix); + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); + } if (obj->assumed_state()) { ESP_LOGCONFIG(tag, "%s Assumed State: YES", prefix); } if (obj->is_inverted()) { ESP_LOGCONFIG(tag, "%s Inverted: YES", prefix); } - obj->log_device_class(tag, prefix); + if (!obj->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); + } } } diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index 18d5ba3f507..74d08eda8a7 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -12,7 +12,9 @@ namespace text { #define LOG_TEXT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ + } \ } /** Base-class for all text inputs. diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index e5eea3c7af9..0294d65861c 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -7,10 +7,18 @@ namespace text_sensor { static const char *const TAG = "text_sensor"; void log_text_sensor(const char *tag, const char *prefix, const char *type, TextSensor *obj) { - if (obj != nullptr) { - ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - obj->log_device_class(tag, prefix); - obj->log_icon(tag, prefix); + if (obj == nullptr) { + return; + } + + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + + if (!obj->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); + } + + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); } } diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index cd41c6d34a2..ab7ff5abe1e 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -19,7 +19,9 @@ const extern float VALVE_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - (obj)->log_device_class(TAG, prefix); \ + if (!(obj)->get_device_class_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ + } \ } class Valve; diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 404297cbfd3..4883c72cf13 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -45,14 +45,6 @@ void EntityBase::set_icon(const char *icon) { #endif } -void EntityBase::log_icon(const char *tag, const char *prefix) const { -#ifdef USE_ENTITY_ICON - if (!this->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, this->get_icon_ref().c_str()); - } -#endif -} - // Check if the object_id is dynamic (changes with MAC suffix) bool EntityBase::is_object_id_dynamic_() const { return !this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled(); @@ -99,12 +91,6 @@ std::string EntityBase_DeviceClass::get_device_class() { void EntityBase_DeviceClass::set_device_class(const char *device_class) { this->device_class_ = device_class; } -void EntityBase_DeviceClass::log_device_class(const char *tag, const char *prefix) const { - if (!this->get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, this->get_device_class_ref().c_str()); - } -} - std::string EntityBase_UnitOfMeasurement::get_unit_of_measurement() { if (this->unit_of_measurement_ == nullptr) return ""; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 94436c0d479..6e5362464f0 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -74,9 +74,6 @@ class EntityBase { #endif } - /// Log entity icon if present (guarded by USE_ENTITY_ICON) - void log_icon(const char *tag, const char *prefix) const; - #ifdef USE_DEVICES // Get/set this entity's device id uint32_t get_device_id() const { @@ -174,9 +171,6 @@ class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) return this->device_class_ == nullptr ? EMPTY_STRING : StringRef(this->device_class_); } - /// Log entity device class if present - void log_device_class(const char *tag, const char *prefix) const; - protected: const char *device_class_{nullptr}; ///< Device class override }; @@ -253,5 +247,4 @@ template class StatefulEntityBase : public EntityBase { CallbackManager previous, optional current)> *full_state_callbacks_{}; CallbackManager *state_callbacks_{}; }; - } // namespace esphome From 9168d5e422b06f5cdb9e655feb9ffce9485a1107 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 09:58:03 -0600 Subject: [PATCH 133/461] [socket] Deduplicate IP formatting in LWIP raw TCP implementation --- .../components/socket/lwip_raw_tcp_impl.cpp | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 4dedeffb6a2..e0d93d8e2f1 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -172,16 +172,7 @@ class LWIPRawImpl : public Socket { errno = ECONNRESET; return ""; } - char buffer[50] = {}; - if (IP_IS_V4_VAL(pcb_->remote_ip)) { - inet_ntoa_r(pcb_->remote_ip, buffer, sizeof(buffer)); - } -#if LWIP_IPV6 - else if (IP_IS_V6_VAL(pcb_->remote_ip)) { - inet6_ntoa_r(pcb_->remote_ip, buffer, sizeof(buffer)); - } -#endif - return std::string(buffer); + return this->format_ip_address_(pcb_->remote_ip); } int getsockname(struct sockaddr *name, socklen_t *addrlen) override { if (pcb_ == nullptr) { @@ -199,16 +190,7 @@ class LWIPRawImpl : public Socket { errno = ECONNRESET; return ""; } - char buffer[50] = {}; - if (IP_IS_V4_VAL(pcb_->local_ip)) { - inet_ntoa_r(pcb_->local_ip, buffer, sizeof(buffer)); - } -#if LWIP_IPV6 - else if (IP_IS_V6_VAL(pcb_->local_ip)) { - inet6_ntoa_r(pcb_->local_ip, buffer, sizeof(buffer)); - } -#endif - return std::string(buffer); + return this->format_ip_address_(pcb_->local_ip); } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { if (pcb_ == nullptr) { @@ -499,6 +481,19 @@ class LWIPRawImpl : public Socket { } protected: + std::string format_ip_address_(const ip_addr_t &ip) { + char buffer[50] = {}; + if (IP_IS_V4_VAL(ip)) { + inet_ntoa_r(ip, buffer, sizeof(buffer)); + } +#if LWIP_IPV6 + else if (IP_IS_V6_VAL(ip)) { + inet6_ntoa_r(ip, buffer, sizeof(buffer)); + } +#endif + return std::string(buffer); + } + int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { if (family_ == AF_INET) { if (*addrlen < sizeof(struct sockaddr_in)) { From 20f2d409f77f2bd71fda1b701ddffcdb590ef443 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:06:57 -0600 Subject: [PATCH 134/461] wip --- esphome/components/wifi/wifi_component.cpp | 165 ++++++++++++++------- esphome/components/wifi/wifi_component.h | 18 ++- 2 files changed, 125 insertions(+), 58 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 51b5756f296..1c7b810772d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -111,10 +111,12 @@ void WiFiComponent::start() { #ifdef USE_WIFI_FAST_CONNECT this->trying_loaded_ap_ = this->load_fast_connect_settings_(); if (!this->trying_loaded_ap_) { + // Fast connect failed - start from first configured AP without scan result this->ap_index_ = 0; - this->selected_ap_ = this->sta_[this->ap_index_]; + this->selected_ap_index_ = 0; + this->selected_scan_index_ = -1; } - this->start_connecting(this->selected_ap_, false); + this->start_connecting_to_selected_(false); #else this->start_scanning(); #endif @@ -170,14 +172,12 @@ void WiFiComponent::loop() { if (millis() - this->action_started_ > 5000) { #ifdef USE_WIFI_FAST_CONNECT // NOTE: This check may not make sense here as it could interfere with AP cycling - if (!this->selected_ap_.get_bssid().has_value()) - this->selected_ap_ = this->sta_[0]; - this->start_connecting(this->selected_ap_, false); + this->reset_selected_ap_to_first_if_invalid_(); + this->start_connecting_to_selected_(false); #else if (this->retry_hidden_) { - if (!this->selected_ap_.get_bssid().has_value()) - this->selected_ap_ = this->sta_[0]; - this->start_connecting(this->selected_ap_, false); + this->reset_selected_ap_to_first_if_invalid_(); + this->start_connecting_to_selected_(false); } else { this->start_scanning(); } @@ -336,8 +336,61 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->clear_sta(); this->init_sta(1); this->add_sta(ap); + this->selected_ap_index_ = 0; + this->selected_scan_index_ = -1; +} +void WiFiComponent::clear_sta() { + this->sta_.clear(); + this->selected_ap_index_ = -1; + this->selected_scan_index_ = -1; +} + +WiFiAP WiFiComponent::build_selected_ap_() const { + WiFiAP params; + + if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_ap_index_]; + + // Copy config data + params.set_password(config.get_password()); + params.set_manual_ip(config.get_manual_ip()); + params.set_priority(config.get_priority()); + +#ifdef USE_WIFI_WPA2_EAP + params.set_eap(config.get_eap()); +#endif + + // Use config SSID for hidden networks + if (config.get_hidden()) { + params.set_hidden(true); + params.set_ssid(config.get_ssid()); + } + } + + // Overlay scan result data (if available) + if (this->selected_scan_index_ >= 0 && this->selected_scan_index_ < this->scan_result_.size()) { + const WiFiScanResult &scan = this->scan_result_[this->selected_scan_index_]; + + if (!params.get_hidden()) { + // For visible networks, use scan data to limit connection to exactly this network + // (network selection is done during scan phase). + params.set_ssid(scan.get_ssid()); + params.set_bssid(scan.get_bssid()); + params.set_channel(scan.get_channel()); + } + // For hidden networks, don't use scan BSSID/channel - there might be multiple hidden networks + // and we can't know which one is correct. Rely on probe-req with just SSID. + } + + return params; +} + +WiFiAP WiFiComponent::get_sta() { + if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { + return this->sta_[this->selected_ap_index_]; + } + return WiFiAP{}; } -void WiFiComponent::clear_sta() { this->sta_.clear(); } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 @@ -485,8 +538,11 @@ void WiFiComponent::print_connect_params_() { LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE - if (this->selected_ap_.get_bssid().has_value()) { - ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid())); + if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (config.get_bssid().has_value()) { + ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config.get_bssid())); + } } #endif #ifdef USE_WIFI_11KV_SUPPORT @@ -639,49 +695,23 @@ void WiFiComponent::check_scanning_finished() { return; } - // Build connection params directly into selected_ap_ to avoid extra copy + // Find matching config and set indices for on-demand connection params building const WiFiScanResult &scan_res = this->scan_result_[0]; - WiFiAP &selected = this->selected_ap_; - for (auto &config : this->sta_) { + this->selected_scan_index_ = 0; + + for (size_t i = 0; i < this->sta_.size(); i++) { // search for matching STA config, at least one will match (from checks before) - if (!scan_res.matches(config)) { + if (!scan_res.matches(this->sta_[i])) { continue; } - if (config.get_hidden()) { - // selected network is hidden, we use the data from the config - selected.set_hidden(true); - selected.set_ssid(config.get_ssid()); - // Clear channel and BSSID for hidden networks - there might be multiple hidden networks - // but we can't know which one is the correct one. Rely on probe-req with just SSID. - selected.set_channel(0); - selected.set_bssid(optional{}); - } else { - // selected network is visible, we use the data from the scan - // limit the connect params to only connect to exactly this network - // (network selection is done during scan phase). - selected.set_hidden(false); - selected.set_ssid(scan_res.get_ssid()); - selected.set_channel(scan_res.get_channel()); - selected.set_bssid(scan_res.get_bssid()); - } - // copy manual IP (if set) - selected.set_manual_ip(config.get_manual_ip()); - -#ifdef USE_WIFI_WPA2_EAP - // copy EAP parameters (if set) - selected.set_eap(config.get_eap()); -#endif - - // copy password (if set) - selected.set_password(config.get_password()); - + this->selected_ap_index_ = i; break; } yield(); - this->start_connecting(this->selected_ap_, false); + this->start_connecting_to_selected_(false); } void WiFiComponent::dump_config() { @@ -701,8 +731,11 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGI(TAG, "Connected"); // We won't retry hidden networks unless a reconnect fails more than three times again - if (this->retry_hidden_ && !this->selected_ap_.get_hidden()) - ESP_LOGW(TAG, "Network '%s' should be marked as hidden", this->selected_ap_.get_ssid().c_str()); + if (this->retry_hidden_ && this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (!config.get_hidden()) + ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config.get_ssid().c_str()); + } this->retry_hidden_ = false; this->print_connect_params_(); @@ -772,10 +805,13 @@ void WiFiComponent::check_connecting_finished() { } void WiFiComponent::retry_connect() { - if (this->selected_ap_.get_bssid()) { - auto bssid = *this->selected_ap_.get_bssid(); - float priority = this->get_sta_priority(bssid); - this->set_sta_priority(bssid, priority - 1.0f); + if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (config.get_bssid()) { + auto bssid = *config.get_bssid(); + float priority = this->get_sta_priority(bssid); + this->set_sta_priority(bssid, priority - 1.0f); + } } delay(10); @@ -794,7 +830,8 @@ void WiFiComponent::retry_connect() { this->ap_index_++; } this->num_retried_ = 0; - this->selected_ap_ = this->sta_[this->ap_index_]; + this->selected_ap_index_ = this->ap_index_; + this->selected_scan_index_ = -1; #else if (this->num_retried_ > 5) { // If retry failed for more than 5 times, let's restart STA @@ -813,7 +850,7 @@ void WiFiComponent::retry_connect() { if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { yield(); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; - this->start_connecting(this->selected_ap_, true); + this->start_connecting_to_selected_(true); return; } @@ -856,12 +893,19 @@ bool WiFiComponent::load_fast_connect_settings_() { SavedWifiFastConnectSettings fast_connect_save{}; if (this->fast_connect_pref_.load(&fast_connect_save)) { + // Load BSSID from saved settings bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); + + // Create a temporary scan result with the fast connect BSSID and channel + this->scan_result_.init(1); + WiFiScanResult fast_connect_scan(bssid, "", fast_connect_save.channel, 0, false, false); + this->scan_result_.push_back(fast_connect_scan); + + // Set indices to use the loaded AP config and temporary scan result this->ap_index_ = fast_connect_save.ap_index; - this->selected_ap_ = this->sta_[this->ap_index_]; - this->selected_ap_.set_bssid(bssid); - this->selected_ap_.set_channel(fast_connect_save.channel); + this->selected_ap_index_ = this->ap_index_; + this->selected_scan_index_ = 0; ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; @@ -874,7 +918,16 @@ void WiFiComponent::save_fast_connect_settings_() { bssid_t bssid = wifi_bssid(); uint8_t channel = get_wifi_channel(); - if (bssid != this->selected_ap_.get_bssid() || channel != this->selected_ap_.get_channel()) { + // Check if we need to save (compare with current scan result if available) + bool should_save = true; + if (this->selected_scan_index_ >= 0 && this->selected_scan_index_ < this->scan_result_.size()) { + const WiFiScanResult &scan = this->scan_result_[this->selected_scan_index_]; + if (bssid == scan.get_bssid() && channel == scan.get_channel()) { + should_save = false; + } + } + + if (should_save) { SavedWifiFastConnectSettings fast_connect_save{}; memcpy(fast_connect_save.bssid, bssid.data(), 6); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ac63e0eb0c5..93b5a2d0a2a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -218,7 +218,7 @@ class WiFiComponent : public Component { WiFiComponent(); void set_sta(const WiFiAP &ap); - WiFiAP get_sta() { return this->selected_ap_; } + WiFiAP get_sta(); void init_sta(size_t count); void add_sta(const WiFiAP &ap); void clear_sta(); @@ -337,6 +337,19 @@ class WiFiComponent : public Component { #endif // USE_WIFI_AP void print_connect_params_(); + WiFiAP build_selected_ap_() const; + + void reset_selected_ap_to_first_if_invalid_() { + if (this->selected_ap_index_ < 0 || this->selected_ap_index_ >= this->sta_.size()) { + this->selected_ap_index_ = this->sta_.empty() ? -1 : 0; + this->selected_scan_index_ = -1; + } + } + + void start_connecting_to_selected_(bool two) { + WiFiAP connection_params = this->build_selected_ap_(); + this->start_connecting(connection_params, two); + } void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); @@ -396,7 +409,6 @@ class WiFiComponent : public Component { FixedVector sta_; std::vector sta_priorities_; wifi_scan_vector_t scan_result_; - WiFiAP selected_ap_; WiFiAP ap_; optional output_power_; ESPPreferenceObject pref_; @@ -417,6 +429,8 @@ class WiFiComponent : public Component { #ifdef USE_WIFI_FAST_CONNECT uint8_t ap_index_{0}; #endif + int8_t selected_ap_index_{-1}; + int8_t selected_scan_index_{-1}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ From 378e591e70d5a19ebf0d415d32c5650b426b205b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:11:58 -0600 Subject: [PATCH 135/461] preen --- esphome/components/wifi/wifi_component.cpp | 15 ++++++--------- esphome/components/wifi/wifi_component.h | 3 --- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 1c7b810772d..2ede813289a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -112,7 +112,6 @@ void WiFiComponent::start() { this->trying_loaded_ap_ = this->load_fast_connect_settings_(); if (!this->trying_loaded_ap_) { // Fast connect failed - start from first configured AP without scan result - this->ap_index_ = 0; this->selected_ap_index_ = 0; this->selected_scan_index_ = -1; } @@ -820,17 +819,16 @@ void WiFiComponent::retry_connect() { #ifdef USE_WIFI_FAST_CONNECT if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; - this->ap_index_ = 0; // Retry from the first configured AP - } else if (this->ap_index_ >= this->sta_.size() - 1) { + this->selected_ap_index_ = 0; // Retry from the first configured AP + } else if (this->selected_ap_index_ >= static_cast(this->sta_.size()) - 1) { ESP_LOGW(TAG, "No more APs to try"); - this->ap_index_ = 0; + this->selected_ap_index_ = 0; this->restart_adapter(); } else { // Try next AP - this->ap_index_++; + this->selected_ap_index_++; } this->num_retried_ = 0; - this->selected_ap_index_ = this->ap_index_; this->selected_scan_index_ = -1; #else if (this->num_retried_ > 5) { @@ -903,8 +901,7 @@ bool WiFiComponent::load_fast_connect_settings_() { this->scan_result_.push_back(fast_connect_scan); // Set indices to use the loaded AP config and temporary scan result - this->ap_index_ = fast_connect_save.ap_index; - this->selected_ap_index_ = this->ap_index_; + this->selected_ap_index_ = fast_connect_save.ap_index; this->selected_scan_index_ = 0; ESP_LOGD(TAG, "Loaded fast_connect settings"); @@ -932,7 +929,7 @@ void WiFiComponent::save_fast_connect_settings_() { memcpy(fast_connect_save.bssid, bssid.data(), 6); fast_connect_save.channel = channel; - fast_connect_save.ap_index = this->ap_index_; + fast_connect_save.ap_index = this->selected_ap_index_ >= 0 ? this->selected_ap_index_ : 0; this->fast_connect_pref_.save(&fast_connect_save); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 93b5a2d0a2a..ca500a52462 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -426,9 +426,6 @@ class WiFiComponent : public Component { WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; uint8_t num_retried_{0}; -#ifdef USE_WIFI_FAST_CONNECT - uint8_t ap_index_{0}; -#endif int8_t selected_ap_index_{-1}; int8_t selected_scan_index_{-1}; #if USE_NETWORK_IPV6 From 13ee597ce04daf215f72a1687cdfc13cea21f4f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:17:17 -0600 Subject: [PATCH 136/461] preen --- esphome/components/wifi/wifi_component.cpp | 20 ++++++++------------ esphome/components/wifi/wifi_component.h | 4 ++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2ede813289a..96dc421e7f3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -915,26 +915,22 @@ void WiFiComponent::save_fast_connect_settings_() { bssid_t bssid = wifi_bssid(); uint8_t channel = get_wifi_channel(); - // Check if we need to save (compare with current scan result if available) - bool should_save = true; + // Skip save if settings haven't changed (compare with current scan result if available) if (this->selected_scan_index_ >= 0 && this->selected_scan_index_ < this->scan_result_.size()) { const WiFiScanResult &scan = this->scan_result_[this->selected_scan_index_]; if (bssid == scan.get_bssid() && channel == scan.get_channel()) { - should_save = false; + return; // No change, nothing to save } } - if (should_save) { - SavedWifiFastConnectSettings fast_connect_save{}; + SavedWifiFastConnectSettings fast_connect_save{}; + memcpy(fast_connect_save.bssid, bssid.data(), 6); + fast_connect_save.channel = channel; + fast_connect_save.ap_index = this->selected_ap_index_ >= 0 ? this->selected_ap_index_ : 0; - memcpy(fast_connect_save.bssid, bssid.data(), 6); - fast_connect_save.channel = channel; - fast_connect_save.ap_index = this->selected_ap_index_ >= 0 ? this->selected_ap_index_ : 0; + this->fast_connect_pref_.save(&fast_connect_save); - this->fast_connect_pref_.save(&fast_connect_save); - - ESP_LOGD(TAG, "Saved fast_connect settings"); - } + ESP_LOGD(TAG, "Saved fast_connect settings"); } #endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ca500a52462..155c1f419e8 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -426,7 +426,11 @@ class WiFiComponent : public Component { WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; uint8_t num_retried_{0}; + // Index into sta_ array for the currently selected AP configuration (-1 = none selected) + // Used to access password, manual_ip, priority, EAP settings, and hidden flag int8_t selected_ap_index_{-1}; + // Index into scan_result_ array for the currently selected scan result (-1 = no scan data) + // Used to access scanned SSID, BSSID, and channel. Also used for fast connect (synthetic scan result) int8_t selected_scan_index_{-1}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; From 34317ab343afbf7cea1bf173dbde9ca06943aba1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:21:44 -0600 Subject: [PATCH 137/461] preen --- esphome/components/wifi/wifi_component.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 96dc421e7f3..4f0f37fed0c 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -363,6 +363,11 @@ WiFiAP WiFiComponent::build_selected_ap_() const { if (config.get_hidden()) { params.set_hidden(true); params.set_ssid(config.get_ssid()); + // Clear BSSID and channel for hidden networks - there might be multiple hidden networks + // and we can't know which one is correct. Rely on probe-req with just SSID. + // Leaving channel empty triggers ALL_CHANNEL_SCAN instead of FAST_SCAN. + params.set_bssid(optional{}); + params.set_channel(optional{}); } } @@ -377,8 +382,6 @@ WiFiAP WiFiComponent::build_selected_ap_() const { params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); } - // For hidden networks, don't use scan BSSID/channel - there might be multiple hidden networks - // and we can't know which one is correct. Rely on probe-req with just SSID. } return params; From 4500006aab9a97d15621b9f80236dbbd12c937b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:32:06 -0600 Subject: [PATCH 138/461] preen --- esphome/components/wifi/wifi_component.cpp | 40 +++++++++++----------- esphome/components/wifi/wifi_component.h | 6 ++-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 4f0f37fed0c..976b76b1959 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -112,7 +112,7 @@ void WiFiComponent::start() { this->trying_loaded_ap_ = this->load_fast_connect_settings_(); if (!this->trying_loaded_ap_) { // Fast connect failed - start from first configured AP without scan result - this->selected_ap_index_ = 0; + this->selected_sta_index_ = 0; this->selected_scan_index_ = -1; } this->start_connecting_to_selected_(false); @@ -335,20 +335,20 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->clear_sta(); this->init_sta(1); this->add_sta(ap); - this->selected_ap_index_ = 0; + this->selected_sta_index_ = 0; this->selected_scan_index_ = -1; } void WiFiComponent::clear_sta() { this->sta_.clear(); - this->selected_ap_index_ = -1; + this->selected_sta_index_ = -1; this->selected_scan_index_ = -1; } WiFiAP WiFiComponent::build_selected_ap_() const { WiFiAP params; - if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_sta_index_]; // Copy config data params.set_password(config.get_password()); @@ -388,8 +388,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { } WiFiAP WiFiComponent::get_sta() { - if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { - return this->sta_[this->selected_ap_index_]; + if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + return this->sta_[this->selected_sta_index_]; } return WiFiAP{}; } @@ -540,8 +540,8 @@ void WiFiComponent::print_connect_params_() { LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE - if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_sta_index_]; if (config.get_bssid().has_value()) { ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config.get_bssid())); } @@ -707,7 +707,7 @@ void WiFiComponent::check_scanning_finished() { continue; } - this->selected_ap_index_ = i; + this->selected_sta_index_ = i; break; } @@ -733,8 +733,8 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGI(TAG, "Connected"); // We won't retry hidden networks unless a reconnect fails more than three times again - if (this->retry_hidden_ && this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (this->retry_hidden_ && this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_sta_index_]; if (!config.get_hidden()) ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config.get_ssid().c_str()); } @@ -807,8 +807,8 @@ void WiFiComponent::check_connecting_finished() { } void WiFiComponent::retry_connect() { - if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_sta_index_]; if (config.get_bssid()) { auto bssid = *config.get_bssid(); float priority = this->get_sta_priority(bssid); @@ -822,14 +822,14 @@ void WiFiComponent::retry_connect() { #ifdef USE_WIFI_FAST_CONNECT if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; - this->selected_ap_index_ = 0; // Retry from the first configured AP - } else if (this->selected_ap_index_ >= static_cast(this->sta_.size()) - 1) { + this->selected_sta_index_ = 0; // Retry from the first configured AP + } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { ESP_LOGW(TAG, "No more APs to try"); - this->selected_ap_index_ = 0; + this->selected_sta_index_ = 0; this->restart_adapter(); } else { // Try next AP - this->selected_ap_index_++; + this->selected_sta_index_++; } this->num_retried_ = 0; this->selected_scan_index_ = -1; @@ -904,7 +904,7 @@ bool WiFiComponent::load_fast_connect_settings_() { this->scan_result_.push_back(fast_connect_scan); // Set indices to use the loaded AP config and temporary scan result - this->selected_ap_index_ = fast_connect_save.ap_index; + this->selected_sta_index_ = fast_connect_save.ap_index; this->selected_scan_index_ = 0; ESP_LOGD(TAG, "Loaded fast_connect settings"); @@ -929,7 +929,7 @@ void WiFiComponent::save_fast_connect_settings_() { SavedWifiFastConnectSettings fast_connect_save{}; memcpy(fast_connect_save.bssid, bssid.data(), 6); fast_connect_save.channel = channel; - fast_connect_save.ap_index = this->selected_ap_index_ >= 0 ? this->selected_ap_index_ : 0; + fast_connect_save.ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0; this->fast_connect_pref_.save(&fast_connect_save); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 155c1f419e8..d787e35a639 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -340,8 +340,8 @@ class WiFiComponent : public Component { WiFiAP build_selected_ap_() const; void reset_selected_ap_to_first_if_invalid_() { - if (this->selected_ap_index_ < 0 || this->selected_ap_index_ >= this->sta_.size()) { - this->selected_ap_index_ = this->sta_.empty() ? -1 : 0; + if (this->selected_sta_index_ < 0 || this->selected_sta_index_ >= this->sta_.size()) { + this->selected_sta_index_ = this->sta_.empty() ? -1 : 0; this->selected_scan_index_ = -1; } } @@ -428,7 +428,7 @@ class WiFiComponent : public Component { uint8_t num_retried_{0}; // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag - int8_t selected_ap_index_{-1}; + int8_t selected_sta_index_{-1}; // Index into scan_result_ array for the currently selected scan result (-1 = no scan data) // Used to access scanned SSID, BSSID, and channel. Also used for fast connect (synthetic scan result) int8_t selected_scan_index_{-1}; From 670d85090c6113e94aca14994d969cfe18bcdae3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:34:15 -0600 Subject: [PATCH 139/461] preen --- esphome/components/wifi/wifi_component.cpp | 43 ++++++++++------------ esphome/components/wifi/wifi_component.h | 7 ++++ 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 976b76b1959..d5489b06410 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -347,22 +347,20 @@ void WiFiComponent::clear_sta() { WiFiAP WiFiComponent::build_selected_ap_() const { WiFiAP params; - if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_sta_index_]; - + if (const WiFiAP *config = this->get_selected_sta_()) { // Copy config data - params.set_password(config.get_password()); - params.set_manual_ip(config.get_manual_ip()); - params.set_priority(config.get_priority()); + params.set_password(config->get_password()); + params.set_manual_ip(config->get_manual_ip()); + params.set_priority(config->get_priority()); #ifdef USE_WIFI_WPA2_EAP - params.set_eap(config.get_eap()); + params.set_eap(config->get_eap()); #endif // Use config SSID for hidden networks - if (config.get_hidden()) { + if (config->get_hidden()) { params.set_hidden(true); - params.set_ssid(config.get_ssid()); + params.set_ssid(config->get_ssid()); // Clear BSSID and channel for hidden networks - there might be multiple hidden networks // and we can't know which one is correct. Rely on probe-req with just SSID. // Leaving channel empty triggers ALL_CHANNEL_SCAN instead of FAST_SCAN. @@ -388,8 +386,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { } WiFiAP WiFiComponent::get_sta() { - if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { - return this->sta_[this->selected_sta_index_]; + if (const WiFiAP *config = this->get_selected_sta_()) { + return *config; } return WiFiAP{}; } @@ -540,10 +538,9 @@ void WiFiComponent::print_connect_params_() { LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE - if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_sta_index_]; - if (config.get_bssid().has_value()) { - ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config.get_bssid())); + if (const WiFiAP *config = this->get_selected_sta_()) { + if (config->get_bssid().has_value()) { + ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config->get_bssid())); } } #endif @@ -733,10 +730,11 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGI(TAG, "Connected"); // We won't retry hidden networks unless a reconnect fails more than three times again - if (this->retry_hidden_ && this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_sta_index_]; - if (!config.get_hidden()) - ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config.get_ssid().c_str()); + if (this->retry_hidden_) { + if (const WiFiAP *config = this->get_selected_sta_()) { + if (!config->get_hidden()) + ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); + } } this->retry_hidden_ = false; @@ -807,10 +805,9 @@ void WiFiComponent::check_connecting_finished() { } void WiFiComponent::retry_connect() { - if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_sta_index_]; - if (config.get_bssid()) { - auto bssid = *config.get_bssid(); + if (const WiFiAP *config = this->get_selected_sta_()) { + if (config->get_bssid()) { + auto bssid = *config->get_bssid(); float priority = this->get_sta_priority(bssid); this->set_sta_priority(bssid, priority - 1.0f); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d787e35a639..6ccb4fbff25 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -339,6 +339,13 @@ class WiFiComponent : public Component { void print_connect_params_(); WiFiAP build_selected_ap_() const; + const WiFiAP *get_selected_sta_() const { + if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + return &this->sta_[this->selected_sta_index_]; + } + return nullptr; + } + void reset_selected_ap_to_first_if_invalid_() { if (this->selected_sta_index_ < 0 || this->selected_sta_index_ >= this->sta_.size()) { this->selected_sta_index_ = this->sta_.empty() ? -1 : 0; From e7e2df5c6d9b1c05fc611318dc4b0900051882c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:35:57 -0600 Subject: [PATCH 140/461] preen --- esphome/components/wifi/wifi_component.cpp | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d5489b06410..7fe683ef0ef 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -538,10 +538,8 @@ void WiFiComponent::print_connect_params_() { LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE - if (const WiFiAP *config = this->get_selected_sta_()) { - if (config->get_bssid().has_value()) { - ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config->get_bssid())); - } + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) { + ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config->get_bssid())); } #endif #ifdef USE_WIFI_11KV_SUPPORT @@ -731,9 +729,8 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGI(TAG, "Connected"); // We won't retry hidden networks unless a reconnect fails more than three times again if (this->retry_hidden_) { - if (const WiFiAP *config = this->get_selected_sta_()) { - if (!config->get_hidden()) - ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); + if (const WiFiAP *config = this->get_selected_sta_(); config && !config->get_hidden()) { + ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); } } this->retry_hidden_ = false; @@ -805,12 +802,10 @@ void WiFiComponent::check_connecting_finished() { } void WiFiComponent::retry_connect() { - if (const WiFiAP *config = this->get_selected_sta_()) { - if (config->get_bssid()) { - auto bssid = *config->get_bssid(); - float priority = this->get_sta_priority(bssid); - this->set_sta_priority(bssid, priority - 1.0f); - } + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) { + auto bssid = *config->get_bssid(); + float priority = this->get_sta_priority(bssid); + this->set_sta_priority(bssid, priority - 1.0f); } delay(10); From 7d4b3ff3a6ad5c731d9978b9fbffb12d83c53eef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:36:52 -0600 Subject: [PATCH 141/461] preen --- esphome/components/wifi/wifi_component.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7fe683ef0ef..d2a44ce8da8 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -728,10 +728,8 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGI(TAG, "Connected"); // We won't retry hidden networks unless a reconnect fails more than three times again - if (this->retry_hidden_) { - if (const WiFiAP *config = this->get_selected_sta_(); config && !config->get_hidden()) { - ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); - } + if (const WiFiAP *config = this->get_selected_sta_(); this->retry_hidden_ && config && !config->get_hidden()) { + ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); } this->retry_hidden_ = false; From bfca9cb6c245102dfb24bd35ec6d16e332ffa318 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:37:08 -0600 Subject: [PATCH 142/461] preen --- esphome/components/wifi/wifi_component.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d2a44ce8da8..db99c78329b 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -386,10 +386,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { } WiFiAP WiFiComponent::get_sta() { - if (const WiFiAP *config = this->get_selected_sta_()) { - return *config; - } - return WiFiAP{}; + const WiFiAP *config = this->get_selected_sta_(); + return config ? *config : WiFiAP{}; } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination From 03fd2eef2f5466f5c1de665a8f940439b2ad2abb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:46:59 -0600 Subject: [PATCH 143/461] preen --- esphome/components/wifi/wifi_component.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index db99c78329b..109af317ecf 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -751,16 +751,17 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; +#ifdef USE_WIFI_FAST_CONNECT + this->save_fast_connect_settings_(); +#endif + // Free scan results memory unless a component needs them if (!this->keep_scan_results_) { this->scan_result_.clear(); this->scan_result_.shrink_to_fit(); + this->selected_scan_index_ = -1; // Invalidate index since scan results are gone } -#ifdef USE_WIFI_FAST_CONNECT - this->save_fast_connect_settings_(); -#endif - return; } From 27fb72a1d3be3974d1dcdaa708b825e4cc661235 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:47:58 -0600 Subject: [PATCH 144/461] preen --- esphome/components/wifi/wifi_component.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 109af317ecf..660be761a0e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -348,7 +348,7 @@ WiFiAP WiFiComponent::build_selected_ap_() const { WiFiAP params; if (const WiFiAP *config = this->get_selected_sta_()) { - // Copy config data + // Copy config data (password, manual IP, priority, EAP) params.set_password(config->get_password()); params.set_manual_ip(config->get_manual_ip()); params.set_priority(config->get_priority()); @@ -357,12 +357,12 @@ WiFiAP WiFiComponent::build_selected_ap_() const { params.set_eap(config->get_eap()); #endif - // Use config SSID for hidden networks + // Selected network is hidden, we use the data from the config if (config->get_hidden()) { params.set_hidden(true); params.set_ssid(config->get_ssid()); // Clear BSSID and channel for hidden networks - there might be multiple hidden networks - // and we can't know which one is correct. Rely on probe-req with just SSID. + // but we can't know which one is the correct one. Rely on probe-req with just SSID. // Leaving channel empty triggers ALL_CHANNEL_SCAN instead of FAST_SCAN. params.set_bssid(optional{}); params.set_channel(optional{}); @@ -374,7 +374,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { const WiFiScanResult &scan = this->scan_result_[this->selected_scan_index_]; if (!params.get_hidden()) { - // For visible networks, use scan data to limit connection to exactly this network + // Selected network is visible, we use the data from the scan. + // Limit the connect params to only connect to exactly this network // (network selection is done during scan phase). params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); From 083f41c43f8d7438f421dc3fd3987823ad8be589 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:54:09 -0600 Subject: [PATCH 145/461] preen --- esphome/components/wifi/wifi_component.cpp | 20 +++++++------------- esphome/components/wifi/wifi_component.h | 4 ---- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 660be761a0e..a7a3fb89495 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -113,7 +113,6 @@ void WiFiComponent::start() { if (!this->trying_loaded_ap_) { // Fast connect failed - start from first configured AP without scan result this->selected_sta_index_ = 0; - this->selected_scan_index_ = -1; } this->start_connecting_to_selected_(false); #else @@ -336,12 +335,10 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->init_sta(1); this->add_sta(ap); this->selected_sta_index_ = 0; - this->selected_scan_index_ = -1; } void WiFiComponent::clear_sta() { this->sta_.clear(); this->selected_sta_index_ = -1; - this->selected_scan_index_ = -1; } WiFiAP WiFiComponent::build_selected_ap_() const { @@ -370,8 +367,9 @@ WiFiAP WiFiComponent::build_selected_ap_() const { } // Overlay scan result data (if available) - if (this->selected_scan_index_ >= 0 && this->selected_scan_index_ < this->scan_result_.size()) { - const WiFiScanResult &scan = this->scan_result_[this->selected_scan_index_]; + // Scan results are sorted, so index 0 is always the best network + if (!this->scan_result_.empty()) { + const WiFiScanResult &scan = this->scan_result_[0]; if (!params.get_hidden()) { // Selected network is visible, we use the data from the scan. @@ -691,9 +689,8 @@ void WiFiComponent::check_scanning_finished() { return; } - // Find matching config and set indices for on-demand connection params building + // Find matching config for on-demand connection params building const WiFiScanResult &scan_res = this->scan_result_[0]; - this->selected_scan_index_ = 0; for (size_t i = 0; i < this->sta_.size(); i++) { // search for matching STA config, at least one will match (from checks before) @@ -760,7 +757,6 @@ void WiFiComponent::check_connecting_finished() { if (!this->keep_scan_results_) { this->scan_result_.clear(); this->scan_result_.shrink_to_fit(); - this->selected_scan_index_ = -1; // Invalidate index since scan results are gone } return; @@ -822,7 +818,6 @@ void WiFiComponent::retry_connect() { this->selected_sta_index_++; } this->num_retried_ = 0; - this->selected_scan_index_ = -1; #else if (this->num_retried_ > 5) { // If retry failed for more than 5 times, let's restart STA @@ -893,9 +888,8 @@ bool WiFiComponent::load_fast_connect_settings_() { WiFiScanResult fast_connect_scan(bssid, "", fast_connect_save.channel, 0, false, false); this->scan_result_.push_back(fast_connect_scan); - // Set indices to use the loaded AP config and temporary scan result + // Set index to use the loaded AP config with temporary scan result this->selected_sta_index_ = fast_connect_save.ap_index; - this->selected_scan_index_ = 0; ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; @@ -909,8 +903,8 @@ void WiFiComponent::save_fast_connect_settings_() { uint8_t channel = get_wifi_channel(); // Skip save if settings haven't changed (compare with current scan result if available) - if (this->selected_scan_index_ >= 0 && this->selected_scan_index_ < this->scan_result_.size()) { - const WiFiScanResult &scan = this->scan_result_[this->selected_scan_index_]; + if (!this->scan_result_.empty()) { + const WiFiScanResult &scan = this->scan_result_[0]; if (bssid == scan.get_bssid() && channel == scan.get_channel()) { return; // No change, nothing to save } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 6ccb4fbff25..ae8dafe14b1 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -349,7 +349,6 @@ class WiFiComponent : public Component { void reset_selected_ap_to_first_if_invalid_() { if (this->selected_sta_index_ < 0 || this->selected_sta_index_ >= this->sta_.size()) { this->selected_sta_index_ = this->sta_.empty() ? -1 : 0; - this->selected_scan_index_ = -1; } } @@ -436,9 +435,6 @@ class WiFiComponent : public Component { // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag int8_t selected_sta_index_{-1}; - // Index into scan_result_ array for the currently selected scan result (-1 = no scan data) - // Used to access scanned SSID, BSSID, and channel. Also used for fast connect (synthetic scan result) - int8_t selected_scan_index_{-1}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ From 2c110a9e7e259f9d75625a1c7a6ce661d9c0e390 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:57:23 -0600 Subject: [PATCH 146/461] preen --- esphome/components/wifi/wifi_component.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a7a3fb89495..76797f4a7da 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -683,15 +683,15 @@ void WiFiComponent::check_scanning_finished() { log_scan_result(res); } - if (!this->scan_result_[0].get_matches()) { + // Find matching config for on-demand connection params building + const WiFiScanResult &scan_res = this->scan_result_[0]; + + if (!scan_res.get_matches()) { ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); return; } - // Find matching config for on-demand connection params building - const WiFiScanResult &scan_res = this->scan_result_[0]; - for (size_t i = 0; i < this->sta_.size(); i++) { // search for matching STA config, at least one will match (from checks before) if (!scan_res.matches(this->sta_[i])) { From 60d6144574308b578394d0786de81286d6f11af1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 12:02:18 -0600 Subject: [PATCH 147/461] preen --- esphome/components/wifi/wifi_component.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 76797f4a7da..ee4a27e4d44 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -367,7 +367,11 @@ WiFiAP WiFiComponent::build_selected_ap_() const { } // Overlay scan result data (if available) - // Scan results are sorted, so index 0 is always the best network + // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: + // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) + // - It then finds which sta_[i] config matches scan_result_[0] + // - Sets selected_sta_index_ = i to record that matching config + // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] if (!this->scan_result_.empty()) { const WiFiScanResult &scan = this->scan_result_[0]; From 25ef0043d24906d84a952fa10433adfd2fc0bda9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 12:07:42 -0600 Subject: [PATCH 148/461] preen --- esphome/components/wifi/wifi_component.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index ee4a27e4d44..d2db78ae006 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -111,7 +111,9 @@ void WiFiComponent::start() { #ifdef USE_WIFI_FAST_CONNECT this->trying_loaded_ap_ = this->load_fast_connect_settings_(); if (!this->trying_loaded_ap_) { - // Fast connect failed - start from first configured AP without scan result + // FAST CONNECT FALLBACK: No saved settings available + // Set selected_sta_index_ to first config without any scan result + // build_selected_ap_() will use config data only (no SSID/BSSID/channel from scan) this->selected_sta_index_ = 0; } this->start_connecting_to_selected_(false); @@ -688,6 +690,10 @@ void WiFiComponent::check_scanning_finished() { } // Find matching config for on-demand connection params building + // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ + // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config + // matches that network and record it in selected_sta_index_. This keeps the two indices + // synchronized so build_selected_ap_() can safely use both to build connection parameters. const WiFiScanResult &scan_res = this->scan_result_[0]; if (!scan_res.get_matches()) { @@ -702,7 +708,7 @@ void WiFiComponent::check_scanning_finished() { continue; } - this->selected_sta_index_ = i; + this->selected_sta_index_ = i; // Links scan_result_[0] with sta_[i] break; } @@ -887,12 +893,15 @@ bool WiFiComponent::load_fast_connect_settings_() { bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); + // FAST CONNECT SUCCESS: Restore saved settings without scanning // Create a temporary scan result with the fast connect BSSID and channel this->scan_result_.init(1); WiFiScanResult fast_connect_scan(bssid, "", fast_connect_save.channel, 0, false, false); this->scan_result_.push_back(fast_connect_scan); - // Set index to use the loaded AP config with temporary scan result + // SYNCHRONIZATION: Link scan_result_[0] (temporary) with sta_[saved_index] + // Unlike wifi_scan_done() which sorts then finds the match, here we know exactly + // which config was used before and create a matching temporary scan result this->selected_sta_index_ = fast_connect_save.ap_index; ESP_LOGD(TAG, "Loaded fast_connect settings"); From 5543acf3ab7b1a7dfe553466c18624726ce56a72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 12:10:18 -0600 Subject: [PATCH 149/461] preen --- esphome/components/wifi/wifi_component.cpp | 28 ++++------------------ esphome/components/wifi/wifi_component.h | 28 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d2db78ae006..e3f2e50d208 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -112,8 +112,7 @@ void WiFiComponent::start() { this->trying_loaded_ap_ = this->load_fast_connect_settings_(); if (!this->trying_loaded_ap_) { // FAST CONNECT FALLBACK: No saved settings available - // Set selected_sta_index_ to first config without any scan result - // build_selected_ap_() will use config data only (no SSID/BSSID/channel from scan) + // Use first config without any scan result (config data only, no SSID/BSSID/channel) this->selected_sta_index_ = 0; } this->start_connecting_to_selected_(false); @@ -689,29 +688,16 @@ void WiFiComponent::check_scanning_finished() { log_scan_result(res); } - // Find matching config for on-demand connection params building // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config // matches that network and record it in selected_sta_index_. This keeps the two indices // synchronized so build_selected_ap_() can safely use both to build connection parameters. - const WiFiScanResult &scan_res = this->scan_result_[0]; - - if (!scan_res.get_matches()) { + if (!this->sync_selected_sta_to_best_scan_result_()) { ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); return; } - for (size_t i = 0; i < this->sta_.size(); i++) { - // search for matching STA config, at least one will match (from checks before) - if (!scan_res.matches(this->sta_[i])) { - continue; - } - - this->selected_sta_index_ = i; // Links scan_result_[0] with sta_[i] - break; - } - yield(); this->start_connecting_to_selected_(false); @@ -894,15 +880,11 @@ bool WiFiComponent::load_fast_connect_settings_() { std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); // FAST CONNECT SUCCESS: Restore saved settings without scanning - // Create a temporary scan result with the fast connect BSSID and channel - this->scan_result_.init(1); - WiFiScanResult fast_connect_scan(bssid, "", fast_connect_save.channel, 0, false, false); - this->scan_result_.push_back(fast_connect_scan); - - // SYNCHRONIZATION: Link scan_result_[0] (temporary) with sta_[saved_index] + // SYNCHRONIZATION: Link temporary scan result with sta_[saved_index] // Unlike wifi_scan_done() which sorts then finds the match, here we know exactly // which config was used before and create a matching temporary scan result - this->selected_sta_index_ = fast_connect_save.ap_index; + WiFiScanResult fast_connect_scan(bssid, "", fast_connect_save.channel, 0, false, false); + this->set_selected_sta_with_scan_(fast_connect_save.ap_index, fast_connect_scan); ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ae8dafe14b1..a89c1fb4304 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -352,6 +352,34 @@ class WiFiComponent : public Component { } } + // SYNCHRONIZATION HELPERS: Encapsulate the relationship between selected_sta_index_ and scan_result_ + + // Set selected sta with a temporary scan result (fast connect path) + void set_selected_sta_with_scan_(int8_t sta_index, const WiFiScanResult &scan) { + this->scan_result_.init(1); + this->scan_result_.push_back(scan); + this->selected_sta_index_ = sta_index; + } + + // Find which sta_[i] matches scan_result_[0] and set selected_sta_index_ (scan done path) + // Returns true if match found, false otherwise + bool sync_selected_sta_to_best_scan_result_() { + if (this->scan_result_.empty()) + return false; + + const WiFiScanResult &scan_res = this->scan_result_[0]; + if (!scan_res.get_matches()) + return false; + + for (size_t i = 0; i < this->sta_.size(); i++) { + if (scan_res.matches(this->sta_[i])) { + this->selected_sta_index_ = i; // Links scan_result_[0] with sta_[i] + return true; + } + } + return false; + } + void start_connecting_to_selected_(bool two) { WiFiAP connection_params = this->build_selected_ap_(); this->start_connecting(connection_params, two); From d38703c18ac6e3eeb94999bd0b62d15a931cfd2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 12:31:14 -0600 Subject: [PATCH 150/461] [wifi] Refactor AP selection with synchronization helpers --- esphome/components/wifi/wifi_component.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index a89c1fb4304..b230ef38bbf 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -340,14 +340,14 @@ class WiFiComponent : public Component { WiFiAP build_selected_ap_() const; const WiFiAP *get_selected_sta_() const { - if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + if (this->selected_sta_index_ >= 0 && static_cast(this->selected_sta_index_) < this->sta_.size()) { return &this->sta_[this->selected_sta_index_]; } return nullptr; } void reset_selected_ap_to_first_if_invalid_() { - if (this->selected_sta_index_ < 0 || this->selected_sta_index_ >= this->sta_.size()) { + if (this->selected_sta_index_ < 0 || static_cast(this->selected_sta_index_) >= this->sta_.size()) { this->selected_sta_index_ = this->sta_.empty() ? -1 : 0; } } @@ -373,7 +373,7 @@ class WiFiComponent : public Component { for (size_t i = 0; i < this->sta_.size(); i++) { if (scan_res.matches(this->sta_[i])) { - this->selected_sta_index_ = i; // Links scan_result_[0] with sta_[i] + this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] return true; } } From 37620e61f948ea6b406a4e5c78885f0f4e7b0145 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 12:36:35 -0600 Subject: [PATCH 151/461] fast connect fixes --- esphome/components/wifi/wifi_component.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e3f2e50d208..d6715dbf40a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -875,6 +875,12 @@ bool WiFiComponent::load_fast_connect_settings_() { SavedWifiFastConnectSettings fast_connect_save{}; if (this->fast_connect_pref_.load(&fast_connect_save)) { + // Validate saved AP index + if (fast_connect_save.ap_index >= this->sta_.size()) { + ESP_LOGW(TAG, "Saved AP index out of bounds"); + return false; + } + // Load BSSID from saved settings bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); @@ -883,7 +889,9 @@ bool WiFiComponent::load_fast_connect_settings_() { // SYNCHRONIZATION: Link temporary scan result with sta_[saved_index] // Unlike wifi_scan_done() which sorts then finds the match, here we know exactly // which config was used before and create a matching temporary scan result - WiFiScanResult fast_connect_scan(bssid, "", fast_connect_save.channel, 0, false, false); + // Use SSID from config for the temporary scan result + const std::string &ssid = this->sta_[fast_connect_save.ap_index].get_ssid(); + WiFiScanResult fast_connect_scan(bssid, ssid, fast_connect_save.channel, 0, false, false); this->set_selected_sta_with_scan_(fast_connect_save.ap_index, fast_connect_scan); ESP_LOGD(TAG, "Loaded fast_connect settings"); From 6d958a6640e755db8cdf8c15aba5072ab6170eab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 12:58:03 -0600 Subject: [PATCH 152/461] fixes for no fast connect yet --- esphome/components/wifi/wifi_component.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d6715dbf40a..105f2e4cb94 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -112,7 +112,7 @@ void WiFiComponent::start() { this->trying_loaded_ap_ = this->load_fast_connect_settings_(); if (!this->trying_loaded_ap_) { // FAST CONNECT FALLBACK: No saved settings available - // Use first config without any scan result (config data only, no SSID/BSSID/channel) + // Use first config (will use SSID from config since scan_result_ is empty) this->selected_sta_index_ = 0; } this->start_connecting_to_selected_(false); @@ -355,15 +355,20 @@ WiFiAP WiFiComponent::build_selected_ap_() const { params.set_eap(config->get_eap()); #endif - // Selected network is hidden, we use the data from the config + // Set network parameters from config + // These will be used as-is for hidden networks, or overridden by scan for visible networks + params.set_ssid(config->get_ssid()); + if (config->get_hidden()) { params.set_hidden(true); - params.set_ssid(config->get_ssid()); - // Clear BSSID and channel for hidden networks - there might be multiple hidden networks - // but we can't know which one is the correct one. Rely on probe-req with just SSID. - // Leaving channel empty triggers ALL_CHANNEL_SCAN instead of FAST_SCAN. + // For hidden networks, clear BSSID and channel even if set in config + // There might be multiple hidden networks with same SSID but we can't know which is correct + // Rely on probe-req with just SSID. Leaving channel empty triggers ALL_CHANNEL_SCAN. params.set_bssid(optional{}); params.set_channel(optional{}); + } else { + params.set_bssid(config->get_bssid()); + params.set_channel(config->get_channel()); } } @@ -377,7 +382,7 @@ WiFiAP WiFiComponent::build_selected_ap_() const { const WiFiScanResult &scan = this->scan_result_[0]; if (!params.get_hidden()) { - // Selected network is visible, we use the data from the scan. + // Selected network is visible, override with data from the scan. // Limit the connect params to only connect to exactly this network // (network selection is done during scan phase). params.set_ssid(scan.get_ssid()); From 57a88e82118f9d22a3bee55bbc1e839d2a0c7222 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 13:11:06 -0600 Subject: [PATCH 153/461] fixes for no fast connect yet --- esphome/components/wifi/wifi_component.cpp | 26 +++++++++++++++++----- esphome/components/wifi/wifi_component.h | 1 + 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 105f2e4cb94..1c043e221e7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -170,9 +170,14 @@ void WiFiComponent::loop() { this->status_set_warning(LOG_STR("waiting to reconnect")); if (millis() - this->action_started_ > 5000) { #ifdef USE_WIFI_FAST_CONNECT - // NOTE: This check may not make sense here as it could interfere with AP cycling - this->reset_selected_ap_to_first_if_invalid_(); - this->start_connecting_to_selected_(false); + if (this->fast_connect_exhausted_) { + // All APs tried, fall back to scanning + this->start_scanning(); + } else { + // NOTE: This check may not make sense here as it could interfere with AP cycling + this->reset_selected_ap_to_first_if_invalid_(); + this->start_connecting_to_selected_(false); + } #else if (this->retry_hidden_) { this->reset_selected_ap_to_first_if_invalid_(); @@ -703,6 +708,11 @@ void WiFiComponent::check_scanning_finished() { return; } +#ifdef USE_WIFI_FAST_CONNECT + // Scan found a network, reset exhausted flag to allow fast connect to work next time + this->fast_connect_exhausted_ = false; +#endif + yield(); this->start_connecting_to_selected_(false); @@ -751,6 +761,7 @@ void WiFiComponent::check_connecting_finished() { this->num_retried_ = 0; #ifdef USE_WIFI_FAST_CONNECT + this->fast_connect_exhausted_ = false; // Reset on successful connection this->save_fast_connect_settings_(); #endif @@ -810,15 +821,18 @@ void WiFiComponent::retry_connect() { if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP + this->num_retried_ = 0; } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { - ESP_LOGW(TAG, "No more APs to try"); - this->selected_sta_index_ = 0; + // Exhausted all configured APs, fall back to full scan + ESP_LOGW(TAG, "No more APs to try, starting scan"); + this->fast_connect_exhausted_ = true; this->restart_adapter(); + return; } else { // Try next AP this->selected_sta_index_++; + this->num_retried_ = 0; } - this->num_retried_ = 0; #else if (this->num_retried_ > 5) { // If retry failed for more than 5 times, let's restart STA diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index b230ef38bbf..2e7a0728b43 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -470,6 +470,7 @@ class WiFiComponent : public Component { // Group all boolean values together #ifdef USE_WIFI_FAST_CONNECT bool trying_loaded_ap_{false}; + bool fast_connect_exhausted_{false}; // All APs tried, fall back to scan #endif bool retry_hidden_{false}; bool has_ap_{false}; From 047773e62f748f228cef6b8463f309f4089a5f56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 13:32:51 -0600 Subject: [PATCH 154/461] fixes for no fast connect yet --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 1c043e221e7..f774f6b64aa 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -895,7 +895,7 @@ bool WiFiComponent::load_fast_connect_settings_() { if (this->fast_connect_pref_.load(&fast_connect_save)) { // Validate saved AP index - if (fast_connect_save.ap_index >= this->sta_.size()) { + if (static_cast(fast_connect_save.ap_index) >= this->sta_.size()) { ESP_LOGW(TAG, "Saved AP index out of bounds"); return false; } From 703b1cf314c18dada13f349983d4613db588deb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 13:51:47 -0600 Subject: [PATCH 155/461] cleanup --- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/components/wifi/wifi_component.h | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index f774f6b64aa..da07f0eab73 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -821,7 +821,7 @@ void WiFiComponent::retry_connect() { if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP - this->num_retried_ = 0; + this->reset_for_next_ap_attempt_(); } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { // Exhausted all configured APs, fall back to full scan ESP_LOGW(TAG, "No more APs to try, starting scan"); @@ -831,7 +831,7 @@ void WiFiComponent::retry_connect() { } else { // Try next AP this->selected_sta_index_++; - this->num_retried_ = 0; + this->reset_for_next_ap_attempt_(); } #else if (this->num_retried_ > 5) { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 2e7a0728b43..739585c6fca 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -385,6 +385,13 @@ class WiFiComponent : public Component { this->start_connecting(connection_params, two); } + // Reset state for next fast connect AP attempt + // Clears old scan data so the new AP is tried with config only (SSID without specific BSSID/channel) + void reset_for_next_ap_attempt_() { + this->num_retried_ = 0; + this->scan_result_.clear(); + } + void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); From 4439b45fba239f37aa99035e3fa7e72239fe78e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 13:52:32 -0600 Subject: [PATCH 156/461] cleanup --- esphome/components/wifi/wifi_component.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 739585c6fca..284c2eb12fb 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -385,12 +385,14 @@ class WiFiComponent : public Component { this->start_connecting(connection_params, two); } +#ifdef USE_WIFI_FAST_CONNECT // Reset state for next fast connect AP attempt // Clears old scan data so the new AP is tried with config only (SSID without specific BSSID/channel) void reset_for_next_ap_attempt_() { this->num_retried_ = 0; this->scan_result_.clear(); } +#endif void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); From ef680933dceb66625de3f58a90b2aedf25c9aa34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 13:55:08 -0600 Subject: [PATCH 157/461] cleanup --- esphome/components/wifi/wifi_component.cpp | 20 +++++++++----------- esphome/components/wifi/wifi_component.h | 3 +++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index da07f0eab73..1373a53079d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -174,7 +174,8 @@ void WiFiComponent::loop() { // All APs tried, fall back to scanning this->start_scanning(); } else { - // NOTE: This check may not make sense here as it could interfere with AP cycling + // Safety check: Ensure selected_sta_index_ is valid before retrying + // (should already be set by retry_connect(), but check for robustness) this->reset_selected_ap_to_first_if_invalid_(); this->start_connecting_to_selected_(false); } @@ -708,11 +709,6 @@ void WiFiComponent::check_scanning_finished() { return; } -#ifdef USE_WIFI_FAST_CONNECT - // Scan found a network, reset exhausted flag to allow fast connect to work next time - this->fast_connect_exhausted_ = false; -#endif - yield(); this->start_connecting_to_selected_(false); @@ -923,11 +919,13 @@ bool WiFiComponent::load_fast_connect_settings_() { void WiFiComponent::save_fast_connect_settings_() { bssid_t bssid = wifi_bssid(); uint8_t channel = get_wifi_channel(); + int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0; - // Skip save if settings haven't changed (compare with current scan result if available) - if (!this->scan_result_.empty()) { - const WiFiScanResult &scan = this->scan_result_[0]; - if (bssid == scan.get_bssid() && channel == scan.get_channel()) { + // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear) + SavedWifiFastConnectSettings previous_save{}; + if (this->fast_connect_pref_.load(&previous_save)) { + if (memcmp(previous_save.bssid, bssid.data(), 6) == 0 && previous_save.channel == channel && + previous_save.ap_index == ap_index) { return; // No change, nothing to save } } @@ -935,7 +933,7 @@ void WiFiComponent::save_fast_connect_settings_() { SavedWifiFastConnectSettings fast_connect_save{}; memcpy(fast_connect_save.bssid, bssid.data(), 6); fast_connect_save.channel = channel; - fast_connect_save.ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0; + fast_connect_save.ap_index = ap_index; this->fast_connect_pref_.save(&fast_connect_save); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 284c2eb12fb..f81597fba5c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -218,6 +218,8 @@ class WiFiComponent : public Component { WiFiComponent(); void set_sta(const WiFiAP &ap); + // Returns a copy of the currently selected AP configuration + // Note: This copies the 88-byte WiFiAP. Only used by WiFiConfigureAction for state save/restore. WiFiAP get_sta(); void init_sta(size_t count); void add_sta(const WiFiAP &ap); @@ -471,6 +473,7 @@ class WiFiComponent : public Component { uint8_t num_retried_{0}; // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag + // int8_t limits to 127 APs which should be sufficient for all practical use cases int8_t selected_sta_index_{-1}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; From 190668c25f6391cb2b06b23510b6e4cb70df3604 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 13:58:39 -0600 Subject: [PATCH 158/461] fix false positive logging --- esphome/components/wifi/wifi_component.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 1373a53079d..65a94918972 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -730,8 +730,10 @@ void WiFiComponent::check_connecting_finished() { } ESP_LOGI(TAG, "Connected"); - // We won't retry hidden networks unless a reconnect fails more than three times again - if (const WiFiAP *config = this->get_selected_sta_(); this->retry_hidden_ && config && !config->get_hidden()) { + // Warn if we had to retry with hidden network mode for a network that's not marked hidden + // Only warn if we actually connected without scan data (SSID only), not if scan succeeded on retry + if (const WiFiAP *config = this->get_selected_sta_(); + this->retry_hidden_ && config && !config->get_hidden() && this->scan_result_.empty()) { ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); } this->retry_hidden_ = false; From d8b419b60cbf3dcf24a20d4671f21aa3d6f5f809 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 14:42:42 -0600 Subject: [PATCH 159/461] not needed --- esphome/components/wifi/wifi_component.cpp | 23 ++++++++-------------- esphome/components/wifi/wifi_component.h | 1 - 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 65a94918972..880b6960ea4 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -170,15 +170,10 @@ void WiFiComponent::loop() { this->status_set_warning(LOG_STR("waiting to reconnect")); if (millis() - this->action_started_ > 5000) { #ifdef USE_WIFI_FAST_CONNECT - if (this->fast_connect_exhausted_) { - // All APs tried, fall back to scanning - this->start_scanning(); - } else { - // Safety check: Ensure selected_sta_index_ is valid before retrying - // (should already be set by retry_connect(), but check for robustness) - this->reset_selected_ap_to_first_if_invalid_(); - this->start_connecting_to_selected_(false); - } + // Safety check: Ensure selected_sta_index_ is valid before retrying + // (should already be set by retry_connect(), but check for robustness) + this->reset_selected_ap_to_first_if_invalid_(); + this->start_connecting_to_selected_(false); #else if (this->retry_hidden_) { this->reset_selected_ap_to_first_if_invalid_(); @@ -759,7 +754,6 @@ void WiFiComponent::check_connecting_finished() { this->num_retried_ = 0; #ifdef USE_WIFI_FAST_CONNECT - this->fast_connect_exhausted_ = false; // Reset on successful connection this->save_fast_connect_settings_(); #endif @@ -821,11 +815,10 @@ void WiFiComponent::retry_connect() { this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { - // Exhausted all configured APs, fall back to full scan - ESP_LOGW(TAG, "No more APs to try, starting scan"); - this->fast_connect_exhausted_ = true; - this->restart_adapter(); - return; + // Exhausted all configured APs, cycle back to first + // Each AP is tried with SSID only (no BSSID/channel) which triggers ESP-IDF internal scanning + this->selected_sta_index_ = 0; + this->reset_for_next_ap_attempt_(); } else { // Try next AP this->selected_sta_index_++; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f81597fba5c..9c7f9a4e7ab 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -482,7 +482,6 @@ class WiFiComponent : public Component { // Group all boolean values together #ifdef USE_WIFI_FAST_CONNECT bool trying_loaded_ap_{false}; - bool fast_connect_exhausted_{false}; // All APs tried, fall back to scan #endif bool retry_hidden_{false}; bool has_ap_{false}; From ebda7dace0852b56b0aa02e6a909437c5bc352be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 14:46:11 -0600 Subject: [PATCH 160/461] not needed --- esphome/components/wifi/wifi_component.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 880b6960ea4..12c2dda1284 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -815,10 +815,13 @@ void WiFiComponent::retry_connect() { this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { - // Exhausted all configured APs, cycle back to first + // Exhausted all configured APs, restart adapter and cycle back to first + // Restart clears any stuck WiFi driver state // Each AP is tried with SSID only (no BSSID/channel) which triggers ESP-IDF internal scanning + ESP_LOGW(TAG, "No more APs to try"); this->selected_sta_index_ = 0; this->reset_for_next_ap_attempt_(); + this->restart_adapter(); } else { // Try next AP this->selected_sta_index_++; From e4a56c6bc9b498fe7830705b16c2619cc2ebf86b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 14:49:52 -0600 Subject: [PATCH 161/461] not needed --- esphome/components/wifi/wifi_component.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 12c2dda1284..59e6faa17ca 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -817,7 +817,8 @@ void WiFiComponent::retry_connect() { } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state - // Each AP is tried with SSID only (no BSSID/channel) which triggers ESP-IDF internal scanning + // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) + // Typically SSID only, which triggers ESP-IDF internal scanning ESP_LOGW(TAG, "No more APs to try"); this->selected_sta_index_ = 0; this->reset_for_next_ap_attempt_(); From 1fb233e22fe890cc2132d5d9df3bd51090462836 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:07:47 -0600 Subject: [PATCH 162/461] fix false positive logging --- esphome/components/wifi/wifi_component.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 59e6faa17ca..c9b72a0c726 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -922,11 +922,9 @@ void WiFiComponent::save_fast_connect_settings_() { // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear) SavedWifiFastConnectSettings previous_save{}; - if (this->fast_connect_pref_.load(&previous_save)) { - if (memcmp(previous_save.bssid, bssid.data(), 6) == 0 && previous_save.channel == channel && - previous_save.ap_index == ap_index) { - return; // No change, nothing to save - } + if (this->fast_connect_pref_.load(&previous_save) && memcmp(previous_save.bssid, bssid.data(), 6) == 0 && + previous_save.channel == channel && previous_save.ap_index == ap_index) { + return; // No change, nothing to save } SavedWifiFastConnectSettings fast_connect_save{}; From 0044c51474cf6a07bb8db9874ef9dc8fc404cb4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:37:01 -0600 Subject: [PATCH 163/461] defensive to make bot happy --- esphome/components/wifi/wifi_component.cpp | 11 ++++++++--- esphome/components/wifi/wifi_component.h | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c9b72a0c726..2bd86949e33 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -364,7 +364,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { params.set_hidden(true); // For hidden networks, clear BSSID and channel even if set in config // There might be multiple hidden networks with same SSID but we can't know which is correct - // Rely on probe-req with just SSID. Leaving channel empty triggers ALL_CHANNEL_SCAN. + // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. + // Note: Scan data is never used for hidden networks (see check below at line ~390) params.set_bssid(optional{}); params.set_channel(optional{}); } else { @@ -810,7 +811,11 @@ void WiFiComponent::retry_connect() { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && (this->num_retried_ > 3 || this->error_from_callback_)) { #ifdef USE_WIFI_FAST_CONNECT - if (this->trying_loaded_ap_) { + if (this->sta_.empty()) { + // No configured networks - shouldn't happen in fast_connect mode, but handle defensively + ESP_LOGW(TAG, "No configured networks available"); + this->restart_adapter(); + } else if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); @@ -890,7 +895,7 @@ bool WiFiComponent::load_fast_connect_settings_() { if (this->fast_connect_pref_.load(&fast_connect_save)) { // Validate saved AP index - if (static_cast(fast_connect_save.ap_index) >= this->sta_.size()) { + if (fast_connect_save.ap_index < 0 || static_cast(fast_connect_save.ap_index) >= this->sta_.size()) { ESP_LOGW(TAG, "Saved AP index out of bounds"); return false; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 9c7f9a4e7ab..defce1d0ac2 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -375,6 +375,10 @@ class WiFiComponent : public Component { for (size_t i = 0; i < this->sta_.size(); i++) { if (scan_res.matches(this->sta_[i])) { + if (i > std::numeric_limits::max()) { + ESP_LOGE(TAG, "Matched AP index %zu exceeds int8_t range", i); + continue; + } this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] return true; } From b74f415509f7e50a792f603538eba1969e29cc2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:37:58 -0600 Subject: [PATCH 164/461] defensive to make bot happy --- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/components/wifi/wifi_component.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2bd86949e33..a6f44ac758f 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -813,7 +813,7 @@ void WiFiComponent::retry_connect() { #ifdef USE_WIFI_FAST_CONNECT if (this->sta_.empty()) { // No configured networks - shouldn't happen in fast_connect mode, but handle defensively - ESP_LOGW(TAG, "No configured networks available"); + ESP_LOGW(TAG, "No configured networks"); this->restart_adapter(); } else if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; @@ -896,7 +896,7 @@ bool WiFiComponent::load_fast_connect_settings_() { if (this->fast_connect_pref_.load(&fast_connect_save)) { // Validate saved AP index if (fast_connect_save.ap_index < 0 || static_cast(fast_connect_save.ap_index) >= this->sta_.size()) { - ESP_LOGW(TAG, "Saved AP index out of bounds"); + ESP_LOGW(TAG, "AP index out of bounds"); return false; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index defce1d0ac2..3dc31880a8a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -376,7 +376,7 @@ class WiFiComponent : public Component { for (size_t i = 0; i < this->sta_.size(); i++) { if (scan_res.matches(this->sta_[i])) { if (i > std::numeric_limits::max()) { - ESP_LOGE(TAG, "Matched AP index %zu exceeds int8_t range", i); + ESP_LOGE(TAG, "AP index %zu too large", i); continue; } this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] From b366bc8dbad8ece73fd6681b57793b50a8c952f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:40:32 -0600 Subject: [PATCH 165/461] defensive to make bot happy --- esphome/components/wifi/wifi_component.cpp | 21 +++++++++++++++++++++ esphome/components/wifi/wifi_component.h | 21 +-------------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a6f44ac758f..6e4611d7651 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -396,6 +396,27 @@ WiFiAP WiFiComponent::build_selected_ap_() const { return params; } +bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { + if (this->scan_result_.empty()) + return false; + + const WiFiScanResult &scan_res = this->scan_result_[0]; + if (!scan_res.get_matches()) + return false; + + for (size_t i = 0; i < this->sta_.size(); i++) { + if (scan_res.matches(this->sta_[i])) { + if (i > std::numeric_limits::max()) { + ESP_LOGE(TAG, "AP index %zu too large", i); + return false; + } + this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] + return true; + } + } + return false; +} + WiFiAP WiFiComponent::get_sta() { const WiFiAP *config = this->get_selected_sta_(); return config ? *config : WiFiAP{}; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 3dc31880a8a..6cdab1660fa 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -365,26 +365,7 @@ class WiFiComponent : public Component { // Find which sta_[i] matches scan_result_[0] and set selected_sta_index_ (scan done path) // Returns true if match found, false otherwise - bool sync_selected_sta_to_best_scan_result_() { - if (this->scan_result_.empty()) - return false; - - const WiFiScanResult &scan_res = this->scan_result_[0]; - if (!scan_res.get_matches()) - return false; - - for (size_t i = 0; i < this->sta_.size(); i++) { - if (scan_res.matches(this->sta_[i])) { - if (i > std::numeric_limits::max()) { - ESP_LOGE(TAG, "AP index %zu too large", i); - continue; - } - this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] - return true; - } - } - return false; - } + bool sync_selected_sta_to_best_scan_result_(); void start_connecting_to_selected_(bool two) { WiFiAP connection_params = this->build_selected_ap_(); From 0eafe5259f00be1860943ed8234810f94b4872e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:44:48 -0600 Subject: [PATCH 166/461] defensive to make bot happy --- esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 6e4611d7651..131fd122d72 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -365,7 +365,7 @@ WiFiAP WiFiComponent::build_selected_ap_() const { // For hidden networks, clear BSSID and channel even if set in config // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - // Note: Scan data is never used for hidden networks (see check below at line ~390) + // Note: Scan data is never used for hidden networks (see !params.get_hidden() check below) params.set_bssid(optional{}); params.set_channel(optional{}); } else { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 6cdab1660fa..04adb57b44d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -358,7 +358,12 @@ class WiFiComponent : public Component { // Set selected sta with a temporary scan result (fast connect path) void set_selected_sta_with_scan_(int8_t sta_index, const WiFiScanResult &scan) { +#ifdef USE_RP2040 + this->scan_result_.clear(); + this->scan_result_.reserve(1); +#else this->scan_result_.init(1); +#endif this->scan_result_.push_back(scan); this->selected_sta_index_ = sta_index; } From db0b1e0b5c50ceb27b1bc104fb507b87de0a69e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:45:41 -0600 Subject: [PATCH 167/461] defensive to make bot happy --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 131fd122d72..d70903b0d59 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -840,7 +840,7 @@ void WiFiComponent::retry_connect() { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); - } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { + } else if (static_cast(this->selected_sta_index_) >= this->sta_.size() - 1) { // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) From 8eb509f8f0845e620aa44cd46011b87159d418cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:52:11 -0600 Subject: [PATCH 168/461] revert copilot suggestion .. we will never have more then 5 anyways --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d70903b0d59..c4d106c0fe1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -840,7 +840,7 @@ void WiFiComponent::retry_connect() { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); - } else if (static_cast(this->selected_sta_index_) >= this->sta_.size() - 1) { + } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) From 47874ef516d4d3014ae60ea2ad929f9552b39e6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:53:48 -0600 Subject: [PATCH 169/461] revert copilot suggestion .. we will never have more then 5 anyways --- esphome/components/wifi/wifi_component.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c4d106c0fe1..0d44f2f47f6 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -841,6 +841,7 @@ void WiFiComponent::retry_connect() { this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { + // Cast size to int (not int8_t which overflows at 127, not size_t which wraps negative indices) // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) @@ -944,6 +945,8 @@ bool WiFiComponent::load_fast_connect_settings_() { void WiFiComponent::save_fast_connect_settings_() { bssid_t bssid = wifi_bssid(); uint8_t channel = get_wifi_channel(); + // selected_sta_index_ is always valid here (called only after successful connection) + // Fallback to 0 is defensive programming for robustness int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0; // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear) From 38cf003bf3758a0344115dff0ef7b6f0cd9de8c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:58:05 -0600 Subject: [PATCH 170/461] preen --- esphome/components/wifi/wifi_component.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0d44f2f47f6..a2de77db95c 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -366,8 +366,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. // Note: Scan data is never used for hidden networks (see !params.get_hidden() check below) - params.set_bssid(optional{}); - params.set_channel(optional{}); + params.set_bssid({}); + params.set_channel({}); } else { params.set_bssid(config->get_bssid()); params.set_channel(config->get_channel()); From 541e0cfde8db500db3b9739a0ffbcf270766ac66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:00:37 -0600 Subject: [PATCH 171/461] preen --- esphome/components/wifi/wifi_component.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a2de77db95c..a7bc8093126 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -840,8 +840,7 @@ void WiFiComponent::retry_connect() { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); - } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { - // Cast size to int (not int8_t which overflows at 127, not size_t which wraps negative indices) + } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) From 7d48df9fe152105e1d1c69901cfc795ea29b1f83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:04:00 -0600 Subject: [PATCH 172/461] remove overly defensive suggestions from copilot --- esphome/components/wifi/__init__.py | 8 +++++++- esphome/components/wifi/wifi_component.cpp | 14 +++++--------- esphome/components/wifi/wifi_component.h | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b980bab4aa8..5f4190a9331 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -54,6 +54,10 @@ AUTO_LOAD = ["network"] NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" +# Maximum number of WiFi networks that can be configured +# Limited to 127 because selected_sta_index_ is int8_t in C++ +MAX_WIFI_NETWORKS = 127 + wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") ManualIP = wifi_ns.struct("ManualIP") @@ -260,7 +264,9 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(WiFiComponent), - cv.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA), + cv.Optional(CONF_NETWORKS): cv.All( + cv.ensure_list(WIFI_NETWORK_STA), cv.Length(max=MAX_WIFI_NETWORKS) + ), cv.Optional(CONF_SSID): cv.ssid, cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a7bc8093126..9e85b194c79 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -406,10 +406,8 @@ bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { for (size_t i = 0; i < this->sta_.size(); i++) { if (scan_res.matches(this->sta_[i])) { - if (i > std::numeric_limits::max()) { - ESP_LOGE(TAG, "AP index %zu too large", i); - return false; - } + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation + // No overflow check needed - YAML validation prevents >127 networks this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] return true; } @@ -832,15 +830,13 @@ void WiFiComponent::retry_connect() { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && (this->num_retried_ > 3 || this->error_from_callback_)) { #ifdef USE_WIFI_FAST_CONNECT - if (this->sta_.empty()) { - // No configured networks - shouldn't happen in fast_connect mode, but handle defensively - ESP_LOGW(TAG, "No configured networks"); - this->restart_adapter(); - } else if (this->trying_loaded_ap_) { + // No empty check needed - YAML validation requires at least one network for fast_connect + if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 04adb57b44d..772d63b7013 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -463,7 +463,7 @@ class WiFiComponent : public Component { uint8_t num_retried_{0}; // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag - // int8_t limits to 127 APs which should be sufficient for all practical use cases + // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) int8_t selected_sta_index_{-1}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; From 7041c3324bb3b413c2e2e46f44731beaea3d4c1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:05:15 -0600 Subject: [PATCH 173/461] revert yet another bad copilot suggesiton --- esphome/components/wifi/wifi_component.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 9e85b194c79..38d4fc44b54 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -366,8 +366,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. // Note: Scan data is never used for hidden networks (see !params.get_hidden() check below) - params.set_bssid({}); - params.set_channel({}); + params.set_bssid(optional{}); + params.set_channel(optional{}); } else { params.set_bssid(config->get_bssid()); params.set_channel(config->get_channel()); From 8a927918872bc328ae2d7823e0cc81203a27628a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:08:14 -0600 Subject: [PATCH 174/461] remove non-logical check --- esphome/components/wifi/wifi_component.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 38d4fc44b54..8812f1becf9 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -382,15 +382,11 @@ WiFiAP WiFiComponent::build_selected_ap_() const { // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] if (!this->scan_result_.empty()) { const WiFiScanResult &scan = this->scan_result_[0]; - - if (!params.get_hidden()) { - // Selected network is visible, override with data from the scan. - // Limit the connect params to only connect to exactly this network - // (network selection is done during scan phase). - params.set_ssid(scan.get_ssid()); - params.set_bssid(scan.get_bssid()); - params.set_channel(scan.get_channel()); - } + // If we have scan data, the network is visible (not hidden) - use it regardless of config + // Hidden networks don't appear in scan results, so presence of scan data is ground truth + params.set_ssid(scan.get_ssid()); + params.set_bssid(scan.get_bssid()); + params.set_channel(scan.get_channel()); } return params; From cde767d83da400a90c1c3bf39379745b18723a68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:12:19 -0600 Subject: [PATCH 175/461] improve comment --- esphome/components/wifi/wifi_component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 772d63b7013..ad879ef7d59 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -356,7 +356,7 @@ class WiFiComponent : public Component { // SYNCHRONIZATION HELPERS: Encapsulate the relationship between selected_sta_index_ and scan_result_ - // Set selected sta with a temporary scan result (fast connect path) + // Add temporary scan result and set selected sta index (fast connect path) void set_selected_sta_with_scan_(int8_t sta_index, const WiFiScanResult &scan) { #ifdef USE_RP2040 this->scan_result_.clear(); From a0b273c6f32dcbb2f9a1d270671c2cb163f0c9cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:23:56 -0600 Subject: [PATCH 176/461] not hidden if found --- esphome/components/wifi/wifi_component.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8812f1becf9..2f3f78de089 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -384,6 +384,7 @@ WiFiAP WiFiComponent::build_selected_ap_() const { const WiFiScanResult &scan = this->scan_result_[0]; // If we have scan data, the network is visible (not hidden) - use it regardless of config // Hidden networks don't appear in scan results, so presence of scan data is ground truth + params.set_hidden(false); params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); From f4d2b000dae6f359e2f06a3202a4e93e3f33c057 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:25:23 -0600 Subject: [PATCH 177/461] reduce --- esphome/components/wifi/wifi_component.cpp | 58 ++++++++++------------ 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2f3f78de089..a2d43b7d2e1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -347,49 +347,43 @@ WiFiAP WiFiComponent::build_selected_ap_() const { WiFiAP params; if (const WiFiAP *config = this->get_selected_sta_()) { - // Copy config data (password, manual IP, priority, EAP) + // Copy config data that's never overridden (password, manual IP, priority, EAP) params.set_password(config->get_password()); params.set_manual_ip(config->get_manual_ip()); params.set_priority(config->get_priority()); - #ifdef USE_WIFI_WPA2_EAP params.set_eap(config->get_eap()); #endif - // Set network parameters from config - // These will be used as-is for hidden networks, or overridden by scan for visible networks - params.set_ssid(config->get_ssid()); - - if (config->get_hidden()) { - params.set_hidden(true); - // For hidden networks, clear BSSID and channel even if set in config - // There might be multiple hidden networks with same SSID but we can't know which is correct - // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - // Note: Scan data is never used for hidden networks (see !params.get_hidden() check below) - params.set_bssid(optional{}); - params.set_channel(optional{}); + // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: + // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) + // - It then finds which sta_[i] config matches scan_result_[0] + // - Sets selected_sta_index_ = i to record that matching config + // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] + if (!this->scan_result_.empty()) { + // Use scan data - proves network is visible (not hidden) + const WiFiScanResult &scan = this->scan_result_[0]; + params.set_hidden(false); + params.set_ssid(scan.get_ssid()); + params.set_bssid(scan.get_bssid()); + params.set_channel(scan.get_channel()); } else { - params.set_bssid(config->get_bssid()); - params.set_channel(config->get_channel()); + // Use config settings + params.set_ssid(config->get_ssid()); + if (config->get_hidden()) { + params.set_hidden(true); + // For hidden networks, clear BSSID and channel even if set in config + // There might be multiple hidden networks with same SSID but we can't know which is correct + // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. + params.set_bssid(optional{}); + params.set_channel(optional{}); + } else { + params.set_bssid(config->get_bssid()); + params.set_channel(config->get_channel()); + } } } - // Overlay scan result data (if available) - // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: - // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) - // - It then finds which sta_[i] config matches scan_result_[0] - // - Sets selected_sta_index_ = i to record that matching config - // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] - if (!this->scan_result_.empty()) { - const WiFiScanResult &scan = this->scan_result_[0]; - // If we have scan data, the network is visible (not hidden) - use it regardless of config - // Hidden networks don't appear in scan results, so presence of scan data is ground truth - params.set_hidden(false); - params.set_ssid(scan.get_ssid()); - params.set_bssid(scan.get_bssid()); - params.set_channel(scan.get_channel()); - } - return params; } From 645820304fa4cbf5f2ce85de35d5f10edfef8a55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:28:54 -0600 Subject: [PATCH 178/461] reduce complexity --- esphome/components/wifi/wifi_component.cpp | 6 +++--- esphome/components/wifi/wifi_component.h | 7 +------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a2d43b7d2e1..fb7ca15a7a6 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -343,7 +343,7 @@ void WiFiComponent::clear_sta() { this->selected_sta_index_ = -1; } -WiFiAP WiFiComponent::build_selected_ap_() const { +void WiFiComponent::start_connecting_to_selected_(bool two) { WiFiAP params; if (const WiFiAP *config = this->get_selected_sta_()) { @@ -384,7 +384,7 @@ WiFiAP WiFiComponent::build_selected_ap_() const { } } - return params; + this->start_connecting(params, two); } bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { @@ -708,7 +708,7 @@ void WiFiComponent::check_scanning_finished() { // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config // matches that network and record it in selected_sta_index_. This keeps the two indices - // synchronized so build_selected_ap_() can safely use both to build connection parameters. + // synchronized so start_connecting_to_selected_() can safely use both to build connection parameters. if (!this->sync_selected_sta_to_best_scan_result_()) { ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ad879ef7d59..aba087c07b3 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -339,7 +339,7 @@ class WiFiComponent : public Component { #endif // USE_WIFI_AP void print_connect_params_(); - WiFiAP build_selected_ap_() const; + void start_connecting_to_selected_(bool two); const WiFiAP *get_selected_sta_() const { if (this->selected_sta_index_ >= 0 && static_cast(this->selected_sta_index_) < this->sta_.size()) { @@ -372,11 +372,6 @@ class WiFiComponent : public Component { // Returns true if match found, false otherwise bool sync_selected_sta_to_best_scan_result_(); - void start_connecting_to_selected_(bool two) { - WiFiAP connection_params = this->build_selected_ap_(); - this->start_connecting(connection_params, two); - } - #ifdef USE_WIFI_FAST_CONNECT // Reset state for next fast connect AP attempt // Clears old scan data so the new AP is tried with config only (SSID without specific BSSID/channel) From df1ffbaf5d70e9685e32e94579733de7fdf5d2d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:30:12 -0600 Subject: [PATCH 179/461] reduce complexity --- esphome/components/wifi/wifi_component.cpp | 67 +++++++++++----------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index fb7ca15a7a6..b30b477e5e1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -344,43 +344,46 @@ void WiFiComponent::clear_sta() { } void WiFiComponent::start_connecting_to_selected_(bool two) { - WiFiAP params; + const WiFiAP *config = this->get_selected_sta_(); + if (!config) { + ESP_LOGE(TAG, "No config selected"); + return; + } - if (const WiFiAP *config = this->get_selected_sta_()) { - // Copy config data that's never overridden (password, manual IP, priority, EAP) - params.set_password(config->get_password()); - params.set_manual_ip(config->get_manual_ip()); - params.set_priority(config->get_priority()); + WiFiAP params; + // Copy config data that's never overridden (password, manual IP, priority, EAP) + params.set_password(config->get_password()); + params.set_manual_ip(config->get_manual_ip()); + params.set_priority(config->get_priority()); #ifdef USE_WIFI_WPA2_EAP - params.set_eap(config->get_eap()); + params.set_eap(config->get_eap()); #endif - // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: - // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) - // - It then finds which sta_[i] config matches scan_result_[0] - // - Sets selected_sta_index_ = i to record that matching config - // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] - if (!this->scan_result_.empty()) { - // Use scan data - proves network is visible (not hidden) - const WiFiScanResult &scan = this->scan_result_[0]; - params.set_hidden(false); - params.set_ssid(scan.get_ssid()); - params.set_bssid(scan.get_bssid()); - params.set_channel(scan.get_channel()); + // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: + // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) + // - It then finds which sta_[i] config matches scan_result_[0] + // - Sets selected_sta_index_ = i to record that matching config + // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] + if (!this->scan_result_.empty()) { + // Use scan data - proves network is visible (not hidden) + const WiFiScanResult &scan = this->scan_result_[0]; + params.set_hidden(false); + params.set_ssid(scan.get_ssid()); + params.set_bssid(scan.get_bssid()); + params.set_channel(scan.get_channel()); + } else { + // Use config settings + params.set_ssid(config->get_ssid()); + if (config->get_hidden()) { + params.set_hidden(true); + // For hidden networks, clear BSSID and channel even if set in config + // There might be multiple hidden networks with same SSID but we can't know which is correct + // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. + params.set_bssid(optional{}); + params.set_channel(optional{}); } else { - // Use config settings - params.set_ssid(config->get_ssid()); - if (config->get_hidden()) { - params.set_hidden(true); - // For hidden networks, clear BSSID and channel even if set in config - // There might be multiple hidden networks with same SSID but we can't know which is correct - // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - params.set_bssid(optional{}); - params.set_channel(optional{}); - } else { - params.set_bssid(config->get_bssid()); - params.set_channel(config->get_channel()); - } + params.set_bssid(config->get_bssid()); + params.set_channel(config->get_channel()); } } From 936a6cb71e501acafd392fbd0d531a16ff08c2f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:31:00 -0600 Subject: [PATCH 180/461] reduce complexity --- esphome/components/wifi/wifi_component.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index b30b477e5e1..a0293cdaa33 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -365,9 +365,8 @@ void WiFiComponent::start_connecting_to_selected_(bool two) { // - Sets selected_sta_index_ = i to record that matching config // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] if (!this->scan_result_.empty()) { - // Use scan data - proves network is visible (not hidden) + // Use scan data - network is visible (hidden defaults to false) const WiFiScanResult &scan = this->scan_result_[0]; - params.set_hidden(false); params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); From df35036c8da50cdf32f522adc1046c19e362035d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:52:30 -0600 Subject: [PATCH 181/461] refator --- esphome/components/wifi/wifi_component.cpp | 57 +++++++++++++--------- esphome/components/wifi/wifi_component.h | 22 ++------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a0293cdaa33..8bcd5ec35c4 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -109,13 +109,15 @@ void WiFiComponent::start() { } #ifdef USE_WIFI_FAST_CONNECT - this->trying_loaded_ap_ = this->load_fast_connect_settings_(); + WiFiAP params; + this->trying_loaded_ap_ = this->load_fast_connect_settings_(params); if (!this->trying_loaded_ap_) { // FAST CONNECT FALLBACK: No saved settings available - // Use first config (will use SSID from config since scan_result_ is empty) + // Use first config (will use SSID from config) this->selected_sta_index_ = 0; + params = this->build_wifi_ap_from_selected_(); } - this->start_connecting_to_selected_(false); + this->start_connecting(params, false); #else this->start_scanning(); #endif @@ -173,11 +175,13 @@ void WiFiComponent::loop() { // Safety check: Ensure selected_sta_index_ is valid before retrying // (should already be set by retry_connect(), but check for robustness) this->reset_selected_ap_to_first_if_invalid_(); - this->start_connecting_to_selected_(false); + WiFiAP params = this->build_wifi_ap_from_selected_(); + this->start_connecting(params, false); #else if (this->retry_hidden_) { this->reset_selected_ap_to_first_if_invalid_(); - this->start_connecting_to_selected_(false); + WiFiAP params = this->build_wifi_ap_from_selected_(); + this->start_connecting(params, false); } else { this->start_scanning(); } @@ -343,11 +347,11 @@ void WiFiComponent::clear_sta() { this->selected_sta_index_ = -1; } -void WiFiComponent::start_connecting_to_selected_(bool two) { +WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { const WiFiAP *config = this->get_selected_sta_(); if (!config) { ESP_LOGE(TAG, "No config selected"); - return; + return {}; } WiFiAP params; @@ -386,7 +390,7 @@ void WiFiComponent::start_connecting_to_selected_(bool two) { } } - this->start_connecting(params, two); + return params; } bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { @@ -710,7 +714,7 @@ void WiFiComponent::check_scanning_finished() { // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config // matches that network and record it in selected_sta_index_. This keeps the two indices - // synchronized so start_connecting_to_selected_() can safely use both to build connection parameters. + // synchronized so build_wifi_ap_from_selected_() can safely use both to build connection parameters. if (!this->sync_selected_sta_to_best_scan_result_()) { ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); @@ -719,7 +723,8 @@ void WiFiComponent::check_scanning_finished() { yield(); - this->start_connecting_to_selected_(false); + WiFiAP params = this->build_wifi_ap_from_selected_(); + this->start_connecting(params, false); } void WiFiComponent::dump_config() { @@ -861,7 +866,8 @@ void WiFiComponent::retry_connect() { if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { yield(); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; - this->start_connecting_to_selected_(true); + WiFiAP params = this->build_wifi_ap_from_selected_(); + this->start_connecting(params, true); return; } @@ -900,7 +906,7 @@ bool WiFiComponent::is_esp32_improv_active_() { } #ifdef USE_WIFI_FAST_CONNECT -bool WiFiComponent::load_fast_connect_settings_() { +bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { SavedWifiFastConnectSettings fast_connect_save{}; if (this->fast_connect_pref_.load(&fast_connect_save)) { @@ -910,18 +916,25 @@ bool WiFiComponent::load_fast_connect_settings_() { return false; } - // Load BSSID from saved settings + // Set selected index for future operations (save, retry, etc) + this->selected_sta_index_ = fast_connect_save.ap_index; + + // Build WiFiAP directly from saved settings + config + const WiFiAP &config = this->sta_[fast_connect_save.ap_index]; + params.set_password(config.get_password()); + params.set_manual_ip(config.get_manual_ip()); + params.set_priority(config.get_priority()); +#ifdef USE_WIFI_WPA2_EAP + params.set_eap(config.get_eap()); +#endif + + // Use saved BSSID/channel from fast connect, SSID from config + params.set_ssid(config.get_ssid()); bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); - - // FAST CONNECT SUCCESS: Restore saved settings without scanning - // SYNCHRONIZATION: Link temporary scan result with sta_[saved_index] - // Unlike wifi_scan_done() which sorts then finds the match, here we know exactly - // which config was used before and create a matching temporary scan result - // Use SSID from config for the temporary scan result - const std::string &ssid = this->sta_[fast_connect_save.ap_index].get_ssid(); - WiFiScanResult fast_connect_scan(bssid, ssid, fast_connect_save.channel, 0, false, false); - this->set_selected_sta_with_scan_(fast_connect_save.ap_index, fast_connect_scan); + params.set_bssid(bssid); + params.set_channel(fast_connect_save.channel); + // hidden defaults to false (network was found before, so not hidden) ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index aba087c07b3..48ecb3fe8da 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -339,7 +339,7 @@ class WiFiComponent : public Component { #endif // USE_WIFI_AP void print_connect_params_(); - void start_connecting_to_selected_(bool two); + WiFiAP build_wifi_ap_from_selected_() const; const WiFiAP *get_selected_sta_() const { if (this->selected_sta_index_ >= 0 && static_cast(this->selected_sta_index_) < this->sta_.size()) { @@ -354,22 +354,8 @@ class WiFiComponent : public Component { } } - // SYNCHRONIZATION HELPERS: Encapsulate the relationship between selected_sta_index_ and scan_result_ - - // Add temporary scan result and set selected sta index (fast connect path) - void set_selected_sta_with_scan_(int8_t sta_index, const WiFiScanResult &scan) { -#ifdef USE_RP2040 - this->scan_result_.clear(); - this->scan_result_.reserve(1); -#else - this->scan_result_.init(1); -#endif - this->scan_result_.push_back(scan); - this->selected_sta_index_ = sta_index; - } - - // Find which sta_[i] matches scan_result_[0] and set selected_sta_index_ (scan done path) - // Returns true if match found, false otherwise + // SYNCHRONIZATION HELPER: Find which sta_[i] matches scan_result_[0] and set selected_sta_index_ + // Returns true if match found, false otherwise (scan done path) bool sync_selected_sta_to_best_scan_result_(); #ifdef USE_WIFI_FAST_CONNECT @@ -408,7 +394,7 @@ class WiFiComponent : public Component { bool is_esp32_improv_active_(); #ifdef USE_WIFI_FAST_CONNECT - bool load_fast_connect_settings_(); + bool load_fast_connect_settings_(WiFiAP ¶ms); void save_fast_connect_settings_(); #endif From 03c5655201071491bc0bf454c13c2780b762dd32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:58:40 -0600 Subject: [PATCH 182/461] dry --- esphome/components/wifi/wifi_component.cpp | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8bcd5ec35c4..a15c8d826fa 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -347,6 +347,16 @@ void WiFiComponent::clear_sta() { this->selected_sta_index_ = -1; } +// Helper to copy base config settings (password, manual_ip, priority, EAP) from source to dest +static void copy_wifi_ap_base_config(WiFiAP &dest, const WiFiAP &source) { + dest.set_password(source.get_password()); + dest.set_manual_ip(source.get_manual_ip()); + dest.set_priority(source.get_priority()); +#ifdef USE_WIFI_WPA2_EAP + dest.set_eap(source.get_eap()); +#endif +} + WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { const WiFiAP *config = this->get_selected_sta_(); if (!config) { @@ -356,12 +366,7 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { WiFiAP params; // Copy config data that's never overridden (password, manual IP, priority, EAP) - params.set_password(config->get_password()); - params.set_manual_ip(config->get_manual_ip()); - params.set_priority(config->get_priority()); -#ifdef USE_WIFI_WPA2_EAP - params.set_eap(config->get_eap()); -#endif + copy_wifi_ap_base_config(params, *config); // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) @@ -921,12 +926,8 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { // Build WiFiAP directly from saved settings + config const WiFiAP &config = this->sta_[fast_connect_save.ap_index]; - params.set_password(config.get_password()); - params.set_manual_ip(config.get_manual_ip()); - params.set_priority(config.get_priority()); -#ifdef USE_WIFI_WPA2_EAP - params.set_eap(config.get_eap()); -#endif + // Copy config data that's never overridden (password, manual IP, priority, EAP) + copy_wifi_ap_base_config(params, config); // Use saved BSSID/channel from fast connect, SSID from config params.set_ssid(config.get_ssid()); From 2e1fd30ea0e21b3e31dd8ecf19c0a4df1e1d2305 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:00:42 -0600 Subject: [PATCH 183/461] dry --- esphome/components/wifi/wifi_component.cpp | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a15c8d826fa..c42a69f5e81 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -382,19 +382,14 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { } else { // Use config settings params.set_ssid(config->get_ssid()); - if (config->get_hidden()) { - params.set_hidden(true); - // For hidden networks, clear BSSID and channel even if set in config - // There might be multiple hidden networks with same SSID but we can't know which is correct - // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - params.set_bssid(optional{}); - params.set_channel(optional{}); - } else { - params.set_bssid(config->get_bssid()); - params.set_channel(config->get_channel()); - } + const auto hidden = config->get_hidden(); + params.set_hidden(hidden); + // For hidden networks, clear BSSID and channel even if set in config + // There might be multiple hidden networks with same SSID but we can't know which is correct + // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. + params.set_bssid(hidden ? optional{} : config->get_bssid()); + params.set_channel(hidden ? optional{} : config->get_channel()); } - return params; } From 90feecb7bf383e55527f7e8195e78a0ae386ca87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:06:04 -0600 Subject: [PATCH 184/461] dry --- esphome/components/wifi/wifi_component.cpp | 40 +++++++--------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c42a69f5e81..00e47b65597 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -347,16 +347,6 @@ void WiFiComponent::clear_sta() { this->selected_sta_index_ = -1; } -// Helper to copy base config settings (password, manual_ip, priority, EAP) from source to dest -static void copy_wifi_ap_base_config(WiFiAP &dest, const WiFiAP &source) { - dest.set_password(source.get_password()); - dest.set_manual_ip(source.get_manual_ip()); - dest.set_priority(source.get_priority()); -#ifdef USE_WIFI_WPA2_EAP - dest.set_eap(source.get_eap()); -#endif -} - WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { const WiFiAP *config = this->get_selected_sta_(); if (!config) { @@ -364,9 +354,8 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { return {}; } - WiFiAP params; - // Copy config data that's never overridden (password, manual IP, priority, EAP) - copy_wifi_ap_base_config(params, *config); + // Start with a copy of the entire config + WiFiAP params = *config; // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) @@ -374,22 +363,20 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { // - Sets selected_sta_index_ = i to record that matching config // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] if (!this->scan_result_.empty()) { - // Use scan data - network is visible (hidden defaults to false) + // Override with scan data - network is visible (hidden defaults to false) const WiFiScanResult &scan = this->scan_result_[0]; + params.set_hidden(false); params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); - } else { - // Use config settings - params.set_ssid(config->get_ssid()); - const auto hidden = config->get_hidden(); - params.set_hidden(hidden); + } else if (config->get_hidden()) { // For hidden networks, clear BSSID and channel even if set in config // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - params.set_bssid(hidden ? optional{} : config->get_bssid()); - params.set_channel(hidden ? optional{} : config->get_channel()); + params.set_bssid(optional{}); + params.set_channel(optional{}); } + return params; } @@ -919,18 +906,15 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { // Set selected index for future operations (save, retry, etc) this->selected_sta_index_ = fast_connect_save.ap_index; - // Build WiFiAP directly from saved settings + config - const WiFiAP &config = this->sta_[fast_connect_save.ap_index]; - // Copy config data that's never overridden (password, manual IP, priority, EAP) - copy_wifi_ap_base_config(params, config); + // Copy entire config, then override with fast connect data + params = this->sta_[fast_connect_save.ap_index]; - // Use saved BSSID/channel from fast connect, SSID from config - params.set_ssid(config.get_ssid()); + // Override with saved BSSID/channel from fast connect (SSID/password/hidden/etc already copied) bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); params.set_bssid(bssid); params.set_channel(fast_connect_save.channel); - // hidden defaults to false (network was found before, so not hidden) + // Network was found before, so not hidden (already false in default-constructed WiFiAP) ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; From 400a18fddc5c1b04c0a0747d5e3933f861ac8013 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:12:05 -0600 Subject: [PATCH 185/461] dry --- esphome/components/wifi/wifi_component.cpp | 38 +++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 00e47b65597..5c9b23d025d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -347,6 +347,17 @@ void WiFiComponent::clear_sta() { this->selected_sta_index_ = -1; } +// Helper to copy base config settings (password, manual_ip, priority, EAP) from source to dest +// These fields are never overridden by scan data, so we copy them once to avoid full WiFiAP copy +static void copy_wifi_ap_base_config(WiFiAP &dest, const WiFiAP &source) { + dest.set_password(source.get_password()); + dest.set_manual_ip(source.get_manual_ip()); + dest.set_priority(source.get_priority()); +#ifdef USE_WIFI_WPA2_EAP + dest.set_eap(source.get_eap()); +#endif +} + WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { const WiFiAP *config = this->get_selected_sta_(); if (!config) { @@ -354,8 +365,9 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { return {}; } - // Start with a copy of the entire config - WiFiAP params = *config; + WiFiAP params; + // Copy only base fields (password, manual_ip, priority, EAP) to avoid copying strings we'll override + copy_wifi_ap_base_config(params, *config); // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) @@ -365,16 +377,23 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { if (!this->scan_result_.empty()) { // Override with scan data - network is visible (hidden defaults to false) const WiFiScanResult &scan = this->scan_result_[0]; - params.set_hidden(false); params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); + // hidden defaults to false, no need to set explicitly } else if (config->get_hidden()) { - // For hidden networks, clear BSSID and channel even if set in config + // For hidden networks, use config SSID but clear BSSID and channel // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. + params.set_ssid(config->get_ssid()); + params.set_hidden(true); params.set_bssid(optional{}); params.set_channel(optional{}); + } else { + // No scan data, visible network - use all config values + params.set_ssid(config->get_ssid()); + params.set_bssid(config->get_bssid()); + params.set_channel(config->get_channel()); } return params; @@ -906,15 +925,18 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { // Set selected index for future operations (save, retry, etc) this->selected_sta_index_ = fast_connect_save.ap_index; - // Copy entire config, then override with fast connect data - params = this->sta_[fast_connect_save.ap_index]; + // Build WiFiAP from config + saved fast connect data + const WiFiAP &config = this->sta_[fast_connect_save.ap_index]; + // Copy only base fields (password, manual_ip, priority, EAP) to avoid unnecessary string copies + copy_wifi_ap_base_config(params, config); - // Override with saved BSSID/channel from fast connect (SSID/password/hidden/etc already copied) + // Use SSID from config, BSSID/channel from saved fast connect data + params.set_ssid(config.get_ssid()); bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); params.set_bssid(bssid); params.set_channel(fast_connect_save.channel); - // Network was found before, so not hidden (already false in default-constructed WiFiAP) + // Network was found before, so not hidden (hidden defaults to false) ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; From 6e685f1b2d84d787edac3fefcde0a9aa1a77abf5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:15:57 -0600 Subject: [PATCH 186/461] dry --- esphome/components/wifi/wifi_component.cpp | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 5c9b23d025d..bd18cc6f1be 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -381,19 +381,16 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); // hidden defaults to false, no need to set explicitly - } else if (config->get_hidden()) { - // For hidden networks, use config SSID but clear BSSID and channel + } else { + // No scan data - use config SSID + params.set_ssid(config->get_ssid()); + const bool hidden = config->get_hidden(); + params.set_hidden(hidden); + // Hidden network - clear BSSID and channel even if set in config // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - params.set_ssid(config->get_ssid()); - params.set_hidden(true); - params.set_bssid(optional{}); - params.set_channel(optional{}); - } else { - // No scan data, visible network - use all config values - params.set_ssid(config->get_ssid()); - params.set_bssid(config->get_bssid()); - params.set_channel(config->get_channel()); + params.set_bssid(hidden ? optional{} : config->get_bssid()); + params.set_channel(hidden ? optional{} : config->get_channel()); } return params; From bf52b9fe06cceb5ecf8249cf112725c122216ad2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:20:01 -0600 Subject: [PATCH 187/461] dry --- esphome/components/wifi/wifi_component.cpp | 34 ++++++---------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index bd18cc6f1be..6c92d1f9554 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -347,17 +347,6 @@ void WiFiComponent::clear_sta() { this->selected_sta_index_ = -1; } -// Helper to copy base config settings (password, manual_ip, priority, EAP) from source to dest -// These fields are never overridden by scan data, so we copy them once to avoid full WiFiAP copy -static void copy_wifi_ap_base_config(WiFiAP &dest, const WiFiAP &source) { - dest.set_password(source.get_password()); - dest.set_manual_ip(source.get_manual_ip()); - dest.set_priority(source.get_priority()); -#ifdef USE_WIFI_WPA2_EAP - dest.set_eap(source.get_eap()); -#endif -} - WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { const WiFiAP *config = this->get_selected_sta_(); if (!config) { @@ -365,9 +354,8 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { return {}; } - WiFiAP params; - // Copy only base fields (password, manual_ip, priority, EAP) to avoid copying strings we'll override - copy_wifi_ap_base_config(params, *config); + // Start with a copy of the entire config + WiFiAP params = *config; // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) @@ -375,15 +363,14 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { // - Sets selected_sta_index_ = i to record that matching config // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] if (!this->scan_result_.empty()) { - // Override with scan data - network is visible (hidden defaults to false) + // Override with scan data - network is visible const WiFiScanResult &scan = this->scan_result_[0]; + params.set_hidden(false); params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); - // hidden defaults to false, no need to set explicitly } else { - // No scan data - use config SSID - params.set_ssid(config->get_ssid()); + // No scan data - use config values const bool hidden = config->get_hidden(); params.set_hidden(hidden); // Hidden network - clear BSSID and channel even if set in config @@ -922,18 +909,15 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { // Set selected index for future operations (save, retry, etc) this->selected_sta_index_ = fast_connect_save.ap_index; - // Build WiFiAP from config + saved fast connect data - const WiFiAP &config = this->sta_[fast_connect_save.ap_index]; - // Copy only base fields (password, manual_ip, priority, EAP) to avoid unnecessary string copies - copy_wifi_ap_base_config(params, config); + // Copy entire config, then override with fast connect data + params = this->sta_[fast_connect_save.ap_index]; - // Use SSID from config, BSSID/channel from saved fast connect data - params.set_ssid(config.get_ssid()); + // Override with saved BSSID/channel from fast connect (SSID/password/hidden/etc already copied) bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); params.set_bssid(bssid); params.set_channel(fast_connect_save.channel); - // Network was found before, so not hidden (hidden defaults to false) + // Network was found before, so not hidden (already false in default-constructed WiFiAP) ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; From 0a741007bf5850112ee081acfffe2315f5a74e72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:21:14 -0600 Subject: [PATCH 188/461] dry --- esphome/components/wifi/wifi_component.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 6c92d1f9554..4dd344e4d82 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -369,15 +369,12 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); - } else { - // No scan data - use config values - const bool hidden = config->get_hidden(); - params.set_hidden(hidden); + } else if (config->get_hidden()) { // Hidden network - clear BSSID and channel even if set in config // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - params.set_bssid(hidden ? optional{} : config->get_bssid()); - params.set_channel(hidden ? optional{} : config->get_channel()); + params.set_bssid(optional{}); + params.set_channel(optional{}); } return params; From b4b24c500cd5fa39f73fef51ae5daa221db7bd66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:24:07 -0600 Subject: [PATCH 189/461] dry --- esphome/components/wifi/wifi_component.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 4dd344e4d82..8e3fae99cdc 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -348,15 +348,12 @@ void WiFiComponent::clear_sta() { } WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { - const WiFiAP *config = this->get_selected_sta_(); - if (!config) { + WiFiAP params = this->get_sta(); + if (params.get_ssid().empty()) { ESP_LOGE(TAG, "No config selected"); return {}; } - // Start with a copy of the entire config - WiFiAP params = *config; - // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) // - It then finds which sta_[i] config matches scan_result_[0] @@ -369,7 +366,7 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); - } else if (config->get_hidden()) { + } else if (params.get_hidden()) { // Hidden network - clear BSSID and channel even if set in config // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. From 3b9570d916bb4451f2ad7020b81bdb4ae2483255 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:25:52 -0600 Subject: [PATCH 190/461] dry --- esphome/components/wifi/wifi_component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 48ecb3fe8da..040dad32532 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -220,7 +220,7 @@ class WiFiComponent : public Component { void set_sta(const WiFiAP &ap); // Returns a copy of the currently selected AP configuration // Note: This copies the 88-byte WiFiAP. Only used by WiFiConfigureAction for state save/restore. - WiFiAP get_sta(); + WiFiAP get_sta() const; void init_sta(size_t count); void add_sta(const WiFiAP &ap); void clear_sta(); From 0893de4f297887a7e4196aac364dc5937ca5ed9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:26:51 -0600 Subject: [PATCH 191/461] dry --- esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8e3fae99cdc..9531c6dfc80 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -396,7 +396,7 @@ bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { return false; } -WiFiAP WiFiComponent::get_sta() { +WiFiAP WiFiComponent::get_sta() const { const WiFiAP *config = this->get_selected_sta_(); return config ? *config : WiFiAP{}; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 040dad32532..f5b50f29e9d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -218,8 +218,7 @@ class WiFiComponent : public Component { WiFiComponent(); void set_sta(const WiFiAP &ap); - // Returns a copy of the currently selected AP configuration - // Note: This copies the 88-byte WiFiAP. Only used by WiFiConfigureAction for state save/restore. + // Returns a copy of the currently selected AP configuration (88 bytes) WiFiAP get_sta() const; void init_sta(size_t count); void add_sta(const WiFiAP &ap); From 81bc2d82d691bf64eb53eee0aa6e125531d9d5f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:30:16 -0600 Subject: [PATCH 192/461] dry --- esphome/components/wifi/wifi_component.cpp | 39 +++++++++++----------- esphome/components/wifi/wifi_component.h | 4 --- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 9531c6dfc80..910edb01ac0 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -377,25 +377,6 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { return params; } -bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { - if (this->scan_result_.empty()) - return false; - - const WiFiScanResult &scan_res = this->scan_result_[0]; - if (!scan_res.get_matches()) - return false; - - for (size_t i = 0; i < this->sta_.size(); i++) { - if (scan_res.matches(this->sta_[i])) { - // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation - // No overflow check needed - YAML validation prevents >127 networks - this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] - return true; - } - } - return false; -} - WiFiAP WiFiComponent::get_sta() const { const WiFiAP *config = this->get_selected_sta_(); return config ? *config : WiFiAP{}; @@ -699,7 +680,25 @@ void WiFiComponent::check_scanning_finished() { // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config // matches that network and record it in selected_sta_index_. This keeps the two indices // synchronized so build_wifi_ap_from_selected_() can safely use both to build connection parameters. - if (!this->sync_selected_sta_to_best_scan_result_()) { + const WiFiScanResult &scan_res = this->scan_result_[0]; + if (!scan_res.get_matches()) { + ESP_LOGW(TAG, "No matching network found"); + this->retry_connect(); + return; + } + + bool found_match = false; + for (size_t i = 0; i < this->sta_.size(); i++) { + if (scan_res.matches(this->sta_[i])) { + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation + // No overflow check needed - YAML validation prevents >127 networks + this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] + found_match = true; + break; + } + } + + if (!found_match) { ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); return; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f5b50f29e9d..a457ae6bd95 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -353,10 +353,6 @@ class WiFiComponent : public Component { } } - // SYNCHRONIZATION HELPER: Find which sta_[i] matches scan_result_[0] and set selected_sta_index_ - // Returns true if match found, false otherwise (scan done path) - bool sync_selected_sta_to_best_scan_result_(); - #ifdef USE_WIFI_FAST_CONNECT // Reset state for next fast connect AP attempt // Clears old scan data so the new AP is tried with config only (SSID without specific BSSID/channel) From c6da4e4777625174f2bf16c4ebaeb03d88c3782f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:38:26 -0600 Subject: [PATCH 193/461] dry --- esphome/components/wifi/wifi_component.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 910edb01ac0..1bf07bbde60 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -349,10 +349,9 @@ void WiFiComponent::clear_sta() { WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { WiFiAP params = this->get_sta(); - if (params.get_ssid().empty()) { - ESP_LOGE(TAG, "No config selected"); - return {}; - } + // PRECONDITION: selected_sta_index_ must be valid (ensured by all callers) + // If SSID is empty, it means selected_sta_index_ was invalid - this is a bug + assert(!params.get_ssid().empty() && "build_wifi_ap_from_selected_() called with invalid selected_sta_index_"); // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) From 148cbc03db69fbf60b67058a852d6aa5bb9b3bae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:39:23 -0600 Subject: [PATCH 194/461] dry --- esphome/components/wifi/wifi_component.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 1bf07bbde60..2b085f66f76 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,5 +1,6 @@ #include "wifi_component.h" #ifdef USE_WIFI +#include #include #ifdef USE_ESP32 From 282f6e04b37a7159bd1d8a7cf4e43d6b9020eb4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:40:53 -0600 Subject: [PATCH 195/461] dry --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2b085f66f76..0ac675c0a37 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -352,7 +352,7 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { WiFiAP params = this->get_sta(); // PRECONDITION: selected_sta_index_ must be valid (ensured by all callers) // If SSID is empty, it means selected_sta_index_ was invalid - this is a bug - assert(!params.get_ssid().empty() && "build_wifi_ap_from_selected_() called with invalid selected_sta_index_"); + assert(!params.get_ssid().empty()); // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) From d96e8a9c4b47469688085016c99fc24b3dfc4fd8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:43:52 -0600 Subject: [PATCH 196/461] dry --- esphome/components/wifi/wifi_component.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0ac675c0a37..4a76603691d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -349,10 +349,10 @@ void WiFiComponent::clear_sta() { } WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { - WiFiAP params = this->get_sta(); // PRECONDITION: selected_sta_index_ must be valid (ensured by all callers) - // If SSID is empty, it means selected_sta_index_ was invalid - this is a bug - assert(!params.get_ssid().empty()); + const WiFiAP *config = this->get_selected_sta_(); + assert(config != nullptr); + WiFiAP params = *config; // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) From d6528f906ebd73db248845a86e3cad2d1a1340f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:49:45 -0600 Subject: [PATCH 197/461] dry --- esphome/components/wifi/wifi_component.cpp | 4 ---- esphome/components/wifi/wifi_component.h | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 4a76603691d..a47cba169a2 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -343,10 +343,6 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->add_sta(ap); this->selected_sta_index_ = 0; } -void WiFiComponent::clear_sta() { - this->sta_.clear(); - this->selected_sta_index_ = -1; -} WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { // PRECONDITION: selected_sta_index_ must be valid (ensured by all callers) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index a457ae6bd95..394f00fd57d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -222,7 +222,10 @@ class WiFiComponent : public Component { WiFiAP get_sta() const; void init_sta(size_t count); void add_sta(const WiFiAP &ap); - void clear_sta(); + void clear_sta() { + this->sta_.clear(); + this->selected_sta_index_ = -1; + } #ifdef USE_WIFI_AP /** Setup an Access Point that should be created if no connection to a station can be made. From d46d6f08bddda5fedca027aee5da8cd857e1965e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 18:04:57 -0600 Subject: [PATCH 198/461] [wifi] Guard AP-related members with USE_WIFI_AP to save RAM --- esphome/components/wifi/wifi_component.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ac63e0eb0c5..ef595e98914 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -233,6 +233,7 @@ class WiFiComponent : public Component { */ void set_ap(const WiFiAP &ap); WiFiAP get_ap() { return this->ap_; } + void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; } #endif // USE_WIFI_AP void enable(); @@ -241,7 +242,6 @@ class WiFiComponent : public Component { void start_scanning(); void check_scanning_finished(); void start_connecting(const WiFiAP &ap, bool two); - void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; } void check_connecting_finished(); @@ -397,7 +397,9 @@ class WiFiComponent : public Component { std::vector sta_priorities_; wifi_scan_vector_t scan_result_; WiFiAP selected_ap_; +#ifdef USE_WIFI_AP WiFiAP ap_; +#endif optional output_power_; ESPPreferenceObject pref_; #ifdef USE_WIFI_FAST_CONNECT @@ -408,7 +410,9 @@ class WiFiComponent : public Component { uint32_t action_started_; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; +#ifdef USE_WIFI_AP uint32_t ap_timeout_{}; +#endif // Group all 8-bit values together WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; From f3c9ab7cb463b66c4b7fd567f224b3f39315f8e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 18:06:10 -0600 Subject: [PATCH 199/461] address final bot comments --- esphome/components/wifi/wifi_component.cpp | 9 +++++---- esphome/components/wifi/wifi_component.h | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a47cba169a2..789c22bae10 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -350,11 +350,11 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { assert(config != nullptr); WiFiAP params = *config; - // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: + // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync after wifi_scan_done(): // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) // - It then finds which sta_[i] config matches scan_result_[0] // - Sets selected_sta_index_ = i to record that matching config - // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] + // This sync holds until scan_result_ is cleared (e.g., after connection or in reset_for_next_ap_attempt_()) if (!this->scan_result_.empty()) { // Override with scan data - network is visible const WiFiScanResult &scan = this->scan_result_[0]; @@ -901,12 +901,13 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { // Copy entire config, then override with fast connect data params = this->sta_[fast_connect_save.ap_index]; - // Override with saved BSSID/channel from fast connect (SSID/password/hidden/etc already copied) + // Override with saved BSSID/channel from fast connect (SSID/password/etc already copied from config) bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); params.set_bssid(bssid); params.set_channel(fast_connect_save.channel); - // Network was found before, so not hidden (already false in default-constructed WiFiAP) + // Fast connect uses specific BSSID+channel, not hidden network probe (even if config has hidden: true) + params.set_hidden(false); ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 394f00fd57d..228894076e1 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -218,7 +218,7 @@ class WiFiComponent : public Component { WiFiComponent(); void set_sta(const WiFiAP &ap); - // Returns a copy of the currently selected AP configuration (88 bytes) + // Returns a copy of the currently selected AP configuration WiFiAP get_sta() const; void init_sta(size_t count); void add_sta(const WiFiAP &ap); From 5cdb891b580882842c420b458ddc141775162031 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 19:21:58 -0600 Subject: [PATCH 200/461] [socket] Deduplicate IP formatting in LWIP raw TCP implementation (#11747) --- .../components/socket/lwip_raw_tcp_impl.cpp | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 4dedeffb6a2..e0d93d8e2f1 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -172,16 +172,7 @@ class LWIPRawImpl : public Socket { errno = ECONNRESET; return ""; } - char buffer[50] = {}; - if (IP_IS_V4_VAL(pcb_->remote_ip)) { - inet_ntoa_r(pcb_->remote_ip, buffer, sizeof(buffer)); - } -#if LWIP_IPV6 - else if (IP_IS_V6_VAL(pcb_->remote_ip)) { - inet6_ntoa_r(pcb_->remote_ip, buffer, sizeof(buffer)); - } -#endif - return std::string(buffer); + return this->format_ip_address_(pcb_->remote_ip); } int getsockname(struct sockaddr *name, socklen_t *addrlen) override { if (pcb_ == nullptr) { @@ -199,16 +190,7 @@ class LWIPRawImpl : public Socket { errno = ECONNRESET; return ""; } - char buffer[50] = {}; - if (IP_IS_V4_VAL(pcb_->local_ip)) { - inet_ntoa_r(pcb_->local_ip, buffer, sizeof(buffer)); - } -#if LWIP_IPV6 - else if (IP_IS_V6_VAL(pcb_->local_ip)) { - inet6_ntoa_r(pcb_->local_ip, buffer, sizeof(buffer)); - } -#endif - return std::string(buffer); + return this->format_ip_address_(pcb_->local_ip); } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { if (pcb_ == nullptr) { @@ -499,6 +481,19 @@ class LWIPRawImpl : public Socket { } protected: + std::string format_ip_address_(const ip_addr_t &ip) { + char buffer[50] = {}; + if (IP_IS_V4_VAL(ip)) { + inet_ntoa_r(ip, buffer, sizeof(buffer)); + } +#if LWIP_IPV6 + else if (IP_IS_V6_VAL(ip)) { + inet6_ntoa_r(ip, buffer, sizeof(buffer)); + } +#endif + return std::string(buffer); + } + int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { if (family_ == AF_INET) { if (*addrlen < sizeof(struct sockaddr_in)) { From ba5fa7c10a97c0c62f4d6126f254e165d71cccc6 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 6 Nov 2025 20:22:50 -0500 Subject: [PATCH 201/461] [psram] Add option to disable ignore not found sdkconfig setting (#11411) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/const/__init__.py | 1 + esphome/components/psram/__init__.py | 6 +++++- tests/components/psram/test.esp32-s3-idf.yaml | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 0c22b2d27e4..12a69551f56 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -9,6 +9,7 @@ BYTE_ORDER_BIG = "big_endian" CONF_COLOR_DEPTH = "color_depth" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ENABLED = "enabled" +CONF_IGNORE_NOT_FOUND = "ignore_not_found" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index df49e088793..11c238c1bfb 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -2,6 +2,7 @@ import logging import textwrap import esphome.codegen as cg +from esphome.components.const import CONF_IGNORE_NOT_FOUND from esphome.components.esp32 import ( CONF_CPU_FREQUENCY, CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, @@ -123,6 +124,7 @@ def get_config_schema(config): cv.Optional(CONF_ENABLE_ECC, default=False): cv.boolean, cv.Optional(CONF_SPEED, default=speeds[0]): cv.one_of(*speeds, upper=True), cv.Optional(CONF_DISABLED, default=False): cv.boolean, + cv.Optional(CONF_IGNORE_NOT_FOUND, default=True): cv.boolean, } )(config) @@ -147,7 +149,9 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_SPIRAM", True) add_idf_sdkconfig_option("CONFIG_SPIRAM_USE", True) add_idf_sdkconfig_option("CONFIG_SPIRAM_USE_CAPS_ALLOC", True) - add_idf_sdkconfig_option("CONFIG_SPIRAM_IGNORE_NOTFOUND", True) + add_idf_sdkconfig_option( + "CONFIG_SPIRAM_IGNORE_NOTFOUND", config[CONF_IGNORE_NOT_FOUND] + ) add_idf_sdkconfig_option(f"CONFIG_SPIRAM_MODE_{SDK_MODES[config[CONF_MODE]]}", True) diff --git a/tests/components/psram/test.esp32-s3-idf.yaml b/tests/components/psram/test.esp32-s3-idf.yaml index 75d4ee539c1..548b8324d0c 100644 --- a/tests/components/psram/test.esp32-s3-idf.yaml +++ b/tests/components/psram/test.esp32-s3-idf.yaml @@ -9,3 +9,4 @@ psram: mode: octal speed: 120MHz enable_ecc: true + ignore_not_found: false From 5d20e3a3b4ab7981d0d17a822493532eef267b75 Mon Sep 17 00:00:00 2001 From: philippderdiedas <56478008+philippderdiedas@users.noreply.github.com> Date: Fri, 7 Nov 2025 02:25:14 +0100 Subject: [PATCH 202/461] Add MCP3221 i2c A-D-Converter (#7764) --- CODEOWNERS | 1 + esphome/components/mcp3221/__init__.py | 1 + esphome/components/mcp3221/mcp3221_sensor.cpp | 31 ++++++++++++ esphome/components/mcp3221/mcp3221_sensor.h | 28 +++++++++++ esphome/components/mcp3221/sensor.py | 49 +++++++++++++++++++ tests/components/mcp3221/common.yaml | 6 +++ .../components/mcp3221/test.esp32-c3-idf.yaml | 4 ++ tests/components/mcp3221/test.esp32-idf.yaml | 4 ++ .../components/mcp3221/test.esp32-s3-idf.yaml | 4 ++ .../components/mcp3221/test.esp8266-ard.yaml | 4 ++ tests/components/mcp3221/test.rp2040-ard.yaml | 4 ++ 11 files changed, 136 insertions(+) create mode 100644 esphome/components/mcp3221/__init__.py create mode 100644 esphome/components/mcp3221/mcp3221_sensor.cpp create mode 100644 esphome/components/mcp3221/mcp3221_sensor.h create mode 100644 esphome/components/mcp3221/sensor.py create mode 100644 tests/components/mcp3221/common.yaml create mode 100644 tests/components/mcp3221/test.esp32-c3-idf.yaml create mode 100644 tests/components/mcp3221/test.esp32-idf.yaml create mode 100644 tests/components/mcp3221/test.esp32-s3-idf.yaml create mode 100644 tests/components/mcp3221/test.esp8266-ard.yaml create mode 100644 tests/components/mcp3221/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 4d458eceb87..7e785db451f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -290,6 +290,7 @@ esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp3204/* @rsumner +esphome/components/mcp3221/* @philippderdiedas esphome/components/mcp4461/* @p1ngb4ck esphome/components/mcp4728/* @berfenger esphome/components/mcp47a1/* @jesserockz diff --git a/esphome/components/mcp3221/__init__.py b/esphome/components/mcp3221/__init__.py new file mode 100644 index 00000000000..677bb78c358 --- /dev/null +++ b/esphome/components/mcp3221/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@philippderdiedas"] diff --git a/esphome/components/mcp3221/mcp3221_sensor.cpp b/esphome/components/mcp3221/mcp3221_sensor.cpp new file mode 100644 index 00000000000..c04b1c0b935 --- /dev/null +++ b/esphome/components/mcp3221/mcp3221_sensor.cpp @@ -0,0 +1,31 @@ +#include "mcp3221_sensor.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3221 { + +static const char *const TAG = "mcp3221"; + +float MCP3221Sensor::sample() { + uint8_t data[2]; + if (this->read(data, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Read failed"); + this->status_set_warning(); + return NAN; + } + this->status_clear_warning(); + + uint16_t value = encode_uint16(data[0], data[1]); + float voltage = value * this->reference_voltage_ / 4096.0f; + + return voltage; +} + +void MCP3221Sensor::update() { + float v = this->sample(); + this->publish_state(v); +} + +} // namespace mcp3221 +} // namespace esphome diff --git a/esphome/components/mcp3221/mcp3221_sensor.h b/esphome/components/mcp3221/mcp3221_sensor.h new file mode 100644 index 00000000000..c83caccabfe --- /dev/null +++ b/esphome/components/mcp3221/mcp3221_sensor.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +#include + +namespace esphome { +namespace mcp3221 { + +class MCP3221Sensor : public sensor::Sensor, + public PollingComponent, + public voltage_sampler::VoltageSampler, + public i2c::I2CDevice { + public: + void set_reference_voltage(float reference_voltage) { this->reference_voltage_ = reference_voltage; } + void update() override; + float sample() override; + + protected: + float reference_voltage_; +}; + +} // namespace mcp3221 +} // namespace esphome diff --git a/esphome/components/mcp3221/sensor.py b/esphome/components/mcp3221/sensor.py new file mode 100644 index 00000000000..993876c2c83 --- /dev/null +++ b/esphome/components/mcp3221/sensor.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor, voltage_sampler +import esphome.config_validation as cv +from esphome.const import ( + CONF_REFERENCE_VOLTAGE, + DEVICE_CLASS_VOLTAGE, + ICON_SCALE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, +) + +AUTO_LOAD = ["voltage_sampler"] +DEPENDENCIES = ["i2c"] + + +mcp3221_ns = cg.esphome_ns.namespace("mcp3221") +MCP3221Sensor = mcp3221_ns.class_( + "MCP3221Sensor", + sensor.Sensor, + voltage_sampler.VoltageSampler, + cg.PollingComponent, + i2c.I2CDevice, +) + + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + MCP3221Sensor, + icon=ICON_SCALE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=UNIT_VOLT, + ) + .extend( + { + cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + cg.add(var.set_reference_voltage(config[CONF_REFERENCE_VOLTAGE])) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/tests/components/mcp3221/common.yaml b/tests/components/mcp3221/common.yaml new file mode 100644 index 00000000000..cc3eadbf4fd --- /dev/null +++ b/tests/components/mcp3221/common.yaml @@ -0,0 +1,6 @@ +sensor: + - platform: mcp3221 + id: test_id + name: voltage + i2c_id: i2c_bus + reference_voltage: 3.3V diff --git a/tests/components/mcp3221/test.esp32-c3-idf.yaml b/tests/components/mcp3221/test.esp32-c3-idf.yaml new file mode 100644 index 00000000000..9990d96d290 --- /dev/null +++ b/tests/components/mcp3221/test.esp32-c3-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/mcp3221/test.esp32-idf.yaml b/tests/components/mcp3221/test.esp32-idf.yaml new file mode 100644 index 00000000000..b47e39c3898 --- /dev/null +++ b/tests/components/mcp3221/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/mcp3221/test.esp32-s3-idf.yaml b/tests/components/mcp3221/test.esp32-s3-idf.yaml new file mode 100644 index 00000000000..0fd8684a2c0 --- /dev/null +++ b/tests/components/mcp3221/test.esp32-s3-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/mcp3221/test.esp8266-ard.yaml b/tests/components/mcp3221/test.esp8266-ard.yaml new file mode 100644 index 00000000000..4a98b9388ab --- /dev/null +++ b/tests/components/mcp3221/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/mcp3221/test.rp2040-ard.yaml b/tests/components/mcp3221/test.rp2040-ard.yaml new file mode 100644 index 00000000000..319a7c71a65 --- /dev/null +++ b/tests/components/mcp3221/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From d0b399d77167d5ab48e5aeb8fd275eef151ccfb5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 20:44:01 -0600 Subject: [PATCH 203/461] [ci] Reduce release time by removing 468 redundant ESP32-C3 IDF tests (#11737) --- .../components/a01nyub/test.esp32-c3-idf.yaml | 8 ------- .../components/a02yyuw/test.esp32-c3-idf.yaml | 8 ------- tests/components/a4988/test.esp32-c3-idf.yaml | 6 ------ .../absolute_humidity/test.esp32-c3-idf.yaml | 1 - .../adc128s102/test.esp32-c3-idf.yaml | 7 ------- .../components/ade7880/test.esp32-c3-idf.yaml | 9 -------- .../ade7953_i2c/test.esp32-c3-idf.yaml | 7 ------- .../ade7953_spi/test.esp32-c3-idf.yaml | 7 ------- .../components/ads1115/test.esp32-c3-idf.yaml | 4 ---- tests/components/ags10/test.esp32-c3-idf.yaml | 4 ---- tests/components/aht10/test.esp32-c3-idf.yaml | 4 ---- .../components/aic3204/test.esp32-c3-idf.yaml | 4 ---- .../test.esp32-c3-idf.yaml | 4 ---- .../test.esp32-c3-idf.yaml | 4 ---- .../test.esp32-c3-idf.yaml | 1 - .../components/alpha3/test.esp32-c3-idf.yaml | 4 ---- .../components/am2315c/test.esp32-c3-idf.yaml | 4 ---- .../components/am2320/test.esp32-c3-idf.yaml | 4 ---- tests/components/am43/test.esp32-c3-idf.yaml | 4 ---- .../analog_threshold/test.esp32-c3-idf.yaml | 1 - .../animation/test.esp32-c3-idf.yaml | 13 ------------ tests/components/anova/test.esp32-c3-idf.yaml | 4 ---- .../apds9306/test.esp32-c3-idf.yaml | 4 ---- .../apds9960/test.esp32-c3-idf.yaml | 4 ---- tests/components/api/test.esp32-c3-idf.yaml | 5 ----- .../as3935_i2c/test.esp32-c3-idf.yaml | 7 ------- .../as3935_spi/test.esp32-c3-idf.yaml | 7 ------- .../components/as5600/test.esp32-c3-idf.yaml | 7 ------- .../components/as7341/test.esp32-c3-idf.yaml | 4 ---- .../components/at581x/test.esp32-c3-idf.yaml | 4 ---- .../atc_mithermometer/test.esp32-c3-idf.yaml | 4 ---- .../atm90e26/test.esp32-c3-idf.yaml | 6 ------ .../atm90e32/test.esp32-c3-idf.yaml | 6 ------ .../axs15231/test.esp32-c3-idf.yaml | 4 ---- .../b_parasite/test.esp32-c3-idf.yaml | 4 ---- .../bang_bang/test.esp32-c3-idf.yaml | 1 - .../components/bedjet/test.esp32-c3-idf.yaml | 4 ---- .../components/bh1750/test.esp32-c3-idf.yaml | 4 ---- .../bh1900nux/test.esp32-c3-idf.yaml | 4 ---- .../binary_sensor/test.esp32-c3-idf.yaml | 2 -- .../binary_sensor_map/test.esp32-c3-idf.yaml | 1 - .../components/bl0906/test.esp32-c3-idf.yaml | 4 ---- .../components/bl0939/test.esp32-c3-idf.yaml | 5 ----- .../components/bl0940/test.esp32-c3-idf.yaml | 5 ----- .../components/bl0942/test.esp32-c3-idf.yaml | 5 ----- .../ble_client/test.esp32-c3-idf.yaml | 4 ---- .../ble_presence/test.esp32-c3-idf.yaml | 4 ---- .../ble_rssi/test.esp32-c3-idf.yaml | 4 ---- .../ble_scanner/test.esp32-c3-idf.yaml | 4 ---- .../bme280_i2c/test.esp32-c3-idf.yaml | 4 ---- .../bme280_spi/test.esp32-c3-idf.yaml | 6 ------ .../components/bme680/test.esp32-c3-idf.yaml | 4 ---- .../bme68x_bsec2_i2c/test.esp32-c3-idf.yaml | 4 ---- .../components/bmi160/test.esp32-c3-idf.yaml | 4 ---- .../components/bmp085/test.esp32-c3-idf.yaml | 4 ---- .../bmp280_i2c/test.esp32-c3-idf.yaml | 4 ---- .../bmp280_spi/test.esp32-c3-idf.yaml | 6 ------ .../bmp3xx_i2c/test.esp32-c3-idf.yaml | 4 ---- .../bmp3xx_spi/test.esp32-c3-idf.yaml | 6 ------ .../components/bmp581/test.esp32-c3-idf.yaml | 4 ---- .../bp1658cj/test.esp32-c3-idf.yaml | 5 ----- .../components/bp5758d/test.esp32-c3-idf.yaml | 5 ----- .../components/button/test.esp32-c3-idf.yaml | 1 - .../bytebuffer/test.esp32-c3-idf.yaml | 1 - .../components/cap1188/test.esp32-c3-idf.yaml | 7 ------- .../captive_portal/test.esp32-c3-idf.yaml | 1 - .../components/ccs811/test.esp32-c3-idf.yaml | 4 ---- .../cd74hc4067/test.esp32-c3-idf.yaml | 8 ------- .../components/ch422g/test.esp32-c3-idf.yaml | 4 ---- .../components/chsc6x/test.esp32-c3-idf.yaml | 20 ------------------ .../climate_ir_lg/test.esp32-c3-idf.yaml | 4 ---- .../components/cm1106/test.esp32-c3-idf.yaml | 5 ----- tests/components/color/test.esp32-c3-idf.yaml | 1 - .../color_temperature/test.esp32-c3-idf.yaml | 6 ------ .../combination/test.esp32-c3-idf.yaml | 1 - .../components/coolix/test.esp32-c3-idf.yaml | 4 ---- tests/components/copy/test.esp32-c3-idf.yaml | 5 ----- .../components/cs5460a/test.esp32-c3-idf.yaml | 6 ------ .../components/cse7761/test.esp32-c3-idf.yaml | 5 ----- .../components/cse7766/test.esp32-c3-idf.yaml | 5 ----- .../components/cst226/test.esp32-c3-idf.yaml | 11 ---------- .../components/cst816/test.esp32-c3-idf.yaml | 11 ---------- .../ct_clamp/test.esp32-c3-idf.yaml | 4 ---- .../current_based/test.esp32-c3-idf.yaml | 7 ------- tests/components/cwww/test.esp32-c3-idf.yaml | 17 --------------- .../components/dac7678/test.esp32-c3-idf.yaml | 4 ---- .../daikin_brc/test.esp32-c3-idf.yaml | 4 ---- .../dallas_temp/test.esp32-c3-idf.yaml | 7 ------- .../daly_bms/test.esp32-c3-idf.yaml | 5 ----- tests/components/debug/test.esp32-c3-idf.yaml | 1 - .../delonghi/test.esp32-c3-idf.yaml | 4 ---- .../dfplayer/test.esp32-c3-idf.yaml | 5 ----- .../dfrobot_sen0395/test.esp32-c3-idf.yaml | 5 ----- tests/components/dht/test.esp32-c3-idf.yaml | 1 - tests/components/dht12/test.esp32-c3-idf.yaml | 4 ---- .../components/dps310/test.esp32-c3-idf.yaml | 4 ---- .../components/ds1307/test.esp32-c3-idf.yaml | 4 ---- .../components/ds2484/test.esp32-c3-idf.yaml | 4 ---- .../duty_cycle/test.esp32-c3-idf.yaml | 1 - .../duty_time/test.esp32-c3-idf.yaml | 1 - tests/components/e131/test.esp32-c3-idf.yaml | 5 ----- tests/components/ee895/test.esp32-c3-idf.yaml | 4 ---- .../ektf2232/test.esp32-c3-idf.yaml | 9 -------- .../components/emc2101/test.esp32-c3-idf.yaml | 4 ---- .../components/endstop/test.esp32-c3-idf.yaml | 1 - .../ens160_i2c/test.esp32-c3-idf.yaml | 4 ---- .../ens160_spi/test.esp32-c3-idf.yaml | 6 ------ .../components/ens210/test.esp32-c3-idf.yaml | 4 ---- .../components/es7210/test.esp32-c3-idf.yaml | 4 ---- .../components/es7243e/test.esp32-c3-idf.yaml | 4 ---- .../components/es8156/test.esp32-c3-idf.yaml | 4 ---- .../components/es8311/test.esp32-c3-idf.yaml | 4 ---- .../components/es8388/test.esp32-c3-idf.yaml | 4 ---- .../components/esphome/test.esp32-c3-idf.yaml | 1 - tests/components/event/test.esp32-c3-idf.yaml | 1 - .../test.esp32-c3-idf.yaml | 4 ---- .../test.esp32-c3-idf.yaml | 1 - tests/components/ezo/test.esp32-c3-idf.yaml | 4 ---- .../components/ezo_pmp/test.esp32-c3-idf.yaml | 4 ---- .../factory_reset/test.esp32-c3-idf.yaml | 1 - .../feedback/test.esp32-c3-idf.yaml | 1 - .../fingerprint_grow/test.esp32-c3-idf.yaml | 6 ------ tests/components/font/test.esp32-c3-idf.yaml | 7 ------- .../components/fs3000/test.esp32-c3-idf.yaml | 4 ---- .../components/ft5x06/test.esp32-c3-idf.yaml | 7 ------- .../components/ft63x6/test.esp32-c3-idf.yaml | 8 ------- .../fujitsu_general/test.esp32-c3-idf.yaml | 4 ---- tests/components/gcja5/test.esp32-c3-idf.yaml | 4 ---- .../gl_r01_i2c/test.esp32-c3-idf.yaml | 4 ---- .../components/globals/test.esp32-c3-idf.yaml | 1 - .../gp2y1010au0f/test.esp32-c3-idf.yaml | 5 ----- .../components/gp8403/test.esp32-c3-idf.yaml | 4 ---- tests/components/gps/test.esp32-c3-idf.yaml | 5 ----- tests/components/graph/test.esp32-c3-idf.yaml | 7 ------- .../test.esp32-c3-idf.yaml | 7 ------- tests/components/gree/test.esp32-c3-idf.yaml | 4 ---- .../grove_gas_mc_v2/test.esp32-c3-idf.yaml | 4 ---- .../grove_tb6612fng/test.esp32-c3-idf.yaml | 4 ---- .../growatt_solar/test.esp32-c3-idf.yaml | 6 ------ tests/components/gt911/test.esp32-c3-idf.yaml | 9 -------- tests/components/haier/test.esp32-c3-idf.yaml | 5 ----- .../havells_solar/test.esp32-c3-idf.yaml | 6 ------ .../components/hbridge/test.esp32-c3-idf.yaml | 17 --------------- .../components/hdc1080/test.esp32-c3-idf.yaml | 4 ---- .../components/hdc2010/test.esp32-c3-idf.yaml | 4 ---- tests/components/he60r/test.esp32-c3-idf.yaml | 4 ---- .../hitachi_ac344/test.esp32-c3-idf.yaml | 4 ---- .../hitachi_ac424/test.esp32-c3-idf.yaml | 4 ---- .../components/hlw8012/test.esp32-c3-idf.yaml | 6 ------ .../components/hm3301/test.esp32-c3-idf.yaml | 4 ---- .../hmc5883l/test.esp32-c3-idf.yaml | 4 ---- .../homeassistant/test.esp32-c3-idf.yaml | 2 -- .../honeywell_hih_i2c/test.esp32-c3-idf.yaml | 4 ---- .../honeywellabp/test.esp32-c3-idf.yaml | 6 ------ .../honeywellabp2_i2c/test.esp32-c3-idf.yaml | 4 ---- .../hrxl_maxsonar_wr/test.esp32-c3-idf.yaml | 5 ----- .../components/hte501/test.esp32-c3-idf.yaml | 4 ---- .../http_request/test.esp32-c3-idf.yaml | 4 ---- .../components/htu21d/test.esp32-c3-idf.yaml | 4 ---- .../components/htu31d/test.esp32-c3-idf.yaml | 4 ---- tests/components/hx711/test.esp32-c3-idf.yaml | 5 ----- .../hydreon_rgxx/test.esp32-c3-idf.yaml | 5 ----- .../components/hyt271/test.esp32-c3-idf.yaml | 4 ---- tests/components/i2c/test.esp32-c3-idf.yaml | 4 ---- .../i2c_device/test.esp32-c3-idf.yaml | 4 ---- .../components/iaqcore/test.esp32-c3-idf.yaml | 4 ---- .../components/ili9xxx/test.esp32-c3-idf.yaml | 11 ---------- .../components/ina219/test.esp32-c3-idf.yaml | 4 ---- .../components/ina226/test.esp32-c3-idf.yaml | 4 ---- .../components/ina260/test.esp32-c3-idf.yaml | 4 ---- .../ina2xx_i2c/test.esp32-c3-idf.yaml | 4 ---- .../ina2xx_spi/test.esp32-c3-idf.yaml | 6 ------ .../components/ina3221/test.esp32-c3-idf.yaml | 4 ---- .../test.esp32-c3-idf.yaml | 4 ---- .../integration/test.esp32-c3-idf.yaml | 4 ---- .../interval/test.esp32-c3-idf.yaml | 1 - .../jsn_sr04t/test.esp32-c3-idf.yaml | 5 ----- .../key_collector/test.esp32-c3-idf.yaml | 7 ------- .../kmeteriso/test.esp32-c3-idf.yaml | 4 ---- .../components/kuntze/test.esp32-c3-idf.yaml | 6 ------ .../lc709203f/test.esp32-c3-idf.yaml | 4 ---- .../lcd_gpio/test.esp32-c3-idf.yaml | 9 -------- .../lcd_menu/test.esp32-c3-idf.yaml | 9 -------- .../lcd_pcf8574/test.esp32-c3-idf.yaml | 4 ---- .../components/ld2410/test.esp32-c3-idf.yaml | 8 ------- .../components/ld2412/test.esp32-c3-idf.yaml | 8 ------- .../components/ld2420/test.esp32-c3-idf.yaml | 5 ----- .../components/ld2450/test.esp32-c3-idf.yaml | 8 ------- tests/components/light/test.esp32-c3-idf.yaml | 21 ------------------- .../lilygo_t5_47/test.esp32-c3-idf.yaml | 8 ------- tests/components/lm75b/test.esp32-c3-idf.yaml | 4 ---- tests/components/lock/test.esp32-c3-idf.yaml | 1 - tests/components/lps22/test.esp32-c3-idf.yaml | 4 ---- .../components/ltr390/test.esp32-c3-idf.yaml | 4 ---- .../components/ltr501/test.esp32-c3-idf.yaml | 4 ---- .../ltr_als_ps/test.esp32-c3-idf.yaml | 4 ---- .../m5stack_8angle/test.esp32-c3-idf.yaml | 4 ---- .../components/mapping/test.esp32-c3-idf.yaml | 13 ------------ .../matrix_keypad/test.esp32-c3-idf.yaml | 15 ------------- .../max17043/test.esp32-c3-idf.yaml | 4 ---- .../max31855/test.esp32-c3-idf.yaml | 6 ------ .../max31856/test.esp32-c3-idf.yaml | 6 ------ .../max31865/test.esp32-c3-idf.yaml | 6 ------ .../max44009/test.esp32-c3-idf.yaml | 4 ---- .../components/max6675/test.esp32-c3-idf.yaml | 6 ------ .../components/max6956/test.esp32-c3-idf.yaml | 4 ---- .../components/max7219/test.esp32-c3-idf.yaml | 6 ------ .../max7219digit/test.esp32-c3-idf.yaml | 6 ------ .../components/max9611/test.esp32-c3-idf.yaml | 4 ---- .../mcp23008/test.esp32-c3-idf.yaml | 4 ---- .../mcp23016/test.esp32-c3-idf.yaml | 4 ---- .../mcp23017/test.esp32-c3-idf.yaml | 4 ---- .../mcp23s08/test.esp32-c3-idf.yaml | 7 ------- .../mcp23s17/test.esp32-c3-idf.yaml | 7 ------- .../components/mcp2515/test.esp32-c3-idf.yaml | 6 ------ .../components/mcp3008/test.esp32-c3-idf.yaml | 6 ------ .../components/mcp3204/test.esp32-c3-idf.yaml | 6 ------ .../components/mcp4461/test.esp32-c3-idf.yaml | 4 ---- .../components/mcp4725/test.esp32-c3-idf.yaml | 4 ---- .../components/mcp4728/test.esp32-c3-idf.yaml | 4 ---- .../components/mcp47a1/test.esp32-c3-idf.yaml | 4 ---- .../components/mcp9600/test.esp32-c3-idf.yaml | 4 ---- .../components/mcp9808/test.esp32-c3-idf.yaml | 4 ---- .../mdns/test-enabled.esp32-c3-idf.yaml | 1 - tests/components/mhz19/test.esp32-c3-idf.yaml | 5 ----- .../micronova/test.esp32-c3-idf.yaml | 7 ------- .../microphone/test.esp32-c3-idf.yaml | 7 ------- .../mics_4514/test.esp32-c3-idf.yaml | 4 ---- .../midea_ir/test.esp32-c3-idf.yaml | 4 ---- .../mitsubishi/test.esp32-c3-idf.yaml | 4 ---- tests/components/mixer/test.esp32-c3-idf.yaml | 7 ------- .../mlx90393/test.esp32-c3-idf.yaml | 4 ---- .../mlx90614/test.esp32-c3-idf.yaml | 4 ---- .../components/mmc5603/test.esp32-c3-idf.yaml | 4 ---- .../components/mmc5983/test.esp32-c3-idf.yaml | 4 ---- .../components/modbus/test.esp32-c3-idf.yaml | 6 ------ .../modbus_controller/test.esp32-c3-idf.yaml | 4 ---- .../monochromatic/test.esp32-c3-idf.yaml | 5 ----- .../mopeka_ble/test.esp32-c3-idf.yaml | 4 ---- .../mopeka_pro_check/test.esp32-c3-idf.yaml | 4 ---- .../mopeka_std_check/test.esp32-c3-idf.yaml | 4 ---- .../mpl3115a2/test.esp32-c3-idf.yaml | 4 ---- .../components/mpr121/test.esp32-c3-idf.yaml | 8 ------- .../components/mpu6050/test.esp32-c3-idf.yaml | 4 ---- .../components/mpu6886/test.esp32-c3-idf.yaml | 4 ---- tests/components/mqtt/test.esp32-c3-idf.yaml | 3 --- .../mqtt_subscribe/test.esp32-c3-idf.yaml | 1 - .../components/ms5611/test.esp32-c3-idf.yaml | 8 ------- .../components/msa3xx/test.esp32-c3-idf.yaml | 4 ---- .../components/my9231/test.esp32-c3-idf.yaml | 1 - .../components/nau7802/test.esp32-c3-idf.yaml | 4 ---- .../network/test-ipv6.esp32-c3-idf.yaml | 4 ---- .../components/network/test.esp32-c3-idf.yaml | 1 - .../components/nextion/test.esp32-c3-idf.yaml | 7 ------- .../components/noblex/test.esp32-c3-idf.yaml | 5 ----- tests/components/ntc/test.esp32-c3-idf.yaml | 4 ---- .../components/opt3001/test.esp32-c3-idf.yaml | 4 ---- tests/components/ota/test.esp32-c3-idf.yaml | 1 - .../components/output/test.esp32-c3-idf.yaml | 5 ----- .../packet_transport/test.esp32-c3-idf.yaml | 4 ---- .../pca6416a/test.esp32-c3-idf.yaml | 4 ---- .../components/pca9554/test.esp32-c3-idf.yaml | 4 ---- .../components/pca9685/test.esp32-c3-idf.yaml | 4 ---- .../components/pcd8544/test.esp32-c3-idf.yaml | 8 ------- .../pcf85063/test.esp32-c3-idf.yaml | 4 ---- .../components/pcf8563/test.esp32-c3-idf.yaml | 4 ---- .../components/pcf8574/test.esp32-c3-idf.yaml | 4 ---- tests/components/pid/test.esp32-c3-idf.yaml | 1 - .../pipsolar/test.esp32-c3-idf.yaml | 5 ----- .../components/pm1006/test.esp32-c3-idf.yaml | 5 ----- .../components/pm2005/test.esp32-c3-idf.yaml | 4 ---- .../pmsa003i/test.esp32-c3-idf.yaml | 4 ---- .../components/pmsx003/test.esp32-c3-idf.yaml | 5 ----- .../components/pmwcs3/test.esp32-c3-idf.yaml | 4 ---- .../pn532_i2c/test.esp32-c3-idf.yaml | 4 ---- .../pn532_spi/test.esp32-c3-idf.yaml | 6 ------ .../pn7150_i2c/test.esp32-c3-idf.yaml | 8 ------- .../pn7160_i2c/test.esp32-c3-idf.yaml | 8 ------- .../pn7160_spi/test.esp32-c3-idf.yaml | 8 ------- .../power_supply/test.esp32-c3-idf.yaml | 1 - .../prometheus/test.esp32-c3-idf.yaml | 7 ------- .../pulse_meter/test.esp32-c3-idf.yaml | 1 - .../pulse_width/test.esp32-c3-idf.yaml | 1 - .../pvvx_mithermometer/test.esp32-c3-idf.yaml | 4 ---- .../pylontech/test.esp32-c3-idf.yaml | 5 ----- .../pzem004t/test.esp32-c3-idf.yaml | 5 ----- .../components/pzemac/test.esp32-c3-idf.yaml | 5 ----- .../components/pzemdc/test.esp32-c3-idf.yaml | 5 ----- .../qmc5883l/test.esp32-c3-idf.yaml | 7 ------- .../components/qmp6988/test.esp32-c3-idf.yaml | 4 ---- .../components/qr_code/test.esp32-c3-idf.yaml | 8 ------- .../qwiic_pir/test.esp32-c3-idf.yaml | 4 ---- .../radon_eye_ble/test.esp32-c3-idf.yaml | 4 ---- .../radon_eye_rd200/test.esp32-c3-idf.yaml | 4 ---- .../rc522_i2c/test.esp32-c3-idf.yaml | 4 ---- .../rc522_spi/test.esp32-c3-idf.yaml | 6 ------ .../components/rdm6300/test.esp32-c3-idf.yaml | 5 ----- .../resampler/test.esp32-c3-idf.yaml | 7 ------- .../resistance/test.esp32-c3-idf.yaml | 4 ---- .../components/restart/test.esp32-c3-idf.yaml | 1 - .../rf_bridge/test.esp32-c3-idf.yaml | 4 ---- tests/components/rgb/test.esp32-c3-idf.yaml | 7 ------- tests/components/rgbct/test.esp32-c3-idf.yaml | 9 -------- tests/components/rgbw/test.esp32-c3-idf.yaml | 8 ------- tests/components/rgbww/test.esp32-c3-idf.yaml | 9 -------- .../rotary_encoder/test.esp32-c3-idf.yaml | 6 ------ tests/components/rtttl/test.esp32-c3-idf.yaml | 5 ----- .../ruuvi_ble/test.esp32-c3-idf.yaml | 4 ---- .../ruuvitag/test.esp32-c3-idf.yaml | 4 ---- .../safe_mode/test-enabled.esp32-c3-idf.yaml | 1 - tests/components/scd30/test.esp32-c3-idf.yaml | 4 ---- tests/components/scd4x/test.esp32-c3-idf.yaml | 4 ---- .../components/script/test.esp32-c3-idf.yaml | 1 - .../sdm_meter/test.esp32-c3-idf.yaml | 5 ----- tests/components/sdp3x/test.esp32-c3-idf.yaml | 4 ---- .../components/sds011/test.esp32-c3-idf.yaml | 5 ----- .../seeed_mr24hpc1/test.esp32-c3-idf.yaml | 4 ---- .../seeed_mr60bha2/test.esp32-c3-idf.yaml | 4 ---- .../seeed_mr60fda2/test.esp32-c3-idf.yaml | 4 ---- .../selec_meter/test.esp32-c3-idf.yaml | 7 ------- .../components/sen0321/test.esp32-c3-idf.yaml | 4 ---- .../sen21231/test.esp32-c3-idf.yaml | 4 ---- tests/components/sen5x/test.esp32-c3-idf.yaml | 4 ---- .../senseair/test.esp32-c3-idf.yaml | 5 ----- tests/components/servo/test.esp32-c3-idf.yaml | 5 ----- tests/components/sfa30/test.esp32-c3-idf.yaml | 4 ---- tests/components/sgp30/test.esp32-c3-idf.yaml | 4 ---- tests/components/sgp4x/test.esp32-c3-idf.yaml | 4 ---- .../components/sht3xd/test.esp32-c3-idf.yaml | 4 ---- tests/components/sht4x/test.esp32-c3-idf.yaml | 4 ---- tests/components/shtcx/test.esp32-c3-idf.yaml | 4 ---- .../shutdown/test.esp32-c3-idf.yaml | 1 - .../components/sim800l/test.esp32-c3-idf.yaml | 5 ----- .../slow_pwm/test.esp32-c3-idf.yaml | 1 - .../components/sm16716/test.esp32-c3-idf.yaml | 5 ----- .../components/sm2135/test.esp32-c3-idf.yaml | 5 ----- .../components/sm2235/test.esp32-c3-idf.yaml | 5 ----- .../components/sm2335/test.esp32-c3-idf.yaml | 5 ----- .../components/sm300d2/test.esp32-c3-idf.yaml | 5 ----- tests/components/sml/test.esp32-c3-idf.yaml | 5 ----- .../components/smt100/test.esp32-c3-idf.yaml | 5 ----- .../sn74hc165/test.esp32-c3-idf.yaml | 7 ------- .../sn74hc595/test.esp32-c3-idf.yaml | 12 ----------- tests/components/sntp/test.esp32-c3-idf.yaml | 1 - .../sonoff_d1/test.esp32-c3-idf.yaml | 4 ---- .../sound_level/test.esp32-c3-idf.yaml | 6 ------ .../speaker/audio_dac.esp32-c3-idf.yaml | 10 --------- .../components/speaker/test.esp32-c3-idf.yaml | 12 ----------- tests/components/speed/test.esp32-c3-idf.yaml | 5 ----- .../spi_device/test.esp32-c3-idf.yaml | 4 ---- .../spi_led_strip/test.esp32-c3-idf.yaml | 4 ---- .../sprinkler/test.esp32-c3-idf.yaml | 1 - tests/components/sps30/test.esp32-c3-idf.yaml | 4 ---- .../ssd1306_i2c/test.esp32-c3-idf.yaml | 7 ------- .../ssd1306_spi/test.esp32-c3-idf.yaml | 8 ------- .../ssd1322_spi/test.esp32-c3-idf.yaml | 8 ------- .../ssd1325_spi/test.esp32-c3-idf.yaml | 8 ------- .../ssd1327_i2c/test.esp32-c3-idf.yaml | 7 ------- .../ssd1327_spi/test.esp32-c3-idf.yaml | 8 ------- .../ssd1331_spi/test.esp32-c3-idf.yaml | 8 ------- .../ssd1351_spi/test.esp32-c3-idf.yaml | 8 ------- .../st7567_i2c/test.esp32-c3-idf.yaml | 7 ------- .../st7567_spi/test.esp32-c3-idf.yaml | 8 ------- .../components/st7735/test.esp32-c3-idf.yaml | 8 ------- .../components/st7789v/test.esp32-c3-idf.yaml | 10 --------- .../components/st7920/test.esp32-c3-idf.yaml | 6 ------ .../components/statsD/test.esp32-c3-idf.yaml | 2 -- .../components/status/test.esp32-c3-idf.yaml | 1 - .../status_led/test.esp32-c3-idf.yaml | 1 - .../components/stepper/test.esp32-c3-idf.yaml | 1 - tests/components/sts3x/test.esp32-c3-idf.yaml | 4 ---- tests/components/sun/test.esp32-c3-idf.yaml | 1 - .../sun_gtil2/test.esp32-c3-idf.yaml | 5 ----- .../components/switch/test.esp32-c3-idf.yaml | 2 -- .../components/sx126x/test.esp32-c3-idf.yaml | 10 --------- .../components/sx127x/test.esp32-c3-idf.yaml | 8 ------- .../components/sx1509/test.esp32-c3-idf.yaml | 4 ---- .../components/syslog/test.esp32-c3-idf.yaml | 1 - tests/components/t6615/test.esp32-c3-idf.yaml | 4 ---- tests/components/tc74/test.esp32-c3-idf.yaml | 4 ---- .../tca9548a/test.esp32-c3-idf.yaml | 4 ---- .../components/tca9555/test.esp32-c3-idf.yaml | 4 ---- .../components/tcl112/test.esp32-c3-idf.yaml | 4 ---- .../tcs34725/test.esp32-c3-idf.yaml | 4 ---- .../components/tee501/test.esp32-c3-idf.yaml | 4 ---- .../teleinfo/test.esp32-c3-idf.yaml | 5 ----- .../template/test.esp32-c3-idf.yaml | 2 -- .../thermostat/test.esp32-c3-idf.yaml | 1 - tests/components/time/test.esp32-c3-idf.yaml | 1 - .../time_based/test.esp32-c3-idf.yaml | 1 - .../tlc59208f/test.esp32-c3-idf.yaml | 4 ---- .../components/tlc5947/test.esp32-c3-idf.yaml | 7 ------- .../components/tlc5971/test.esp32-c3-idf.yaml | 6 ------ .../components/tm1621/test.esp32-c3-idf.yaml | 7 ------- .../components/tm1637/test.esp32-c3-idf.yaml | 5 ----- .../components/tm1638/test.esp32-c3-idf.yaml | 1 - .../components/tm1651/test.esp32-c3-idf.yaml | 1 - .../components/tmp102/test.esp32-c3-idf.yaml | 4 ---- .../components/tmp1075/test.esp32-c3-idf.yaml | 4 ---- .../components/tmp117/test.esp32-c3-idf.yaml | 4 ---- .../tof10120/test.esp32-c3-idf.yaml | 4 ---- .../tormatic/test.esp32-c3-idf.yaml | 5 ----- .../components/toshiba/test.esp32-c3-idf.yaml | 4 ---- .../toshiba/test_ras2819t.esp32-c3-idf.yaml | 5 ----- .../total_daily_energy/test.esp32-c3-idf.yaml | 6 ------ .../components/tsl2561/test.esp32-c3-idf.yaml | 4 ---- .../components/tsl2591/test.esp32-c3-idf.yaml | 4 ---- .../components/tt21100/test.esp32-c3-idf.yaml | 9 -------- .../ttp229_bsf/test.esp32-c3-idf.yaml | 8 ------- .../ttp229_lsf/test.esp32-c3-idf.yaml | 4 ---- tests/components/tuya/test.esp32-c3-idf.yaml | 6 ------ tests/components/tx20/test.esp32-c3-idf.yaml | 1 - tests/components/udp/test.esp32-c3-idf.yaml | 4 ---- .../ufire_ec/test.esp32-c3-idf.yaml | 4 ---- .../ufire_ise/test.esp32-c3-idf.yaml | 4 ---- .../components/uln2003/test.esp32-c3-idf.yaml | 7 ------- .../ultrasonic/test.esp32-c3-idf.yaml | 1 - .../uponor_smatrix/test.esp32-c3-idf.yaml | 5 ----- .../components/uptime/test.esp32-c3-idf.yaml | 1 - tests/components/vbus/test.esp32-c3-idf.yaml | 4 ---- .../veml3235/test.esp32-c3-idf.yaml | 4 ---- .../veml7700/test.esp32-c3-idf.yaml | 4 ---- .../components/version/test.esp32-c3-idf.yaml | 1 - .../components/vl53l0x/test.esp32-c3-idf.yaml | 4 ---- .../voice_assistant/test.esp32-c3-idf.yaml | 8 ------- .../wake_on_lan/test.esp32-c3-idf.yaml | 4 ---- .../waveshare_epaper/test.esp32-c3-idf.yaml | 9 -------- .../web_server/test.esp32-c3-idf.yaml | 1 - .../whirlpool/test.esp32-c3-idf.yaml | 4 ---- .../components/whynter/test.esp32-c3-idf.yaml | 4 ---- .../components/wiegand/test.esp32-c3-idf.yaml | 1 - tests/components/wifi/test.esp32-c3-idf.yaml | 1 - .../wifi_info/test.esp32-c3-idf.yaml | 4 ---- .../wifi_signal/test.esp32-c3-idf.yaml | 1 - .../wireguard/test.esp32-c3-idf.yaml | 4 ---- .../components/wl_134/test.esp32-c3-idf.yaml | 5 ----- tests/components/wts01/test.esp32-c3-idf.yaml | 5 ----- tests/components/x9c/test.esp32-c3-idf.yaml | 6 ------ .../xgzp68xx/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_ble/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_cgd1/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_cgdk2/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_cgg1/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_cgpr1/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_gcls002/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_hhccjcy01/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_hhccpot002/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_lywsd02/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_lywsdcgq/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_mhoc303/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_mhoc401/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_miscale/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_mjyd02yla/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_mue4094rt/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_wx08zm/test.esp32-c3-idf.yaml | 4 ---- .../xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml | 4 ---- .../components/xl9535/test.esp32-c3-idf.yaml | 4 ---- .../components/xpt2046/test.esp32-c3-idf.yaml | 10 --------- .../components/yashima/test.esp32-c3-idf.yaml | 4 ---- .../components/zhlt01/test.esp32-c3-idf.yaml | 4 ---- .../zio_ultrasonic/test.esp32-c3-idf.yaml | 4 ---- .../zwave_proxy/test.esp32-c3-idf.yaml | 5 ----- .../components/zyaura/test.esp32-c3-idf.yaml | 5 ----- 467 files changed, 2212 deletions(-) delete mode 100644 tests/components/a01nyub/test.esp32-c3-idf.yaml delete mode 100644 tests/components/a02yyuw/test.esp32-c3-idf.yaml delete mode 100644 tests/components/a4988/test.esp32-c3-idf.yaml delete mode 100644 tests/components/absolute_humidity/test.esp32-c3-idf.yaml delete mode 100644 tests/components/adc128s102/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ade7880/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ade7953_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ade7953_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ads1115/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ags10/test.esp32-c3-idf.yaml delete mode 100644 tests/components/aht10/test.esp32-c3-idf.yaml delete mode 100644 tests/components/aic3204/test.esp32-c3-idf.yaml delete mode 100644 tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml delete mode 100644 tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml delete mode 100644 tests/components/alarm_control_panel/test.esp32-c3-idf.yaml delete mode 100644 tests/components/alpha3/test.esp32-c3-idf.yaml delete mode 100644 tests/components/am2315c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/am2320/test.esp32-c3-idf.yaml delete mode 100644 tests/components/am43/test.esp32-c3-idf.yaml delete mode 100644 tests/components/analog_threshold/test.esp32-c3-idf.yaml delete mode 100644 tests/components/animation/test.esp32-c3-idf.yaml delete mode 100644 tests/components/anova/test.esp32-c3-idf.yaml delete mode 100644 tests/components/apds9306/test.esp32-c3-idf.yaml delete mode 100644 tests/components/apds9960/test.esp32-c3-idf.yaml delete mode 100644 tests/components/api/test.esp32-c3-idf.yaml delete mode 100644 tests/components/as3935_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/as3935_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/as5600/test.esp32-c3-idf.yaml delete mode 100644 tests/components/as7341/test.esp32-c3-idf.yaml delete mode 100644 tests/components/at581x/test.esp32-c3-idf.yaml delete mode 100644 tests/components/atc_mithermometer/test.esp32-c3-idf.yaml delete mode 100644 tests/components/atm90e26/test.esp32-c3-idf.yaml delete mode 100644 tests/components/atm90e32/test.esp32-c3-idf.yaml delete mode 100644 tests/components/axs15231/test.esp32-c3-idf.yaml delete mode 100644 tests/components/b_parasite/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bang_bang/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bedjet/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bh1750/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bh1900nux/test.esp32-c3-idf.yaml delete mode 100644 tests/components/binary_sensor/test.esp32-c3-idf.yaml delete mode 100644 tests/components/binary_sensor_map/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bl0906/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bl0939/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bl0940/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bl0942/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ble_client/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ble_presence/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ble_rssi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ble_scanner/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bme280_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bme280_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bme680/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bme68x_bsec2_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bmi160/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bmp085/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bmp280_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bmp280_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bmp581/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bp1658cj/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bp5758d/test.esp32-c3-idf.yaml delete mode 100644 tests/components/button/test.esp32-c3-idf.yaml delete mode 100644 tests/components/bytebuffer/test.esp32-c3-idf.yaml delete mode 100644 tests/components/cap1188/test.esp32-c3-idf.yaml delete mode 100644 tests/components/captive_portal/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ccs811/test.esp32-c3-idf.yaml delete mode 100644 tests/components/cd74hc4067/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ch422g/test.esp32-c3-idf.yaml delete mode 100644 tests/components/chsc6x/test.esp32-c3-idf.yaml delete mode 100644 tests/components/climate_ir_lg/test.esp32-c3-idf.yaml delete mode 100644 tests/components/cm1106/test.esp32-c3-idf.yaml delete mode 100644 tests/components/color/test.esp32-c3-idf.yaml delete mode 100644 tests/components/color_temperature/test.esp32-c3-idf.yaml delete mode 100644 tests/components/combination/test.esp32-c3-idf.yaml delete mode 100644 tests/components/coolix/test.esp32-c3-idf.yaml delete mode 100644 tests/components/copy/test.esp32-c3-idf.yaml delete mode 100644 tests/components/cs5460a/test.esp32-c3-idf.yaml delete mode 100644 tests/components/cse7761/test.esp32-c3-idf.yaml delete mode 100644 tests/components/cse7766/test.esp32-c3-idf.yaml delete mode 100644 tests/components/cst226/test.esp32-c3-idf.yaml delete mode 100644 tests/components/cst816/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ct_clamp/test.esp32-c3-idf.yaml delete mode 100644 tests/components/current_based/test.esp32-c3-idf.yaml delete mode 100644 tests/components/cwww/test.esp32-c3-idf.yaml delete mode 100644 tests/components/dac7678/test.esp32-c3-idf.yaml delete mode 100644 tests/components/daikin_brc/test.esp32-c3-idf.yaml delete mode 100644 tests/components/dallas_temp/test.esp32-c3-idf.yaml delete mode 100644 tests/components/daly_bms/test.esp32-c3-idf.yaml delete mode 100644 tests/components/debug/test.esp32-c3-idf.yaml delete mode 100644 tests/components/delonghi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/dfplayer/test.esp32-c3-idf.yaml delete mode 100644 tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml delete mode 100644 tests/components/dht/test.esp32-c3-idf.yaml delete mode 100644 tests/components/dht12/test.esp32-c3-idf.yaml delete mode 100644 tests/components/dps310/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ds1307/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ds2484/test.esp32-c3-idf.yaml delete mode 100644 tests/components/duty_cycle/test.esp32-c3-idf.yaml delete mode 100644 tests/components/duty_time/test.esp32-c3-idf.yaml delete mode 100644 tests/components/e131/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ee895/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ektf2232/test.esp32-c3-idf.yaml delete mode 100644 tests/components/emc2101/test.esp32-c3-idf.yaml delete mode 100644 tests/components/endstop/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ens160_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ens160_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ens210/test.esp32-c3-idf.yaml delete mode 100644 tests/components/es7210/test.esp32-c3-idf.yaml delete mode 100644 tests/components/es7243e/test.esp32-c3-idf.yaml delete mode 100644 tests/components/es8156/test.esp32-c3-idf.yaml delete mode 100644 tests/components/es8311/test.esp32-c3-idf.yaml delete mode 100644 tests/components/es8388/test.esp32-c3-idf.yaml delete mode 100644 tests/components/esphome/test.esp32-c3-idf.yaml delete mode 100644 tests/components/event/test.esp32-c3-idf.yaml delete mode 100644 tests/components/exposure_notifications/test.esp32-c3-idf.yaml delete mode 100644 tests/components/external_components/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ezo/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ezo_pmp/test.esp32-c3-idf.yaml delete mode 100644 tests/components/factory_reset/test.esp32-c3-idf.yaml delete mode 100644 tests/components/feedback/test.esp32-c3-idf.yaml delete mode 100644 tests/components/fingerprint_grow/test.esp32-c3-idf.yaml delete mode 100644 tests/components/font/test.esp32-c3-idf.yaml delete mode 100644 tests/components/fs3000/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ft5x06/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ft63x6/test.esp32-c3-idf.yaml delete mode 100644 tests/components/fujitsu_general/test.esp32-c3-idf.yaml delete mode 100644 tests/components/gcja5/test.esp32-c3-idf.yaml delete mode 100644 tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/globals/test.esp32-c3-idf.yaml delete mode 100644 tests/components/gp2y1010au0f/test.esp32-c3-idf.yaml delete mode 100644 tests/components/gp8403/test.esp32-c3-idf.yaml delete mode 100644 tests/components/gps/test.esp32-c3-idf.yaml delete mode 100644 tests/components/graph/test.esp32-c3-idf.yaml delete mode 100644 tests/components/graphical_display_menu/test.esp32-c3-idf.yaml delete mode 100644 tests/components/gree/test.esp32-c3-idf.yaml delete mode 100644 tests/components/grove_gas_mc_v2/test.esp32-c3-idf.yaml delete mode 100644 tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml delete mode 100644 tests/components/growatt_solar/test.esp32-c3-idf.yaml delete mode 100644 tests/components/gt911/test.esp32-c3-idf.yaml delete mode 100644 tests/components/haier/test.esp32-c3-idf.yaml delete mode 100644 tests/components/havells_solar/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hbridge/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hdc1080/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hdc2010/test.esp32-c3-idf.yaml delete mode 100644 tests/components/he60r/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hitachi_ac344/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hitachi_ac424/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hlw8012/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hm3301/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hmc5883l/test.esp32-c3-idf.yaml delete mode 100644 tests/components/homeassistant/test.esp32-c3-idf.yaml delete mode 100644 tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/honeywellabp/test.esp32-c3-idf.yaml delete mode 100644 tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hte501/test.esp32-c3-idf.yaml delete mode 100644 tests/components/http_request/test.esp32-c3-idf.yaml delete mode 100644 tests/components/htu21d/test.esp32-c3-idf.yaml delete mode 100644 tests/components/htu31d/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hx711/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml delete mode 100644 tests/components/hyt271/test.esp32-c3-idf.yaml delete mode 100644 tests/components/i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/i2c_device/test.esp32-c3-idf.yaml delete mode 100644 tests/components/iaqcore/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ili9xxx/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ina219/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ina226/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ina260/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ina2xx_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ina3221/test.esp32-c3-idf.yaml delete mode 100644 tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml delete mode 100644 tests/components/integration/test.esp32-c3-idf.yaml delete mode 100644 tests/components/interval/test.esp32-c3-idf.yaml delete mode 100644 tests/components/jsn_sr04t/test.esp32-c3-idf.yaml delete mode 100644 tests/components/key_collector/test.esp32-c3-idf.yaml delete mode 100644 tests/components/kmeteriso/test.esp32-c3-idf.yaml delete mode 100644 tests/components/kuntze/test.esp32-c3-idf.yaml delete mode 100644 tests/components/lc709203f/test.esp32-c3-idf.yaml delete mode 100644 tests/components/lcd_gpio/test.esp32-c3-idf.yaml delete mode 100644 tests/components/lcd_menu/test.esp32-c3-idf.yaml delete mode 100644 tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ld2410/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ld2412/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ld2420/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ld2450/test.esp32-c3-idf.yaml delete mode 100644 tests/components/light/test.esp32-c3-idf.yaml delete mode 100644 tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml delete mode 100644 tests/components/lm75b/test.esp32-c3-idf.yaml delete mode 100644 tests/components/lock/test.esp32-c3-idf.yaml delete mode 100644 tests/components/lps22/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ltr390/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ltr501/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ltr_als_ps/test.esp32-c3-idf.yaml delete mode 100644 tests/components/m5stack_8angle/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mapping/test.esp32-c3-idf.yaml delete mode 100644 tests/components/matrix_keypad/test.esp32-c3-idf.yaml delete mode 100644 tests/components/max17043/test.esp32-c3-idf.yaml delete mode 100644 tests/components/max31855/test.esp32-c3-idf.yaml delete mode 100644 tests/components/max31856/test.esp32-c3-idf.yaml delete mode 100644 tests/components/max31865/test.esp32-c3-idf.yaml delete mode 100644 tests/components/max44009/test.esp32-c3-idf.yaml delete mode 100644 tests/components/max6675/test.esp32-c3-idf.yaml delete mode 100644 tests/components/max6956/test.esp32-c3-idf.yaml delete mode 100644 tests/components/max7219/test.esp32-c3-idf.yaml delete mode 100644 tests/components/max7219digit/test.esp32-c3-idf.yaml delete mode 100644 tests/components/max9611/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp23008/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp23016/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp23017/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp23s08/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp23s17/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp2515/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp3008/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp3204/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp4461/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp4725/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp4728/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp47a1/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp9600/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mcp9808/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mdns/test-enabled.esp32-c3-idf.yaml delete mode 100644 tests/components/mhz19/test.esp32-c3-idf.yaml delete mode 100644 tests/components/micronova/test.esp32-c3-idf.yaml delete mode 100644 tests/components/microphone/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mics_4514/test.esp32-c3-idf.yaml delete mode 100644 tests/components/midea_ir/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mitsubishi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mixer/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mlx90393/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mlx90614/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mmc5603/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mmc5983/test.esp32-c3-idf.yaml delete mode 100644 tests/components/modbus/test.esp32-c3-idf.yaml delete mode 100644 tests/components/modbus_controller/test.esp32-c3-idf.yaml delete mode 100644 tests/components/monochromatic/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mopeka_ble/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mopeka_std_check/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mpl3115a2/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mpr121/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mpu6050/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mpu6886/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mqtt/test.esp32-c3-idf.yaml delete mode 100644 tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ms5611/test.esp32-c3-idf.yaml delete mode 100644 tests/components/msa3xx/test.esp32-c3-idf.yaml delete mode 100644 tests/components/my9231/test.esp32-c3-idf.yaml delete mode 100644 tests/components/nau7802/test.esp32-c3-idf.yaml delete mode 100644 tests/components/network/test-ipv6.esp32-c3-idf.yaml delete mode 100644 tests/components/network/test.esp32-c3-idf.yaml delete mode 100644 tests/components/nextion/test.esp32-c3-idf.yaml delete mode 100644 tests/components/noblex/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ntc/test.esp32-c3-idf.yaml delete mode 100644 tests/components/opt3001/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ota/test.esp32-c3-idf.yaml delete mode 100644 tests/components/output/test.esp32-c3-idf.yaml delete mode 100644 tests/components/packet_transport/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pca6416a/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pca9554/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pca9685/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pcd8544/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pcf85063/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pcf8563/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pcf8574/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pid/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pipsolar/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pm1006/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pm2005/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pmsa003i/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pmsx003/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pmwcs3/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pn532_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pn532_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pn7150_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pn7160_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pn7160_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/power_supply/test.esp32-c3-idf.yaml delete mode 100644 tests/components/prometheus/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pulse_meter/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pulse_width/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pylontech/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pzem004t/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pzemac/test.esp32-c3-idf.yaml delete mode 100644 tests/components/pzemdc/test.esp32-c3-idf.yaml delete mode 100644 tests/components/qmc5883l/test.esp32-c3-idf.yaml delete mode 100644 tests/components/qmp6988/test.esp32-c3-idf.yaml delete mode 100644 tests/components/qr_code/test.esp32-c3-idf.yaml delete mode 100644 tests/components/qwiic_pir/test.esp32-c3-idf.yaml delete mode 100644 tests/components/radon_eye_ble/test.esp32-c3-idf.yaml delete mode 100644 tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml delete mode 100644 tests/components/rc522_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/rc522_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/rdm6300/test.esp32-c3-idf.yaml delete mode 100644 tests/components/resampler/test.esp32-c3-idf.yaml delete mode 100644 tests/components/resistance/test.esp32-c3-idf.yaml delete mode 100644 tests/components/restart/test.esp32-c3-idf.yaml delete mode 100644 tests/components/rf_bridge/test.esp32-c3-idf.yaml delete mode 100644 tests/components/rgb/test.esp32-c3-idf.yaml delete mode 100644 tests/components/rgbct/test.esp32-c3-idf.yaml delete mode 100644 tests/components/rgbw/test.esp32-c3-idf.yaml delete mode 100644 tests/components/rgbww/test.esp32-c3-idf.yaml delete mode 100644 tests/components/rotary_encoder/test.esp32-c3-idf.yaml delete mode 100644 tests/components/rtttl/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ruuvi_ble/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ruuvitag/test.esp32-c3-idf.yaml delete mode 100644 tests/components/safe_mode/test-enabled.esp32-c3-idf.yaml delete mode 100644 tests/components/scd30/test.esp32-c3-idf.yaml delete mode 100644 tests/components/scd4x/test.esp32-c3-idf.yaml delete mode 100644 tests/components/script/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sdm_meter/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sdp3x/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sds011/test.esp32-c3-idf.yaml delete mode 100644 tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml delete mode 100644 tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml delete mode 100644 tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml delete mode 100644 tests/components/selec_meter/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sen0321/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sen21231/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sen5x/test.esp32-c3-idf.yaml delete mode 100644 tests/components/senseair/test.esp32-c3-idf.yaml delete mode 100644 tests/components/servo/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sfa30/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sgp30/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sgp4x/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sht3xd/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sht4x/test.esp32-c3-idf.yaml delete mode 100644 tests/components/shtcx/test.esp32-c3-idf.yaml delete mode 100644 tests/components/shutdown/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sim800l/test.esp32-c3-idf.yaml delete mode 100644 tests/components/slow_pwm/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sm16716/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sm2135/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sm2235/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sm2335/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sm300d2/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sml/test.esp32-c3-idf.yaml delete mode 100644 tests/components/smt100/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sn74hc165/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sn74hc595/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sntp/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sonoff_d1/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sound_level/test.esp32-c3-idf.yaml delete mode 100644 tests/components/speaker/audio_dac.esp32-c3-idf.yaml delete mode 100644 tests/components/speaker/test.esp32-c3-idf.yaml delete mode 100644 tests/components/speed/test.esp32-c3-idf.yaml delete mode 100644 tests/components/spi_device/test.esp32-c3-idf.yaml delete mode 100644 tests/components/spi_led_strip/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sprinkler/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sps30/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ssd1306_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ssd1322_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ssd1325_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ssd1327_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ssd1331_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ssd1351_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/st7567_i2c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/st7567_spi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/st7735/test.esp32-c3-idf.yaml delete mode 100644 tests/components/st7789v/test.esp32-c3-idf.yaml delete mode 100644 tests/components/st7920/test.esp32-c3-idf.yaml delete mode 100644 tests/components/statsD/test.esp32-c3-idf.yaml delete mode 100644 tests/components/status/test.esp32-c3-idf.yaml delete mode 100644 tests/components/status_led/test.esp32-c3-idf.yaml delete mode 100644 tests/components/stepper/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sts3x/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sun/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sun_gtil2/test.esp32-c3-idf.yaml delete mode 100644 tests/components/switch/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sx126x/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sx127x/test.esp32-c3-idf.yaml delete mode 100644 tests/components/sx1509/test.esp32-c3-idf.yaml delete mode 100644 tests/components/syslog/test.esp32-c3-idf.yaml delete mode 100644 tests/components/t6615/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tc74/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tca9548a/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tca9555/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tcl112/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tcs34725/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tee501/test.esp32-c3-idf.yaml delete mode 100644 tests/components/teleinfo/test.esp32-c3-idf.yaml delete mode 100644 tests/components/template/test.esp32-c3-idf.yaml delete mode 100644 tests/components/thermostat/test.esp32-c3-idf.yaml delete mode 100644 tests/components/time/test.esp32-c3-idf.yaml delete mode 100644 tests/components/time_based/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tlc59208f/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tlc5947/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tlc5971/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tm1621/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tm1637/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tm1638/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tm1651/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tmp102/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tmp1075/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tmp117/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tof10120/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tormatic/test.esp32-c3-idf.yaml delete mode 100644 tests/components/toshiba/test.esp32-c3-idf.yaml delete mode 100644 tests/components/toshiba/test_ras2819t.esp32-c3-idf.yaml delete mode 100644 tests/components/total_daily_energy/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tsl2561/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tsl2591/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tt21100/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ttp229_bsf/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ttp229_lsf/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tuya/test.esp32-c3-idf.yaml delete mode 100644 tests/components/tx20/test.esp32-c3-idf.yaml delete mode 100644 tests/components/udp/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ufire_ec/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ufire_ise/test.esp32-c3-idf.yaml delete mode 100644 tests/components/uln2003/test.esp32-c3-idf.yaml delete mode 100644 tests/components/ultrasonic/test.esp32-c3-idf.yaml delete mode 100644 tests/components/uponor_smatrix/test.esp32-c3-idf.yaml delete mode 100644 tests/components/uptime/test.esp32-c3-idf.yaml delete mode 100644 tests/components/vbus/test.esp32-c3-idf.yaml delete mode 100644 tests/components/veml3235/test.esp32-c3-idf.yaml delete mode 100644 tests/components/veml7700/test.esp32-c3-idf.yaml delete mode 100644 tests/components/version/test.esp32-c3-idf.yaml delete mode 100644 tests/components/vl53l0x/test.esp32-c3-idf.yaml delete mode 100644 tests/components/voice_assistant/test.esp32-c3-idf.yaml delete mode 100644 tests/components/wake_on_lan/test.esp32-c3-idf.yaml delete mode 100644 tests/components/waveshare_epaper/test.esp32-c3-idf.yaml delete mode 100644 tests/components/web_server/test.esp32-c3-idf.yaml delete mode 100644 tests/components/whirlpool/test.esp32-c3-idf.yaml delete mode 100644 tests/components/whynter/test.esp32-c3-idf.yaml delete mode 100644 tests/components/wiegand/test.esp32-c3-idf.yaml delete mode 100644 tests/components/wifi/test.esp32-c3-idf.yaml delete mode 100644 tests/components/wifi_info/test.esp32-c3-idf.yaml delete mode 100644 tests/components/wifi_signal/test.esp32-c3-idf.yaml delete mode 100644 tests/components/wireguard/test.esp32-c3-idf.yaml delete mode 100644 tests/components/wl_134/test.esp32-c3-idf.yaml delete mode 100644 tests/components/wts01/test.esp32-c3-idf.yaml delete mode 100644 tests/components/x9c/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xgzp68xx/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_ble/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xl9535/test.esp32-c3-idf.yaml delete mode 100644 tests/components/xpt2046/test.esp32-c3-idf.yaml delete mode 100644 tests/components/yashima/test.esp32-c3-idf.yaml delete mode 100644 tests/components/zhlt01/test.esp32-c3-idf.yaml delete mode 100644 tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml delete mode 100644 tests/components/zwave_proxy/test.esp32-c3-idf.yaml delete mode 100644 tests/components/zyaura/test.esp32-c3-idf.yaml diff --git a/tests/components/a01nyub/test.esp32-c3-idf.yaml b/tests/components/a01nyub/test.esp32-c3-idf.yaml deleted file mode 100644 index 2cda8deaf9f..00000000000 --- a/tests/components/a01nyub/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/a02yyuw/test.esp32-c3-idf.yaml b/tests/components/a02yyuw/test.esp32-c3-idf.yaml deleted file mode 100644 index 2cda8deaf9f..00000000000 --- a/tests/components/a02yyuw/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/a4988/test.esp32-c3-idf.yaml b/tests/components/a4988/test.esp32-c3-idf.yaml deleted file mode 100644 index 25caba75b5e..00000000000 --- a/tests/components/a4988/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - step_pin: GPIO2 - dir_pin: GPIO3 - sleep_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/absolute_humidity/test.esp32-c3-idf.yaml b/tests/components/absolute_humidity/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/absolute_humidity/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/adc128s102/test.esp32-c3-idf.yaml b/tests/components/adc128s102/test.esp32-c3-idf.yaml deleted file mode 100644 index a60568a736c..00000000000 --- a/tests/components/adc128s102/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - cs_pin: GPIO2 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp32-c3-idf.yaml b/tests/components/ade7880/test.esp32-c3-idf.yaml deleted file mode 100644 index 7d5b41fc5ae..00000000000 --- a/tests/components/ade7880/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - irq0_pin: GPIO6 - irq1_pin: GPIO7 - reset_pin: GPIO9 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml b/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 59296a1e6ea..00000000000 --- a/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - irq_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ade7953_spi/test.esp32-c3-idf.yaml b/tests/components/ade7953_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 5e7e2dc82c0..00000000000 --- a/tests/components/ade7953_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - irq_pin: GPIO9 - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ads1115/test.esp32-c3-idf.yaml b/tests/components/ads1115/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ads1115/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ags10/test.esp32-c3-idf.yaml b/tests/components/ags10/test.esp32-c3-idf.yaml deleted file mode 100644 index 72703301a10..00000000000 --- a/tests/components/ags10/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/aht10/test.esp32-c3-idf.yaml b/tests/components/aht10/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/aht10/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/aic3204/test.esp32-c3-idf.yaml b/tests/components/aic3204/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/aic3204/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml b/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml b/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/alarm_control_panel/test.esp32-c3-idf.yaml b/tests/components/alarm_control_panel/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/alarm_control_panel/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/alpha3/test.esp32-c3-idf.yaml b/tests/components/alpha3/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/alpha3/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/am2315c/test.esp32-c3-idf.yaml b/tests/components/am2315c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/am2315c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/am2320/test.esp32-c3-idf.yaml b/tests/components/am2320/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/am2320/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/am43/test.esp32-c3-idf.yaml b/tests/components/am43/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/am43/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/analog_threshold/test.esp32-c3-idf.yaml b/tests/components/analog_threshold/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/analog_threshold/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/animation/test.esp32-c3-idf.yaml b/tests/components/animation/test.esp32-c3-idf.yaml deleted file mode 100644 index a08a6833337..00000000000 --- a/tests/components/animation/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,13 +0,0 @@ -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - animation: !include common.yaml - -display: - - platform: ili9xxx - id: main_lcd - spi_id: spi_bus - model: ili9342 - cs_pin: 8 - dc_pin: 9 - reset_pin: 10 - invert_colors: false diff --git a/tests/components/anova/test.esp32-c3-idf.yaml b/tests/components/anova/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/anova/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/apds9306/test.esp32-c3-idf.yaml b/tests/components/apds9306/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/apds9306/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/apds9960/test.esp32-c3-idf.yaml b/tests/components/apds9960/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/apds9960/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/api/test.esp32-c3-idf.yaml b/tests/components/api/test.esp32-c3-idf.yaml deleted file mode 100644 index 46c01d926f2..00000000000 --- a/tests/components/api/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -<<: !include common.yaml - -wifi: - ssid: MySSID - password: password1 diff --git a/tests/components/as3935_i2c/test.esp32-c3-idf.yaml b/tests/components/as3935_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 59296a1e6ea..00000000000 --- a/tests/components/as3935_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - irq_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/as3935_spi/test.esp32-c3-idf.yaml b/tests/components/as3935_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 5e7e2dc82c0..00000000000 --- a/tests/components/as3935_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - irq_pin: GPIO9 - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/as5600/test.esp32-c3-idf.yaml b/tests/components/as5600/test.esp32-c3-idf.yaml deleted file mode 100644 index 03a87ed6c48..00000000000 --- a/tests/components/as5600/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - dir_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/as7341/test.esp32-c3-idf.yaml b/tests/components/as7341/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/as7341/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/at581x/test.esp32-c3-idf.yaml b/tests/components/at581x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/at581x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml b/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/atm90e26/test.esp32-c3-idf.yaml b/tests/components/atm90e26/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/atm90e26/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/atm90e32/test.esp32-c3-idf.yaml b/tests/components/atm90e32/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/atm90e32/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/axs15231/test.esp32-c3-idf.yaml b/tests/components/axs15231/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/axs15231/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/b_parasite/test.esp32-c3-idf.yaml b/tests/components/b_parasite/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/b_parasite/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bang_bang/test.esp32-c3-idf.yaml b/tests/components/bang_bang/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/bang_bang/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/bedjet/test.esp32-c3-idf.yaml b/tests/components/bedjet/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/bedjet/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bh1750/test.esp32-c3-idf.yaml b/tests/components/bh1750/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bh1750/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bh1900nux/test.esp32-c3-idf.yaml b/tests/components/bh1900nux/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bh1900nux/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-c3-idf.yaml b/tests/components/binary_sensor/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/binary_sensor/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml b/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp32-c3-idf.yaml b/tests/components/bl0906/test.esp32-c3-idf.yaml deleted file mode 100644 index 147d967dd43..00000000000 --- a/tests/components/bl0906/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart_19200: !include ../../test_build_components/common/uart_19200/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bl0939/test.esp32-c3-idf.yaml b/tests/components/bl0939/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/bl0939/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bl0940/test.esp32-c3-idf.yaml b/tests/components/bl0940/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/bl0940/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bl0942/test.esp32-c3-idf.yaml b/tests/components/bl0942/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/bl0942/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ble_client/test.esp32-c3-idf.yaml b/tests/components/ble_client/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/ble_client/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ble_presence/test.esp32-c3-idf.yaml b/tests/components/ble_presence/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/ble_presence/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ble_rssi/test.esp32-c3-idf.yaml b/tests/components/ble_rssi/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/ble_rssi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ble_scanner/test.esp32-c3-idf.yaml b/tests/components/ble_scanner/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/ble_scanner/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bme280_i2c/test.esp32-c3-idf.yaml b/tests/components/bme280_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bme280_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bme280_spi/test.esp32-c3-idf.yaml b/tests/components/bme280_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/bme280_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bme680/test.esp32-c3-idf.yaml b/tests/components/bme680/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bme680/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-c3-idf.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmi160/test.esp32-c3-idf.yaml b/tests/components/bmi160/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bmi160/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmp085/test.esp32-c3-idf.yaml b/tests/components/bmp085/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bmp085/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml b/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp32-c3-idf.yaml b/tests/components/bmp280_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/bmp280_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml b/tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml b/tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmp581/test.esp32-c3-idf.yaml b/tests/components/bmp581/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bmp581/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bp1658cj/test.esp32-c3-idf.yaml b/tests/components/bp1658cj/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/bp1658cj/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bp5758d/test.esp32-c3-idf.yaml b/tests/components/bp5758d/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/bp5758d/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/button/test.esp32-c3-idf.yaml b/tests/components/button/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/button/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/bytebuffer/test.esp32-c3-idf.yaml b/tests/components/bytebuffer/test.esp32-c3-idf.yaml deleted file mode 100644 index 380ca876286..00000000000 --- a/tests/components/bytebuffer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -!include common.yaml diff --git a/tests/components/cap1188/test.esp32-c3-idf.yaml b/tests/components/cap1188/test.esp32-c3-idf.yaml deleted file mode 100644 index c97f30d52c1..00000000000 --- a/tests/components/cap1188/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/captive_portal/test.esp32-c3-idf.yaml b/tests/components/captive_portal/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/captive_portal/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ccs811/test.esp32-c3-idf.yaml b/tests/components/ccs811/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ccs811/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cd74hc4067/test.esp32-c3-idf.yaml b/tests/components/cd74hc4067/test.esp32-c3-idf.yaml deleted file mode 100644 index 5e8784c1fc5..00000000000 --- a/tests/components/cd74hc4067/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - pin_s0: GPIO2 - pin_s1: GPIO3 - pin_s2: GPIO4 - pin_s3: GPIO5 - pin: GPIO0 - -<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp32-c3-idf.yaml b/tests/components/ch422g/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ch422g/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/chsc6x/test.esp32-c3-idf.yaml b/tests/components/chsc6x/test.esp32-c3-idf.yaml deleted file mode 100644 index f0de4107d79..00000000000 --- a/tests/components/chsc6x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,20 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -display: - - platform: ili9xxx - id: ili9xxx_display - model: GC9A01A - invert_colors: True - cs_pin: 11 - dc_pin: 7 - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - -touchscreen: - - platform: chsc6x - display: ili9xxx_display - interrupt_pin: 20 diff --git a/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml b/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cm1106/test.esp32-c3-idf.yaml b/tests/components/cm1106/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/cm1106/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/color/test.esp32-c3-idf.yaml b/tests/components/color/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/color/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/color_temperature/test.esp32-c3-idf.yaml b/tests/components/color_temperature/test.esp32-c3-idf.yaml deleted file mode 100644 index 016f315d9f1..00000000000 --- a/tests/components/color_temperature/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - light_platform: ledc - pin_o1: GPIO6 - pin_o2: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/combination/test.esp32-c3-idf.yaml b/tests/components/combination/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/combination/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/coolix/test.esp32-c3-idf.yaml b/tests/components/coolix/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/coolix/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/copy/test.esp32-c3-idf.yaml b/tests/components/copy/test.esp32-c3-idf.yaml deleted file mode 100644 index 76272beb776..00000000000 --- a/tests/components/copy/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - pwm_platform: ledc - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/cs5460a/test.esp32-c3-idf.yaml b/tests/components/cs5460a/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/cs5460a/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cse7761/test.esp32-c3-idf.yaml b/tests/components/cse7761/test.esp32-c3-idf.yaml deleted file mode 100644 index 4e11c6e7cbb..00000000000 --- a/tests/components/cse7761/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart_38400: !include ../../test_build_components/common/uart_38400/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cse7766/test.esp32-c3-idf.yaml b/tests/components/cse7766/test.esp32-c3-idf.yaml deleted file mode 100644 index dc95c985c7a..00000000000 --- a/tests/components/cse7766/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart_4800_even: !include ../../test_build_components/common/uart_4800_even/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cst226/test.esp32-c3-idf.yaml b/tests/components/cst226/test.esp32-c3-idf.yaml deleted file mode 100644 index ffc12867d0f..00000000000 --- a/tests/components/cst226/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - cs_pin: GPIO7 - dc_pin: GPIO9 - disp_reset_pin: GPIO18 - interrupt_pin: GPIO2 - reset_pin: GPIO3 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cst816/test.esp32-c3-idf.yaml b/tests/components/cst816/test.esp32-c3-idf.yaml deleted file mode 100644 index ffc12867d0f..00000000000 --- a/tests/components/cst816/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - cs_pin: GPIO7 - dc_pin: GPIO9 - disp_reset_pin: GPIO18 - interrupt_pin: GPIO2 - reset_pin: GPIO3 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ct_clamp/test.esp32-c3-idf.yaml b/tests/components/ct_clamp/test.esp32-c3-idf.yaml deleted file mode 100644 index a8f29c98ae8..00000000000 --- a/tests/components/ct_clamp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO0 - -<<: !include common.yaml diff --git a/tests/components/current_based/test.esp32-c3-idf.yaml b/tests/components/current_based/test.esp32-c3-idf.yaml deleted file mode 100644 index 59296a1e6ea..00000000000 --- a/tests/components/current_based/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - irq_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cwww/test.esp32-c3-idf.yaml b/tests/components/cwww/test.esp32-c3-idf.yaml deleted file mode 100644 index 51571b34cfe..00000000000 --- a/tests/components/cwww/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,17 +0,0 @@ -substitutions: - light_platform: ledc - pin_o1: GPIO6 - pin_o2: GPIO7 - -output: - - platform: ${light_platform} - id: light_output_1 - pin: ${pin_o1} - channel: 0 - - platform: ${light_platform} - id: light_output_2 - pin: ${pin_o2} - channel: 1 - phase_angle: 180° - -<<: !include common.yaml diff --git a/tests/components/dac7678/test.esp32-c3-idf.yaml b/tests/components/dac7678/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/dac7678/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/daikin_brc/test.esp32-c3-idf.yaml b/tests/components/daikin_brc/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/daikin_brc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/dallas_temp/test.esp32-c3-idf.yaml b/tests/components/dallas_temp/test.esp32-c3-idf.yaml deleted file mode 100644 index 49bf988eb41..00000000000 --- a/tests/components/dallas_temp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - one_wire_pin: "4" - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/daly_bms/test.esp32-c3-idf.yaml b/tests/components/daly_bms/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/daly_bms/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-c3-idf.yaml b/tests/components/debug/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/debug/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/delonghi/test.esp32-c3-idf.yaml b/tests/components/delonghi/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/delonghi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/dfplayer/test.esp32-c3-idf.yaml b/tests/components/dfplayer/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/dfplayer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml b/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/dht/test.esp32-c3-idf.yaml b/tests/components/dht/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/dht/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/dht12/test.esp32-c3-idf.yaml b/tests/components/dht12/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/dht12/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/dps310/test.esp32-c3-idf.yaml b/tests/components/dps310/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/dps310/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ds1307/test.esp32-c3-idf.yaml b/tests/components/ds1307/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ds1307/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ds2484/test.esp32-c3-idf.yaml b/tests/components/ds2484/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ds2484/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.esp32-c3-idf.yaml b/tests/components/duty_cycle/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/duty_cycle/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/duty_time/test.esp32-c3-idf.yaml b/tests/components/duty_time/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/duty_time/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/e131/test.esp32-c3-idf.yaml b/tests/components/e131/test.esp32-c3-idf.yaml deleted file mode 100644 index d9dc4f68048..00000000000 --- a/tests/components/e131/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - light_platform: esp32_rmt_led_strip - pin: GPIO2 - -<<: !include common-idf.yaml diff --git a/tests/components/ee895/test.esp32-c3-idf.yaml b/tests/components/ee895/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ee895/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ektf2232/test.esp32-c3-idf.yaml b/tests/components/ektf2232/test.esp32-c3-idf.yaml deleted file mode 100644 index 708d352a595..00000000000 --- a/tests/components/ektf2232/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - display_reset_pin: GPIO3 - interrupt_pin: GPIO6 - touch_reset_pin: GPIO7 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/emc2101/test.esp32-c3-idf.yaml b/tests/components/emc2101/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/emc2101/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/endstop/test.esp32-c3-idf.yaml b/tests/components/endstop/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/endstop/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ens160_i2c/test.esp32-c3-idf.yaml b/tests/components/ens160_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ens160_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ens160_spi/test.esp32-c3-idf.yaml b/tests/components/ens160_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/ens160_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ens210/test.esp32-c3-idf.yaml b/tests/components/ens210/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ens210/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/es7210/test.esp32-c3-idf.yaml b/tests/components/es7210/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/es7210/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/es7243e/test.esp32-c3-idf.yaml b/tests/components/es7243e/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/es7243e/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/es8156/test.esp32-c3-idf.yaml b/tests/components/es8156/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/es8156/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/es8311/test.esp32-c3-idf.yaml b/tests/components/es8311/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/es8311/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/es8388/test.esp32-c3-idf.yaml b/tests/components/es8388/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/es8388/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/esphome/test.esp32-c3-idf.yaml b/tests/components/esphome/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/esphome/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/event/test.esp32-c3-idf.yaml b/tests/components/event/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/event/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/exposure_notifications/test.esp32-c3-idf.yaml b/tests/components/exposure_notifications/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/exposure_notifications/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/external_components/test.esp32-c3-idf.yaml b/tests/components/external_components/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/external_components/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ezo/test.esp32-c3-idf.yaml b/tests/components/ezo/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ezo/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ezo_pmp/test.esp32-c3-idf.yaml b/tests/components/ezo_pmp/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ezo_pmp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/factory_reset/test.esp32-c3-idf.yaml b/tests/components/factory_reset/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/factory_reset/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/feedback/test.esp32-c3-idf.yaml b/tests/components/feedback/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/feedback/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/fingerprint_grow/test.esp32-c3-idf.yaml b/tests/components/fingerprint_grow/test.esp32-c3-idf.yaml deleted file mode 100644 index dff4f7fd924..00000000000 --- a/tests/components/fingerprint_grow/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - sensing_pin: GPIO6 -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/font/test.esp32-c3-idf.yaml b/tests/components/font/test.esp32-c3-idf.yaml deleted file mode 100644 index 2090db7e6d1..00000000000 --- a/tests/components/font/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - display_reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/fs3000/test.esp32-c3-idf.yaml b/tests/components/fs3000/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/fs3000/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ft5x06/test.esp32-c3-idf.yaml b/tests/components/ft5x06/test.esp32-c3-idf.yaml deleted file mode 100644 index c97f30d52c1..00000000000 --- a/tests/components/ft5x06/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ft63x6/test.esp32-c3-idf.yaml b/tests/components/ft63x6/test.esp32-c3-idf.yaml deleted file mode 100644 index febf38d8e10..00000000000 --- a/tests/components/ft63x6/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/fujitsu_general/test.esp32-c3-idf.yaml b/tests/components/fujitsu_general/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/fujitsu_general/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/gcja5/test.esp32-c3-idf.yaml b/tests/components/gcja5/test.esp32-c3-idf.yaml deleted file mode 100644 index 236529042fd..00000000000 --- a/tests/components/gcja5/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart_9600_even: !include ../../test_build_components/common/uart_9600_even/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml b/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/globals/test.esp32-c3-idf.yaml b/tests/components/globals/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/globals/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/gp2y1010au0f/test.esp32-c3-idf.yaml b/tests/components/gp2y1010au0f/test.esp32-c3-idf.yaml deleted file mode 100644 index 0e331c893c5..00000000000 --- a/tests/components/gp2y1010au0f/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - adc_pin: GPIO0 - output_pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/gp8403/test.esp32-c3-idf.yaml b/tests/components/gp8403/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/gp8403/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/gps/test.esp32-c3-idf.yaml b/tests/components/gps/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/gps/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/graph/test.esp32-c3-idf.yaml b/tests/components/graph/test.esp32-c3-idf.yaml deleted file mode 100644 index c97f30d52c1..00000000000 --- a/tests/components/graph/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/graphical_display_menu/test.esp32-c3-idf.yaml b/tests/components/graphical_display_menu/test.esp32-c3-idf.yaml deleted file mode 100644 index c97f30d52c1..00000000000 --- a/tests/components/graphical_display_menu/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/gree/test.esp32-c3-idf.yaml b/tests/components/gree/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/gree/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/grove_gas_mc_v2/test.esp32-c3-idf.yaml b/tests/components/grove_gas_mc_v2/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/grove_gas_mc_v2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml b/tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/growatt_solar/test.esp32-c3-idf.yaml b/tests/components/growatt_solar/test.esp32-c3-idf.yaml deleted file mode 100644 index 17940aafcf2..00000000000 --- a/tests/components/growatt_solar/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - flow_control_pin: GPIO3 -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/gt911/test.esp32-c3-idf.yaml b/tests/components/gt911/test.esp32-c3-idf.yaml deleted file mode 100644 index 5e15963b7e6..00000000000 --- a/tests/components/gt911/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - display_reset_pin: "18" - interrupt_pin: "20" - reset_pin: "21" - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/haier/test.esp32-c3-idf.yaml b/tests/components/haier/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/haier/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/havells_solar/test.esp32-c3-idf.yaml b/tests/components/havells_solar/test.esp32-c3-idf.yaml deleted file mode 100644 index 17940aafcf2..00000000000 --- a/tests/components/havells_solar/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - flow_control_pin: GPIO3 -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hbridge/test.esp32-c3-idf.yaml b/tests/components/hbridge/test.esp32-c3-idf.yaml deleted file mode 100644 index 93a6cb58184..00000000000 --- a/tests/components/hbridge/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,17 +0,0 @@ -substitutions: - pwm_platform: "ledc" - output1_pin: "4" - output2_pin: "5" - output3_pin: "6" - output4_pin: "7" - hbridge_on_pin: "2" - hbridge_off_pin: "3" - -<<: !include common.yaml - -switch: - - platform: hbridge - id: switch_hbridge - on_pin: ${hbridge_on_pin} - off_pin: ${hbridge_off_pin} - pulse_length: 60ms diff --git a/tests/components/hdc1080/test.esp32-c3-idf.yaml b/tests/components/hdc1080/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/hdc1080/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hdc2010/test.esp32-c3-idf.yaml b/tests/components/hdc2010/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/hdc2010/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/he60r/test.esp32-c3-idf.yaml b/tests/components/he60r/test.esp32-c3-idf.yaml deleted file mode 100644 index b0c8c5de3d5..00000000000 --- a/tests/components/he60r/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart_1200_even: !include ../../test_build_components/common/uart_1200_even/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml b/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml b/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hlw8012/test.esp32-c3-idf.yaml b/tests/components/hlw8012/test.esp32-c3-idf.yaml deleted file mode 100644 index 8b0d069ce2e..00000000000 --- a/tests/components/hlw8012/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - sel_pin: GPIO2 - cf_pin: GPIO3 - cf1_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hm3301/test.esp32-c3-idf.yaml b/tests/components/hm3301/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/hm3301/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hmc5883l/test.esp32-c3-idf.yaml b/tests/components/hmc5883l/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/hmc5883l/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/homeassistant/test.esp32-c3-idf.yaml b/tests/components/homeassistant/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/homeassistant/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml b/tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/honeywellabp/test.esp32-c3-idf.yaml b/tests/components/honeywellabp/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/honeywellabp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml b/tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml b/tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hte501/test.esp32-c3-idf.yaml b/tests/components/hte501/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/hte501/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/http_request/test.esp32-c3-idf.yaml b/tests/components/http_request/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2f5aa59b8..00000000000 --- a/tests/components/http_request/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - verify_ssl: "true" - -<<: !include common.yaml diff --git a/tests/components/htu21d/test.esp32-c3-idf.yaml b/tests/components/htu21d/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/htu21d/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/htu31d/test.esp32-c3-idf.yaml b/tests/components/htu31d/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/htu31d/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hx711/test.esp32-c3-idf.yaml b/tests/components/hx711/test.esp32-c3-idf.yaml deleted file mode 100644 index defef165e31..00000000000 --- a/tests/components/hx711/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO4 - dout_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml b/tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hyt271/test.esp32-c3-idf.yaml b/tests/components/hyt271/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/hyt271/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/i2c/test.esp32-c3-idf.yaml b/tests/components/i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/i2c_device/test.esp32-c3-idf.yaml b/tests/components/i2c_device/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/i2c_device/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/iaqcore/test.esp32-c3-idf.yaml b/tests/components/iaqcore/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/iaqcore/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ili9xxx/test.esp32-c3-idf.yaml b/tests/components/ili9xxx/test.esp32-c3-idf.yaml deleted file mode 100644 index 1eea1e85f76..00000000000 --- a/tests/components/ili9xxx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - cs_pin1: GPIO8 - dc_pin1: GPIO9 - reset_pin1: GPIO10 - cs_pin2: GPIO2 - dc_pin2: GPIO3 - reset_pin2: GPIO7 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ina219/test.esp32-c3-idf.yaml b/tests/components/ina219/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ina219/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ina226/test.esp32-c3-idf.yaml b/tests/components/ina226/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ina226/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ina260/test.esp32-c3-idf.yaml b/tests/components/ina260/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ina260/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml b/tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ina2xx_spi/test.esp32-c3-idf.yaml b/tests/components/ina2xx_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/ina2xx_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ina3221/test.esp32-c3-idf.yaml b/tests/components/ina3221/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ina3221/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml b/tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/integration/test.esp32-c3-idf.yaml b/tests/components/integration/test.esp32-c3-idf.yaml deleted file mode 100644 index 5105e645f3c..00000000000 --- a/tests/components/integration/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO1 - -<<: !include common-esp32.yaml diff --git a/tests/components/interval/test.esp32-c3-idf.yaml b/tests/components/interval/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/interval/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/jsn_sr04t/test.esp32-c3-idf.yaml b/tests/components/jsn_sr04t/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/jsn_sr04t/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/key_collector/test.esp32-c3-idf.yaml b/tests/components/key_collector/test.esp32-c3-idf.yaml deleted file mode 100644 index b580ab78432..00000000000 --- a/tests/components/key_collector/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - pin_r0: GPIO2 - pin_r1: GPIO3 - pin_c0: GPIO4 - pin_c1: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/kmeteriso/test.esp32-c3-idf.yaml b/tests/components/kmeteriso/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/kmeteriso/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/kuntze/test.esp32-c3-idf.yaml b/tests/components/kuntze/test.esp32-c3-idf.yaml deleted file mode 100644 index 17940aafcf2..00000000000 --- a/tests/components/kuntze/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - flow_control_pin: GPIO3 -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/lc709203f/test.esp32-c3-idf.yaml b/tests/components/lc709203f/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/lc709203f/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/lcd_gpio/test.esp32-c3-idf.yaml b/tests/components/lcd_gpio/test.esp32-c3-idf.yaml deleted file mode 100644 index b6b05f3ab4f..00000000000 --- a/tests/components/lcd_gpio/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - d0_pin: GPIO1 - d1_pin: GPIO2 - d2_pin: GPIO3 - d3_pin: GPIO4 - enable_pin: GPIO5 - rs_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/lcd_menu/test.esp32-c3-idf.yaml b/tests/components/lcd_menu/test.esp32-c3-idf.yaml deleted file mode 100644 index b6b05f3ab4f..00000000000 --- a/tests/components/lcd_menu/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - d0_pin: GPIO1 - d1_pin: GPIO2 - d2_pin: GPIO3 - d3_pin: GPIO4 - enable_pin: GPIO5 - rs_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml b/tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ld2410/test.esp32-c3-idf.yaml b/tests/components/ld2410/test.esp32-c3-idf.yaml deleted file mode 100644 index 7a8f790ed8b..00000000000 --- a/tests/components/ld2410/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp32-c3-idf.yaml b/tests/components/ld2412/test.esp32-c3-idf.yaml deleted file mode 100644 index 7a8f790ed8b..00000000000 --- a/tests/components/ld2412/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ld2420/test.esp32-c3-idf.yaml b/tests/components/ld2420/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/ld2420/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ld2450/test.esp32-c3-idf.yaml b/tests/components/ld2450/test.esp32-c3-idf.yaml deleted file mode 100644 index 7a8f790ed8b..00000000000 --- a/tests/components/ld2450/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/light/test.esp32-c3-idf.yaml b/tests/components/light/test.esp32-c3-idf.yaml deleted file mode 100644 index 317d5748a3d..00000000000 --- a/tests/components/light/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,21 +0,0 @@ -output: - - platform: gpio - id: test_binary - pin: 0 - - platform: ledc - id: test_ledc_1 - pin: 1 - - platform: ledc - id: test_ledc_2 - pin: 2 - - platform: ledc - id: test_ledc_3 - pin: 3 - - platform: ledc - id: test_ledc_4 - pin: 4 - - platform: ledc - id: test_ledc_5 - pin: 5 - -<<: !include common.yaml diff --git a/tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml b/tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml deleted file mode 100644 index febf38d8e10..00000000000 --- a/tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/lm75b/test.esp32-c3-idf.yaml b/tests/components/lm75b/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/lm75b/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/lock/test.esp32-c3-idf.yaml b/tests/components/lock/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/lock/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/lps22/test.esp32-c3-idf.yaml b/tests/components/lps22/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/lps22/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ltr390/test.esp32-c3-idf.yaml b/tests/components/ltr390/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ltr390/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp32-c3-idf.yaml b/tests/components/ltr501/test.esp32-c3-idf.yaml deleted file mode 100644 index 72703301a10..00000000000 --- a/tests/components/ltr501/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ltr_als_ps/test.esp32-c3-idf.yaml b/tests/components/ltr_als_ps/test.esp32-c3-idf.yaml deleted file mode 100644 index 72703301a10..00000000000 --- a/tests/components/ltr_als_ps/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/m5stack_8angle/test.esp32-c3-idf.yaml b/tests/components/m5stack_8angle/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/m5stack_8angle/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mapping/test.esp32-c3-idf.yaml b/tests/components/mapping/test.esp32-c3-idf.yaml deleted file mode 100644 index 7911eb7edc7..00000000000 --- a/tests/components/mapping/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,13 +0,0 @@ -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - map: !include common.yaml - -display: - spi_id: spi_bus - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 8 - dc_pin: 9 - reset_pin: 10 - invert_colors: false diff --git a/tests/components/matrix_keypad/test.esp32-c3-idf.yaml b/tests/components/matrix_keypad/test.esp32-c3-idf.yaml deleted file mode 100644 index 75d9c0b263a..00000000000 --- a/tests/components/matrix_keypad/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,15 +0,0 @@ -packages: - common: !include common.yaml - -matrix_keypad: - id: keypad - rows: - - pin: 1 - - pin: 2 - columns: - - pin: 3 - - pin: 4 - keys: "1234" - has_pulldowns: true - on_key: - - lambda: ESP_LOGI("KEY", "key %d pressed", x); diff --git a/tests/components/max17043/test.esp32-c3-idf.yaml b/tests/components/max17043/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/max17043/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max31855/test.esp32-c3-idf.yaml b/tests/components/max31855/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/max31855/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max31856/test.esp32-c3-idf.yaml b/tests/components/max31856/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/max31856/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max31865/test.esp32-c3-idf.yaml b/tests/components/max31865/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/max31865/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max44009/test.esp32-c3-idf.yaml b/tests/components/max44009/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/max44009/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max6675/test.esp32-c3-idf.yaml b/tests/components/max6675/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/max6675/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max6956/test.esp32-c3-idf.yaml b/tests/components/max6956/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/max6956/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max7219/test.esp32-c3-idf.yaml b/tests/components/max7219/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/max7219/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max7219digit/test.esp32-c3-idf.yaml b/tests/components/max7219digit/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/max7219digit/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max9611/test.esp32-c3-idf.yaml b/tests/components/max9611/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/max9611/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp23008/test.esp32-c3-idf.yaml b/tests/components/mcp23008/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp23008/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp23016/test.esp32-c3-idf.yaml b/tests/components/mcp23016/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp23016/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp23017/test.esp32-c3-idf.yaml b/tests/components/mcp23017/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp23017/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp23s08/test.esp32-c3-idf.yaml b/tests/components/mcp23s08/test.esp32-c3-idf.yaml deleted file mode 100644 index b11ec9cdc67..00000000000 --- a/tests/components/mcp23s08/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - cs_pin: GPIO8 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp23s17/test.esp32-c3-idf.yaml b/tests/components/mcp23s17/test.esp32-c3-idf.yaml deleted file mode 100644 index b11ec9cdc67..00000000000 --- a/tests/components/mcp23s17/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - cs_pin: GPIO8 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp2515/test.esp32-c3-idf.yaml b/tests/components/mcp2515/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/mcp2515/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp3008/test.esp32-c3-idf.yaml b/tests/components/mcp3008/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/mcp3008/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp3204/test.esp32-c3-idf.yaml b/tests/components/mcp3204/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/mcp3204/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp4461/test.esp32-c3-idf.yaml b/tests/components/mcp4461/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp4461/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp4725/test.esp32-c3-idf.yaml b/tests/components/mcp4725/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp4725/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp4728/test.esp32-c3-idf.yaml b/tests/components/mcp4728/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp4728/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp47a1/test.esp32-c3-idf.yaml b/tests/components/mcp47a1/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp47a1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp9600/test.esp32-c3-idf.yaml b/tests/components/mcp9600/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp9600/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp9808/test.esp32-c3-idf.yaml b/tests/components/mcp9808/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp9808/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mdns/test-enabled.esp32-c3-idf.yaml b/tests/components/mdns/test-enabled.esp32-c3-idf.yaml deleted file mode 100644 index 97fd63d70e3..00000000000 --- a/tests/components/mdns/test-enabled.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/mhz19/test.esp32-c3-idf.yaml b/tests/components/mhz19/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/mhz19/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/micronova/test.esp32-c3-idf.yaml b/tests/components/micronova/test.esp32-c3-idf.yaml deleted file mode 100644 index eed876ae746..00000000000 --- a/tests/components/micronova/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - enable_rx_pin: GPIO3 - -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/microphone/test.esp32-c3-idf.yaml b/tests/components/microphone/test.esp32-c3-idf.yaml deleted file mode 100644 index c28dc553f5b..00000000000 --- a/tests/components/microphone/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO6 - i2s_lrclk_pin: GPIO7 - i2s_mclk_pin: GPIO8 - i2s_din_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/mics_4514/test.esp32-c3-idf.yaml b/tests/components/mics_4514/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mics_4514/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/midea_ir/test.esp32-c3-idf.yaml b/tests/components/midea_ir/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/midea_ir/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mitsubishi/test.esp32-c3-idf.yaml b/tests/components/mitsubishi/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/mitsubishi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mixer/test.esp32-c3-idf.yaml b/tests/components/mixer/test.esp32-c3-idf.yaml deleted file mode 100644 index f1721f08625..00000000000 --- a/tests/components/mixer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - lrclk_pin: GPIO4 - bclk_pin: GPIO5 - mclk_pin: GPIO6 - dout_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/mlx90393/test.esp32-c3-idf.yaml b/tests/components/mlx90393/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mlx90393/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mlx90614/test.esp32-c3-idf.yaml b/tests/components/mlx90614/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mlx90614/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mmc5603/test.esp32-c3-idf.yaml b/tests/components/mmc5603/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mmc5603/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mmc5983/test.esp32-c3-idf.yaml b/tests/components/mmc5983/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mmc5983/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/modbus/test.esp32-c3-idf.yaml b/tests/components/modbus/test.esp32-c3-idf.yaml deleted file mode 100644 index 430c6818cb4..00000000000 --- a/tests/components/modbus/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - flow_control_pin: GPIO3 -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/modbus_controller/test.esp32-c3-idf.yaml b/tests/components/modbus_controller/test.esp32-c3-idf.yaml deleted file mode 100644 index db826676ee1..00000000000 --- a/tests/components/modbus_controller/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/monochromatic/test.esp32-c3-idf.yaml b/tests/components/monochromatic/test.esp32-c3-idf.yaml deleted file mode 100644 index feabf013fdf..00000000000 --- a/tests/components/monochromatic/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - light_platform: ledc - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/mopeka_ble/test.esp32-c3-idf.yaml b/tests/components/mopeka_ble/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/mopeka_ble/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml b/tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mopeka_std_check/test.esp32-c3-idf.yaml b/tests/components/mopeka_std_check/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/mopeka_std_check/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mpl3115a2/test.esp32-c3-idf.yaml b/tests/components/mpl3115a2/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mpl3115a2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mpr121/test.esp32-c3-idf.yaml b/tests/components/mpr121/test.esp32-c3-idf.yaml deleted file mode 100644 index d1abb03369b..00000000000 --- a/tests/components/mpr121/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - i2c_scl: GPIO5 - i2c_sda: GPIO4 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mpu6050/test.esp32-c3-idf.yaml b/tests/components/mpu6050/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mpu6050/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mpu6886/test.esp32-c3-idf.yaml b/tests/components/mpu6886/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mpu6886/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mqtt/test.esp32-c3-idf.yaml b/tests/components/mqtt/test.esp32-c3-idf.yaml deleted file mode 100644 index d19609b55e9..00000000000 --- a/tests/components/mqtt/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,3 +0,0 @@ -packages: - common: !include common.yaml - update: !include common-update.yaml diff --git a/tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml b/tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml deleted file mode 100644 index 2cb6d825367..00000000000 --- a/tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-idf.yaml diff --git a/tests/components/ms5611/test.esp32-c3-idf.yaml b/tests/components/ms5611/test.esp32-c3-idf.yaml deleted file mode 100644 index d1abb03369b..00000000000 --- a/tests/components/ms5611/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - i2c_scl: GPIO5 - i2c_sda: GPIO4 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/msa3xx/test.esp32-c3-idf.yaml b/tests/components/msa3xx/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/msa3xx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/my9231/test.esp32-c3-idf.yaml b/tests/components/my9231/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/my9231/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/nau7802/test.esp32-c3-idf.yaml b/tests/components/nau7802/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/nau7802/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/network/test-ipv6.esp32-c3-idf.yaml b/tests/components/network/test-ipv6.esp32-c3-idf.yaml deleted file mode 100644 index da1324b17ef..00000000000 --- a/tests/components/network/test-ipv6.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - network_enable_ipv6: "true" - -<<: !include common.yaml diff --git a/tests/components/network/test.esp32-c3-idf.yaml b/tests/components/network/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/network/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/nextion/test.esp32-c3-idf.yaml b/tests/components/nextion/test.esp32-c3-idf.yaml deleted file mode 100644 index 888693f909a..00000000000 --- a/tests/components/nextion/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft diff --git a/tests/components/noblex/test.esp32-c3-idf.yaml b/tests/components/noblex/test.esp32-c3-idf.yaml deleted file mode 100644 index fe77c44eed1..00000000000 --- a/tests/components/noblex/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ntc/test.esp32-c3-idf.yaml b/tests/components/ntc/test.esp32-c3-idf.yaml deleted file mode 100644 index 37fb325f4a5..00000000000 --- a/tests/components/ntc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - 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 deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/opt3001/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ota/test.esp32-c3-idf.yaml b/tests/components/ota/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/ota/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/output/test.esp32-c3-idf.yaml b/tests/components/output/test.esp32-c3-idf.yaml deleted file mode 100644 index 22276437034..00000000000 --- a/tests/components/output/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/packet_transport/test.esp32-c3-idf.yaml b/tests/components/packet_transport/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/packet_transport/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pca6416a/test.esp32-c3-idf.yaml b/tests/components/pca6416a/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pca6416a/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pca9554/test.esp32-c3-idf.yaml b/tests/components/pca9554/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pca9554/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pca9685/test.esp32-c3-idf.yaml b/tests/components/pca9685/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pca9685/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pcd8544/test.esp32-c3-idf.yaml b/tests/components/pcd8544/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/pcd8544/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pcf85063/test.esp32-c3-idf.yaml b/tests/components/pcf85063/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pcf85063/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pcf8563/test.esp32-c3-idf.yaml b/tests/components/pcf8563/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pcf8563/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pcf8574/test.esp32-c3-idf.yaml b/tests/components/pcf8574/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pcf8574/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pid/test.esp32-c3-idf.yaml b/tests/components/pid/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/pid/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pipsolar/test.esp32-c3-idf.yaml b/tests/components/pipsolar/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/pipsolar/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pm1006/test.esp32-c3-idf.yaml b/tests/components/pm1006/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/pm1006/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp32-c3-idf.yaml b/tests/components/pm2005/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pm2005/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pmsa003i/test.esp32-c3-idf.yaml b/tests/components/pmsa003i/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pmsa003i/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pmsx003/test.esp32-c3-idf.yaml b/tests/components/pmsx003/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/pmsx003/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pmwcs3/test.esp32-c3-idf.yaml b/tests/components/pmwcs3/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pmwcs3/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pn532_i2c/test.esp32-c3-idf.yaml b/tests/components/pn532_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pn532_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pn532_spi/test.esp32-c3-idf.yaml b/tests/components/pn532_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/pn532_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pn7150_i2c/test.esp32-c3-idf.yaml b/tests/components/pn7150_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index cdf84452630..00000000000 --- a/tests/components/pn7150_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - irq_pin: GPIO6 - ven_pin: GPIO7 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pn7160_i2c/test.esp32-c3-idf.yaml b/tests/components/pn7160_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index cdf84452630..00000000000 --- a/tests/components/pn7160_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - irq_pin: GPIO6 - ven_pin: GPIO7 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pn7160_spi/test.esp32-c3-idf.yaml b/tests/components/pn7160_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index ac18bfff5c0..00000000000 --- a/tests/components/pn7160_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - irq_pin: GPIO9 - ven_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/power_supply/test.esp32-c3-idf.yaml b/tests/components/power_supply/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/power_supply/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/prometheus/test.esp32-c3-idf.yaml b/tests/components/prometheus/test.esp32-c3-idf.yaml deleted file mode 100644 index fedeaf822a9..00000000000 --- a/tests/components/prometheus/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - verify_ssl: "false" - -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pulse_meter/test.esp32-c3-idf.yaml b/tests/components/pulse_meter/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/pulse_meter/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pulse_width/test.esp32-c3-idf.yaml b/tests/components/pulse_width/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/pulse_width/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml b/tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pylontech/test.esp32-c3-idf.yaml b/tests/components/pylontech/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/pylontech/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pzem004t/test.esp32-c3-idf.yaml b/tests/components/pzem004t/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/pzem004t/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pzemac/test.esp32-c3-idf.yaml b/tests/components/pzemac/test.esp32-c3-idf.yaml deleted file mode 100644 index 6c6e95488fc..00000000000 --- a/tests/components/pzemac/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pzemdc/test.esp32-c3-idf.yaml b/tests/components/pzemdc/test.esp32-c3-idf.yaml deleted file mode 100644 index 6c6e95488fc..00000000000 --- a/tests/components/pzemdc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp32-c3-idf.yaml b/tests/components/qmc5883l/test.esp32-c3-idf.yaml deleted file mode 100644 index 854ddc25e72..00000000000 --- a/tests/components/qmc5883l/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - drdy_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/qmp6988/test.esp32-c3-idf.yaml b/tests/components/qmp6988/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/qmp6988/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/qr_code/test.esp32-c3-idf.yaml b/tests/components/qr_code/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/qr_code/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/qwiic_pir/test.esp32-c3-idf.yaml b/tests/components/qwiic_pir/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/qwiic_pir/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/radon_eye_ble/test.esp32-c3-idf.yaml b/tests/components/radon_eye_ble/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/radon_eye_ble/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml b/tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/rc522_i2c/test.esp32-c3-idf.yaml b/tests/components/rc522_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/rc522_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/rc522_spi/test.esp32-c3-idf.yaml b/tests/components/rc522_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/rc522_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/rdm6300/test.esp32-c3-idf.yaml b/tests/components/rdm6300/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/rdm6300/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/resampler/test.esp32-c3-idf.yaml b/tests/components/resampler/test.esp32-c3-idf.yaml deleted file mode 100644 index f1721f08625..00000000000 --- a/tests/components/resampler/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - lrclk_pin: GPIO4 - bclk_pin: GPIO5 - mclk_pin: GPIO6 - dout_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/resistance/test.esp32-c3-idf.yaml b/tests/components/resistance/test.esp32-c3-idf.yaml deleted file mode 100644 index 37fb325f4a5..00000000000 --- a/tests/components/resistance/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/restart/test.esp32-c3-idf.yaml b/tests/components/restart/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/restart/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/rf_bridge/test.esp32-c3-idf.yaml b/tests/components/rf_bridge/test.esp32-c3-idf.yaml deleted file mode 100644 index a19013bf544..00000000000 --- a/tests/components/rf_bridge/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/rgb/test.esp32-c3-idf.yaml b/tests/components/rgb/test.esp32-c3-idf.yaml deleted file mode 100644 index 1fe4a4bb909..00000000000 --- a/tests/components/rgb/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/rgbct/test.esp32-c3-idf.yaml b/tests/components/rgbct/test.esp32-c3-idf.yaml deleted file mode 100644 index 27a1fbca4de..00000000000 --- a/tests/components/rgbct/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - pin4: GPIO5 - pin5: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/rgbw/test.esp32-c3-idf.yaml b/tests/components/rgbw/test.esp32-c3-idf.yaml deleted file mode 100644 index b44734344eb..00000000000 --- a/tests/components/rgbw/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - pin4: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/rgbww/test.esp32-c3-idf.yaml b/tests/components/rgbww/test.esp32-c3-idf.yaml deleted file mode 100644 index 27a1fbca4de..00000000000 --- a/tests/components/rgbww/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - pin4: GPIO5 - pin5: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/rotary_encoder/test.esp32-c3-idf.yaml b/tests/components/rotary_encoder/test.esp32-c3-idf.yaml deleted file mode 100644 index b71a454bdd7..00000000000 --- a/tests/components/rotary_encoder/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - pin_a: GPIO2 - pin_b: GPIO3 - pin_reset: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/rtttl/test.esp32-c3-idf.yaml b/tests/components/rtttl/test.esp32-c3-idf.yaml deleted file mode 100644 index 74769635918..00000000000 --- a/tests/components/rtttl/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ruuvi_ble/test.esp32-c3-idf.yaml b/tests/components/ruuvi_ble/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/ruuvi_ble/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ruuvitag/test.esp32-c3-idf.yaml b/tests/components/ruuvitag/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/ruuvitag/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/safe_mode/test-enabled.esp32-c3-idf.yaml b/tests/components/safe_mode/test-enabled.esp32-c3-idf.yaml deleted file mode 100644 index 97fd63d70e3..00000000000 --- a/tests/components/safe_mode/test-enabled.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/scd30/test.esp32-c3-idf.yaml b/tests/components/scd30/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/scd30/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/scd4x/test.esp32-c3-idf.yaml b/tests/components/scd4x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/scd4x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/script/test.esp32-c3-idf.yaml b/tests/components/script/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/script/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sdm_meter/test.esp32-c3-idf.yaml b/tests/components/sdm_meter/test.esp32-c3-idf.yaml deleted file mode 100644 index 6c6e95488fc..00000000000 --- a/tests/components/sdm_meter/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sdp3x/test.esp32-c3-idf.yaml b/tests/components/sdp3x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sdp3x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sds011/test.esp32-c3-idf.yaml b/tests/components/sds011/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/sds011/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml b/tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml deleted file mode 100644 index a19013bf544..00000000000 --- a/tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml b/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml deleted file mode 100644 index fea89f57684..00000000000 --- a/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart_115200: !include ../../test_build_components/common/uart_115200/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml b/tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml deleted file mode 100644 index fea89f57684..00000000000 --- a/tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart_115200: !include ../../test_build_components/common/uart_115200/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/selec_meter/test.esp32-c3-idf.yaml b/tests/components/selec_meter/test.esp32-c3-idf.yaml deleted file mode 100644 index beb90e14710..00000000000 --- a/tests/components/selec_meter/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - flow_control_pin: GPIO10 - -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sen0321/test.esp32-c3-idf.yaml b/tests/components/sen0321/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sen0321/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sen21231/test.esp32-c3-idf.yaml b/tests/components/sen21231/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sen21231/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sen5x/test.esp32-c3-idf.yaml b/tests/components/sen5x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sen5x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/senseair/test.esp32-c3-idf.yaml b/tests/components/senseair/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/senseair/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/servo/test.esp32-c3-idf.yaml b/tests/components/servo/test.esp32-c3-idf.yaml deleted file mode 100644 index 74769635918..00000000000 --- a/tests/components/servo/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sfa30/test.esp32-c3-idf.yaml b/tests/components/sfa30/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sfa30/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sgp30/test.esp32-c3-idf.yaml b/tests/components/sgp30/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sgp30/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sgp4x/test.esp32-c3-idf.yaml b/tests/components/sgp4x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sgp4x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sht3xd/test.esp32-c3-idf.yaml b/tests/components/sht3xd/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sht3xd/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sht4x/test.esp32-c3-idf.yaml b/tests/components/sht4x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sht4x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/shtcx/test.esp32-c3-idf.yaml b/tests/components/shtcx/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/shtcx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/shutdown/test.esp32-c3-idf.yaml b/tests/components/shutdown/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/shutdown/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sim800l/test.esp32-c3-idf.yaml b/tests/components/sim800l/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/sim800l/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/slow_pwm/test.esp32-c3-idf.yaml b/tests/components/slow_pwm/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/slow_pwm/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sm16716/test.esp32-c3-idf.yaml b/tests/components/sm16716/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/sm16716/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm2135/test.esp32-c3-idf.yaml b/tests/components/sm2135/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/sm2135/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm2235/test.esp32-c3-idf.yaml b/tests/components/sm2235/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/sm2235/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm2335/test.esp32-c3-idf.yaml b/tests/components/sm2335/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/sm2335/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm300d2/test.esp32-c3-idf.yaml b/tests/components/sm300d2/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/sm300d2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sml/test.esp32-c3-idf.yaml b/tests/components/sml/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/sml/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/smt100/test.esp32-c3-idf.yaml b/tests/components/smt100/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/smt100/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sn74hc165/test.esp32-c3-idf.yaml b/tests/components/sn74hc165/test.esp32-c3-idf.yaml deleted file mode 100644 index 0a3db917b78..00000000000 --- a/tests/components/sn74hc165/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clock_pin: GPIO3 - data_pin: GPIO4 - load_pin: GPIO5 - clock_inhibit_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/sn74hc595/test.esp32-c3-idf.yaml b/tests/components/sn74hc595/test.esp32-c3-idf.yaml deleted file mode 100644 index 74b5e855fa6..00000000000 --- a/tests/components/sn74hc595/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,12 +0,0 @@ -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -substitutions: - clock_pin: GPIO7 - data_pin: GPIO10 - latch_pin1: GPIO1 - oe_pin1: GPIO2 - latch_pin2: GPIO3 - oe_pin2: GPIO9 - -<<: !include common.yaml diff --git a/tests/components/sntp/test.esp32-c3-idf.yaml b/tests/components/sntp/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/sntp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sonoff_d1/test.esp32-c3-idf.yaml b/tests/components/sonoff_d1/test.esp32-c3-idf.yaml deleted file mode 100644 index a19013bf544..00000000000 --- a/tests/components/sonoff_d1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sound_level/test.esp32-c3-idf.yaml b/tests/components/sound_level/test.esp32-c3-idf.yaml deleted file mode 100644 index aeb7d9f0af5..00000000000 --- a/tests/components/sound_level/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO6 - i2s_lrclk_pin: GPIO7 - i2s_dout_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/speaker/audio_dac.esp32-c3-idf.yaml b/tests/components/speaker/audio_dac.esp32-c3-idf.yaml deleted file mode 100644 index 30900f19201..00000000000 --- a/tests/components/speaker/audio_dac.esp32-c3-idf.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO7 - i2s_lrclk_pin: GPIO6 - i2s_mclk_pin: GPIO9 - i2s_dout_pin: GPIO8 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common-audio_dac.yaml diff --git a/tests/components/speaker/test.esp32-c3-idf.yaml b/tests/components/speaker/test.esp32-c3-idf.yaml deleted file mode 100644 index 9d1a1cca3bf..00000000000 --- a/tests/components/speaker/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - i2s_bclk_pin: GPIO7 - i2s_lrclk_pin: GPIO6 - i2s_mclk_pin: GPIO9 - i2s_dout_pin: GPIO8 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/speed/test.esp32-c3-idf.yaml b/tests/components/speed/test.esp32-c3-idf.yaml deleted file mode 100644 index 74769635918..00000000000 --- a/tests/components/speed/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/spi_device/test.esp32-c3-idf.yaml b/tests/components/spi_device/test.esp32-c3-idf.yaml deleted file mode 100644 index 6d64c2b23bb..00000000000 --- a/tests/components/spi_device/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/spi_led_strip/test.esp32-c3-idf.yaml b/tests/components/spi_led_strip/test.esp32-c3-idf.yaml deleted file mode 100644 index 6d64c2b23bb..00000000000 --- a/tests/components/spi_led_strip/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sprinkler/test.esp32-c3-idf.yaml b/tests/components/sprinkler/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/sprinkler/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sps30/test.esp32-c3-idf.yaml b/tests/components/sps30/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sps30/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml b/tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index f8bfab23198..00000000000 --- a/tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1306_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1306_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/ssd1306_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1322_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1322_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/ssd1322_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1325_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1325_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/ssd1325_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml b/tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index f8bfab23198..00000000000 --- a/tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1327_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1327_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/ssd1327_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1331_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1331_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/ssd1331_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1351_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1351_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/ssd1351_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/st7567_i2c/test.esp32-c3-idf.yaml b/tests/components/st7567_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index f8bfab23198..00000000000 --- a/tests/components/st7567_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/st7567_spi/test.esp32-c3-idf.yaml b/tests/components/st7567_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/st7567_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/st7735/test.esp32-c3-idf.yaml b/tests/components/st7735/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/st7735/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/st7789v/test.esp32-c3-idf.yaml b/tests/components/st7789v/test.esp32-c3-idf.yaml deleted file mode 100644 index b4d70edb318..00000000000 --- a/tests/components/st7789v/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - backlight_pin: GPIO7 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/st7920/test.esp32-c3-idf.yaml b/tests/components/st7920/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/st7920/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/statsD/test.esp32-c3-idf.yaml b/tests/components/statsD/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/statsD/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/status/test.esp32-c3-idf.yaml b/tests/components/status/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/status/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/status_led/test.esp32-c3-idf.yaml b/tests/components/status_led/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/status_led/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/stepper/test.esp32-c3-idf.yaml b/tests/components/stepper/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/stepper/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sts3x/test.esp32-c3-idf.yaml b/tests/components/sts3x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sts3x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sun/test.esp32-c3-idf.yaml b/tests/components/sun/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/sun/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sun_gtil2/test.esp32-c3-idf.yaml b/tests/components/sun_gtil2/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/sun_gtil2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/switch/test.esp32-c3-idf.yaml b/tests/components/switch/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/switch/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/sx126x/test.esp32-c3-idf.yaml b/tests/components/sx126x/test.esp32-c3-idf.yaml deleted file mode 100644 index e27f11032e1..00000000000 --- a/tests/components/sx126x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - cs_pin: GPIO1 - rst_pin: GPIO2 - busy_pin: GPIO7 - dio1_pin: GPIO3 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sx127x/test.esp32-c3-idf.yaml b/tests/components/sx127x/test.esp32-c3-idf.yaml deleted file mode 100644 index dfee192545e..00000000000 --- a/tests/components/sx127x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO1 - rst_pin: GPIO2 - dio0_pin: GPIO3 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sx1509/test.esp32-c3-idf.yaml b/tests/components/sx1509/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sx1509/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/syslog/test.esp32-c3-idf.yaml b/tests/components/syslog/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/syslog/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/t6615/test.esp32-c3-idf.yaml b/tests/components/t6615/test.esp32-c3-idf.yaml deleted file mode 100644 index 147d967dd43..00000000000 --- a/tests/components/t6615/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart_19200: !include ../../test_build_components/common/uart_19200/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tc74/test.esp32-c3-idf.yaml b/tests/components/tc74/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tc74/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tca9548a/test.esp32-c3-idf.yaml b/tests/components/tca9548a/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tca9548a/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tca9555/test.esp32-c3-idf.yaml b/tests/components/tca9555/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tca9555/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tcl112/test.esp32-c3-idf.yaml b/tests/components/tcl112/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/tcl112/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tcs34725/test.esp32-c3-idf.yaml b/tests/components/tcs34725/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tcs34725/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tee501/test.esp32-c3-idf.yaml b/tests/components/tee501/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tee501/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/teleinfo/test.esp32-c3-idf.yaml b/tests/components/teleinfo/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/teleinfo/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/template/test.esp32-c3-idf.yaml b/tests/components/template/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/template/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/thermostat/test.esp32-c3-idf.yaml b/tests/components/thermostat/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/thermostat/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/time/test.esp32-c3-idf.yaml b/tests/components/time/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/time/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/time_based/test.esp32-c3-idf.yaml b/tests/components/time_based/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/time_based/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tlc59208f/test.esp32-c3-idf.yaml b/tests/components/tlc59208f/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tlc59208f/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tlc5947/test.esp32-c3-idf.yaml b/tests/components/tlc5947/test.esp32-c3-idf.yaml deleted file mode 100644 index 4694c43642c..00000000000 --- a/tests/components/tlc5947/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - lat_pin: GPIO3 - -packages: - common: !include common.yaml diff --git a/tests/components/tlc5971/test.esp32-c3-idf.yaml b/tests/components/tlc5971/test.esp32-c3-idf.yaml deleted file mode 100644 index d898a21d469..00000000000 --- a/tests/components/tlc5971/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -packages: - common: !include common.yaml diff --git a/tests/components/tm1621/test.esp32-c3-idf.yaml b/tests/components/tm1621/test.esp32-c3-idf.yaml deleted file mode 100644 index 562ced7485c..00000000000 --- a/tests/components/tm1621/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - cs_pin: GPIO6 - data_pin: GPIO7 - read_pin: GPIO2 - write_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/tm1637/test.esp32-c3-idf.yaml b/tests/components/tm1637/test.esp32-c3-idf.yaml deleted file mode 100644 index 0c4d4a9a7a4..00000000000 --- a/tests/components/tm1637/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO7 - dio_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/tm1638/test.esp32-c3-idf.yaml b/tests/components/tm1638/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/tm1638/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tm1651/test.esp32-c3-idf.yaml b/tests/components/tm1651/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/tm1651/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tmp102/test.esp32-c3-idf.yaml b/tests/components/tmp102/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tmp102/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tmp1075/test.esp32-c3-idf.yaml b/tests/components/tmp1075/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tmp1075/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tmp117/test.esp32-c3-idf.yaml b/tests/components/tmp117/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tmp117/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tof10120/test.esp32-c3-idf.yaml b/tests/components/tof10120/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tof10120/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tormatic/test.esp32-c3-idf.yaml b/tests/components/tormatic/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/tormatic/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/toshiba/test.esp32-c3-idf.yaml b/tests/components/toshiba/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/toshiba/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp32-c3-idf.yaml b/tests/components/toshiba/test_ras2819t.esp32-c3-idf.yaml deleted file mode 100644 index 00805baa011..00000000000 --- a/tests/components/toshiba/test_ras2819t.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO5 - rx_pin: GPIO4 - -<<: !include common_ras2819t.yaml diff --git a/tests/components/total_daily_energy/test.esp32-c3-idf.yaml b/tests/components/total_daily_energy/test.esp32-c3-idf.yaml deleted file mode 100644 index 8b0d069ce2e..00000000000 --- a/tests/components/total_daily_energy/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - sel_pin: GPIO2 - cf_pin: GPIO3 - cf1_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tsl2561/test.esp32-c3-idf.yaml b/tests/components/tsl2561/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tsl2561/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tsl2591/test.esp32-c3-idf.yaml b/tests/components/tsl2591/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tsl2591/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tt21100/test.esp32-c3-idf.yaml b/tests/components/tt21100/test.esp32-c3-idf.yaml deleted file mode 100644 index a7265e10b2c..00000000000 --- a/tests/components/tt21100/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - disp_reset_pin: GPIO7 - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ttp229_bsf/test.esp32-c3-idf.yaml b/tests/components/ttp229_bsf/test.esp32-c3-idf.yaml deleted file mode 100644 index ad1c58b40e0..00000000000 --- a/tests/components/ttp229_bsf/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - ttp229_scl_pin: GPIO7 - ttp229_sdo_pin: GPIO4 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ttp229_lsf/test.esp32-c3-idf.yaml b/tests/components/ttp229_lsf/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ttp229_lsf/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tuya/test.esp32-c3-idf.yaml b/tests/components/tuya/test.esp32-c3-idf.yaml deleted file mode 100644 index 43c28ba7b31..00000000000 --- a/tests/components/tuya/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - status_pin: GPIO2 -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tx20/test.esp32-c3-idf.yaml b/tests/components/tx20/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/tx20/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/udp/test.esp32-c3-idf.yaml b/tests/components/udp/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/udp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ufire_ec/test.esp32-c3-idf.yaml b/tests/components/ufire_ec/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ufire_ec/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ufire_ise/test.esp32-c3-idf.yaml b/tests/components/ufire_ise/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ufire_ise/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/uln2003/test.esp32-c3-idf.yaml b/tests/components/uln2003/test.esp32-c3-idf.yaml deleted file mode 100644 index 11d16a4d5da..00000000000 --- a/tests/components/uln2003/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - pin_a: GPIO0 - pin_b: GPIO1 - pin_c: GPIO2 - pin_d: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/ultrasonic/test.esp32-c3-idf.yaml b/tests/components/ultrasonic/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/ultrasonic/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml b/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml deleted file mode 100644 index cd26c783c29..00000000000 --- a/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -packages: - uart_19200: !include ../../test_build_components/common/uart_19200/esp32-c3-idf.yaml - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/uptime/test.esp32-c3-idf.yaml b/tests/components/uptime/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/uptime/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/vbus/test.esp32-c3-idf.yaml b/tests/components/vbus/test.esp32-c3-idf.yaml deleted file mode 100644 index a19013bf544..00000000000 --- a/tests/components/vbus/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/veml3235/test.esp32-c3-idf.yaml b/tests/components/veml3235/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/veml3235/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/veml7700/test.esp32-c3-idf.yaml b/tests/components/veml7700/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/veml7700/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/version/test.esp32-c3-idf.yaml b/tests/components/version/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/version/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/vl53l0x/test.esp32-c3-idf.yaml b/tests/components/vl53l0x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/vl53l0x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/voice_assistant/test.esp32-c3-idf.yaml b/tests/components/voice_assistant/test.esp32-c3-idf.yaml deleted file mode 100644 index 46745e4308c..00000000000 --- a/tests/components/voice_assistant/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - i2s_lrclk_pin: GPIO6 - i2s_bclk_pin: GPIO7 - i2s_mclk_pin: GPIO5 - i2s_din_pin: GPIO3 - i2s_dout_pin: GPIO2 - -<<: !include common-idf.yaml diff --git a/tests/components/wake_on_lan/test.esp32-c3-idf.yaml b/tests/components/wake_on_lan/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/wake_on_lan/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml b/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml deleted file mode 100644 index bdd7e1d3509..00000000000 --- a/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - cs_pin: GPIO7 - dc_pin: GPIO1 - busy_pin: GPIO2 - reset_pin: GPIO3 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/web_server/test.esp32-c3-idf.yaml b/tests/components/web_server/test.esp32-c3-idf.yaml deleted file mode 100644 index 7e6658e20e3..00000000000 --- a/tests/components/web_server/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common_v2.yaml diff --git a/tests/components/whirlpool/test.esp32-c3-idf.yaml b/tests/components/whirlpool/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/whirlpool/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/whynter/test.esp32-c3-idf.yaml b/tests/components/whynter/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/whynter/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wiegand/test.esp32-c3-idf.yaml b/tests/components/wiegand/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/wiegand/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi/test.esp32-c3-idf.yaml b/tests/components/wifi/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/wifi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi_info/test.esp32-c3-idf.yaml b/tests/components/wifi_info/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/wifi_info/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wifi_signal/test.esp32-c3-idf.yaml b/tests/components/wifi_signal/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/wifi_signal/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wireguard/test.esp32-c3-idf.yaml b/tests/components/wireguard/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/wireguard/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wl_134/test.esp32-c3-idf.yaml b/tests/components/wl_134/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/wl_134/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wts01/test.esp32-c3-idf.yaml b/tests/components/wts01/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/wts01/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/x9c/test.esp32-c3-idf.yaml b/tests/components/x9c/test.esp32-c3-idf.yaml deleted file mode 100644 index b06e15a98c3..00000000000 --- a/tests/components/x9c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO3 - inc_pin: GPIO4 - ud_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/xgzp68xx/test.esp32-c3-idf.yaml b/tests/components/xgzp68xx/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/xgzp68xx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_ble/test.esp32-c3-idf.yaml b/tests/components/xiaomi_ble/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_ble/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml b/tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml b/tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml b/tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml b/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml b/tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml b/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml b/tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xl9535/test.esp32-c3-idf.yaml b/tests/components/xl9535/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/xl9535/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xpt2046/test.esp32-c3-idf.yaml b/tests/components/xpt2046/test.esp32-c3-idf.yaml deleted file mode 100644 index ff7a32c26c0..00000000000 --- a/tests/components/xpt2046/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - dc_pin: GPIO7 - cs_pin: GPIO0 - disp_cs_pin: GPIO1 - interrupt_pin: GPIO3 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/yashima/test.esp32-c3-idf.yaml b/tests/components/yashima/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/yashima/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/zhlt01/test.esp32-c3-idf.yaml b/tests/components/zhlt01/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/zhlt01/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml b/tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/zwave_proxy/test.esp32-c3-idf.yaml b/tests/components/zwave_proxy/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/zwave_proxy/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/zyaura/test.esp32-c3-idf.yaml b/tests/components/zyaura/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/zyaura/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml From 182e106bfa31c08024f6e9de702651c4c9f4bc31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 20:44:40 -0600 Subject: [PATCH 204/461] [wifi] Guard AP-related members with USE_WIFI_AP to save RAM (#11753) --- esphome/components/wifi/wifi_component.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ac63e0eb0c5..ef595e98914 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -233,6 +233,7 @@ class WiFiComponent : public Component { */ void set_ap(const WiFiAP &ap); WiFiAP get_ap() { return this->ap_; } + void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; } #endif // USE_WIFI_AP void enable(); @@ -241,7 +242,6 @@ class WiFiComponent : public Component { void start_scanning(); void check_scanning_finished(); void start_connecting(const WiFiAP &ap, bool two); - void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; } void check_connecting_finished(); @@ -397,7 +397,9 @@ class WiFiComponent : public Component { std::vector sta_priorities_; wifi_scan_vector_t scan_result_; WiFiAP selected_ap_; +#ifdef USE_WIFI_AP WiFiAP ap_; +#endif optional output_power_; ESPPreferenceObject pref_; #ifdef USE_WIFI_FAST_CONNECT @@ -408,7 +410,9 @@ class WiFiComponent : public Component { uint32_t action_started_; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; +#ifdef USE_WIFI_AP uint32_t ap_timeout_{}; +#endif // Group all 8-bit values together WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; From 7c30d57391ff082e610c575b547298255b4090b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 21:26:53 -0600 Subject: [PATCH 205/461] [wifi] Refactor AP selection to use index instead of copy (saves 88 bytes) (#11749) --- esphome/components/wifi/__init__.py | 8 +- esphome/components/wifi/wifi_component.cpp | 219 +++++++++++++-------- esphome/components/wifi/wifi_component.h | 41 +++- 3 files changed, 177 insertions(+), 91 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b980bab4aa8..5f4190a9331 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -54,6 +54,10 @@ AUTO_LOAD = ["network"] NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" +# Maximum number of WiFi networks that can be configured +# Limited to 127 because selected_sta_index_ is int8_t in C++ +MAX_WIFI_NETWORKS = 127 + wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") ManualIP = wifi_ns.struct("ManualIP") @@ -260,7 +264,9 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(WiFiComponent), - cv.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA), + cv.Optional(CONF_NETWORKS): cv.All( + cv.ensure_list(WIFI_NETWORK_STA), cv.Length(max=MAX_WIFI_NETWORKS) + ), cv.Optional(CONF_SSID): cv.ssid, cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 51b5756f296..789c22bae10 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,5 +1,6 @@ #include "wifi_component.h" #ifdef USE_WIFI +#include #include #ifdef USE_ESP32 @@ -109,12 +110,15 @@ void WiFiComponent::start() { } #ifdef USE_WIFI_FAST_CONNECT - this->trying_loaded_ap_ = this->load_fast_connect_settings_(); + WiFiAP params; + this->trying_loaded_ap_ = this->load_fast_connect_settings_(params); if (!this->trying_loaded_ap_) { - this->ap_index_ = 0; - this->selected_ap_ = this->sta_[this->ap_index_]; + // FAST CONNECT FALLBACK: No saved settings available + // Use first config (will use SSID from config) + this->selected_sta_index_ = 0; + params = this->build_wifi_ap_from_selected_(); } - this->start_connecting(this->selected_ap_, false); + this->start_connecting(params, false); #else this->start_scanning(); #endif @@ -169,15 +173,16 @@ void WiFiComponent::loop() { this->status_set_warning(LOG_STR("waiting to reconnect")); if (millis() - this->action_started_ > 5000) { #ifdef USE_WIFI_FAST_CONNECT - // NOTE: This check may not make sense here as it could interfere with AP cycling - if (!this->selected_ap_.get_bssid().has_value()) - this->selected_ap_ = this->sta_[0]; - this->start_connecting(this->selected_ap_, false); + // Safety check: Ensure selected_sta_index_ is valid before retrying + // (should already be set by retry_connect(), but check for robustness) + this->reset_selected_ap_to_first_if_invalid_(); + WiFiAP params = this->build_wifi_ap_from_selected_(); + this->start_connecting(params, false); #else if (this->retry_hidden_) { - if (!this->selected_ap_.get_bssid().has_value()) - this->selected_ap_ = this->sta_[0]; - this->start_connecting(this->selected_ap_, false); + this->reset_selected_ap_to_first_if_invalid_(); + WiFiAP params = this->build_wifi_ap_from_selected_(); + this->start_connecting(params, false); } else { this->start_scanning(); } @@ -336,8 +341,42 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->clear_sta(); this->init_sta(1); this->add_sta(ap); + this->selected_sta_index_ = 0; +} + +WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { + // PRECONDITION: selected_sta_index_ must be valid (ensured by all callers) + const WiFiAP *config = this->get_selected_sta_(); + assert(config != nullptr); + WiFiAP params = *config; + + // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync after wifi_scan_done(): + // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) + // - It then finds which sta_[i] config matches scan_result_[0] + // - Sets selected_sta_index_ = i to record that matching config + // This sync holds until scan_result_ is cleared (e.g., after connection or in reset_for_next_ap_attempt_()) + if (!this->scan_result_.empty()) { + // Override with scan data - network is visible + const WiFiScanResult &scan = this->scan_result_[0]; + params.set_hidden(false); + params.set_ssid(scan.get_ssid()); + params.set_bssid(scan.get_bssid()); + params.set_channel(scan.get_channel()); + } else if (params.get_hidden()) { + // Hidden network - clear BSSID and channel even if set in config + // There might be multiple hidden networks with same SSID but we can't know which is correct + // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. + params.set_bssid(optional{}); + params.set_channel(optional{}); + } + + return params; +} + +WiFiAP WiFiComponent::get_sta() const { + const WiFiAP *config = this->get_selected_sta_(); + return config ? *config : WiFiAP{}; } -void WiFiComponent::clear_sta() { this->sta_.clear(); } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 @@ -485,8 +524,8 @@ void WiFiComponent::print_connect_params_() { LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE - if (this->selected_ap_.get_bssid().has_value()) { - ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid())); + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) { + ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config->get_bssid())); } #endif #ifdef USE_WIFI_11KV_SUPPORT @@ -633,55 +672,38 @@ void WiFiComponent::check_scanning_finished() { log_scan_result(res); } - if (!this->scan_result_[0].get_matches()) { + // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ + // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config + // matches that network and record it in selected_sta_index_. This keeps the two indices + // synchronized so build_wifi_ap_from_selected_() can safely use both to build connection parameters. + const WiFiScanResult &scan_res = this->scan_result_[0]; + if (!scan_res.get_matches()) { ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); return; } - // Build connection params directly into selected_ap_ to avoid extra copy - const WiFiScanResult &scan_res = this->scan_result_[0]; - WiFiAP &selected = this->selected_ap_; - for (auto &config : this->sta_) { - // search for matching STA config, at least one will match (from checks before) - if (!scan_res.matches(config)) { - continue; + bool found_match = false; + for (size_t i = 0; i < this->sta_.size(); i++) { + if (scan_res.matches(this->sta_[i])) { + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation + // No overflow check needed - YAML validation prevents >127 networks + this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] + found_match = true; + break; } + } - if (config.get_hidden()) { - // selected network is hidden, we use the data from the config - selected.set_hidden(true); - selected.set_ssid(config.get_ssid()); - // Clear channel and BSSID for hidden networks - there might be multiple hidden networks - // but we can't know which one is the correct one. Rely on probe-req with just SSID. - selected.set_channel(0); - selected.set_bssid(optional{}); - } else { - // selected network is visible, we use the data from the scan - // limit the connect params to only connect to exactly this network - // (network selection is done during scan phase). - selected.set_hidden(false); - selected.set_ssid(scan_res.get_ssid()); - selected.set_channel(scan_res.get_channel()); - selected.set_bssid(scan_res.get_bssid()); - } - // copy manual IP (if set) - selected.set_manual_ip(config.get_manual_ip()); - -#ifdef USE_WIFI_WPA2_EAP - // copy EAP parameters (if set) - selected.set_eap(config.get_eap()); -#endif - - // copy password (if set) - selected.set_password(config.get_password()); - - break; + if (!found_match) { + ESP_LOGW(TAG, "No matching network found"); + this->retry_connect(); + return; } yield(); - this->start_connecting(this->selected_ap_, false); + WiFiAP params = this->build_wifi_ap_from_selected_(); + this->start_connecting(params, false); } void WiFiComponent::dump_config() { @@ -700,9 +722,12 @@ void WiFiComponent::check_connecting_finished() { } ESP_LOGI(TAG, "Connected"); - // We won't retry hidden networks unless a reconnect fails more than three times again - if (this->retry_hidden_ && !this->selected_ap_.get_hidden()) - ESP_LOGW(TAG, "Network '%s' should be marked as hidden", this->selected_ap_.get_ssid().c_str()); + // Warn if we had to retry with hidden network mode for a network that's not marked hidden + // Only warn if we actually connected without scan data (SSID only), not if scan succeeded on retry + if (const WiFiAP *config = this->get_selected_sta_(); + this->retry_hidden_ && config && !config->get_hidden() && this->scan_result_.empty()) { + ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); + } this->retry_hidden_ = false; this->print_connect_params_(); @@ -725,16 +750,16 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; +#ifdef USE_WIFI_FAST_CONNECT + this->save_fast_connect_settings_(); +#endif + // Free scan results memory unless a component needs them if (!this->keep_scan_results_) { this->scan_result_.clear(); this->scan_result_.shrink_to_fit(); } -#ifdef USE_WIFI_FAST_CONNECT - this->save_fast_connect_settings_(); -#endif - return; } @@ -772,8 +797,8 @@ void WiFiComponent::check_connecting_finished() { } void WiFiComponent::retry_connect() { - if (this->selected_ap_.get_bssid()) { - auto bssid = *this->selected_ap_.get_bssid(); + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) { + auto bssid = *config->get_bssid(); float priority = this->get_sta_priority(bssid); this->set_sta_priority(bssid, priority - 1.0f); } @@ -782,19 +807,26 @@ void WiFiComponent::retry_connect() { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && (this->num_retried_ > 3 || this->error_from_callback_)) { #ifdef USE_WIFI_FAST_CONNECT + // No empty check needed - YAML validation requires at least one network for fast_connect if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; - this->ap_index_ = 0; // Retry from the first configured AP - } else if (this->ap_index_ >= this->sta_.size() - 1) { + this->selected_sta_index_ = 0; // Retry from the first configured AP + this->reset_for_next_ap_attempt_(); + } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation + // Exhausted all configured APs, restart adapter and cycle back to first + // Restart clears any stuck WiFi driver state + // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) + // Typically SSID only, which triggers ESP-IDF internal scanning ESP_LOGW(TAG, "No more APs to try"); - this->ap_index_ = 0; + this->selected_sta_index_ = 0; + this->reset_for_next_ap_attempt_(); this->restart_adapter(); } else { // Try next AP - this->ap_index_++; + this->selected_sta_index_++; + this->reset_for_next_ap_attempt_(); } - this->num_retried_ = 0; - this->selected_ap_ = this->sta_[this->ap_index_]; #else if (this->num_retried_ > 5) { // If retry failed for more than 5 times, let's restart STA @@ -813,7 +845,8 @@ void WiFiComponent::retry_connect() { if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { yield(); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; - this->start_connecting(this->selected_ap_, true); + WiFiAP params = this->build_wifi_ap_from_selected_(); + this->start_connecting(params, true); return; } @@ -852,16 +885,29 @@ bool WiFiComponent::is_esp32_improv_active_() { } #ifdef USE_WIFI_FAST_CONNECT -bool WiFiComponent::load_fast_connect_settings_() { +bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { SavedWifiFastConnectSettings fast_connect_save{}; if (this->fast_connect_pref_.load(&fast_connect_save)) { + // Validate saved AP index + if (fast_connect_save.ap_index < 0 || static_cast(fast_connect_save.ap_index) >= this->sta_.size()) { + ESP_LOGW(TAG, "AP index out of bounds"); + return false; + } + + // Set selected index for future operations (save, retry, etc) + this->selected_sta_index_ = fast_connect_save.ap_index; + + // Copy entire config, then override with fast connect data + params = this->sta_[fast_connect_save.ap_index]; + + // Override with saved BSSID/channel from fast connect (SSID/password/etc already copied from config) bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); - this->ap_index_ = fast_connect_save.ap_index; - this->selected_ap_ = this->sta_[this->ap_index_]; - this->selected_ap_.set_bssid(bssid); - this->selected_ap_.set_channel(fast_connect_save.channel); + params.set_bssid(bssid); + params.set_channel(fast_connect_save.channel); + // Fast connect uses specific BSSID+channel, not hidden network probe (even if config has hidden: true) + params.set_hidden(false); ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; @@ -873,18 +919,25 @@ bool WiFiComponent::load_fast_connect_settings_() { void WiFiComponent::save_fast_connect_settings_() { bssid_t bssid = wifi_bssid(); uint8_t channel = get_wifi_channel(); + // selected_sta_index_ is always valid here (called only after successful connection) + // Fallback to 0 is defensive programming for robustness + int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0; - if (bssid != this->selected_ap_.get_bssid() || channel != this->selected_ap_.get_channel()) { - SavedWifiFastConnectSettings fast_connect_save{}; - - memcpy(fast_connect_save.bssid, bssid.data(), 6); - fast_connect_save.channel = channel; - fast_connect_save.ap_index = this->ap_index_; - - this->fast_connect_pref_.save(&fast_connect_save); - - ESP_LOGD(TAG, "Saved fast_connect settings"); + // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear) + SavedWifiFastConnectSettings previous_save{}; + if (this->fast_connect_pref_.load(&previous_save) && memcmp(previous_save.bssid, bssid.data(), 6) == 0 && + previous_save.channel == channel && previous_save.ap_index == ap_index) { + return; // No change, nothing to save } + + SavedWifiFastConnectSettings fast_connect_save{}; + memcpy(fast_connect_save.bssid, bssid.data(), 6); + fast_connect_save.channel = channel; + fast_connect_save.ap_index = ap_index; + + this->fast_connect_pref_.save(&fast_connect_save); + + ESP_LOGD(TAG, "Saved fast_connect settings"); } #endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ef595e98914..cb75edf5a03 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -218,10 +218,14 @@ class WiFiComponent : public Component { WiFiComponent(); void set_sta(const WiFiAP &ap); - WiFiAP get_sta() { return this->selected_ap_; } + // Returns a copy of the currently selected AP configuration + WiFiAP get_sta() const; void init_sta(size_t count); void add_sta(const WiFiAP &ap); - void clear_sta(); + void clear_sta() { + this->sta_.clear(); + this->selected_sta_index_ = -1; + } #ifdef USE_WIFI_AP /** Setup an Access Point that should be created if no connection to a station can be made. @@ -337,6 +341,29 @@ class WiFiComponent : public Component { #endif // USE_WIFI_AP void print_connect_params_(); + WiFiAP build_wifi_ap_from_selected_() const; + + const WiFiAP *get_selected_sta_() const { + if (this->selected_sta_index_ >= 0 && static_cast(this->selected_sta_index_) < this->sta_.size()) { + return &this->sta_[this->selected_sta_index_]; + } + return nullptr; + } + + void reset_selected_ap_to_first_if_invalid_() { + if (this->selected_sta_index_ < 0 || static_cast(this->selected_sta_index_) >= this->sta_.size()) { + this->selected_sta_index_ = this->sta_.empty() ? -1 : 0; + } + } + +#ifdef USE_WIFI_FAST_CONNECT + // Reset state for next fast connect AP attempt + // Clears old scan data so the new AP is tried with config only (SSID without specific BSSID/channel) + void reset_for_next_ap_attempt_() { + this->num_retried_ = 0; + this->scan_result_.clear(); + } +#endif void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); @@ -365,7 +392,7 @@ class WiFiComponent : public Component { bool is_esp32_improv_active_(); #ifdef USE_WIFI_FAST_CONNECT - bool load_fast_connect_settings_(); + bool load_fast_connect_settings_(WiFiAP ¶ms); void save_fast_connect_settings_(); #endif @@ -396,7 +423,6 @@ class WiFiComponent : public Component { FixedVector sta_; std::vector sta_priorities_; wifi_scan_vector_t scan_result_; - WiFiAP selected_ap_; #ifdef USE_WIFI_AP WiFiAP ap_; #endif @@ -418,9 +444,10 @@ class WiFiComponent : public Component { WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; uint8_t num_retried_{0}; -#ifdef USE_WIFI_FAST_CONNECT - uint8_t ap_index_{0}; -#endif + // Index into sta_ array for the currently selected AP configuration (-1 = none selected) + // Used to access password, manual_ip, priority, EAP settings, and hidden flag + // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) + int8_t selected_sta_index_{-1}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ From 3c41e080c578f33f4b6d1fcecd5e024b2f30dbab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 21:37:02 -0600 Subject: [PATCH 206/461] [core] Use ESPDEPRECATED macro for deprecation warnings (#11755) --- esphome/components/select/select.h | 2 +- esphome/core/entity_base.h | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index f859594cd18..7459c9d1469 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -35,7 +35,7 @@ class Select : public EntityBase { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" /// @deprecated Use current_option() instead. This member will be removed in ESPHome 2026.5.0. - __attribute__((deprecated("Use current_option() instead of .state. Will be removed in 2026.5.0"))) + ESPDEPRECATED("Use current_option() instead of .state. Will be removed in 2026.5.0", "2025.11.0") std::string state{}; Select() = default; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 6e5362464f0..1486ff53602 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -61,9 +61,10 @@ class EntityBase { } // Get/set this entity's icon - [[deprecated("Use get_icon_ref() instead for better performance (avoids string copy). Will stop working in ESPHome " - "2026.5.0")]] std::string - get_icon() const; + ESPDEPRECATED( + "Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0", + "2025.11.0") + std::string get_icon() const; void set_icon(const char *icon); StringRef get_icon_ref() const { static constexpr auto EMPTY_STRING = StringRef::from_lit(""); @@ -160,9 +161,10 @@ class EntityBase { class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) public: /// Get the device class, using the manual override if set. - [[deprecated("Use get_device_class_ref() instead for better performance (avoids string copy). Will stop working in " - "ESPHome 2026.5.0")]] std::string - get_device_class(); + ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in " + "ESPHome 2026.5.0", + "2025.11.0") + std::string get_device_class(); /// Manually set the device class. void set_device_class(const char *device_class); /// Get the device class as StringRef @@ -178,9 +180,10 @@ class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) public: /// Get the unit of measurement, using the manual override if set. - [[deprecated("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will stop " - "working in ESPHome 2026.5.0")]] std::string - get_unit_of_measurement(); + ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be " + "removed in ESPHome 2026.5.0", + "2025.11.0") + std::string get_unit_of_measurement(); /// Manually set the unit of measurement. void set_unit_of_measurement(const char *unit_of_measurement); /// Get the unit of measurement as StringRef From 4f08f0750a66299d4de5e562b4b406ce14bf22f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 22:34:53 -0600 Subject: [PATCH 207/461] [ai_instructions] Add public API and breaking changes guidelines (#11756) --- .ai/instructions.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/.ai/instructions.md b/.ai/instructions.md index 9309c67c656..8d81c6cf0fc 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -440,3 +440,45 @@ This document provides essential context for AI models interacting with this pro * **Python:** When adding a new Python dependency, add it to the appropriate `requirements*.txt` file and `pyproject.toml`. * **C++ / PlatformIO:** When adding a new C++ dependency, add it to `platformio.ini` and use `cg.add_library`. * **Build Flags:** Use `cg.add_build_flag(...)` to add compiler flags. + +## 8. Public API and Breaking Changes + +* **Public C++ API:** + * **Components**: Only documented features at [esphome.io](https://esphome.io) are public API. Undocumented `public` members are internal. + * **Core/Base Classes** (`esphome/core/`, `Component`, `Sensor`, etc.): All `public` members are public API. + * **Components with Global Accessors** (`global_api_server`, etc.): All `public` members are public API (except config setters). + +* **Public Python API:** + * All documented configuration options at [esphome.io](https://esphome.io) are public API. + * Python code in `esphome/core/` actively used by existing core components is considered stable API. + * Other Python code is internal unless explicitly documented for external component use. + +* **Breaking Changes Policy:** + * Aim for **6-month deprecation window** when possible + * Clean breaks allowed for: signature changes, deep refactorings, resource constraints + * Must document migration path in PR description (generates release notes) + * Blog post required for core/base class changes or significant architectural changes + * Full details: https://developers.esphome.io/contributing/code/#public-api-and-breaking-changes + +* **Breaking Change Checklist:** + - [ ] Clear justification (RAM/flash savings, architectural improvement) + - [ ] Explored non-breaking alternatives + - [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++) + - [ ] Documented migration path in PR description with before/after examples + - [ ] Updated all internal usage and esphome-docs + - [ ] Tested backward compatibility during deprecation period + +* **Deprecation Pattern (C++):** + ```cpp + // Remove before 2026.6.0 + ESPDEPRECATED("Use new_method() instead. Removed in 2026.6.0", "2025.12.0") + void old_method() { this->new_method(); } + ``` + +* **Deprecation Pattern (Python):** + ```python + # Remove before 2026.6.0 + if CONF_OLD_KEY in config: + _LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0") + config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate + ``` From 85d2565f25057cba2ca362fa2c40db5122f212b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 01:18:43 -0600 Subject: [PATCH 208/461] [tests] Fix determine_jobs tests failing when target branch is beta (#11758) --- tests/script/test_determine_jobs.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 4894a5e28af..cadc7f9cd7a 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -70,6 +70,13 @@ def mock_changed_files() -> Generator[Mock, None, None]: yield mock +@pytest.fixture +def mock_target_branch_dev() -> Generator[Mock, None, None]: + """Mock get_target_branch to return 'dev' for memory impact tests.""" + with patch.object(determine_jobs, "get_target_branch", return_value="dev") as mock: + yield mock + + @pytest.fixture(autouse=True) def clear_determine_jobs_caches() -> None: """Clear all cached functions before each test.""" @@ -688,6 +695,7 @@ def test_main_detects_components_with_variant_tests( # Tests for detect_memory_impact_config function +@pytest.mark.usefixtures("mock_target_branch_dev") def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> None: """Test memory impact detection when components share a common platform.""" # Create test directory structure @@ -722,6 +730,7 @@ def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> Non assert result["use_merged_config"] == "true" +@pytest.mark.usefixtures("mock_target_branch_dev") def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None: """Test memory impact detection with core C++ changes (no component changes).""" # Create test directory structure with fallback component @@ -779,6 +788,7 @@ def test_detect_memory_impact_config_core_python_only_changes(tmp_path: Path) -> assert result["should_run"] == "false" +@pytest.mark.usefixtures("mock_target_branch_dev") def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None: """Test memory impact detection when components have no common platform.""" # Create test directory structure @@ -855,6 +865,7 @@ def test_detect_memory_impact_config_no_components_with_tests(tmp_path: Path) -> assert result["should_run"] == "false" +@pytest.mark.usefixtures("mock_target_branch_dev") def test_detect_memory_impact_config_includes_base_bus_components( tmp_path: Path, ) -> None: @@ -897,6 +908,7 @@ def test_detect_memory_impact_config_includes_base_bus_components( assert result["platform"] == "esp32-idf" # Common platform +@pytest.mark.usefixtures("mock_target_branch_dev") def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None: """Test memory impact detection for components with only variant test files. @@ -1125,6 +1137,7 @@ def test_main_core_files_changed_still_detects_components( assert len(output["changed_components"]) > 0 +@pytest.mark.usefixtures("mock_target_branch_dev") def test_detect_memory_impact_config_filters_incompatible_esp32_on_esp8266( tmp_path: Path, ) -> None: @@ -1178,6 +1191,7 @@ def test_detect_memory_impact_config_filters_incompatible_esp32_on_esp8266( assert result["use_merged_config"] == "true" +@pytest.mark.usefixtures("mock_target_branch_dev") def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32( tmp_path: Path, ) -> None: From a5bf55b6acbf6c1715f20487c167a7ff2593ed2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 01:19:45 -0600 Subject: [PATCH 209/461] =?UTF-8?q?[ci]=20Fix=20component=20batching=20for?= =?UTF-8?q?=20beta/release=20branches=20(3-4=20=E2=86=92=2040=20per=20batc?= =?UTF-8?q?h)=20(#11759)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- script/determine-jobs.py | 18 +++- tests/script/test_determine_jobs.py | 148 ++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 39a7571fbe2..5cc3f2570aa 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -756,11 +756,27 @@ def main() -> None: component_test_batches: list[str] if changed_components_with_tests: tests_dir = Path(root_path) / ESPHOME_TESTS_COMPONENTS_PATH + + # For beta/release branches, group all components for faster CI + # (no isolation, all components are groupable) + target_branch = get_target_branch() + is_release_branch = target_branch and ( + target_branch.startswith("release") or target_branch.startswith("beta") + ) + + if is_release_branch: + # For beta/release: Don't isolate any components - group everything + # This allows components to be merged into single builds + batch_directly_changed = set() # Empty set - no isolation + else: + # Normal PR: only directly changed components are isolated + batch_directly_changed = directly_changed_with_tests + batches, _ = create_intelligent_batches( components=changed_components_with_tests, tests_dir=tests_dir, batch_size=COMPONENT_TEST_BATCH_SIZE, - directly_changed=directly_changed_with_tests, + directly_changed=batch_directly_changed, ) # Convert batches to space-separated strings for CI matrix component_test_batches = [" ".join(batch) for batch in batches] diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index cadc7f9cd7a..291a23967b2 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -19,6 +19,8 @@ sys.path.insert(0, script_dir) # Import helpers module for patching import helpers # noqa: E402 +import script.helpers # noqa: E402 + spec = importlib.util.spec_from_file_location( "determine_jobs", os.path.join(script_dir, "determine-jobs.py") ) @@ -132,6 +134,16 @@ def test_main_all_tests_should_run( ["wifi", "api"] if not deps else ["wifi", "api", "sensor"] ), ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["wifi", "api", "sensor"]], {}), + ), ): determine_jobs.main() @@ -203,6 +215,16 @@ def test_main_no_tests_should_run( patch.object( determine_jobs, "get_components_with_dependencies", return_value=[] ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([], {}), + ), ): determine_jobs.main() @@ -266,6 +288,16 @@ def test_main_with_branch_argument( patch.object( determine_jobs, "get_components_with_dependencies", return_value=["mqtt"] ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["mqtt"]], {}), + ), ): determine_jobs.main() @@ -571,6 +603,11 @@ def test_main_filters_components_without_tests( ), ), patch.object(determine_jobs, "changed_files", return_value=[]), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), ): # Clear the cache since we're mocking root_path determine_jobs.main() @@ -670,6 +707,11 @@ def test_main_detects_components_with_variant_tests( ), ), patch.object(determine_jobs, "changed_files", return_value=[]), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), ): # Clear the cache since we're mocking root_path determine_jobs.main() @@ -1124,6 +1166,16 @@ def test_main_core_files_changed_still_detects_components( else ["select", "api", "bluetooth_proxy", "logger"] ), ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["select", "api", "bluetooth_proxy", "logger"]], {}), + ), ): determine_jobs.main() @@ -1367,3 +1419,99 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) -> # Memory impact should run at exactly 40 components (at limit but not over) assert result["should_run"] == "true" assert len(result["components"]) == 40 + + +def test_component_batching_beta_branch_40_per_batch( + tmp_path: Path, + mock_should_run_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_changed_files: Mock, + mock_determine_cpp_unit_tests: Mock, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test that beta/release branches create batches with 40 actual components each. + + For beta/release branches, all components should be groupable (not isolated), + and each batch should contain 40 actual components with weight 1 each. + This matches the original behavior before consolidation. + """ + # Create 120 test components with test files + component_names = [f"comp_{i:03d}" for i in range(120)] + tests_dir = tmp_path / "tests" / "components" + + for comp in component_names: + comp_dir = tests_dir / comp + comp_dir.mkdir(parents=True) + (comp_dir / "test.esp32-idf.yaml").write_text(f"# Test for {comp}") + + # Setup mocks + mock_should_run_integration_tests.return_value = False + mock_should_run_clang_tidy.return_value = False + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = False + mock_determine_cpp_unit_tests.return_value = (False, []) + + # Mock changed_files to return all component files + changed_files = [ + f"esphome/components/{comp}/{comp}.cpp" for comp in component_names + ] + mock_changed_files.return_value = changed_files + + # Run main function with beta branch + # Don't mock create_intelligent_batches - that's what we're testing! + with ( + patch("sys.argv", ["determine-jobs.py", "--branch", "beta"]), + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(script.helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "get_target_branch", return_value="beta"), + patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), + patch.object( + determine_jobs, + "get_changed_components", + return_value=component_names, + ), + patch.object( + determine_jobs, + "filter_component_and_test_files", + side_effect=lambda f: f.startswith("esphome/components/"), + ), + patch.object( + determine_jobs, + "get_components_with_dependencies", + side_effect=lambda files, deps: component_names, + ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + ): + determine_jobs.main() + + # Check output + captured = capsys.readouterr() + output = json.loads(captured.out) + + # Verify batches are present and properly sized + assert "component_test_batches" in output + batches = output["component_test_batches"] + + # Should have 3 batches (120 components / 40 per batch = 3) + assert len(batches) == 3, f"Expected 3 batches, got {len(batches)}" + + # Each batch should have approximately 40 components (all weight=1, groupable) + for i, batch_str in enumerate(batches): + batch_components = batch_str.split() + assert len(batch_components) == 40, ( + f"Batch {i} should have 40 components, got {len(batch_components)}" + ) + + # Verify all 120 components are in batches + all_components = [] + for batch_str in batches: + all_components.extend(batch_str.split()) + assert len(all_components) == 120 + assert set(all_components) == set(component_names) From dc3c18974ed9a92972d5ee954d0e85e2ae2f33a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 11:28:25 -0600 Subject: [PATCH 210/461] [event] Store event types in flash memory --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_pb2.cpp | 10 ++++---- esphome/components/api/api_pb2.h | 2 +- esphome/components/api/api_pb2_dump.cpp | 2 +- esphome/components/event/event.cpp | 14 ++++++------ esphome/components/event/event.h | 24 ++++++++++++++++---- esphome/components/mqtt/mqtt_event.cpp | 4 ++-- esphome/components/web_server/web_server.cpp | 2 +- 9 files changed, 39 insertions(+), 23 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 7a50fa6b179..e115e4630d8 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2147,7 +2147,7 @@ message ListEntitiesEventResponse { EntityCategory entity_category = 7; string device_class = 8; - repeated string event_types = 9; + repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message EventResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5ab8a6eb050..84b578876cf 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1310,7 +1310,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c auto *event = static_cast(entity); ListEntitiesEventResponse msg; msg.set_device_class(event->get_device_class_ref()); - for (const auto &event_type : event->get_event_types()) + for (const char *event_type : event->get_event_types()) msg.event_types.push_back(event_type); return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index dfa1a1320fe..0a073fb662f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2877,8 +2877,8 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_string(8, this->device_class_ref_); - for (auto &it : this->event_types) { - buffer.encode_string(9, it, true); + for (const char *it : *this->event_types) { + buffer.encode_string(9, it, strlen(it), true); } #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); @@ -2894,9 +2894,9 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); size.add_length(1, this->device_class_ref_.size()); - if (!this->event_types.empty()) { - for (const auto &it : this->event_types) { - size.add_length_force(1, it.size()); + if (!this->event_types->empty()) { + for (const char *it : *this->event_types) { + size.add_length_force(1, strlen(it)); } } #ifdef USE_DEVICES diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 716f1a6e9b5..358049026ec 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2788,7 +2788,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class_ref_{}; void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } - std::vector event_types{}; + const FixedVector *event_types{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index d94ceaaa9c4..d9662483bf5 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2053,7 +2053,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); dump_field(out, "device_class", this->device_class_ref_); - for (const auto &it : this->event_types) { + for (const auto &it : *this->event_types) { dump_field(out, "event_types", it, 4); } #ifdef USE_DEVICES diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 20549ad0a50..ccb221d4418 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -8,11 +8,11 @@ namespace event { static const char *const TAG = "event"; void Event::trigger(const std::string &event_type) { - // Linear search - faster than std::set for small datasets (1-5 items typical) - const std::string *found = nullptr; - for (const auto &type : this->types_) { - if (type == event_type) { - found = &type; + // Linear search with strcmp - faster than std::set for small datasets (1-5 items typical) + const char *found = nullptr; + for (const char *type : this->types_) { + if (strcmp(type, event_type.c_str()) == 0) { + found = type; break; } } @@ -20,8 +20,8 @@ void Event::trigger(const std::string &event_type) { ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); return; } - last_event_type = found; - ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); + this->last_event_type = found; + ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type); this->event_callback_.call(event_type); } diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 2f6267a2006..5219288d2a9 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "esphome/core/component.h" @@ -22,16 +23,31 @@ namespace event { class Event : public EntityBase, public EntityBase_DeviceClass { public: - const std::string *last_event_type; + const char *last_event_type{nullptr}; void trigger(const std::string &event_type); - void set_event_types(const std::initializer_list &event_types) { this->types_ = event_types; } - const FixedVector &get_event_types() const { return this->types_; } + + /// Set the event types supported by this event (from initializer list). + void set_event_types(std::initializer_list event_types) { this->types_ = event_types; } + /// Set the event types supported by this event (from FixedVector). + void set_event_types(const FixedVector &event_types) { this->types_ = event_types; } + /// Set the event types supported by this event (from C array). + template void set_event_types(const char *const (&event_types)[N]) { + this->types_.assign(event_types, event_types + N); + } + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_event_types(std::initializer_list event_types) = delete; + void set_event_types(const FixedVector &event_types) = delete; + + /// Return the event types supported by this event. + const FixedVector &get_event_types() const { return this->types_; } + void add_on_event_callback(std::function &&callback); protected: CallbackManager event_callback_; - FixedVector types_; + FixedVector types_; }; } // namespace event diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index e2063354464..fd095ea041b 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -38,8 +38,8 @@ void MQTTEventComponent::setup() { void MQTTEventComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Event '%s': ", this->event_->get_name().c_str()); ESP_LOGCONFIG(TAG, "Event Types: "); - for (const auto &event_type : this->event_->get_event_types()) { - ESP_LOGCONFIG(TAG, "- %s", event_type.c_str()); + for (const char *event_type : this->event_->get_event_types()) { + ESP_LOGCONFIG(TAG, "- %s", event_type); } LOG_MQTT_COMPONENT(true, true); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f1d1a75875a..adadfd20e8d 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1649,7 +1649,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty } if (start_config == DETAIL_ALL) { JsonArray event_types = root["event_types"].to(); - for (auto const &event_type : obj->get_event_types()) { + for (const char *event_type : obj->get_event_types()) { event_types.add(event_type); } root["device_class"] = obj->get_device_class_ref(); From fca80d81c8ed534d0cf03b5cccb37ec325e300a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 11:30:34 -0600 Subject: [PATCH 211/461] [event] Store event types in flash memory --- esphome/components/event/event.cpp | 4 ++-- esphome/components/event/event.h | 21 +++++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index ccb221d4418..6b79fa8c114 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -20,8 +20,8 @@ void Event::trigger(const std::string &event_type) { ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); return; } - this->last_event_type = found; - ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type); + this->last_event_type_ = found; + ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_); this->event_callback_.call(event_type); } diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 5219288d2a9..74709dcac19 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -23,17 +23,22 @@ namespace event { class Event : public EntityBase, public EntityBase_DeviceClass { public: - const char *last_event_type{nullptr}; - void trigger(const std::string &event_type); /// Set the event types supported by this event (from initializer list). - void set_event_types(std::initializer_list event_types) { this->types_ = event_types; } + void set_event_types(std::initializer_list event_types) { + this->types_ = event_types; + this->last_event_type_ = nullptr; // Reset when types change + } /// Set the event types supported by this event (from FixedVector). - void set_event_types(const FixedVector &event_types) { this->types_ = event_types; } + void set_event_types(const FixedVector &event_types) { + this->types_ = event_types; + this->last_event_type_ = nullptr; // Reset when types change + } /// Set the event types supported by this event (from C array). template void set_event_types(const char *const (&event_types)[N]) { this->types_.assign(event_types, event_types + N); + this->last_event_type_ = nullptr; // Reset when types change } // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages @@ -43,11 +48,19 @@ class Event : public EntityBase, public EntityBase_DeviceClass { /// Return the event types supported by this event. const FixedVector &get_event_types() const { return this->types_; } + /// Return the last triggered event type (pointer to string in types_), or nullptr if no event triggered yet. + const char *get_last_event_type() const { return this->last_event_type_; } + void add_on_event_callback(std::function &&callback); protected: CallbackManager event_callback_; FixedVector types_; + + private: + /// Last triggered event type - must point to entry in types_ to ensure valid lifetime. + /// Set by trigger() after validation, reset to nullptr when types_ changes. + const char *last_event_type_{nullptr}; }; } // namespace event From 499ffd84a78259a954cc7ae0f21e819b48d34608 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 11:31:24 -0600 Subject: [PATCH 212/461] [event] Store event types in flash memory --- esphome/components/web_server/web_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index adadfd20e8d..e5ca83eb1ab 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1628,7 +1628,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa } static std::string get_event_type(event::Event *event) { - return (event && event->last_event_type) ? *event->last_event_type : ""; + return (event && event->get_last_event_type()) ? event->get_last_event_type() : ""; } std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { From a823fd322e98428a563d234ad1a192382db3cdde Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 11:35:19 -0600 Subject: [PATCH 213/461] fixes --- esphome/components/api/api_connection.cpp | 2 +- esphome/components/event/event.h | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 84b578876cf..7f8438af25d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1311,7 +1311,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c ListEntitiesEventResponse msg; msg.set_device_class(event->get_device_class_ref()); for (const char *event_type : event->get_event_types()) - msg.event_types.push_back(event_type); + msg.event_types->push_back(event_type); return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 74709dcac19..822785dece8 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -30,16 +30,6 @@ class Event : public EntityBase, public EntityBase_DeviceClass { this->types_ = event_types; this->last_event_type_ = nullptr; // Reset when types change } - /// Set the event types supported by this event (from FixedVector). - void set_event_types(const FixedVector &event_types) { - this->types_ = event_types; - this->last_event_type_ = nullptr; // Reset when types change - } - /// Set the event types supported by this event (from C array). - template void set_event_types(const char *const (&event_types)[N]) { - this->types_.assign(event_types, event_types + N); - this->last_event_type_ = nullptr; // Reset when types change - } // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages void set_event_types(std::initializer_list event_types) = delete; From f4fea1a00f17411422bfba4fccae495b58c670dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 11:37:49 -0600 Subject: [PATCH 214/461] [event] Store event types in flash memory --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_pb2.h | 2 +- esphome/components/event/event.h | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e115e4630d8..f6eb8ec1c97 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2147,7 +2147,7 @@ message ListEntitiesEventResponse { EntityCategory entity_category = 7; string device_class = 8; - repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector"]; + repeated string event_types = 9 [(container_pointer_no_template) = "std::vector"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message EventResponse { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 358049026ec..df793eb2623 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2788,7 +2788,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class_ref_{}; void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } - const FixedVector *event_types{}; + const std::vector *event_types{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 822785dece8..3107a11477f 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -2,6 +2,7 @@ #include #include +#include #include "esphome/core/component.h" #include "esphome/core/entity_base.h" @@ -30,13 +31,23 @@ class Event : public EntityBase, public EntityBase_DeviceClass { this->types_ = event_types; this->last_event_type_ = nullptr; // Reset when types change } + /// Set the event types supported by this event (from vector). + void set_event_types(const std::vector &event_types) { + this->types_ = event_types; + this->last_event_type_ = nullptr; // Reset when types change + } + /// Set the event types supported by this event (from C array). + template void set_event_types(const char *const (&event_types)[N]) { + this->types_.assign(event_types, event_types + N); + this->last_event_type_ = nullptr; // Reset when types change + } // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages void set_event_types(std::initializer_list event_types) = delete; - void set_event_types(const FixedVector &event_types) = delete; + void set_event_types(const std::vector &event_types) = delete; /// Return the event types supported by this event. - const FixedVector &get_event_types() const { return this->types_; } + const std::vector &get_event_types() const { return this->types_; } /// Return the last triggered event type (pointer to string in types_), or nullptr if no event triggered yet. const char *get_last_event_type() const { return this->last_event_type_; } @@ -45,7 +56,7 @@ class Event : public EntityBase, public EntityBase_DeviceClass { protected: CallbackManager event_callback_; - FixedVector types_; + std::vector types_; private: /// Last triggered event type - must point to entry in types_ to ensure valid lifetime. From 51a238f3d28f0191bb01331494e69e932d10ad72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 11:39:17 -0600 Subject: [PATCH 215/461] [event] Store event types in flash memory --- esphome/components/event/event.h | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 3107a11477f..efa4c14464e 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -36,11 +36,6 @@ class Event : public EntityBase, public EntityBase_DeviceClass { this->types_ = event_types; this->last_event_type_ = nullptr; // Reset when types change } - /// Set the event types supported by this event (from C array). - template void set_event_types(const char *const (&event_types)[N]) { - this->types_.assign(event_types, event_types + N); - this->last_event_type_ = nullptr; // Reset when types change - } // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages void set_event_types(std::initializer_list event_types) = delete; From e2d949c2875821247a85c90e1bba3619c2dfbc8f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 13:39:57 -0600 Subject: [PATCH 216/461] fixed vector will work here --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_connection.cpp | 3 +-- esphome/components/api/api_pb2.h | 2 +- esphome/components/event/event.cpp | 16 ++++++++++++++++ esphome/components/event/event.h | 12 ++++++------ 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index f6eb8ec1c97..e115e4630d8 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2147,7 +2147,7 @@ message ListEntitiesEventResponse { EntityCategory entity_category = 7; string device_class = 8; - repeated string event_types = 9 [(container_pointer_no_template) = "std::vector"]; + repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message EventResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7f8438af25d..8c293b41a29 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1310,8 +1310,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c auto *event = static_cast(entity); ListEntitiesEventResponse msg; msg.set_device_class(event->get_device_class_ref()); - for (const char *event_type : event->get_event_types()) - msg.event_types->push_back(event_type); + msg.event_types = &event->get_event_types(); return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index df793eb2623..358049026ec 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2788,7 +2788,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class_ref_{}; void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } - const std::vector *event_types{}; + const FixedVector *event_types{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 6b79fa8c114..a14afbd7f55 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -25,6 +25,22 @@ void Event::trigger(const std::string &event_type) { this->event_callback_.call(event_type); } +void Event::set_event_types(const FixedVector &event_types) { + this->types_.init(event_types.size()); + for (const char *type : event_types) { + this->types_.push_back(type); + } + this->last_event_type_ = nullptr; // Reset when types change +} + +void Event::set_event_types(const std::vector &event_types) { + this->types_.init(event_types.size()); + for (const char *type : event_types) { + this->types_.push_back(type); + } + this->last_event_type_ = nullptr; // Reset when types change +} + void Event::add_on_event_callback(std::function &&callback) { this->event_callback_.add(std::move(callback)); } diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index efa4c14464e..e4b2e0b845b 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -31,18 +31,18 @@ class Event : public EntityBase, public EntityBase_DeviceClass { this->types_ = event_types; this->last_event_type_ = nullptr; // Reset when types change } + /// Set the event types supported by this event (from FixedVector). + void set_event_types(const FixedVector &event_types); /// Set the event types supported by this event (from vector). - void set_event_types(const std::vector &event_types) { - this->types_ = event_types; - this->last_event_type_ = nullptr; // Reset when types change - } + void set_event_types(const std::vector &event_types); // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages void set_event_types(std::initializer_list event_types) = delete; + void set_event_types(const FixedVector &event_types) = delete; void set_event_types(const std::vector &event_types) = delete; /// Return the event types supported by this event. - const std::vector &get_event_types() const { return this->types_; } + const FixedVector &get_event_types() const { return this->types_; } /// Return the last triggered event type (pointer to string in types_), or nullptr if no event triggered yet. const char *get_last_event_type() const { return this->last_event_type_; } @@ -51,7 +51,7 @@ class Event : public EntityBase, public EntityBase_DeviceClass { protected: CallbackManager event_callback_; - std::vector types_; + FixedVector types_; private: /// Last triggered event type - must point to entry in types_ to ensure valid lifetime. From 79d1a558af778df183764b656a838ec650d9fb47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:12:15 +0000 Subject: [PATCH 217/461] Bump ruff from 0.14.3 to 0.14.4 (#11768) 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 5356bffd962..dab660b03f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.3 + rev: v0.14.4 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 11367172b11..81cb711eec9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.2 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.3 # also change in .pre-commit-config.yaml when updating +ruff==0.14.4 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating pre-commit From f0bcea77496f2ee211f0d501d3f1c5d734614dc1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 14:31:53 -0600 Subject: [PATCH 218/461] tweaks --- esphome/components/web_server/web_server.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e5ca83eb1ab..91ca0764749 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1628,7 +1628,8 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa } static std::string get_event_type(event::Event *event) { - return (event && event->get_last_event_type()) ? event->get_last_event_type() : ""; + const char *last_type = event ? event->get_last_event_type() : nullptr; + return last_type ? last_type : ""; } std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { From 295fe8da040bd822475e5b047f38154391a2954f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 15:32:46 -0600 Subject: [PATCH 219/461] controller registry phase1/2 --- esphome/components/api/api_server.cpp | 26 +- esphome/components/api/api_server.h | 12 +- esphome/components/web_server/web_server.cpp | 49 +++- esphome/components/web_server/web_server.h | 12 +- esphome/core/controller.h | 12 +- esphome/core/controller_registry.cpp | 177 +++++++++++++ esphome/core/controller_registry.h | 263 +++++++++++++++++++ 7 files changed, 509 insertions(+), 42 deletions(-) create mode 100644 esphome/core/controller_registry.cpp create mode 100644 esphome/core/controller_registry.h diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index e5f0d9795ed..a08554acd8d 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -4,6 +4,7 @@ #include "api_connection.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -34,7 +35,7 @@ APIServer::APIServer() { } void APIServer::setup() { - this->setup_controller(); + ControllerRegistry::register_controller(this); #ifdef USE_API_NOISE uint32_t hash = 88491486UL; @@ -269,7 +270,7 @@ bool APIServer::check_password(const uint8_t *password_data, size_t password_len void APIServer::handle_disconnect(APIConnection *conn) {} -// Macro for entities without extra parameters +// Macro for controller update dispatch #define API_DISPATCH_UPDATE(entity_type, entity_name) \ void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ if (obj->is_internal()) \ @@ -278,15 +279,6 @@ void APIServer::handle_disconnect(APIConnection *conn) {} c->send_##entity_name##_state(obj); \ } -// Macro for entities with extra parameters (but parameters not used in send) -#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \ - void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \ - if (obj->is_internal()) \ - return; \ - for (auto &c : this->clients_) \ - c->send_##entity_name##_state(obj); \ - } - #ifdef USE_BINARY_SENSOR API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) #endif @@ -304,15 +296,15 @@ API_DISPATCH_UPDATE(light::LightState, light) #endif #ifdef USE_SENSOR -API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state) +API_DISPATCH_UPDATE(sensor::Sensor, sensor) #endif #ifdef USE_SWITCH -API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state) +API_DISPATCH_UPDATE(switch_::Switch, switch) #endif #ifdef USE_TEXT_SENSOR -API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state) +API_DISPATCH_UPDATE(text_sensor::TextSensor, text_sensor) #endif #ifdef USE_CLIMATE @@ -320,7 +312,7 @@ API_DISPATCH_UPDATE(climate::Climate, climate) #endif #ifdef USE_NUMBER -API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state) +API_DISPATCH_UPDATE(number::Number, number) #endif #ifdef USE_DATETIME_DATE @@ -336,11 +328,11 @@ API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime) #endif #ifdef USE_TEXT -API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state) +API_DISPATCH_UPDATE(text::Text, text) #endif #ifdef USE_SELECT -API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index) +API_DISPATCH_UPDATE(select::Select, select) #endif #ifdef USE_LOCK diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index f1f44a266d3..4b030239578 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -72,19 +72,19 @@ class APIServer : public Component, public Controller { void on_light_update(light::LightState *obj) override; #endif #ifdef USE_SENSOR - void on_sensor_update(sensor::Sensor *obj, float state) override; + void on_sensor_update(sensor::Sensor *obj) override; #endif #ifdef USE_SWITCH - void on_switch_update(switch_::Switch *obj, bool state) override; + void on_switch_update(switch_::Switch *obj) override; #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override; + void on_text_sensor_update(text_sensor::TextSensor *obj) override; #endif #ifdef USE_CLIMATE void on_climate_update(climate::Climate *obj) override; #endif #ifdef USE_NUMBER - void on_number_update(number::Number *obj, float state) override; + void on_number_update(number::Number *obj) override; #endif #ifdef USE_DATETIME_DATE void on_date_update(datetime::DateEntity *obj) override; @@ -96,10 +96,10 @@ class APIServer : public Component, public Controller { void on_datetime_update(datetime::DateTimeEntity *obj) override; #endif #ifdef USE_TEXT - void on_text_update(text::Text *obj, const std::string &state) override; + void on_text_update(text::Text *obj) override; #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj, const std::string &state, size_t index) override; + void on_select_update(select::Select *obj) override; #endif #ifdef USE_LOCK void on_lock_update(lock::Lock *obj) override; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f1d1a75875a..5c2596d7310 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -3,6 +3,7 @@ #include "esphome/components/json/json_util.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -294,7 +295,7 @@ std::string WebServer::get_config_json() { } void WebServer::setup() { - this->setup_controller(this->include_internal_); + ControllerRegistry::register_controller(this); this->base_->init(); #ifdef USE_LOGGER @@ -430,7 +431,9 @@ static JsonDetail get_request_detail(AsyncWebServerRequest *request) { } #ifdef USE_SENSOR -void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { +void WebServer::on_sensor_update(sensor::Sensor *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", sensor_state_json_generator); } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -473,7 +476,9 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail #endif #ifdef USE_TEXT_SENSOR -void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { +void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", text_sensor_state_json_generator); } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -513,7 +518,9 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: #endif #ifdef USE_SWITCH -void WebServer::on_switch_update(switch_::Switch *obj, bool state) { +void WebServer::on_switch_update(switch_::Switch *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", switch_state_json_generator); } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -625,6 +632,8 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) #ifdef USE_BINARY_SENSOR void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator); } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -664,6 +673,8 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool #ifdef USE_FAN void WebServer::on_fan_update(fan::Fan *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", fan_state_json_generator); } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -738,6 +749,8 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { #ifdef USE_LIGHT void WebServer::on_light_update(light::LightState *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", light_state_json_generator); } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -811,6 +824,8 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi #ifdef USE_COVER void WebServer::on_cover_update(cover::Cover *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", cover_state_json_generator); } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -895,7 +910,9 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { #endif #ifdef USE_NUMBER -void WebServer::on_number_update(number::Number *obj, float state) { +void WebServer::on_number_update(number::Number *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", number_state_json_generator); } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -961,6 +978,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail #ifdef USE_DATETIME_DATE void WebServer::on_date_update(datetime::DateEntity *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", date_state_json_generator); } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1016,6 +1035,8 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con #ifdef USE_DATETIME_TIME void WebServer::on_time_update(datetime::TimeEntity *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", time_state_json_generator); } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1070,6 +1091,8 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con #ifdef USE_DATETIME_DATETIME void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", datetime_state_json_generator); } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1124,7 +1147,9 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s #endif // USE_DATETIME_DATETIME #ifdef USE_TEXT -void WebServer::on_text_update(text::Text *obj, const std::string &state) { +void WebServer::on_text_update(text::Text *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", text_state_json_generator); } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1178,7 +1203,9 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json #endif #ifdef USE_SELECT -void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { +void WebServer::on_select_update(select::Select *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", select_state_json_generator); } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1237,6 +1264,8 @@ std::string WebServer::select_json(select::Select *obj, const char *value, JsonD #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", climate_state_json_generator); } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1378,6 +1407,8 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf #ifdef USE_LOCK void WebServer::on_lock_update(lock::Lock *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", lock_state_json_generator); } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1449,6 +1480,8 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet #ifdef USE_VALVE void WebServer::on_valve_update(valve::Valve *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", valve_state_json_generator); } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1530,6 +1563,8 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { #ifdef USE_ALARM_CONTROL_PANEL void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", alarm_control_panel_state_json_generator); } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 328140cfae9..8e74c42bff7 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -255,7 +255,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SENSOR - void on_sensor_update(sensor::Sensor *obj, float state) override; + void on_sensor_update(sensor::Sensor *obj) override; /// Handle a sensor request under '/sensor/'. void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -266,7 +266,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SWITCH - void on_switch_update(switch_::Switch *obj, bool state) override; + void on_switch_update(switch_::Switch *obj) override; /// Handle a switch request under '/switch//'. void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -324,7 +324,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override; + void on_text_sensor_update(text_sensor::TextSensor *obj) override; /// Handle a text sensor request under '/text_sensor/'. void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -348,7 +348,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_NUMBER - void on_number_update(number::Number *obj, float state) override; + void on_number_update(number::Number *obj) override; /// Handle a number request under '/number/'. void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -392,7 +392,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_TEXT - void on_text_update(text::Text *obj, const std::string &state) override; + void on_text_update(text::Text *obj) override; /// Handle a text input request under '/text/'. void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -403,7 +403,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj, const std::string &state, size_t index) override; + void on_select_update(select::Select *obj) override; /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); diff --git a/esphome/core/controller.h b/esphome/core/controller.h index b475e326ee0..a62e53d9fc0 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -80,22 +80,22 @@ class Controller { virtual void on_light_update(light::LightState *obj){}; #endif #ifdef USE_SENSOR - virtual void on_sensor_update(sensor::Sensor *obj, float state){}; + virtual void on_sensor_update(sensor::Sensor *obj){}; #endif #ifdef USE_SWITCH - virtual void on_switch_update(switch_::Switch *obj, bool state){}; + virtual void on_switch_update(switch_::Switch *obj){}; #endif #ifdef USE_COVER virtual void on_cover_update(cover::Cover *obj){}; #endif #ifdef USE_TEXT_SENSOR - virtual void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state){}; + virtual void on_text_sensor_update(text_sensor::TextSensor *obj){}; #endif #ifdef USE_CLIMATE virtual void on_climate_update(climate::Climate *obj){}; #endif #ifdef USE_NUMBER - virtual void on_number_update(number::Number *obj, float state){}; + virtual void on_number_update(number::Number *obj){}; #endif #ifdef USE_DATETIME_DATE virtual void on_date_update(datetime::DateEntity *obj){}; @@ -107,10 +107,10 @@ class Controller { virtual void on_datetime_update(datetime::DateTimeEntity *obj){}; #endif #ifdef USE_TEXT - virtual void on_text_update(text::Text *obj, const std::string &state){}; + virtual void on_text_update(text::Text *obj){}; #endif #ifdef USE_SELECT - virtual void on_select_update(select::Select *obj, const std::string &state, size_t index){}; + virtual void on_select_update(select::Select *obj){}; #endif #ifdef USE_LOCK virtual void on_lock_update(lock::Lock *obj){}; diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp new file mode 100644 index 00000000000..3223d92170a --- /dev/null +++ b/esphome/core/controller_registry.cpp @@ -0,0 +1,177 @@ +#include "esphome/core/controller_registry.h" +#include "esphome/core/controller.h" + +namespace esphome { + +std::vector ControllerRegistry::controllers_; + +void ControllerRegistry::register_controller(Controller *controller) { controllers_.push_back(controller); } + +void ControllerRegistry::unregister_controller(Controller *controller) { + auto it = std::find(controllers_.begin(), controllers_.end(), controller); + if (it != controllers_.end()) { + controllers_.erase(it); + } +} + +#ifdef USE_BINARY_SENSOR +void ControllerRegistry::notify_binary_sensor_update(binary_sensor::BinarySensor *obj) { + for (auto *controller : controllers_) { + controller->on_binary_sensor_update(obj); + } +} +#endif + +#ifdef USE_FAN +void ControllerRegistry::notify_fan_update(fan::Fan *obj) { + for (auto *controller : controllers_) { + controller->on_fan_update(obj); + } +} +#endif + +#ifdef USE_LIGHT +void ControllerRegistry::notify_light_update(light::LightState *obj) { + for (auto *controller : controllers_) { + controller->on_light_update(obj); + } +} +#endif + +#ifdef USE_SENSOR +void ControllerRegistry::notify_sensor_update(sensor::Sensor *obj) { + for (auto *controller : controllers_) { + controller->on_sensor_update(obj); + } +} +#endif + +#ifdef USE_SWITCH +void ControllerRegistry::notify_switch_update(switch_::Switch *obj) { + for (auto *controller : controllers_) { + controller->on_switch_update(obj); + } +} +#endif + +#ifdef USE_COVER +void ControllerRegistry::notify_cover_update(cover::Cover *obj) { + for (auto *controller : controllers_) { + controller->on_cover_update(obj); + } +} +#endif + +#ifdef USE_TEXT_SENSOR +void ControllerRegistry::notify_text_sensor_update(text_sensor::TextSensor *obj) { + for (auto *controller : controllers_) { + controller->on_text_sensor_update(obj); + } +} +#endif + +#ifdef USE_CLIMATE +void ControllerRegistry::notify_climate_update(climate::Climate *obj) { + for (auto *controller : controllers_) { + controller->on_climate_update(obj); + } +} +#endif + +#ifdef USE_NUMBER +void ControllerRegistry::notify_number_update(number::Number *obj) { + for (auto *controller : controllers_) { + controller->on_number_update(obj); + } +} +#endif + +#ifdef USE_DATETIME_DATE +void ControllerRegistry::notify_date_update(datetime::DateEntity *obj) { + for (auto *controller : controllers_) { + controller->on_date_update(obj); + } +} +#endif + +#ifdef USE_DATETIME_TIME +void ControllerRegistry::notify_time_update(datetime::TimeEntity *obj) { + for (auto *controller : controllers_) { + controller->on_time_update(obj); + } +} +#endif + +#ifdef USE_DATETIME_DATETIME +void ControllerRegistry::notify_datetime_update(datetime::DateTimeEntity *obj) { + for (auto *controller : controllers_) { + controller->on_datetime_update(obj); + } +} +#endif + +#ifdef USE_TEXT +void ControllerRegistry::notify_text_update(text::Text *obj) { + for (auto *controller : controllers_) { + controller->on_text_update(obj); + } +} +#endif + +#ifdef USE_SELECT +void ControllerRegistry::notify_select_update(select::Select *obj) { + for (auto *controller : controllers_) { + controller->on_select_update(obj); + } +} +#endif + +#ifdef USE_LOCK +void ControllerRegistry::notify_lock_update(lock::Lock *obj) { + for (auto *controller : controllers_) { + controller->on_lock_update(obj); + } +} +#endif + +#ifdef USE_VALVE +void ControllerRegistry::notify_valve_update(valve::Valve *obj) { + for (auto *controller : controllers_) { + controller->on_valve_update(obj); + } +} +#endif + +#ifdef USE_MEDIA_PLAYER +void ControllerRegistry::notify_media_player_update(media_player::MediaPlayer *obj) { + for (auto *controller : controllers_) { + controller->on_media_player_update(obj); + } +} +#endif + +#ifdef USE_ALARM_CONTROL_PANEL +void ControllerRegistry::notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { + for (auto *controller : controllers_) { + controller->on_alarm_control_panel_update(obj); + } +} +#endif + +#ifdef USE_EVENT +void ControllerRegistry::notify_event(event::Event *obj, const std::string &event_type) { + for (auto *controller : controllers_) { + controller->on_event(obj, event_type); + } +} +#endif + +#ifdef USE_UPDATE +void ControllerRegistry::notify_update(update::UpdateEntity *obj) { + for (auto *controller : controllers_) { + controller->on_update(obj); + } +} +#endif + +} // namespace esphome diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h new file mode 100644 index 00000000000..33cd2ccebca --- /dev/null +++ b/esphome/core/controller_registry.h @@ -0,0 +1,263 @@ +#pragma once + +#include "esphome/core/defines.h" +#include + +// Forward declarations +namespace esphome { + +class Controller; + +#ifdef USE_BINARY_SENSOR +namespace binary_sensor { +class BinarySensor; +} +#endif + +#ifdef USE_FAN +namespace fan { +class Fan; +} +#endif + +#ifdef USE_LIGHT +namespace light { +class LightState; +} +#endif + +#ifdef USE_SENSOR +namespace sensor { +class Sensor; +} +#endif + +#ifdef USE_SWITCH +namespace switch_ { +class Switch; +} +#endif + +#ifdef USE_COVER +namespace cover { +class Cover; +} +#endif + +#ifdef USE_TEXT_SENSOR +namespace text_sensor { +class TextSensor; +} +#endif + +#ifdef USE_CLIMATE +namespace climate { +class Climate; +} +#endif + +#ifdef USE_NUMBER +namespace number { +class Number; +} +#endif + +#ifdef USE_DATETIME_DATE +namespace datetime { +class DateEntity; +} +#endif + +#ifdef USE_DATETIME_TIME +namespace datetime { +class TimeEntity; +} +#endif + +#ifdef USE_DATETIME_DATETIME +namespace datetime { +class DateTimeEntity; +} +#endif + +#ifdef USE_TEXT +namespace text { +class Text; +} +#endif + +#ifdef USE_SELECT +namespace select { +class Select; +} +#endif + +#ifdef USE_LOCK +namespace lock { +class Lock; +} +#endif + +#ifdef USE_VALVE +namespace valve { +class Valve; +} +#endif + +#ifdef USE_MEDIA_PLAYER +namespace media_player { +class MediaPlayer; +} +#endif + +#ifdef USE_ALARM_CONTROL_PANEL +namespace alarm_control_panel { +class AlarmControlPanel; +} +#endif + +#ifdef USE_EVENT +namespace event { +class Event; +} +#endif + +#ifdef USE_UPDATE +namespace update { +class UpdateEntity; +} +#endif + +/** Global registry for Controllers to receive entity state updates. + * + * This singleton registry allows Controllers (APIServer, WebServer) to receive + * entity state change notifications without storing per-entity callbacks. + * + * Instead of each entity maintaining a list of controller callbacks (32 bytes overhead), + * entities call ControllerRegistry::notify_*_update() which iterates the small list + * of registered controllers (typically 2: API and WebServer). + * + * Memory savings: 32 bytes per entity (2 controllers × 16 bytes std::function overhead) + * For 80 entities: 2,560 bytes saved + */ +class ControllerRegistry { + public: + /** Register a controller to receive entity state updates. + * + * Controllers should call this in their setup() method. + * Typically only APIServer and WebServer register. + */ + static void register_controller(Controller *controller); + + /** Unregister a controller (rarely used). + * + * Controllers are typically never unregistered in ESPHome's lifecycle, + * but this is provided for completeness and testing. + */ + static void unregister_controller(Controller *controller); + +#ifdef USE_BINARY_SENSOR + /** Notify all controllers of a binary sensor state update. */ + static void notify_binary_sensor_update(binary_sensor::BinarySensor *obj); +#endif + +#ifdef USE_FAN + /** Notify all controllers of a fan state update. */ + static void notify_fan_update(fan::Fan *obj); +#endif + +#ifdef USE_LIGHT + /** Notify all controllers of a light state update. */ + static void notify_light_update(light::LightState *obj); +#endif + +#ifdef USE_SENSOR + /** Notify all controllers of a sensor state update. */ + static void notify_sensor_update(sensor::Sensor *obj); +#endif + +#ifdef USE_SWITCH + /** Notify all controllers of a switch state update. */ + static void notify_switch_update(switch_::Switch *obj); +#endif + +#ifdef USE_COVER + /** Notify all controllers of a cover state update. */ + static void notify_cover_update(cover::Cover *obj); +#endif + +#ifdef USE_TEXT_SENSOR + /** Notify all controllers of a text sensor state update. */ + static void notify_text_sensor_update(text_sensor::TextSensor *obj); +#endif + +#ifdef USE_CLIMATE + /** Notify all controllers of a climate state update. */ + static void notify_climate_update(climate::Climate *obj); +#endif + +#ifdef USE_NUMBER + /** Notify all controllers of a number state update. */ + static void notify_number_update(number::Number *obj); +#endif + +#ifdef USE_DATETIME_DATE + /** Notify all controllers of a date entity state update. */ + static void notify_date_update(datetime::DateEntity *obj); +#endif + +#ifdef USE_DATETIME_TIME + /** Notify all controllers of a time entity state update. */ + static void notify_time_update(datetime::TimeEntity *obj); +#endif + +#ifdef USE_DATETIME_DATETIME + /** Notify all controllers of a datetime entity state update. */ + static void notify_datetime_update(datetime::DateTimeEntity *obj); +#endif + +#ifdef USE_TEXT + /** Notify all controllers of a text entity state update. */ + static void notify_text_update(text::Text *obj); +#endif + +#ifdef USE_SELECT + /** Notify all controllers of a select entity state update. */ + static void notify_select_update(select::Select *obj); +#endif + +#ifdef USE_LOCK + /** Notify all controllers of a lock state update. */ + static void notify_lock_update(lock::Lock *obj); +#endif + +#ifdef USE_VALVE + /** Notify all controllers of a valve state update. */ + static void notify_valve_update(valve::Valve *obj); +#endif + +#ifdef USE_MEDIA_PLAYER + /** Notify all controllers of a media player state update. */ + static void notify_media_player_update(media_player::MediaPlayer *obj); +#endif + +#ifdef USE_ALARM_CONTROL_PANEL + /** Notify all controllers of an alarm control panel state update. */ + static void notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj); +#endif + +#ifdef USE_EVENT + /** Notify all controllers of an event trigger. */ + static void notify_event(event::Event *obj, const std::string &event_type); +#endif + +#ifdef USE_UPDATE + /** Notify all controllers of an update entity state update. */ + static void notify_update(update::UpdateEntity *obj); +#endif + + protected: + static std::vector controllers_; +}; + +} // namespace esphome From f1009a7468742a374e53d92650272aa3784719e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 15:44:17 -0600 Subject: [PATCH 220/461] tweak --- .../alarm_control_panel.cpp | 3 +- esphome/components/api/api_server.cpp | 6 +- esphome/components/api/api_server.h | 2 +- .../binary_sensor/binary_sensor.cpp | 2 + esphome/components/climate/climate.cpp | 2 + esphome/components/cover/cover.cpp | 2 + esphome/components/datetime/date_entity.cpp | 3 +- .../components/datetime/datetime_entity.cpp | 3 +- esphome/components/datetime/time_entity.cpp | 3 +- esphome/components/event/event.cpp | 3 +- esphome/components/fan/fan.cpp | 2 + esphome/components/light/light_state.cpp | 6 +- esphome/components/lock/lock.cpp | 2 + .../components/media_player/media_player.cpp | 7 +- esphome/components/number/number.cpp | 2 + esphome/components/select/select.cpp | 3 +- esphome/components/sensor/sensor.cpp | 2 + esphome/components/switch/switch.cpp | 2 + esphome/components/text/text.cpp | 2 + .../components/text_sensor/text_sensor.cpp | 2 + esphome/components/update/update_entity.cpp | 3 +- esphome/components/valve/valve.cpp | 2 + esphome/core/controller.cpp | 134 ------------------ esphome/core/controller.h | 3 +- esphome/core/controller_registry.cpp | 4 +- esphome/core/controller_registry.h | 2 +- 26 files changed, 54 insertions(+), 153 deletions(-) delete mode 100644 esphome/core/controller.cpp diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 9f1485ee905..d7dd06b1aaf 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -1,7 +1,7 @@ #include #include "alarm_control_panel.h" - +#include "esphome/core/controller_registry.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -34,6 +34,7 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; this->state_callback_.call(); + ControllerRegistry::notify_alarm_control_panel_update(this); if (state == ACP_STATE_TRIGGERED) { this->triggered_callback_.call(); } else if (state == ACP_STATE_ARMING) { diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a08554acd8d..c0c4cc3c03d 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -348,12 +348,12 @@ API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) #endif #ifdef USE_EVENT -// Event is a special case - it's the only entity that passes extra parameters to the send method -void APIServer::on_event(event::Event *obj, const std::string &event_type) { +// Event is a special case - it reads event_type from obj->last_event_type +void APIServer::on_event(event::Event *obj) { if (obj->is_internal()) return; for (auto &c : this->clients_) - c->send_event(obj, event_type); + c->send_event(obj, *obj->last_event_type); } #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 4b030239578..2d58063d6cf 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -141,7 +141,7 @@ class APIServer : public Component, public Controller { void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; #endif #ifdef USE_EVENT - void on_event(event::Event *obj, const std::string &event_type) override; + void on_event(event::Event *obj) override; #endif #ifdef USE_UPDATE void on_update(update::UpdateEntity *obj) override; diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 33b3de6d72b..a41bdb032cf 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -1,4 +1,5 @@ #include "binary_sensor.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -37,6 +38,7 @@ void BinarySensor::send_state_internal(bool new_state) { // Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed if (this->set_state_(new_state)) { ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state)); + ControllerRegistry::notify_binary_sensor_update(this); } } diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 7df38758dce..8df81dcc694 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -1,4 +1,5 @@ #include "climate.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/macros.h" namespace esphome { @@ -463,6 +464,7 @@ void Climate::publish_state() { // Send state to frontend this->state_callback_.call(*this); + ControllerRegistry::notify_climate_update(this); // Save state this->save_state_(); } diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 654bb956a5c..b3285a34a47 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -1,5 +1,6 @@ #include "cover.h" #include +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -169,6 +170,7 @@ void Cover::publish_state(bool save) { ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation)); this->state_callback_.call(); + ControllerRegistry::notify_cover_update(this); if (save) { CoverRestoreState restore{}; diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index c164a98b2e3..cc3993eb58d 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -1,5 +1,5 @@ #include "date_entity.h" - +#include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_DATE #include "esphome/core/log.h" @@ -32,6 +32,7 @@ void DateEntity::publish_state() { this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); this->state_callback_.call(); + ControllerRegistry::notify_date_update(this); } DateCall DateEntity::make_call() { return DateCall(this); } diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 4e3b051eb35..cdb5b555b58 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -1,5 +1,5 @@ #include "datetime_entity.h" - +#include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_DATETIME #include "esphome/core/log.h" @@ -48,6 +48,7 @@ void DateTimeEntity::publish_state() { ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, this->day_, this->hour_, this->minute_, this->second_); this->state_callback_.call(); + ControllerRegistry::notify_datetime_update(this); } DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 9b05c2124f9..39533144e84 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -1,5 +1,5 @@ #include "time_entity.h" - +#include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_TIME #include "esphome/core/log.h" @@ -29,6 +29,7 @@ void TimeEntity::publish_state() { ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); this->state_callback_.call(); + ControllerRegistry::notify_time_update(this); } TimeCall TimeEntity::make_call() { return TimeCall(this); } diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 20549ad0a50..649e430e644 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -1,5 +1,5 @@ #include "event.h" - +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -23,6 +23,7 @@ void Event::trigger(const std::string &event_type) { last_event_type = found; ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); this->event_callback_.call(event_type); + ControllerRegistry::notify_event(this); } void Event::add_on_event_callback(std::function &&callback) { diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 959572e9d94..c7377b70707 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -1,4 +1,5 @@ #include "fan.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -181,6 +182,7 @@ void Fan::publish_state() { ESP_LOGD(TAG, " Preset Mode: %s", preset); } this->state_callback_.call(); + ControllerRegistry::notify_fan_update(this); this->save_state_(); } diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 7b0a698bb8f..ed566783a4d 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,3 +1,4 @@ +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" #include "light_output.h" @@ -137,7 +138,10 @@ void LightState::loop() { float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } -void LightState::publish_state() { this->remote_values_callback_.call(); } +void LightState::publish_state() { + this->remote_values_callback_.call(); + ControllerRegistry::notify_light_update(this); +} LightOutput *LightState::get_output() const { return this->output_; } diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index ddc54453496..36015522189 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -1,4 +1,5 @@ #include "lock.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -53,6 +54,7 @@ void Lock::publish_state(LockState state) { this->rtc_.save(&this->state); ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), lock_state_to_string(state)); this->state_callback_.call(); + ControllerRegistry::notify_lock_update(this); } void Lock::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 3f274bf73b3..a16dfccfc2a 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -1,5 +1,5 @@ #include "media_player.h" - +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -148,7 +148,10 @@ void MediaPlayer::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -void MediaPlayer::publish_state() { this->state_callback_.call(); } +void MediaPlayer::publish_state() { + this->state_callback_.call(); + ControllerRegistry::notify_media_player_update(this); +} } // namespace media_player } // namespace esphome diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index da08faf6558..22463a45523 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -1,4 +1,5 @@ #include "number.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -32,6 +33,7 @@ void Number::publish_state(float state) { this->state = state; ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); this->state_callback_.call(state); + ControllerRegistry::notify_number_update(this); } void Number::add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 6bb01ba6e25..12512060e53 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -1,5 +1,5 @@ #include "select.h" -#include "esphome/core/log.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" #include namespace esphome { @@ -33,6 +33,7 @@ void Select::publish_state(size_t index) { ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); // Callback signature requires std::string, create temporary for compatibility this->state_callback_.call(std::string(option), index); + ControllerRegistry::notify_select_update(this); } const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; } diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 92da4345b70..dff76c5e9bc 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -1,4 +1,5 @@ #include "sensor.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -131,6 +132,7 @@ void Sensor::internal_send_state_to_frontend(float state) { ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals()); this->callback_.call(state); + ControllerRegistry::notify_sensor_update(this); } } // namespace sensor diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 02cee91a768..93e3ff5b449 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -1,4 +1,5 @@ #include "switch.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -62,6 +63,7 @@ void Switch::publish_state(bool state) { ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state)); this->state_callback_.call(this->state); + ControllerRegistry::notify_switch_update(this); } bool Switch::assumed_state() { return false; } diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 654893d4e49..5761723dc94 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -1,4 +1,5 @@ #include "text.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -16,6 +17,7 @@ void Text::publish_state(const std::string &state) { ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); } this->state_callback_.call(state); + ControllerRegistry::notify_text_update(this); } void Text::add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 0294d65861c..5c790d5e767 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -1,4 +1,5 @@ #include "text_sensor.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -84,6 +85,7 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); this->callback_.call(state); + ControllerRegistry::notify_text_sensor_update(this); } } // namespace text_sensor diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index ce97fb1b77f..069d08459bf 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -1,5 +1,5 @@ #include "update_entity.h" - +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -32,6 +32,7 @@ void UpdateEntity::publish_state() { this->set_has_state(true); this->state_callback_.call(); + ControllerRegistry::notify_update(this); } } // namespace update diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index b041fe84496..086d0e469f7 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -1,4 +1,5 @@ #include "valve.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" #include @@ -147,6 +148,7 @@ void Valve::publish_state(bool save) { ESP_LOGD(TAG, " Current Operation: %s", valve_operation_to_str(this->current_operation)); this->state_callback_.call(); + ControllerRegistry::notify_valve_update(this); if (save) { ValveRestoreState restore{}; diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp deleted file mode 100644 index f7ff5a9734a..00000000000 --- a/esphome/core/controller.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include "controller.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" - -namespace esphome { - -void Controller::setup_controller(bool include_internal) { -#ifdef USE_BINARY_SENSOR - for (auto *obj : App.get_binary_sensors()) { - if (include_internal || !obj->is_internal()) { - obj->add_full_state_callback( - [this, obj](optional previous, optional state) { this->on_binary_sensor_update(obj); }); - } - } -#endif -#ifdef USE_FAN - for (auto *obj : App.get_fans()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_fan_update(obj); }); - } -#endif -#ifdef USE_LIGHT - for (auto *obj : App.get_lights()) { - if (include_internal || !obj->is_internal()) - obj->add_new_remote_values_callback([this, obj]() { this->on_light_update(obj); }); - } -#endif -#ifdef USE_SENSOR - for (auto *obj : App.get_sensors()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](float state) { this->on_sensor_update(obj, state); }); - } -#endif -#ifdef USE_SWITCH - for (auto *obj : App.get_switches()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](bool state) { this->on_switch_update(obj, state); }); - } -#endif -#ifdef USE_COVER - for (auto *obj : App.get_covers()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_cover_update(obj); }); - } -#endif -#ifdef USE_TEXT_SENSOR - for (auto *obj : App.get_text_sensors()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_sensor_update(obj, state); }); - } -#endif -#ifdef USE_CLIMATE - for (auto *obj : App.get_climates()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](climate::Climate & /*unused*/) { this->on_climate_update(obj); }); - } -#endif -#ifdef USE_NUMBER - for (auto *obj : App.get_numbers()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); }); - } -#endif -#ifdef USE_DATETIME_DATE - for (auto *obj : App.get_dates()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_date_update(obj); }); - } -#endif -#ifdef USE_DATETIME_TIME - for (auto *obj : App.get_times()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_time_update(obj); }); - } -#endif -#ifdef USE_DATETIME_DATETIME - for (auto *obj : App.get_datetimes()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_datetime_update(obj); }); - } -#endif -#ifdef USE_TEXT - for (auto *obj : App.get_texts()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_update(obj, state); }); - } -#endif -#ifdef USE_SELECT - for (auto *obj : App.get_selects()) { - if (include_internal || !obj->is_internal()) { - obj->add_on_state_callback( - [this, obj](const std::string &state, size_t index) { this->on_select_update(obj, state, index); }); - } - } -#endif -#ifdef USE_LOCK - for (auto *obj : App.get_locks()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_lock_update(obj); }); - } -#endif -#ifdef USE_VALVE - for (auto *obj : App.get_valves()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_valve_update(obj); }); - } -#endif -#ifdef USE_MEDIA_PLAYER - for (auto *obj : App.get_media_players()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_media_player_update(obj); }); - } -#endif -#ifdef USE_ALARM_CONTROL_PANEL - for (auto *obj : App.get_alarm_control_panels()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_alarm_control_panel_update(obj); }); - } -#endif -#ifdef USE_EVENT - for (auto *obj : App.get_events()) { - if (include_internal || !obj->is_internal()) - obj->add_on_event_callback([this, obj](const std::string &event_type) { this->on_event(obj, event_type); }); - } -#endif -#ifdef USE_UPDATE - for (auto *obj : App.get_updates()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_update(obj); }); - } -#endif -} - -} // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index a62e53d9fc0..697017217db 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -69,7 +69,6 @@ namespace esphome { class Controller { public: - void setup_controller(bool include_internal = false); #ifdef USE_BINARY_SENSOR virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj){}; #endif @@ -125,7 +124,7 @@ class Controller { virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){}; #endif #ifdef USE_EVENT - virtual void on_event(event::Event *obj, const std::string &event_type){}; + virtual void on_event(event::Event *obj){}; #endif #ifdef USE_UPDATE virtual void on_update(update::UpdateEntity *obj){}; diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index 3223d92170a..c838dceb5fc 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -159,9 +159,9 @@ void ControllerRegistry::notify_alarm_control_panel_update(alarm_control_panel:: #endif #ifdef USE_EVENT -void ControllerRegistry::notify_event(event::Event *obj, const std::string &event_type) { +void ControllerRegistry::notify_event(event::Event *obj) { for (auto *controller : controllers_) { - controller->on_event(obj, event_type); + controller->on_event(obj); } } #endif diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index 33cd2ccebca..16ca768cb36 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -248,7 +248,7 @@ class ControllerRegistry { #ifdef USE_EVENT /** Notify all controllers of an event trigger. */ - static void notify_event(event::Event *obj, const std::string &event_type); + static void notify_event(event::Event *obj); #endif #ifdef USE_UPDATE From c77bb3b269bf7fba517fb359535c59cccef49152 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 15:46:16 -0600 Subject: [PATCH 221/461] [event] Store event types in flash memory (#11767) --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_connection.cpp | 3 +- esphome/components/api/api_pb2.cpp | 10 +++--- esphome/components/api/api_pb2.h | 2 +- esphome/components/api/api_pb2_dump.cpp | 2 +- esphome/components/event/event.cpp | 30 +++++++++++++---- esphome/components/event/event.h | 35 +++++++++++++++++--- esphome/components/mqtt/mqtt_event.cpp | 4 +-- esphome/components/web_server/web_server.cpp | 5 +-- 9 files changed, 67 insertions(+), 26 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 7a50fa6b179..e115e4630d8 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2147,7 +2147,7 @@ message ListEntitiesEventResponse { EntityCategory entity_category = 7; string device_class = 8; - repeated string event_types = 9; + repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message EventResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5ab8a6eb050..8c293b41a29 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1310,8 +1310,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c auto *event = static_cast(entity); ListEntitiesEventResponse msg; msg.set_device_class(event->get_device_class_ref()); - for (const auto &event_type : event->get_event_types()) - msg.event_types.push_back(event_type); + msg.event_types = &event->get_event_types(); return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index dfa1a1320fe..0a073fb662f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2877,8 +2877,8 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_string(8, this->device_class_ref_); - for (auto &it : this->event_types) { - buffer.encode_string(9, it, true); + for (const char *it : *this->event_types) { + buffer.encode_string(9, it, strlen(it), true); } #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); @@ -2894,9 +2894,9 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); size.add_length(1, this->device_class_ref_.size()); - if (!this->event_types.empty()) { - for (const auto &it : this->event_types) { - size.add_length_force(1, it.size()); + if (!this->event_types->empty()) { + for (const char *it : *this->event_types) { + size.add_length_force(1, strlen(it)); } } #ifdef USE_DEVICES diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 716f1a6e9b5..358049026ec 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2788,7 +2788,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class_ref_{}; void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } - std::vector event_types{}; + const FixedVector *event_types{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index d94ceaaa9c4..d9662483bf5 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2053,7 +2053,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); dump_field(out, "device_class", this->device_class_ref_); - for (const auto &it : this->event_types) { + for (const auto &it : *this->event_types) { dump_field(out, "event_types", it, 4); } #ifdef USE_DEVICES diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 20549ad0a50..a14afbd7f55 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -8,11 +8,11 @@ namespace event { static const char *const TAG = "event"; void Event::trigger(const std::string &event_type) { - // Linear search - faster than std::set for small datasets (1-5 items typical) - const std::string *found = nullptr; - for (const auto &type : this->types_) { - if (type == event_type) { - found = &type; + // Linear search with strcmp - faster than std::set for small datasets (1-5 items typical) + const char *found = nullptr; + for (const char *type : this->types_) { + if (strcmp(type, event_type.c_str()) == 0) { + found = type; break; } } @@ -20,11 +20,27 @@ void Event::trigger(const std::string &event_type) { ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); return; } - last_event_type = found; - ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); + this->last_event_type_ = found; + ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_); this->event_callback_.call(event_type); } +void Event::set_event_types(const FixedVector &event_types) { + this->types_.init(event_types.size()); + for (const char *type : event_types) { + this->types_.push_back(type); + } + this->last_event_type_ = nullptr; // Reset when types change +} + +void Event::set_event_types(const std::vector &event_types) { + this->types_.init(event_types.size()); + for (const char *type : event_types) { + this->types_.push_back(type); + } + this->last_event_type_ = nullptr; // Reset when types change +} + void Event::add_on_event_callback(std::function &&callback) { this->event_callback_.add(std::move(callback)); } diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 2f6267a2006..e4b2e0b845b 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include "esphome/core/component.h" #include "esphome/core/entity_base.h" @@ -22,16 +24,39 @@ namespace event { class Event : public EntityBase, public EntityBase_DeviceClass { public: - const std::string *last_event_type; - void trigger(const std::string &event_type); - void set_event_types(const std::initializer_list &event_types) { this->types_ = event_types; } - const FixedVector &get_event_types() const { return this->types_; } + + /// Set the event types supported by this event (from initializer list). + void set_event_types(std::initializer_list event_types) { + this->types_ = event_types; + this->last_event_type_ = nullptr; // Reset when types change + } + /// Set the event types supported by this event (from FixedVector). + void set_event_types(const FixedVector &event_types); + /// Set the event types supported by this event (from vector). + void set_event_types(const std::vector &event_types); + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_event_types(std::initializer_list event_types) = delete; + void set_event_types(const FixedVector &event_types) = delete; + void set_event_types(const std::vector &event_types) = delete; + + /// Return the event types supported by this event. + const FixedVector &get_event_types() const { return this->types_; } + + /// Return the last triggered event type (pointer to string in types_), or nullptr if no event triggered yet. + const char *get_last_event_type() const { return this->last_event_type_; } + void add_on_event_callback(std::function &&callback); protected: CallbackManager event_callback_; - FixedVector types_; + FixedVector types_; + + private: + /// Last triggered event type - must point to entry in types_ to ensure valid lifetime. + /// Set by trigger() after validation, reset to nullptr when types_ changes. + const char *last_event_type_{nullptr}; }; } // namespace event diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index e2063354464..fd095ea041b 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -38,8 +38,8 @@ void MQTTEventComponent::setup() { void MQTTEventComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Event '%s': ", this->event_->get_name().c_str()); ESP_LOGCONFIG(TAG, "Event Types: "); - for (const auto &event_type : this->event_->get_event_types()) { - ESP_LOGCONFIG(TAG, "- %s", event_type.c_str()); + for (const char *event_type : this->event_->get_event_types()) { + ESP_LOGCONFIG(TAG, "- %s", event_type); } LOG_MQTT_COMPONENT(true, true); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f1d1a75875a..91ca0764749 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1628,7 +1628,8 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa } static std::string get_event_type(event::Event *event) { - return (event && event->last_event_type) ? *event->last_event_type : ""; + const char *last_type = event ? event->get_last_event_type() : nullptr; + return last_type ? last_type : ""; } std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { @@ -1649,7 +1650,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty } if (start_config == DETAIL_ALL) { JsonArray event_types = root["event_types"].to(); - for (auto const &event_type : obj->get_event_types()) { + for (const char *event_type : obj->get_event_types()) { event_types.add(event_type); } root["device_class"] = obj->get_device_class_ref(); From 51eb8ea1d0e326c2ec357171800e05e81d4cf9fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 15:48:02 -0600 Subject: [PATCH 222/461] controller registry --- esphome/components/api/api_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index c0c4cc3c03d..d9919bb8499 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -348,12 +348,12 @@ API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) #endif #ifdef USE_EVENT -// Event is a special case - it reads event_type from obj->last_event_type +// Event is a special case - it reads event_type from obj->get_last_event_type() void APIServer::on_event(event::Event *obj) { if (obj->is_internal()) return; for (auto &c : this->clients_) - c->send_event(obj, *obj->last_event_type); + c->send_event(obj, obj->get_last_event_type()); } #endif From 6fa0f1e29051049aa29b9c33fa3b8f0dc944a631 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 15:51:13 -0600 Subject: [PATCH 223/461] controller registry --- esphome/components/select/select.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 12512060e53..ab3ac75a805 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -1,5 +1,6 @@ #include "select.h" -#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/controller_registry.h" +#include "esphome/core/log.h" #include namespace esphome { From 929279dc234768189709ec0f5719afdb35b957ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 15:55:22 -0600 Subject: [PATCH 224/461] controller registry --- esphome/components/web_server/web_server.cpp | 4 +++- esphome/components/web_server/web_server.h | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 8ebabf545f2..6d0cdeb07b2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1642,7 +1642,9 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro #endif #ifdef USE_EVENT -void WebServer::on_event(event::Event *obj, const std::string &event_type) { +void WebServer::on_event(event::Event *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", event_state_json_generator); } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 8e74c42bff7..7e1af886457 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -462,7 +462,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_EVENT - void on_event(event::Event *obj, const std::string &event_type) override; + void on_event(event::Event *obj) override; static std::string event_state_json_generator(WebServer *web_server, void *source); static std::string event_all_json_generator(WebServer *web_server, void *source); From 6ef2763cabaa2e78723a2b713f0fe367c1cce4f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 16:01:45 -0600 Subject: [PATCH 225/461] controller registry --- esphome/core/controller_registry.cpp | 7 ------ esphome/core/controller_registry.h | 35 +++++----------------------- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index c838dceb5fc..da7fd5c8376 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -7,13 +7,6 @@ std::vector ControllerRegistry::controllers_; void ControllerRegistry::register_controller(Controller *controller) { controllers_.push_back(controller); } -void ControllerRegistry::unregister_controller(Controller *controller) { - auto it = std::find(controllers_.begin(), controllers_.end(), controller); - if (it != controllers_.end()) { - controllers_.erase(it); - } -} - #ifdef USE_BINARY_SENSOR void ControllerRegistry::notify_binary_sensor_update(binary_sensor::BinarySensor *obj) { for (auto *controller : controllers_) { diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index 16ca768cb36..6a72195b633 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -133,12 +133,16 @@ class UpdateEntity; * This singleton registry allows Controllers (APIServer, WebServer) to receive * entity state change notifications without storing per-entity callbacks. * - * Instead of each entity maintaining a list of controller callbacks (32 bytes overhead), + * Instead of each entity maintaining controller callbacks (32 bytes overhead per entity), * entities call ControllerRegistry::notify_*_update() which iterates the small list * of registered controllers (typically 2: API and WebServer). * + * Controllers read state directly from entities using existing accessors (obj->state, etc.) + * rather than receiving it as callback parameters that were being ignored anyway. + * * Memory savings: 32 bytes per entity (2 controllers × 16 bytes std::function overhead) - * For 80 entities: 2,560 bytes saved + * Typical config (25 entities): ~780 bytes saved + * Large config (80 entities): ~2,540 bytes saved */ class ControllerRegistry { public: @@ -149,110 +153,83 @@ class ControllerRegistry { */ static void register_controller(Controller *controller); - /** Unregister a controller (rarely used). - * - * Controllers are typically never unregistered in ESPHome's lifecycle, - * but this is provided for completeness and testing. - */ - static void unregister_controller(Controller *controller); - #ifdef USE_BINARY_SENSOR - /** Notify all controllers of a binary sensor state update. */ static void notify_binary_sensor_update(binary_sensor::BinarySensor *obj); #endif #ifdef USE_FAN - /** Notify all controllers of a fan state update. */ static void notify_fan_update(fan::Fan *obj); #endif #ifdef USE_LIGHT - /** Notify all controllers of a light state update. */ static void notify_light_update(light::LightState *obj); #endif #ifdef USE_SENSOR - /** Notify all controllers of a sensor state update. */ static void notify_sensor_update(sensor::Sensor *obj); #endif #ifdef USE_SWITCH - /** Notify all controllers of a switch state update. */ static void notify_switch_update(switch_::Switch *obj); #endif #ifdef USE_COVER - /** Notify all controllers of a cover state update. */ static void notify_cover_update(cover::Cover *obj); #endif #ifdef USE_TEXT_SENSOR - /** Notify all controllers of a text sensor state update. */ static void notify_text_sensor_update(text_sensor::TextSensor *obj); #endif #ifdef USE_CLIMATE - /** Notify all controllers of a climate state update. */ static void notify_climate_update(climate::Climate *obj); #endif #ifdef USE_NUMBER - /** Notify all controllers of a number state update. */ static void notify_number_update(number::Number *obj); #endif #ifdef USE_DATETIME_DATE - /** Notify all controllers of a date entity state update. */ static void notify_date_update(datetime::DateEntity *obj); #endif #ifdef USE_DATETIME_TIME - /** Notify all controllers of a time entity state update. */ static void notify_time_update(datetime::TimeEntity *obj); #endif #ifdef USE_DATETIME_DATETIME - /** Notify all controllers of a datetime entity state update. */ static void notify_datetime_update(datetime::DateTimeEntity *obj); #endif #ifdef USE_TEXT - /** Notify all controllers of a text entity state update. */ static void notify_text_update(text::Text *obj); #endif #ifdef USE_SELECT - /** Notify all controllers of a select entity state update. */ static void notify_select_update(select::Select *obj); #endif #ifdef USE_LOCK - /** Notify all controllers of a lock state update. */ static void notify_lock_update(lock::Lock *obj); #endif #ifdef USE_VALVE - /** Notify all controllers of a valve state update. */ static void notify_valve_update(valve::Valve *obj); #endif #ifdef USE_MEDIA_PLAYER - /** Notify all controllers of a media player state update. */ static void notify_media_player_update(media_player::MediaPlayer *obj); #endif #ifdef USE_ALARM_CONTROL_PANEL - /** Notify all controllers of an alarm control panel state update. */ static void notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj); #endif #ifdef USE_EVENT - /** Notify all controllers of an event trigger. */ static void notify_event(event::Event *obj); #endif #ifdef USE_UPDATE - /** Notify all controllers of an update entity state update. */ static void notify_update(update::UpdateEntity *obj); #endif From 871c5ddb4ef438cc8531368da6b564acc03e1d45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 16:07:54 -0600 Subject: [PATCH 226/461] no ifdefs needed on forward decs --- esphome/core/controller_registry.h | 40 ------------------------------ 1 file changed, 40 deletions(-) diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index 6a72195b633..5e2939ff950 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -8,125 +8,85 @@ namespace esphome { class Controller; -#ifdef USE_BINARY_SENSOR namespace binary_sensor { class BinarySensor; } -#endif -#ifdef USE_FAN namespace fan { class Fan; } -#endif -#ifdef USE_LIGHT namespace light { class LightState; } -#endif -#ifdef USE_SENSOR namespace sensor { class Sensor; } -#endif -#ifdef USE_SWITCH namespace switch_ { class Switch; } -#endif -#ifdef USE_COVER namespace cover { class Cover; } -#endif -#ifdef USE_TEXT_SENSOR namespace text_sensor { class TextSensor; } -#endif -#ifdef USE_CLIMATE namespace climate { class Climate; } -#endif -#ifdef USE_NUMBER namespace number { class Number; } -#endif -#ifdef USE_DATETIME_DATE namespace datetime { class DateEntity; } -#endif -#ifdef USE_DATETIME_TIME namespace datetime { class TimeEntity; } -#endif -#ifdef USE_DATETIME_DATETIME namespace datetime { class DateTimeEntity; } -#endif -#ifdef USE_TEXT namespace text { class Text; } -#endif -#ifdef USE_SELECT namespace select { class Select; } -#endif -#ifdef USE_LOCK namespace lock { class Lock; } -#endif -#ifdef USE_VALVE namespace valve { class Valve; } -#endif -#ifdef USE_MEDIA_PLAYER namespace media_player { class MediaPlayer; } -#endif -#ifdef USE_ALARM_CONTROL_PANEL namespace alarm_control_panel { class AlarmControlPanel; } -#endif -#ifdef USE_EVENT namespace event { class Event; } -#endif -#ifdef USE_UPDATE namespace update { class UpdateEntity; } -#endif /** Global registry for Controllers to receive entity state updates. * From c0e4f415f1d362ccbd9d98b9f3a3103e46683ebb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 16:10:56 -0600 Subject: [PATCH 227/461] Revert "no ifdefs needed on forward decs" This reverts commit 871c5ddb4ef438cc8531368da6b564acc03e1d45. --- esphome/core/controller_registry.h | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index 5e2939ff950..6a72195b633 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -8,85 +8,125 @@ namespace esphome { class Controller; +#ifdef USE_BINARY_SENSOR namespace binary_sensor { class BinarySensor; } +#endif +#ifdef USE_FAN namespace fan { class Fan; } +#endif +#ifdef USE_LIGHT namespace light { class LightState; } +#endif +#ifdef USE_SENSOR namespace sensor { class Sensor; } +#endif +#ifdef USE_SWITCH namespace switch_ { class Switch; } +#endif +#ifdef USE_COVER namespace cover { class Cover; } +#endif +#ifdef USE_TEXT_SENSOR namespace text_sensor { class TextSensor; } +#endif +#ifdef USE_CLIMATE namespace climate { class Climate; } +#endif +#ifdef USE_NUMBER namespace number { class Number; } +#endif +#ifdef USE_DATETIME_DATE namespace datetime { class DateEntity; } +#endif +#ifdef USE_DATETIME_TIME namespace datetime { class TimeEntity; } +#endif +#ifdef USE_DATETIME_DATETIME namespace datetime { class DateTimeEntity; } +#endif +#ifdef USE_TEXT namespace text { class Text; } +#endif +#ifdef USE_SELECT namespace select { class Select; } +#endif +#ifdef USE_LOCK namespace lock { class Lock; } +#endif +#ifdef USE_VALVE namespace valve { class Valve; } +#endif +#ifdef USE_MEDIA_PLAYER namespace media_player { class MediaPlayer; } +#endif +#ifdef USE_ALARM_CONTROL_PANEL namespace alarm_control_panel { class AlarmControlPanel; } +#endif +#ifdef USE_EVENT namespace event { class Event; } +#endif +#ifdef USE_UPDATE namespace update { class UpdateEntity; } +#endif /** Global registry for Controllers to receive entity state updates. * From fc8dc33023e2614dd6e12ae46d98c084f938d80a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 16:13:59 -0600 Subject: [PATCH 228/461] fixes --- esphome/components/alarm_control_panel/alarm_control_panel.cpp | 2 ++ esphome/components/binary_sensor/binary_sensor.cpp | 2 ++ esphome/components/climate/climate.cpp | 2 ++ esphome/components/cover/cover.cpp | 2 ++ esphome/components/datetime/date_entity.cpp | 2 ++ esphome/components/datetime/datetime_entity.cpp | 2 ++ esphome/components/datetime/time_entity.cpp | 2 ++ esphome/components/event/event.cpp | 2 ++ esphome/components/fan/fan.cpp | 2 ++ esphome/components/light/light_state.cpp | 2 ++ esphome/components/lock/lock.cpp | 2 ++ esphome/components/media_player/media_player.cpp | 2 ++ esphome/components/number/number.cpp | 2 ++ esphome/components/select/select.cpp | 2 ++ esphome/components/sensor/sensor.cpp | 2 ++ esphome/components/switch/switch.cpp | 2 ++ esphome/components/text/text.cpp | 2 ++ esphome/components/text_sensor/text_sensor.cpp | 2 ++ esphome/components/update/update_entity.cpp | 2 ++ esphome/components/valve/valve.cpp | 2 ++ 20 files changed, 40 insertions(+) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index d7dd06b1aaf..baa7094a8a5 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -34,7 +34,9 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; this->state_callback_.call(); +#ifdef USE_ALARM_CONTROL_PANEL ControllerRegistry::notify_alarm_control_panel_update(this); +#endif if (state == ACP_STATE_TRIGGERED) { this->triggered_callback_.call(); } else if (state == ACP_STATE_ARMING) { diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index a41bdb032cf..7c35ebe116a 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -38,7 +38,9 @@ void BinarySensor::send_state_internal(bool new_state) { // Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed if (this->set_state_(new_state)) { ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state)); +#ifdef USE_BINARY_SENSOR ControllerRegistry::notify_binary_sensor_update(this); +#endif } } diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 8df81dcc694..0576674f3a9 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -464,7 +464,9 @@ void Climate::publish_state() { // Send state to frontend this->state_callback_.call(*this); +#ifdef USE_CLIMATE ControllerRegistry::notify_climate_update(this); +#endif // Save state this->save_state_(); } diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index b3285a34a47..4542f0300ee 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -170,7 +170,9 @@ void Cover::publish_state(bool save) { ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation)); this->state_callback_.call(); +#ifdef USE_COVER ControllerRegistry::notify_cover_update(this); +#endif if (save) { CoverRestoreState restore{}; diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index cc3993eb58d..d5c63f5e0b6 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -32,7 +32,9 @@ void DateEntity::publish_state() { this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); this->state_callback_.call(); +#ifdef USE_DATETIME_DATE ControllerRegistry::notify_date_update(this); +#endif } DateCall DateEntity::make_call() { return DateCall(this); } diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index cdb5b555b58..56f4e3f7af9 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -48,7 +48,9 @@ void DateTimeEntity::publish_state() { ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, this->day_, this->hour_, this->minute_, this->second_); this->state_callback_.call(); +#ifdef USE_DATETIME_DATETIME ControllerRegistry::notify_datetime_update(this); +#endif } DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 39533144e84..f06f9621779 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -29,7 +29,9 @@ void TimeEntity::publish_state() { ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); this->state_callback_.call(); +#ifdef USE_DATETIME_TIME ControllerRegistry::notify_time_update(this); +#endif } TimeCall TimeEntity::make_call() { return TimeCall(this); } diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 5bcac991545..ffc3cdaa39a 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -23,7 +23,9 @@ void Event::trigger(const std::string &event_type) { this->last_event_type_ = found; ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_); this->event_callback_.call(event_type); +#ifdef USE_EVENT ControllerRegistry::notify_event(this); +#endif } void Event::set_event_types(const FixedVector &event_types) { diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index c7377b70707..ee1962bf60a 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -182,7 +182,9 @@ void Fan::publish_state() { ESP_LOGD(TAG, " Preset Mode: %s", preset); } this->state_callback_.call(); +#ifdef USE_FAN ControllerRegistry::notify_fan_update(this); +#endif this->save_state_(); } diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index ed566783a4d..0f6695d968b 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -140,7 +140,9 @@ float LightState::get_setup_priority() const { return setup_priority::HARDWARE - void LightState::publish_state() { this->remote_values_callback_.call(); +#ifdef USE_LIGHT ControllerRegistry::notify_light_update(this); +#endif } LightOutput *LightState::get_output() const { return this->output_; } diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 36015522189..2cbef07ca60 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -54,7 +54,9 @@ void Lock::publish_state(LockState state) { this->rtc_.save(&this->state); ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), lock_state_to_string(state)); this->state_callback_.call(); +#ifdef USE_LOCK ControllerRegistry::notify_lock_update(this); +#endif } void Lock::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index a16dfccfc2a..878dcfe6f16 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -150,7 +150,9 @@ void MediaPlayer::add_on_state_callback(std::function &&callback) { void MediaPlayer::publish_state() { this->state_callback_.call(); +#ifdef USE_MEDIA_PLAYER ControllerRegistry::notify_media_player_update(this); +#endif } } // namespace media_player diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 22463a45523..35e59ebdabf 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -33,7 +33,9 @@ void Number::publish_state(float state) { this->state = state; ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); this->state_callback_.call(state); +#ifdef USE_NUMBER ControllerRegistry::notify_number_update(this); +#endif } void Number::add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index ab3ac75a805..fc933c15ef3 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -34,7 +34,9 @@ void Select::publish_state(size_t index) { ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); // Callback signature requires std::string, create temporary for compatibility this->state_callback_.call(std::string(option), index); +#ifdef USE_SELECT ControllerRegistry::notify_select_update(this); +#endif } const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; } diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index dff76c5e9bc..ac70e457e03 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -132,7 +132,9 @@ void Sensor::internal_send_state_to_frontend(float state) { ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals()); this->callback_.call(state); +#ifdef USE_SENSOR ControllerRegistry::notify_sensor_update(this); +#endif } } // namespace sensor diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 93e3ff5b449..49954e6f064 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -63,7 +63,9 @@ void Switch::publish_state(bool state) { ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state)); this->state_callback_.call(this->state); +#ifdef USE_SWITCH ControllerRegistry::notify_switch_update(this); +#endif } bool Switch::assumed_state() { return false; } diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 5761723dc94..3028cd85ea2 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -17,7 +17,9 @@ void Text::publish_state(const std::string &state) { ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); } this->state_callback_.call(state); +#ifdef USE_TEXT ControllerRegistry::notify_text_update(this); +#endif } void Text::add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 5c790d5e767..c4a941408ad 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -85,7 +85,9 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); this->callback_.call(state); +#ifdef USE_TEXT_SENSOR ControllerRegistry::notify_text_sensor_update(this); +#endif } } // namespace text_sensor diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 069d08459bf..9ca2c35f810 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -32,7 +32,9 @@ void UpdateEntity::publish_state() { this->set_has_state(true); this->state_callback_.call(); +#ifdef USE_UPDATE ControllerRegistry::notify_update(this); +#endif } } // namespace update diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 086d0e469f7..0982892af34 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -148,7 +148,9 @@ void Valve::publish_state(bool save) { ESP_LOGD(TAG, " Current Operation: %s", valve_operation_to_str(this->current_operation)); this->state_callback_.call(); +#ifdef USE_VALVE ControllerRegistry::notify_valve_update(this); +#endif if (save) { ValveRestoreState restore{}; From c87d07ba70ab657de3786bf4da73c392fe797df0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 16:15:07 -0600 Subject: [PATCH 229/461] fixes --- esphome/core/controller_registry.cpp | 44 ++++++++++++++-------------- esphome/core/controller_registry.h | 2 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index da7fd5c8376..b3f4c646a3e 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -3,13 +3,13 @@ namespace esphome { -std::vector ControllerRegistry::controllers_; +std::vector ControllerRegistry::controllers; -void ControllerRegistry::register_controller(Controller *controller) { controllers_.push_back(controller); } +void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } #ifdef USE_BINARY_SENSOR void ControllerRegistry::notify_binary_sensor_update(binary_sensor::BinarySensor *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_binary_sensor_update(obj); } } @@ -17,7 +17,7 @@ void ControllerRegistry::notify_binary_sensor_update(binary_sensor::BinarySensor #ifdef USE_FAN void ControllerRegistry::notify_fan_update(fan::Fan *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_fan_update(obj); } } @@ -25,7 +25,7 @@ void ControllerRegistry::notify_fan_update(fan::Fan *obj) { #ifdef USE_LIGHT void ControllerRegistry::notify_light_update(light::LightState *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_light_update(obj); } } @@ -33,7 +33,7 @@ void ControllerRegistry::notify_light_update(light::LightState *obj) { #ifdef USE_SENSOR void ControllerRegistry::notify_sensor_update(sensor::Sensor *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_sensor_update(obj); } } @@ -41,7 +41,7 @@ void ControllerRegistry::notify_sensor_update(sensor::Sensor *obj) { #ifdef USE_SWITCH void ControllerRegistry::notify_switch_update(switch_::Switch *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_switch_update(obj); } } @@ -49,7 +49,7 @@ void ControllerRegistry::notify_switch_update(switch_::Switch *obj) { #ifdef USE_COVER void ControllerRegistry::notify_cover_update(cover::Cover *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_cover_update(obj); } } @@ -57,7 +57,7 @@ void ControllerRegistry::notify_cover_update(cover::Cover *obj) { #ifdef USE_TEXT_SENSOR void ControllerRegistry::notify_text_sensor_update(text_sensor::TextSensor *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_text_sensor_update(obj); } } @@ -65,7 +65,7 @@ void ControllerRegistry::notify_text_sensor_update(text_sensor::TextSensor *obj) #ifdef USE_CLIMATE void ControllerRegistry::notify_climate_update(climate::Climate *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_climate_update(obj); } } @@ -73,7 +73,7 @@ void ControllerRegistry::notify_climate_update(climate::Climate *obj) { #ifdef USE_NUMBER void ControllerRegistry::notify_number_update(number::Number *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_number_update(obj); } } @@ -81,7 +81,7 @@ void ControllerRegistry::notify_number_update(number::Number *obj) { #ifdef USE_DATETIME_DATE void ControllerRegistry::notify_date_update(datetime::DateEntity *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_date_update(obj); } } @@ -89,7 +89,7 @@ void ControllerRegistry::notify_date_update(datetime::DateEntity *obj) { #ifdef USE_DATETIME_TIME void ControllerRegistry::notify_time_update(datetime::TimeEntity *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_time_update(obj); } } @@ -97,7 +97,7 @@ void ControllerRegistry::notify_time_update(datetime::TimeEntity *obj) { #ifdef USE_DATETIME_DATETIME void ControllerRegistry::notify_datetime_update(datetime::DateTimeEntity *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_datetime_update(obj); } } @@ -105,7 +105,7 @@ void ControllerRegistry::notify_datetime_update(datetime::DateTimeEntity *obj) { #ifdef USE_TEXT void ControllerRegistry::notify_text_update(text::Text *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_text_update(obj); } } @@ -113,7 +113,7 @@ void ControllerRegistry::notify_text_update(text::Text *obj) { #ifdef USE_SELECT void ControllerRegistry::notify_select_update(select::Select *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_select_update(obj); } } @@ -121,7 +121,7 @@ void ControllerRegistry::notify_select_update(select::Select *obj) { #ifdef USE_LOCK void ControllerRegistry::notify_lock_update(lock::Lock *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_lock_update(obj); } } @@ -129,7 +129,7 @@ void ControllerRegistry::notify_lock_update(lock::Lock *obj) { #ifdef USE_VALVE void ControllerRegistry::notify_valve_update(valve::Valve *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_valve_update(obj); } } @@ -137,7 +137,7 @@ void ControllerRegistry::notify_valve_update(valve::Valve *obj) { #ifdef USE_MEDIA_PLAYER void ControllerRegistry::notify_media_player_update(media_player::MediaPlayer *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_media_player_update(obj); } } @@ -145,7 +145,7 @@ void ControllerRegistry::notify_media_player_update(media_player::MediaPlayer *o #ifdef USE_ALARM_CONTROL_PANEL void ControllerRegistry::notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_alarm_control_panel_update(obj); } } @@ -153,7 +153,7 @@ void ControllerRegistry::notify_alarm_control_panel_update(alarm_control_panel:: #ifdef USE_EVENT void ControllerRegistry::notify_event(event::Event *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_event(obj); } } @@ -161,7 +161,7 @@ void ControllerRegistry::notify_event(event::Event *obj) { #ifdef USE_UPDATE void ControllerRegistry::notify_update(update::UpdateEntity *obj) { - for (auto *controller : controllers_) { + for (auto *controller : controllers) { controller->on_update(obj); } } diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index 6a72195b633..72485c356be 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -234,7 +234,7 @@ class ControllerRegistry { #endif protected: - static std::vector controllers_; + static std::vector controllers; }; } // namespace esphome From 1b6471f4b0ff273e15130bf5e77e1c31e577efdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 16:30:38 -0600 Subject: [PATCH 230/461] cleanups --- esphome/components/api/__init__.py | 3 +++ esphome/components/web_server/__init__.py | 3 +++ esphome/core/__init__.py | 5 +++++ esphome/core/config.py | 9 +++++++++ esphome/core/controller_registry.cpp | 7 ++++++- esphome/core/controller_registry.h | 9 +++++++-- esphome/core/defines.h | 2 ++ 7 files changed, 35 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 449572c0e51..023e4bf115f 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -244,6 +244,9 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + # Track controller registration for StaticVector sizing + CORE.register_controller() + cg.add(var.set_port(config[CONF_PORT])) if config[CONF_PASSWORD]: cg.add_define("USE_API_PASSWORD") diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index a7fdf30eef4..17ad496f30d 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -289,6 +289,9 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) + # Track controller registration for StaticVector sizing + CORE.register_controller() + version = config[CONF_VERSION] cg.add(paren.set_port(config[CONF_PORT])) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index fed5265d6b9..84c9e360022 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -910,6 +910,11 @@ class EsphomeCore: """ self.platform_counts[platform_name] += 1 + def register_controller(self) -> None: + """Track registration of a Controller for ControllerRegistry StaticVector sizing.""" + controller_count = self.data.setdefault("controller_registry_count", 0) + self.data["controller_registry_count"] = controller_count + 1 + @property def cpp_main_section(self): from esphome.cpp_generator import statement diff --git a/esphome/core/config.py b/esphome/core/config.py index 27404538085..3d98eda8bb2 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -462,6 +462,15 @@ async def _add_platform_defines() -> None: cg.add_define(f"USE_{platform_name.upper()}") +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_controller_registry_define() -> None: + # Generate StaticVector size for ControllerRegistry + controller_count = CORE.data.get("controller_registry_count", 0) + if controller_count > 0: + cg.add_define("USE_CONTROLLER_REGISTRY") + cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count) + + @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_global(cg.global_ns.namespace("esphome").using) diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index b3f4c646a3e..b22ec487d52 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -1,9 +1,12 @@ #include "esphome/core/controller_registry.h" + +#ifdef USE_CONTROLLER_REGISTRY + #include "esphome/core/controller.h" namespace esphome { -std::vector ControllerRegistry::controllers; +StaticVector ControllerRegistry::controllers; void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } @@ -168,3 +171,5 @@ void ControllerRegistry::notify_update(update::UpdateEntity *obj) { #endif } // namespace esphome + +#endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index 72485c356be..640a276a0a0 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -1,7 +1,10 @@ #pragma once #include "esphome/core/defines.h" -#include + +#ifdef USE_CONTROLLER_REGISTRY + +#include "esphome/core/helpers.h" // Forward declarations namespace esphome { @@ -234,7 +237,9 @@ class ControllerRegistry { #endif protected: - static std::vector controllers; + static StaticVector controllers; }; } // namespace esphome + +#endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 2be32058ea0..82305180712 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -28,6 +28,7 @@ #define USE_BUTTON #define USE_CAMERA #define USE_CLIMATE +#define USE_CONTROLLER_REGISTRY #define USE_COVER #define USE_DATETIME #define USE_DATETIME_DATE @@ -296,6 +297,7 @@ #define USE_DASHBOARD_IMPORT // Default counts for static analysis +#define CONTROLLER_REGISTRY_MAX 2 #define ESPHOME_COMPONENT_COUNT 50 #define ESPHOME_DEVICE_COUNT 10 #define ESPHOME_AREA_COUNT 10 From 8229e3a47167bb1ac4a4a327cab9f50a4c08cbd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 16:33:01 -0600 Subject: [PATCH 231/461] cleanups --- esphome/components/alarm_control_panel/alarm_control_panel.cpp | 3 ++- esphome/components/binary_sensor/binary_sensor.cpp | 3 ++- esphome/components/climate/climate.cpp | 3 ++- esphome/components/cover/cover.cpp | 3 ++- esphome/components/datetime/date_entity.cpp | 3 ++- esphome/components/datetime/datetime_entity.cpp | 3 ++- esphome/components/datetime/time_entity.cpp | 3 ++- esphome/components/event/event.cpp | 3 ++- esphome/components/fan/fan.cpp | 3 ++- esphome/components/light/light_state.cpp | 3 ++- esphome/components/lock/lock.cpp | 3 ++- esphome/components/media_player/media_player.cpp | 3 ++- esphome/components/number/number.cpp | 3 ++- esphome/components/select/select.cpp | 3 ++- esphome/components/sensor/sensor.cpp | 3 ++- esphome/components/switch/switch.cpp | 3 ++- esphome/components/text/text.cpp | 3 ++- esphome/components/text_sensor/text_sensor.cpp | 3 ++- esphome/components/update/update_entity.cpp | 3 ++- esphome/components/valve/valve.cpp | 3 ++- 20 files changed, 40 insertions(+), 20 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index baa7094a8a5..febd00f2c5c 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -1,4 +1,5 @@ #include +#include "esphome/core/defines.h" #include "alarm_control_panel.h" #include "esphome/core/controller_registry.h" @@ -34,7 +35,7 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; this->state_callback_.call(); -#ifdef USE_ALARM_CONTROL_PANEL +#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_alarm_control_panel_update(this); #endif if (state == ACP_STATE_TRIGGERED) { diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 7c35ebe116a..220ed685db0 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -1,4 +1,5 @@ #include "binary_sensor.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -38,7 +39,7 @@ void BinarySensor::send_state_internal(bool new_state) { // Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed if (this->set_state_(new_state)) { ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state)); -#ifdef USE_BINARY_SENSOR +#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_binary_sensor_update(this); #endif } diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 0576674f3a9..82b75660bae 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -1,4 +1,5 @@ #include "climate.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/macros.h" @@ -464,7 +465,7 @@ void Climate::publish_state() { // Send state to frontend this->state_callback_.call(*this); -#ifdef USE_CLIMATE +#if defined(USE_CLIMATE) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_climate_update(this); #endif // Save state diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 4542f0300ee..ab800a87499 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -1,4 +1,5 @@ #include "cover.h" +#include "esphome/core/defines.h" #include #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -170,7 +171,7 @@ void Cover::publish_state(bool save) { ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation)); this->state_callback_.call(); -#ifdef USE_COVER +#if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_cover_update(this); #endif diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index d5c63f5e0b6..2c2775ecf40 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -1,4 +1,5 @@ #include "date_entity.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_DATE @@ -32,7 +33,7 @@ void DateEntity::publish_state() { this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); this->state_callback_.call(); -#ifdef USE_DATETIME_DATE +#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_date_update(this); #endif } diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 56f4e3f7af9..8606a47fa72 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -1,4 +1,5 @@ #include "datetime_entity.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_DATETIME @@ -48,7 +49,7 @@ void DateTimeEntity::publish_state() { ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, this->day_, this->hour_, this->minute_, this->second_); this->state_callback_.call(); -#ifdef USE_DATETIME_DATETIME +#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_datetime_update(this); #endif } diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index f06f9621779..469be077eab 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -1,4 +1,5 @@ #include "time_entity.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_TIME @@ -29,7 +30,7 @@ void TimeEntity::publish_state() { ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); this->state_callback_.call(); -#ifdef USE_DATETIME_TIME +#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_time_update(this); #endif } diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index ffc3cdaa39a..4c74a113885 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -1,4 +1,5 @@ #include "event.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -23,7 +24,7 @@ void Event::trigger(const std::string &event_type) { this->last_event_type_ = found; ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_); this->event_callback_.call(event_type); -#ifdef USE_EVENT +#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_event(this); #endif } diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index ee1962bf60a..d37825a6513 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -1,4 +1,5 @@ #include "fan.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -182,7 +183,7 @@ void Fan::publish_state() { ESP_LOGD(TAG, " Preset Mode: %s", preset); } this->state_callback_.call(); -#ifdef USE_FAN +#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_fan_update(this); #endif this->save_state_(); diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 0f6695d968b..8b53044554a 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,4 +1,5 @@ #include "esphome/core/controller_registry.h" +#include "esphome/core/defines.h" #include "esphome/core/log.h" #include "light_output.h" @@ -140,7 +141,7 @@ float LightState::get_setup_priority() const { return setup_priority::HARDWARE - void LightState::publish_state() { this->remote_values_callback_.call(); -#ifdef USE_LIGHT +#if defined(USE_LIGHT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_light_update(this); #endif } diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 2cbef07ca60..54fefe8745e 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -1,4 +1,5 @@ #include "lock.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -54,7 +55,7 @@ void Lock::publish_state(LockState state) { this->rtc_.save(&this->state); ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), lock_state_to_string(state)); this->state_callback_.call(); -#ifdef USE_LOCK +#if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_lock_update(this); #endif } diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 878dcfe6f16..b46ec39d302 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -1,4 +1,5 @@ #include "media_player.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -150,7 +151,7 @@ void MediaPlayer::add_on_state_callback(std::function &&callback) { void MediaPlayer::publish_state() { this->state_callback_.call(); -#ifdef USE_MEDIA_PLAYER +#if defined(USE_MEDIA_PLAYER) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_media_player_update(this); #endif } diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 35e59ebdabf..f12e0e9e1e1 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -1,4 +1,5 @@ #include "number.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -33,7 +34,7 @@ void Number::publish_state(float state) { this->state = state; ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); this->state_callback_.call(state); -#ifdef USE_NUMBER +#if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_number_update(this); #endif } diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index fc933c15ef3..9fe7a524227 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -1,4 +1,5 @@ #include "select.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" #include @@ -34,7 +35,7 @@ void Select::publish_state(size_t index) { ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); // Callback signature requires std::string, create temporary for compatibility this->state_callback_.call(std::string(option), index); -#ifdef USE_SELECT +#if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_select_update(this); #endif } diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index ac70e457e03..df6bd644e84 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -1,4 +1,5 @@ #include "sensor.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -132,7 +133,7 @@ void Sensor::internal_send_state_to_frontend(float state) { ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals()); this->callback_.call(state); -#ifdef USE_SENSOR +#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_sensor_update(this); #endif } diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 49954e6f064..3c3a437ff36 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -1,4 +1,5 @@ #include "switch.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -63,7 +64,7 @@ void Switch::publish_state(bool state) { ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state)); this->state_callback_.call(this->state); -#ifdef USE_SWITCH +#if defined(USE_SWITCH) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_switch_update(this); #endif } diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 3028cd85ea2..933d82c85c1 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -1,4 +1,5 @@ #include "text.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -17,7 +18,7 @@ void Text::publish_state(const std::string &state) { ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); } this->state_callback_.call(state); -#ifdef USE_TEXT +#if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_text_update(this); #endif } diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index c4a941408ad..a7bcf199672 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -1,4 +1,5 @@ #include "text_sensor.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -85,7 +86,7 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); this->callback_.call(state); -#ifdef USE_TEXT_SENSOR +#if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_text_sensor_update(this); #endif } diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 9ca2c35f810..567fc9fc8e0 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -1,4 +1,5 @@ #include "update_entity.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -32,7 +33,7 @@ void UpdateEntity::publish_state() { this->set_has_state(true); this->state_callback_.call(); -#ifdef USE_UPDATE +#if defined(USE_UPDATE) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_update(this); #endif } diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 0982892af34..381d9061de3 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -1,4 +1,5 @@ #include "valve.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" #include @@ -148,7 +149,7 @@ void Valve::publish_state(bool save) { ESP_LOGD(TAG, " Current Operation: %s", valve_operation_to_str(this->current_operation)); this->state_callback_.call(); -#ifdef USE_VALVE +#if defined(USE_VALVE) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_valve_update(this); #endif From 327543303cc59f997333265343755d98f6c6cc56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 16:34:37 -0600 Subject: [PATCH 232/461] cleanups --- esphome/components/api/api_server.cpp | 2 +- esphome/components/web_server/web_server.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index d9919bb8499..9453a14eb3c 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -4,8 +4,8 @@ #include "api_connection.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" -#include "esphome/core/controller_registry.h" #include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/util.h" diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 6d0cdeb07b2..5a8128ba431 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -3,6 +3,7 @@ #include "esphome/components/json/json_util.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" From 0962024d99e0ef2705755e6cb17d20a62fc04580 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 16:35:24 -0600 Subject: [PATCH 233/461] cleanups --- esphome/components/light/light_state.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 8b53044554a..5bb9087c5b3 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,5 +1,5 @@ -#include "esphome/core/controller_registry.h" #include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" #include "light_output.h" From ac85949f176b1772d4f4fe00189207e05cc187c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 16:38:32 -0600 Subject: [PATCH 234/461] cleanups --- esphome/core/__init__.py | 7 +++++-- esphome/core/config.py | 9 +++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 84c9e360022..08753b0f2d7 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -48,6 +48,9 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +# Key for tracking controller count in CORE.data for ControllerRegistry StaticVector sizing +KEY_CONTROLLER_REGISTRY_COUNT = "controller_registry_count" + class EsphomeError(Exception): """General ESPHome exception occurred.""" @@ -912,8 +915,8 @@ class EsphomeCore: def register_controller(self) -> None: """Track registration of a Controller for ControllerRegistry StaticVector sizing.""" - controller_count = self.data.setdefault("controller_registry_count", 0) - self.data["controller_registry_count"] = controller_count + 1 + controller_count = self.data.setdefault(KEY_CONTROLLER_REGISTRY_COUNT, 0) + self.data[KEY_CONTROLLER_REGISTRY_COUNT] = controller_count + 1 @property def cpp_main_section(self): diff --git a/esphome/core/config.py b/esphome/core/config.py index 3d98eda8bb2..ce94ec11f2d 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -40,7 +40,12 @@ from esphome.const import ( PlatformFramework, __version__ as ESPHOME_VERSION, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import ( + CORE, + KEY_CONTROLLER_REGISTRY_COUNT, + CoroPriority, + coroutine_with_priority, +) from esphome.helpers import ( copy_file_if_changed, fnv1a_32bit_hash, @@ -465,7 +470,7 @@ async def _add_platform_defines() -> None: @coroutine_with_priority(CoroPriority.FINAL) async def _add_controller_registry_define() -> None: # Generate StaticVector size for ControllerRegistry - controller_count = CORE.data.get("controller_registry_count", 0) + controller_count = CORE.data.get(KEY_CONTROLLER_REGISTRY_COUNT, 0) if controller_count > 0: cg.add_define("USE_CONTROLLER_REGISTRY") cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count) From 6e7f66d393c3aa7cb58aa7b5c1c68c85a3249d98 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 16:40:36 -0600 Subject: [PATCH 235/461] missing registry --- esphome/core/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/config.py b/esphome/core/config.py index ce94ec11f2d..763f9ebd9fd 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -497,6 +497,7 @@ async def to_code(config: ConfigType) -> None: cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids)) CORE.add_job(_add_platform_defines) + CORE.add_job(_add_controller_registry_define) CORE.add_job(_add_automations, config) From e3fb074a604778bc60699a455345cc68c283b667 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 17:14:50 -0600 Subject: [PATCH 236/461] preen --- esphome/components/api/api_server.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 9453a14eb3c..18601d74ff4 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -348,7 +348,8 @@ API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) #endif #ifdef USE_EVENT -// Event is a special case - it reads event_type from obj->get_last_event_type() +// Event is a special case - unlike other entities with simple state fields, +// events store their state in a member accessed via obj->get_last_event_type() void APIServer::on_event(event::Event *obj) { if (obj->is_internal()) return; From c15290e38643d7c547b956846a73340cad9ac48c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 17:53:26 -0600 Subject: [PATCH 237/461] wip --- esphome/components/api/api_connection.cpp | 8 ++++---- esphome/components/api/api_connection.h | 18 +++++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8c293b41a29..d892c53fce4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1294,11 +1294,11 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #endif #ifdef USE_EVENT -void APIConnection::send_event(event::Event *event, const std::string &event_type) { +void APIConnection::send_event(event::Event *event, const char *event_type) { this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, +uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { EventResponse resp; resp.set_event_type(StringRef(event_type)); @@ -1833,10 +1833,10 @@ void APIConnection::process_batch_() { uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const { #ifdef USE_EVENT - // Special case: EventResponse uses string pointer + // Special case: EventResponse uses const char * pointer if (message_type == EventResponse::MESSAGE_TYPE) { auto *e = static_cast(entity); - return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); + return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single); } #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 284fa11a954..4bc03e41db2 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -177,7 +177,7 @@ class APIConnection final : public APIServerConnection { #endif #ifdef USE_EVENT - void send_event(event::Event *event, const std::string &event_type); + void send_event(event::Event *event, const char *event_type); #endif #ifdef USE_UPDATE @@ -450,7 +450,7 @@ class APIConnection final : public APIServerConnection { bool is_single); #endif #ifdef USE_EVENT - static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, + static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single); static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); #endif @@ -508,8 +508,8 @@ class APIConnection final : public APIServerConnection { // Constructor for function pointer MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; } - // Constructor for string state capture - explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); } + // Constructor for const char * (Event types - no allocation needed) + explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; } // No destructor - cleanup must be called explicitly with message_type @@ -537,18 +537,14 @@ class APIConnection final : public APIServerConnection { // Manual cleanup method - must be called before destruction for string types void cleanup(uint8_t message_type) { -#ifdef USE_EVENT - if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) { - delete data_.string_ptr; - data_.string_ptr = nullptr; - } -#endif + // Event types use const char * (no cleanup needed - points to flash) + // All other types use function pointers (no cleanup needed) } private: union Data { MessageCreatorPtr function_ptr; - std::string *string_ptr; + const char *const_char_ptr; } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before }; From a6c669ff51f98231a82c4885986b70903a25541b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 18:03:38 -0600 Subject: [PATCH 238/461] cleanup --- esphome/components/api/api_connection.cpp | 6 ++-- esphome/components/api/api_connection.h | 34 ++--------------------- 2 files changed, 5 insertions(+), 35 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d892c53fce4..47b87d5ea31 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1648,9 +1648,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c // O(n) but optimized for RAM and not performance. for (auto &item : items) { if (item.entity == entity && item.message_type == message_type) { - // Clean up old creator before replacing - item.creator.cleanup(message_type); - // Move assign the new creator + // Replace with new creator item.creator = std::move(creator); return; } @@ -1820,7 +1818,7 @@ void APIConnection::process_batch_() { // Handle remaining items more efficiently if (items_processed < this->deferred_batch_.size()) { - // Remove processed items from the beginning with proper cleanup + // Remove processed items from the beginning this->deferred_batch_.remove_front(items_processed); // Reschedule for remaining items this->schedule_batch_(); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 4bc03e41db2..a77c93a2d56 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -511,8 +511,6 @@ class APIConnection final : public APIServerConnection { // Constructor for const char * (Event types - no allocation needed) explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; } - // No destructor - cleanup must be called explicitly with message_type - // Delete copy operations - MessageCreator should only be moved MessageCreator(const MessageCreator &other) = delete; MessageCreator &operator=(const MessageCreator &other) = delete; @@ -523,8 +521,6 @@ class APIConnection final : public APIServerConnection { // Move assignment MessageCreator &operator=(MessageCreator &&other) noexcept { if (this != &other) { - // IMPORTANT: Caller must ensure cleanup() was called if this contains a string! - // In our usage, this happens in add_item() deduplication and vector::erase() data_ = other.data_; other.data_.function_ptr = nullptr; } @@ -535,12 +531,6 @@ class APIConnection final : public APIServerConnection { uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const; - // Manual cleanup method - must be called before destruction for string types - void cleanup(uint8_t message_type) { - // Event types use const char * (no cleanup needed - points to flash) - // All other types use function pointers (no cleanup needed) - } - private: union Data { MessageCreatorPtr function_ptr; @@ -564,42 +554,24 @@ class APIConnection final : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - private: - // Helper to cleanup items from the beginning - void cleanup_items_(size_t count) { - for (size_t i = 0; i < count; i++) { - items[i].creator.cleanup(items[i].message_type); - } - } - - public: DeferredBatch() { // Pre-allocate capacity for typical batch sizes to avoid reallocation items.reserve(8); } - ~DeferredBatch() { - // Ensure cleanup of any remaining items - clear(); - } - // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); // Add item to the front of the batch (for high priority messages like ping) void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); - // Clear all items with proper cleanup + // Clear all items void clear() { - cleanup_items_(items.size()); items.clear(); batch_start_time = 0; } - // Remove processed items from the front with proper cleanup - void remove_front(size_t count) { - cleanup_items_(count); - items.erase(items.begin(), items.begin() + count); - } + // Remove processed items from the front + void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } bool empty() const { return items.empty(); } size_t size() const { return items.size(); } From b264c6caaca2ec80b2e343721e286a10807f077f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 18:16:22 -0600 Subject: [PATCH 239/461] cleanup defines --- .../components/alarm_control_panel/alarm_control_panel.cpp | 7 ++++--- esphome/components/cover/cover.cpp | 4 +++- esphome/components/light/light_state.cpp | 3 +-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index febd00f2c5c..c29e02c8efd 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -1,8 +1,9 @@ -#include -#include "esphome/core/defines.h" - #include "alarm_control_panel.h" +#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" + +#include + #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index ab800a87499..3062dba28ad 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -1,7 +1,9 @@ #include "cover.h" #include "esphome/core/defines.h" -#include #include "esphome/core/controller_registry.h" + +#include + #include "esphome/core/log.h" namespace esphome { diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 5bb9087c5b3..4c253ec5a83 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,9 +1,8 @@ +#include "light_state.h" #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" - #include "light_output.h" -#include "light_state.h" #include "transformers.h" namespace esphome { From 9a2fc8aa51ef2cffa6248489d7d9ccd81e2f1e47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Nov 2025 23:40:36 -0600 Subject: [PATCH 240/461] part --- esphome/components/sensor/sensor.cpp | 24 +++++--- esphome/components/sensor/sensor.h | 5 +- .../components/text_sensor/text_sensor.cpp | 22 ++++--- esphome/components/text_sensor/text_sensor.h | 6 +- esphome/core/helpers.h | 61 +++++++++++++++++++ 5 files changed, 95 insertions(+), 23 deletions(-) diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 92da4345b70..651f8283e06 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -72,9 +72,9 @@ StateClass Sensor::get_state_class() { void Sensor::publish_state(float state) { this->raw_state = state; - if (this->raw_callback_) { - this->raw_callback_->call(state); - } + + // Call raw callbacks (before filters) + this->callbacks_.call_first(this->raw_count_, state); ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); @@ -85,12 +85,12 @@ void Sensor::publish_state(float state) { } } -void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } +void Sensor::add_on_state_callback(std::function &&callback) { + this->callbacks_.add_second(std::move(callback)); +} + void Sensor::add_on_raw_state_callback(std::function &&callback) { - if (!this->raw_callback_) { - this->raw_callback_ = make_unique>(); - } - this->raw_callback_->add(std::move(callback)); + this->callbacks_.add_first(std::move(callback), &this->raw_count_); } void Sensor::add_filter(Filter *filter) { @@ -130,7 +130,13 @@ void Sensor::internal_send_state_to_frontend(float state) { this->state = state; ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals()); - this->callback_.call(state); + + // Call filtered callbacks (after filters) + this->callbacks_.call_second(this->raw_count_, state); + +#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_sensor_update(this); +#endif } } // namespace sensor diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index a4210e5e6c3..42e540a3491 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -124,8 +124,7 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa void internal_send_state_to_frontend(float state); protected: - std::unique_ptr> raw_callback_; ///< Storage for raw state callbacks (lazy allocated). - CallbackManager callback_; ///< Storage for filtered state callbacks. + PartitionedCallbackManager callbacks_; Filter *filter_list_{nullptr}; ///< Store all active filters. @@ -140,6 +139,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa uint8_t force_update : 1; uint8_t reserved : 5; // Reserved for future use } sensor_flags_{}; + + uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector) }; } // namespace sensor diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 0294d65861c..57e9fd4259e 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -24,9 +24,9 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text void TextSensor::publish_state(const std::string &state) { this->raw_state = state; - if (this->raw_callback_) { - this->raw_callback_->call(state); - } + + // Call raw callbacks (before filters) + this->callbacks_.call_first(this->raw_count_, state); ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str()); @@ -68,13 +68,11 @@ void TextSensor::clear_filters() { } void TextSensor::add_on_state_callback(std::function callback) { - this->callback_.add(std::move(callback)); + this->callbacks_.add_second(std::move(callback)); } + void TextSensor::add_on_raw_state_callback(std::function callback) { - if (!this->raw_callback_) { - this->raw_callback_ = make_unique>(); - } - this->raw_callback_->add(std::move(callback)); + this->callbacks_.add_first(std::move(callback), &this->raw_count_); } std::string TextSensor::get_state() const { return this->state; } @@ -83,7 +81,13 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->state = state; this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); - this->callback_.call(state); + + // Call filtered callbacks (after filters) + this->callbacks_.call_second(this->raw_count_, state); + +#if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_text_sensor_update(this); +#endif } } // namespace text_sensor diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index db2e857ae37..1f4f3170e04 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -58,11 +58,11 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { void internal_send_state_to_frontend(const std::string &state); protected: - std::unique_ptr> - raw_callback_; ///< Storage for raw state callbacks (lazy allocated). - CallbackManager callback_; ///< Storage for filtered state callbacks. + PartitionedCallbackManager callbacks_; Filter *filter_list_{nullptr}; ///< Store all active filters. + + uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector) }; } // namespace text_sensor diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 48af7f674a4..732a8a66af0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -869,6 +869,67 @@ template class CallbackManager { std::vector> callbacks_; }; +template class PartitionedCallbackManager; + +/** Helper class for callbacks partitioned into two sections. + * + * Uses a single vector partitioned into two sections: [first_0, ..., first_m-1, second_0, ..., second_n-1] + * The partition point is tracked externally by the caller (typically stored in the entity class for optimal alignment). + * + * Memory efficient: Only stores a 4-byte pointer. The partition count lives in the entity class where it can be + * packed with other small fields to avoid padding waste. + * + * @tparam Ts The arguments for the callbacks, wrapped in void(). + */ +template class PartitionedCallbackManager { + public: + /// Add a callback to the first partition. + void add_first(std::function &&callback, uint8_t *first_count) { + if (!this->callbacks_) { + this->callbacks_ = make_unique>>(); + } + + // Add to first partition: append then swap into position + this->callbacks_->push_back(std::move(callback)); + if (*first_count < this->callbacks_->size() - 1) { + std::swap((*this->callbacks_)[*first_count], (*this->callbacks_)[this->callbacks_->size() - 1]); + } + (*first_count)++; + } + + /// Add a callback to the second partition. + void add_second(std::function &&callback) { + if (!this->callbacks_) { + this->callbacks_ = make_unique>>(); + } + + // Add to second partition: just append (already at end after first partition) + this->callbacks_->push_back(std::move(callback)); + } + + /// Call all callbacks in the first partition. + void call_first(uint8_t first_count, Ts... args) { + if (this->callbacks_) { + for (size_t i = 0; i < first_count; i++) { + (*this->callbacks_)[i](args...); + } + } + } + + /// Call all callbacks in the second partition. + void call_second(uint8_t first_count, Ts... args) { + if (this->callbacks_) { + for (size_t i = first_count; i < this->callbacks_->size(); i++) { + (*this->callbacks_)[i](args...); + } + } + } + + protected: + /// Partitioned callback storage: [first_0, ..., first_m-1, second_0, ..., second_n-1] + std::unique_ptr>> callbacks_; +}; + /// Helper class to deduplicate items in a series of values. template class Deduplicator { public: From 7e96f10a79b3cd573ffcf7a4790c634eed7af582 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 08:39:23 -0600 Subject: [PATCH 241/461] dry --- esphome/core/controller_registry.cpp | 120 +++++++-------------------- 1 file changed, 30 insertions(+), 90 deletions(-) diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index b22ec487d52..993a0dc241c 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -10,151 +10,88 @@ StaticVector ControllerRegistry::controll void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } -#ifdef USE_BINARY_SENSOR -void ControllerRegistry::notify_binary_sensor_update(binary_sensor::BinarySensor *obj) { - for (auto *controller : controllers) { - controller->on_binary_sensor_update(obj); +// Macro for registry notification dispatch - iterates registered controllers and calls their handler +#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ + void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ + for (auto *controller : controllers) { \ + controller->on_##entity_name##_update(obj); \ + } \ } -} + +#ifdef USE_BINARY_SENSOR +CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor) #endif #ifdef USE_FAN -void ControllerRegistry::notify_fan_update(fan::Fan *obj) { - for (auto *controller : controllers) { - controller->on_fan_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(fan::Fan, fan) #endif #ifdef USE_LIGHT -void ControllerRegistry::notify_light_update(light::LightState *obj) { - for (auto *controller : controllers) { - controller->on_light_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(light::LightState, light) #endif #ifdef USE_SENSOR -void ControllerRegistry::notify_sensor_update(sensor::Sensor *obj) { - for (auto *controller : controllers) { - controller->on_sensor_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(sensor::Sensor, sensor) #endif #ifdef USE_SWITCH -void ControllerRegistry::notify_switch_update(switch_::Switch *obj) { - for (auto *controller : controllers) { - controller->on_switch_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(switch_::Switch, switch) #endif #ifdef USE_COVER -void ControllerRegistry::notify_cover_update(cover::Cover *obj) { - for (auto *controller : controllers) { - controller->on_cover_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(cover::Cover, cover) #endif #ifdef USE_TEXT_SENSOR -void ControllerRegistry::notify_text_sensor_update(text_sensor::TextSensor *obj) { - for (auto *controller : controllers) { - controller->on_text_sensor_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(text_sensor::TextSensor, text_sensor) #endif #ifdef USE_CLIMATE -void ControllerRegistry::notify_climate_update(climate::Climate *obj) { - for (auto *controller : controllers) { - controller->on_climate_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(climate::Climate, climate) #endif #ifdef USE_NUMBER -void ControllerRegistry::notify_number_update(number::Number *obj) { - for (auto *controller : controllers) { - controller->on_number_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(number::Number, number) #endif #ifdef USE_DATETIME_DATE -void ControllerRegistry::notify_date_update(datetime::DateEntity *obj) { - for (auto *controller : controllers) { - controller->on_date_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(datetime::DateEntity, date) #endif #ifdef USE_DATETIME_TIME -void ControllerRegistry::notify_time_update(datetime::TimeEntity *obj) { - for (auto *controller : controllers) { - controller->on_time_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(datetime::TimeEntity, time) #endif #ifdef USE_DATETIME_DATETIME -void ControllerRegistry::notify_datetime_update(datetime::DateTimeEntity *obj) { - for (auto *controller : controllers) { - controller->on_datetime_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(datetime::DateTimeEntity, datetime) #endif #ifdef USE_TEXT -void ControllerRegistry::notify_text_update(text::Text *obj) { - for (auto *controller : controllers) { - controller->on_text_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(text::Text, text) #endif #ifdef USE_SELECT -void ControllerRegistry::notify_select_update(select::Select *obj) { - for (auto *controller : controllers) { - controller->on_select_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(select::Select, select) #endif #ifdef USE_LOCK -void ControllerRegistry::notify_lock_update(lock::Lock *obj) { - for (auto *controller : controllers) { - controller->on_lock_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(lock::Lock, lock) #endif #ifdef USE_VALVE -void ControllerRegistry::notify_valve_update(valve::Valve *obj) { - for (auto *controller : controllers) { - controller->on_valve_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(valve::Valve, valve) #endif #ifdef USE_MEDIA_PLAYER -void ControllerRegistry::notify_media_player_update(media_player::MediaPlayer *obj) { - for (auto *controller : controllers) { - controller->on_media_player_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player) #endif #ifdef USE_ALARM_CONTROL_PANEL -void ControllerRegistry::notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { - for (auto *controller : controllers) { - controller->on_alarm_control_panel_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel) #endif #ifdef USE_EVENT +// Event is a special case - notify_event() calls on_event() (no "_update" suffix) void ControllerRegistry::notify_event(event::Event *obj) { for (auto *controller : controllers) { controller->on_event(obj); @@ -163,6 +100,7 @@ void ControllerRegistry::notify_event(event::Event *obj) { #endif #ifdef USE_UPDATE +// Update is a special case - notify_update() calls on_update() (no "_update" suffix) void ControllerRegistry::notify_update(update::UpdateEntity *obj) { for (auto *controller : controllers) { controller->on_update(obj); @@ -170,6 +108,8 @@ void ControllerRegistry::notify_update(update::UpdateEntity *obj) { } #endif +#undef CONTROLLER_REGISTRY_NOTIFY + } // namespace esphome #endif // USE_CONTROLLER_REGISTRY From 62f43d3353ee2b399023e205b360310a281e215e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 08:41:46 -0600 Subject: [PATCH 242/461] dry --- esphome/core/controller_registry.cpp | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index 993a0dc241c..0a84bb0d0d5 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -10,7 +10,7 @@ StaticVector ControllerRegistry::controll void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } -// Macro for registry notification dispatch - iterates registered controllers and calls their handler +// Macro for standard registry notification dispatch - calls on__update() #define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ for (auto *controller : controllers) { \ @@ -18,6 +18,14 @@ void ControllerRegistry::register_controller(Controller *controller) { controlle } \ } +// Macro for entities where controller method has no "_update" suffix (Event, Update) +#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ + void ControllerRegistry::notify_##entity_name(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ + for (auto *controller : controllers) { \ + controller->on_##entity_name(obj); \ + } \ + } + #ifdef USE_BINARY_SENSOR CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor) #endif @@ -91,24 +99,15 @@ CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control #endif #ifdef USE_EVENT -// Event is a special case - notify_event() calls on_event() (no "_update" suffix) -void ControllerRegistry::notify_event(event::Event *obj) { - for (auto *controller : controllers) { - controller->on_event(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event) #endif #ifdef USE_UPDATE -// Update is a special case - notify_update() calls on_update() (no "_update" suffix) -void ControllerRegistry::notify_update(update::UpdateEntity *obj) { - for (auto *controller : controllers) { - controller->on_update(obj); - } -} +CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(update::UpdateEntity, update) #endif #undef CONTROLLER_REGISTRY_NOTIFY +#undef CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX } // namespace esphome From c2abf363b64f59f6dd91e411986224dd7e9dd9b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 09:45:44 -0600 Subject: [PATCH 243/461] Ensure event paths are enabled in api compile tests --- tests/components/api/common-base.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml index c90fa4dfefc..fc53b8ac7e0 100644 --- a/tests/components/api/common-base.yaml +++ b/tests/components/api/common-base.yaml @@ -178,6 +178,14 @@ api: - logger.log: "Skipped loops" - logger.log: "After combined test" +event: + - platform: template + name: Test Event + id: test_event + event_types: + - single_click + - double_click + globals: - id: api_continuation_test_counter type: int From b9f208b63ada31df0da0cfbc70ebeaf075aedaf3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 10:16:18 -0600 Subject: [PATCH 244/461] [api][event] Send events immediately to prevent loss during rapid triggers --- esphome/components/api/api_connection.cpp | 4 +- esphome/components/api/api_connection.h | 54 ++++++++++++++++++----- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 47b87d5ea31..a4dbcef57ef 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1295,8 +1295,8 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #ifdef USE_EVENT void APIConnection::send_event(event::Event *event, const char *event_type) { - this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, - EventResponse::ESTIMATED_SIZE); + this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, + EventResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index a77c93a2d56..78161442c15 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -650,21 +650,30 @@ class APIConnection final : public APIServerConnection { } #endif + // Helper to check if a message type should bypass batching + // Returns true if: + // 1. It's an UpdateStateResponse (always send immediately to handle cases where + // the main loop is blocked, e.g., during OTA updates) + // 2. It's an EventResponse (events are edge-triggered - every occurrence matters) + // 3. OR: User has opted into immediate sending (should_try_send_immediately = true + // AND batch_delay = 0) + inline bool should_send_immediately_(uint8_t message_type) const { + return ( +#ifdef USE_UPDATE + message_type == UpdateStateResponse::MESSAGE_TYPE || +#endif +#ifdef USE_EVENT + message_type == EventResponse::MESSAGE_TYPE || +#endif + (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)); + } + // Helper method to send a message either immediately or via batching + // Tries immediate send if should_send_immediately_() returns true and buffer has space + // Falls back to batching if immediate send fails or isn't applicable bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, uint8_t estimated_size) { - // Try to send immediately if: - // 1. It's an UpdateStateResponse (always send immediately to handle cases where - // the main loop is blocked, e.g., during OTA updates) - // 2. OR: We should try to send immediately (should_try_send_immediately = true) - // AND Batch delay is 0 (user has opted in to immediate sending) - // 3. AND: Buffer has space available - if (( -#ifdef USE_UPDATE - message_type == UpdateStateResponse::MESSAGE_TYPE || -#endif - (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) && - this->helper_->can_write_without_blocking()) { + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { // Now actually encode and send if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { @@ -682,6 +691,27 @@ class APIConnection final : public APIServerConnection { return this->schedule_message_(entity, creator, message_type, estimated_size); } + // Overload for MessageCreator (used by events which need to capture event_type) + bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { + // Try to send immediately if message type should bypass batching and buffer has space + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { + // Now actually encode and send + if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) && + this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log the message in verbose mode + this->log_proto_message_(entity, std::move(creator), message_type); +#endif + return true; + } + + // If immediate send failed, fall through to batching + } + + // Fall back to scheduled batching + return this->schedule_message_(entity, std::move(creator), message_type, estimated_size); + } + // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size); From 65fd784fa798c0e8b948a38fe4a3c76619783419 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 10:39:09 -0600 Subject: [PATCH 245/461] tidy --- esphome/components/api/api_connection.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 78161442c15..6cfd1089272 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -700,7 +700,7 @@ class APIConnection final : public APIServerConnection { this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { #ifdef HAS_PROTO_MESSAGE_DUMP // Log the message in verbose mode - this->log_proto_message_(entity, std::move(creator), message_type); + this->log_proto_message_(entity, creator, message_type); #endif return true; } From f55c87218075a63bdf39629a38aa34cab3443e40 Mon Sep 17 00:00:00 2001 From: optimusprimespace <62800678+optimusprimespace@users.noreply.github.com> Date: Sat, 8 Nov 2025 22:56:51 +0200 Subject: [PATCH 246/461] Updated AQI calculation for HM3301 to the new standard (#9442) Co-authored-by: J. Nick Koston --- esphome/components/hm3301/aqi_calculator.h | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h index c1b47826a2d..aa01060d2c8 100644 --- a/esphome/components/hm3301/aqi_calculator.h +++ b/esphome/components/hm3301/aqi_calculator.h @@ -1,7 +1,7 @@ #pragma once - +#include #include "abstract_aqi_calculator.h" -// https://www.airnow.gov/sites/default/files/2020-05/aqi-technical-assistance-document-sept2018.pdf +// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf namespace esphome { namespace hm3301 { @@ -16,16 +16,15 @@ class AQICalculator : public AbstractAQICalculator { } protected: - static const int AMOUNT_OF_LEVELS = 7; + static const int AMOUNT_OF_LEVELS = 6; - int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, - {201, 300}, {301, 400}, {401, 500}}; + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; - int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 35}, {36, 55}, {56, 150}, - {151, 250}, {251, 350}, {351, 500}}; + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55}, + {56, 125}, {126, 225}, {226, INT_MAX}}; - int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, {255, 354}, - {355, 424}, {425, 504}, {505, 604}}; + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, + {255, 354}, {355, 424}, {425, INT_MAX}}; int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { int grid_index = get_grid_index_(value, array); From b61027607f53f9344b924a5827f34e1513e44257 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 8 Nov 2025 15:22:40 -0600 Subject: [PATCH 247/461] Bump aioesphomeapi from 42.6.0 to 42.7.0 (#11771) 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 33fa2b64eb0..40802422f2d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==42.6.0 +aioesphomeapi==42.7.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.16 # dashboard_import From e7e091b48cecf5d68c7489f4e7294ecbb131a0b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 19:18:40 -0600 Subject: [PATCH 248/461] [core] Remove deprecated EntityBase::hash_base() method --- esphome/core/entity_base.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 1486ff53602..2b52d66f76e 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -129,9 +129,6 @@ class EntityBase { // Returns empty StringRef if object_id is dynamic (needs allocation) StringRef get_object_id_ref_for_api_() const; - /// The hash_base() function has been deprecated. It is kept in this - /// class for now, to prevent external components from not compiling. - virtual uint32_t hash_base() { return 0L; } void calc_object_id_(); /// Check if the object_id is dynamic (changes with MAC suffix) From f6bf6bd8ee0aff23bbc1961bac67e7d8beb52121 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 21:12:00 -0600 Subject: [PATCH 249/461] [uart] Store static data in flash and use function pointers for lambdas --- esphome/components/uart/__init__.py | 7 ++++-- esphome/components/uart/automation.h | 32 +++++++++++++++++----------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index eb911ed0073..cbc11d0db04 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -31,7 +31,7 @@ from esphome.const import ( PLATFORM_HOST, PlatformFramework, ) -from esphome.core import CORE +from esphome.core import CORE, ID import esphome.final_validate as fv from esphome.yaml_util import make_data_base @@ -446,7 +446,10 @@ async def uart_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(cg.ArrayInitializer(*data))) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index ad2c4d2bf1d..7a3344c2f17 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -10,32 +10,38 @@ namespace uart { template class UARTWriteAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; + void set_data_template(std::vector (*func)(Ts...)) { + // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers + this->data_.func = func; this->static_ = false; } - void set_data_static(std::vector &&data) { - this->data_static_ = std::move(data); - this->static_ = true; - } - void set_data_static(std::initializer_list data) { - this->data_static_ = std::vector(data); + + // Store pointer to static data in flash (no RAM copy) + void set_data_static(const uint8_t *data, size_t len) { + // Simply set pointer and length - no construction needed for POD types + this->data_.static_data.ptr = data; + this->data_.static_data.len = len; this->static_ = true; } void play(const Ts &...x) override { if (this->static_) { - this->parent_->write_array(this->data_static_); + this->parent_->write_array(this->data_.static_data.ptr, this->data_.static_data.len); } else { - auto val = this->data_func_(x...); + auto val = this->data_.func(x...); this->parent_->write_array(val); } } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + bool static_{true}; // Default to static mode (most common case) + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + struct { + const uint8_t *ptr; + size_t len; + } static_data; + } data_; }; } // namespace uart From c5014321a6e0d10eeda7d1b7ae861b1774e33a19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 21:15:28 -0600 Subject: [PATCH 250/461] Expand uart.write tests --- tests/components/uart/test.esp32-idf.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml index 9744a484096..6ffd0d72824 100644 --- a/tests/components/uart/test.esp32-idf.yaml +++ b/tests/components/uart/test.esp32-idf.yaml @@ -3,6 +3,8 @@ esphome: then: - uart.write: 'Hello World' - uart.write: [0x00, 0x20, 0x42] + - uart.write: !lambda |- + return {0xAA, 0xBB, 0xCC}; uart: - id: uart_uart @@ -46,6 +48,15 @@ switch: turn_on: "TURN_ON" turn_off: "TURN_OFF" +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: # Test uart button with array data - platform: uart @@ -57,3 +68,10 @@ button: name: "UART Button String" uart_id: uart_uart data: "BUTTON_PRESS" + # Test uart button with lambda (function pointer) + - platform: template + name: "UART Lambda Test" + on_press: + - uart.write: !lambda |- + std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n"; + return std::vector(cmd.begin(), cmd.end()); From 0d4a6fa350069af81ad7a8459cfb0aa0e46d6ec7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 21:41:17 -0600 Subject: [PATCH 251/461] [ble_client] Optimize ble_write memory usage - store static data in flash --- esphome/components/ble_client/__init__.py | 8 ++++- esphome/components/ble_client/automation.h | 37 +++++++++------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 768a345213d..37db181584b 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VALUE, ) +from esphome.core import ID AUTO_LOAD = ["esp32_ble_client"] CODEOWNERS = ["@buxtronix", "@clydebarrow"] @@ -198,7 +199,12 @@ async def ble_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(value, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_value_template(templ)) else: - cg.add(var.set_value_simple(value)) + # Generate static array in flash to avoid RAM copy + if isinstance(value, bytes): + value = list(value) + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*value)) + cg.add(var.set_value_simple(arr, len(value))) if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): cg.add( diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index ce534501f39..46915be9edb 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -96,11 +96,8 @@ template class BLEClientWriteAction : public Action, publ BLEClientWriteAction(BLEClient *ble_client) { ble_client->register_ble_node(this); ble_client_ = ble_client; - this->construct_simple_value_(); } - ~BLEClientWriteAction() { this->destroy_simple_value_(); } - void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } @@ -110,16 +107,14 @@ template class BLEClientWriteAction : public Action, publ void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_value_template(std::vector (*func)(Ts...)) { - this->destroy_simple_value_(); this->value_.template_func = func; this->has_simple_value_ = false; } - void set_value_simple(const std::vector &value) { - if (!this->has_simple_value_) { - this->construct_simple_value_(); - } - this->value_.simple = value; + // Store pointer to static data in flash (no RAM copy) + void set_value_simple(const uint8_t *data, size_t len) { + this->value_.simple_data.ptr = data; + this->value_.simple_data.len = len; this->has_simple_value_ = true; } @@ -128,7 +123,12 @@ template class BLEClientWriteAction : public Action, publ void play_complex(const Ts &...x) override { this->num_running_++; this->var_ = std::make_tuple(x...); - auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...); + std::vector value; + if (this->has_simple_value_) { + value.assign(this->value_.simple_data.ptr, this->value_.simple_data.ptr + this->value_.simple_data.len); + } else { + value = this->value_.template_func(x...); + } // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. if (!write(value)) this->play_next_(x...); @@ -201,21 +201,14 @@ template class BLEClientWriteAction : public Action, publ } private: - void construct_simple_value_() { new (&this->value_.simple) std::vector(); } - - void destroy_simple_value_() { - if (this->has_simple_value_) { - this->value_.simple.~vector(); - } - } - BLEClient *ble_client_; - bool has_simple_value_ = true; + bool has_simple_value_{true}; union Value { - std::vector simple; std::vector (*template_func)(Ts...); - Value() {} // trivial constructor - ~Value() {} // trivial destructor - we manage lifetime via discriminator + struct { + const uint8_t *ptr; + size_t len; + } simple_data; } value_; espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID char_uuid_; From 4b143e1f3da9ac4fb225082e8505726677fa491c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 21:54:05 -0600 Subject: [PATCH 252/461] Add ble_client lambda compile tests --- tests/components/ble_client/common.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/components/ble_client/common.yaml b/tests/components/ble_client/common.yaml index aa4b6394635..4ea1dd60f38 100644 --- a/tests/components/ble_client/common.yaml +++ b/tests/components/ble_client/common.yaml @@ -52,3 +52,25 @@ sensor: name: "BLE Sensor without Lambda" service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" characteristic_uuid: "abcd1237-abcd-1234-abcd-abcd12345678" + +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + +button: + # Test ble_write with lambda that references a component (function pointer) + - platform: template + name: "BLE Write Lambda Test" + on_press: + - ble_client.ble_write: + id: test_blec + service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" + characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678" + value: !lambda |- + uint8_t val = (uint8_t)id(test_number).state; + return std::vector{0xAA, val, 0xBB}; From dad5a88ecf8edb8c4982ad3efcc23ca87cef708a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 22:03:54 -0600 Subject: [PATCH 253/461] [canbus] Optimize canbus.send memory usage - store static data in flash --- esphome/components/canbus/__init__.py | 7 +++++-- esphome/components/canbus/canbus.h | 30 ++++++++++++++++++--------- tests/components/canbus/common.yaml | 13 ++++++++++++ 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index e1de1eb2f21..7b51c2c45c1 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -4,7 +4,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID -from esphome.core import CORE +from esphome.core import CORE, ID CODEOWNERS = ["@mvturnho", "@danielschramm"] IS_PLATFORM_COMPONENT = True @@ -176,5 +176,8 @@ async def canbus_action_to_code(config, action_id, template_arg, args): else: if isinstance(data, bytes): data = [int(x) for x in data] - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 029eb278c0b..122ccfe39e0 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -112,12 +112,16 @@ class Canbus : public Component { template class CanbusSendAction : public Action, public Parented { public: - void set_data_template(const std::function(Ts...)> func) { - this->data_func_ = func; + void set_data_template(std::vector (*func)(Ts...)) { + // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers + this->data_.func = func; this->static_ = false; } - void set_data_static(const std::vector &data) { - this->data_static_ = data; + + // Store pointer to static data in flash (no RAM copy) + void set_data_static(const uint8_t *data, size_t len) { + this->data_.static_data.ptr = data; + this->data_.static_data.len = len; this->static_ = true; } @@ -133,21 +137,27 @@ template class CanbusSendAction : public Action, public P auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; auto use_extended_id = this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; + std::vector data; if (this->static_) { - this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, this->data_static_); + data.assign(this->data_.static_data.ptr, this->data_.static_data.ptr + this->data_.static_data.len); } else { - auto val = this->data_func_(x...); - this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, val); + data = this->data_.func(x...); } + this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data); } protected: optional can_id_{}; optional use_extended_id_{}; bool remote_transmission_request_{false}; - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + bool static_{true}; // Default to static mode (most common case) + union Data { + std::vector (*func)(Ts...); // 4 bytes on 32-bit + struct { + const uint8_t *ptr; // 4 bytes on 32-bit + size_t len; // 4 bytes on 32-bit + } static_data; // 8 bytes total on 32-bit + } data_; // Union size = 8 bytes (max of 4 and 8) }; class CanbusTrigger : public Trigger, uint32_t, bool>, public Component { diff --git a/tests/components/canbus/common.yaml b/tests/components/canbus/common.yaml index fd146cc3a30..8bddeb74094 100644 --- a/tests/components/canbus/common.yaml +++ b/tests/components/canbus/common.yaml @@ -37,6 +37,15 @@ canbus: break; } +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + button: - platform: template name: Canbus Actions @@ -44,3 +53,7 @@ button: - canbus.send: "abc" - canbus.send: [0, 1, 2] - canbus.send: !lambda return {0, 1, 2}; + # Test canbus.send with lambda that references a component (function pointer) + - canbus.send: !lambda |- + uint8_t val = (uint8_t)id(test_number).state; + return std::vector{0xAA, val, 0xBB}; From 93a57831f40ffacc394b9c14aa5dd60e087ac1aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 22:08:07 -0600 Subject: [PATCH 254/461] Add additional compile time tests for canbus --- tests/components/canbus/common.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/components/canbus/common.yaml b/tests/components/canbus/common.yaml index fd146cc3a30..8bddeb74094 100644 --- a/tests/components/canbus/common.yaml +++ b/tests/components/canbus/common.yaml @@ -37,6 +37,15 @@ canbus: break; } +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + button: - platform: template name: Canbus Actions @@ -44,3 +53,7 @@ button: - canbus.send: "abc" - canbus.send: [0, 1, 2] - canbus.send: !lambda return {0, 1, 2}; + # Test canbus.send with lambda that references a component (function pointer) + - canbus.send: !lambda |- + uint8_t val = (uint8_t)id(test_number).state; + return std::vector{0xAA, val, 0xBB}; From 17df00809216a7505d9fbf12ed6e66d19d93bdf2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 22:16:39 -0600 Subject: [PATCH 255/461] [sx126x] Optimize send_packet action memory usage - store static data in flash --- esphome/components/sx126x/__init__.py | 7 +++++-- esphome/components/sx126x/automation.h | 26 +++++++++++++++++--------- tests/components/sx126x/common.yaml | 11 +++++++++++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 370cd102d49..f8f3b9d1040 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -3,7 +3,7 @@ import esphome.codegen as cg from esphome.components import spi import esphome.config_validation as cv from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID -from esphome.core import TimePeriod +from esphome.core import ID, TimePeriod MULTI_CONF = True CODEOWNERS = ["@swoboda1337"] @@ -329,5 +329,8 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/sx126x/automation.h b/esphome/components/sx126x/automation.h index 6b2371e2539..41f0a888e54 100644 --- a/esphome/components/sx126x/automation.h +++ b/esphome/components/sx126x/automation.h @@ -14,28 +14,36 @@ template class RunImageCalAction : public Action, public template class SendPacketAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; this->static_ = false; } - void set_data_static(const std::vector &data) { - this->data_static_ = data; + void set_data_static(const uint8_t *data, size_t len) { + this->data_.static_data.ptr = data; + this->data_.static_data.len = len; this->static_ = true; } void play(const Ts &...x) override { + std::vector data; if (this->static_) { - this->parent_->transmit_packet(this->data_static_); + data.assign(this->data_.static_data.ptr, this->data_.static_data.ptr + this->data_.static_data.len); } else { - this->parent_->transmit_packet(this->data_func_(x...)); + data = this->data_.func(x...); } + this->parent_->transmit_packet(data); } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + bool static_{true}; + union Data { + std::vector (*func)(Ts...); + struct { + const uint8_t *ptr; + size_t len; + } static_data; + } data_; }; template class SetModeTxAction : public Action, public Parented { diff --git a/tests/components/sx126x/common.yaml b/tests/components/sx126x/common.yaml index 3f540a4baed..659550cc01d 100644 --- a/tests/components/sx126x/common.yaml +++ b/tests/components/sx126x/common.yaml @@ -26,6 +26,15 @@ sx126x: - lambda: |- ESP_LOGD("lambda", "packet %.2f %.2f %s", rssi, snr, format_hex(x).c_str()); +number: + - platform: template + name: "SX126x Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: - platform: template name: "SX126x Button" @@ -37,3 +46,5 @@ button: - sx126x.set_mode_rx - sx126x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] + - sx126x.send_packet: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; From a6feea5415a1109f7d175c5a3b2c9429233b57c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 22:19:47 -0600 Subject: [PATCH 256/461] Add additional sx126x lambda tests --- tests/components/sx126x/common.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/components/sx126x/common.yaml b/tests/components/sx126x/common.yaml index 3f540a4baed..659550cc01d 100644 --- a/tests/components/sx126x/common.yaml +++ b/tests/components/sx126x/common.yaml @@ -26,6 +26,15 @@ sx126x: - lambda: |- ESP_LOGD("lambda", "packet %.2f %.2f %s", rssi, snr, format_hex(x).c_str()); +number: + - platform: template + name: "SX126x Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: - platform: template name: "SX126x Button" @@ -37,3 +46,5 @@ button: - sx126x.set_mode_rx - sx126x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] + - sx126x.send_packet: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; From ba82d968eb40372103f1e9b9844972e5ada95848 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 22:25:41 -0600 Subject: [PATCH 257/461] [sx127x] Optimize send_packet action memory usage - store static data in flash --- esphome/components/sx127x/__init__.py | 6 +++++- esphome/components/sx127x/automation.h | 26 +++++++++++++++++--------- tests/components/sx127x/common.yaml | 11 +++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index 33b556db078..77cb61f7f81 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import spi import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID +from esphome.core import ID MULTI_CONF = True CODEOWNERS = ["@swoboda1337"] @@ -321,5 +322,8 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/sx127x/automation.h b/esphome/components/sx127x/automation.h index eae16c11fa6..52dbf37e09e 100644 --- a/esphome/components/sx127x/automation.h +++ b/esphome/components/sx127x/automation.h @@ -14,28 +14,36 @@ template class RunImageCalAction : public Action, public template class SendPacketAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; this->static_ = false; } - void set_data_static(const std::vector &data) { - this->data_static_ = data; + void set_data_static(const uint8_t *data, size_t len) { + this->data_.static_data.ptr = data; + this->data_.static_data.len = len; this->static_ = true; } void play(const Ts &...x) override { + std::vector data; if (this->static_) { - this->parent_->transmit_packet(this->data_static_); + data.assign(this->data_.static_data.ptr, this->data_.static_data.ptr + this->data_.static_data.len); } else { - this->parent_->transmit_packet(this->data_func_(x...)); + data = this->data_.func(x...); } + this->parent_->transmit_packet(data); } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + bool static_{true}; + union Data { + std::vector (*func)(Ts...); + struct { + const uint8_t *ptr; + size_t len; + } static_data; + } data_; }; template class SetModeTxAction : public Action, public Parented { diff --git a/tests/components/sx127x/common.yaml b/tests/components/sx127x/common.yaml index 540381fc083..6e48952fcca 100644 --- a/tests/components/sx127x/common.yaml +++ b/tests/components/sx127x/common.yaml @@ -26,6 +26,15 @@ sx127x: - sx127x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] +number: + - platform: template + name: "SX127x Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: - platform: template name: "SX127x Button" @@ -38,3 +47,5 @@ button: - sx127x.set_mode_rx - sx127x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] + - sx127x.send_packet: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; From a67a433627448edfdccb48b1a88dd5bc9623731b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 22:28:32 -0600 Subject: [PATCH 258/461] Add additional sx127x lambda tests --- tests/components/sx127x/common.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/components/sx127x/common.yaml b/tests/components/sx127x/common.yaml index 540381fc083..6e48952fcca 100644 --- a/tests/components/sx127x/common.yaml +++ b/tests/components/sx127x/common.yaml @@ -26,6 +26,15 @@ sx127x: - sx127x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] +number: + - platform: template + name: "SX127x Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: - platform: template name: "SX127x Button" @@ -38,3 +47,5 @@ button: - sx127x.set_mode_rx - sx127x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] + - sx127x.send_packet: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; From 2cac99dafaf7e3972127f3d22bec94ebe6634db8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 22:32:47 -0600 Subject: [PATCH 259/461] [udp] Optimize udp.write action memory usage - store static data in flash --- esphome/components/udp/__init__.py | 7 +++++-- esphome/components/udp/automation.h | 25 ++++++++++++++++--------- tests/components/udp/common.yaml | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 6b1e4f8ed87..69abf4b989e 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -12,7 +12,7 @@ from esphome.components.packet_transport import ( ) import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID -from esphome.core import Lambda +from esphome.core import ID, Lambda from esphome.cpp_generator import ExpressionStatement, MockObj CODEOWNERS = ["@clydebarrow"] @@ -158,5 +158,8 @@ async def udp_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/udp/automation.h b/esphome/components/udp/automation.h index c5e5e2eae8c..a3b76fb4ea3 100644 --- a/esphome/components/udp/automation.h +++ b/esphome/components/udp/automation.h @@ -11,28 +11,35 @@ namespace udp { template class UDPWriteAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; this->static_ = false; } - void set_data_static(const std::vector &data) { - this->data_static_ = data; + + void set_data_static(const uint8_t *data, size_t len) { + this->data_.static_data.ptr = data; + this->data_.static_data.len = len; this->static_ = true; } void play(const Ts &...x) override { if (this->static_) { - this->parent_->send_packet(this->data_static_); + this->parent_->send_packet(this->data_.static_data.ptr, this->data_.static_data.len); } else { - auto val = this->data_func_(x...); + auto val = this->data_.func(x...); this->parent_->send_packet(val); } } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + bool static_{true}; + union Data { + std::vector (*func)(Ts...); + struct { + const uint8_t *ptr; + size_t len; + } static_data; + } data_; }; } // namespace udp diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml index 96224d0d1f1..98546d49ef5 100644 --- a/tests/components/udp/common.yaml +++ b/tests/components/udp/common.yaml @@ -17,3 +17,22 @@ udp: id: my_udp data: !lambda |- return std::vector{1,3,4,5,6}; + +number: + - platform: template + name: "UDP Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +button: + - platform: template + name: "UDP Button" + on_press: + then: + - udp.write: + data: [0x01, 0x02, 0x03] + - udp.write: !lambda |- + return {0x10, 0x20, (uint8_t)id(my_number).state}; From 5310512123ae59868969240786e6fd0806c88ef5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 22:33:44 -0600 Subject: [PATCH 260/461] Add additional udp lambda tests --- tests/components/udp/common.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml index 96224d0d1f1..98546d49ef5 100644 --- a/tests/components/udp/common.yaml +++ b/tests/components/udp/common.yaml @@ -17,3 +17,22 @@ udp: id: my_udp data: !lambda |- return std::vector{1,3,4,5,6}; + +number: + - platform: template + name: "UDP Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +button: + - platform: template + name: "UDP Button" + on_press: + then: + - udp.write: + data: [0x01, 0x02, 0x03] + - udp.write: !lambda |- + return {0x10, 0x20, (uint8_t)id(my_number).state}; From ecf7de7743c15981b4592d6a1425101a03198202 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 22:39:51 -0600 Subject: [PATCH 261/461] [speaker] Optimize speaker.play action memory usage - store static data in flash --- esphome/components/speaker/__init__.py | 7 +++++-- esphome/components/speaker/automation.h | 25 ++++++++++++++++--------- tests/components/speaker/common.yaml | 18 ++++++++++++++++++ 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 5f1ba94ee60..18e1d9782cc 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -3,7 +3,7 @@ import esphome.codegen as cg from esphome.components import audio, audio_dac import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME -from esphome.core import CORE +from esphome.core import CORE, ID from esphome.coroutine import CoroPriority, coroutine_with_priority AUTO_LOAD = ["audio"] @@ -90,7 +90,10 @@ async def speaker_play_action(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index 80bba250300..cae429cf8a3 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -10,28 +10,35 @@ namespace speaker { template class PlayAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; this->static_ = false; } - void set_data_static(const std::vector &data) { - this->data_static_ = data; + + void set_data_static(const uint8_t *data, size_t len) { + this->data_.static_data.ptr = data; + this->data_.static_data.len = len; this->static_ = true; } void play(const Ts &...x) override { if (this->static_) { - this->parent_->play(this->data_static_); + this->parent_->play(this->data_.static_data.ptr, this->data_.static_data.len); } else { - auto val = this->data_func_(x...); + auto val = this->data_.func(x...); this->parent_->play(val); } } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + bool static_{true}; + union Data { + std::vector (*func)(Ts...); + struct { + const uint8_t *ptr; + size_t len; + } static_data; + } data_; }; template class VolumeSetAction : public Action, public Parented { diff --git a/tests/components/speaker/common.yaml b/tests/components/speaker/common.yaml index c04674ee29c..fa54fa7e396 100644 --- a/tests/components/speaker/common.yaml +++ b/tests/components/speaker/common.yaml @@ -1,3 +1,12 @@ +number: + - platform: template + name: "Speaker Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + esphome: on_boot: then: @@ -14,6 +23,15 @@ esphome: - speaker.finish: - speaker.stop: +button: + - platform: template + name: "Speaker Button" + on_press: + then: + - speaker.play: [0x10, 0x20, 0x30, 0x40] + - speaker.play: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; + i2s_audio: i2s_lrclk_pin: ${i2s_bclk_pin} i2s_bclk_pin: ${i2s_lrclk_pin} From 99c60bfa42260ce6c1c8e987b94fd2a02691c772 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 22:41:05 -0600 Subject: [PATCH 262/461] Add additional speaker lambda tests --- tests/components/speaker/common.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/components/speaker/common.yaml b/tests/components/speaker/common.yaml index c04674ee29c..fa54fa7e396 100644 --- a/tests/components/speaker/common.yaml +++ b/tests/components/speaker/common.yaml @@ -1,3 +1,12 @@ +number: + - platform: template + name: "Speaker Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + esphome: on_boot: then: @@ -14,6 +23,15 @@ esphome: - speaker.finish: - speaker.stop: +button: + - platform: template + name: "Speaker Button" + on_press: + then: + - speaker.play: [0x10, 0x20, 0x30, 0x40] + - speaker.play: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; + i2s_audio: i2s_lrclk_pin: ${i2s_bclk_pin} i2s_bclk_pin: ${i2s_lrclk_pin} From a239460724a9a6787f69b9061f3252be5b313ed2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 22:48:50 -0600 Subject: [PATCH 263/461] [remote_base] Optimize abbwelcome action memory usage - store static data in flash --- esphome/components/remote_base/__init__.py | 6 ++-- .../remote_base/abbwelcome_protocol.h | 31 +++++++++++++------ .../remote_transmitter/common-buttons.yaml | 25 +++++++++++++++ 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 8d735ea563a..d24d24b0007 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -39,7 +39,7 @@ from esphome.const import ( CONF_WAND_ID, CONF_ZERO, ) -from esphome.core import coroutine +from esphome.core import ID, coroutine from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.util import Registry, SimpleRegistry @@ -2104,7 +2104,9 @@ async def abbwelcome_action(var, config, args): ) cg.add(var.set_data_template(template_)) else: - cg.add(var.set_data_static(data_)) + arr_id = ID(f"{var.base}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data_)) + cg.add(var.set_data_static(arr, len(data_))) # Mirage diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index b258bd920b3..2ed90c502e6 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -214,10 +214,14 @@ template class ABBWelcomeAction : public RemoteTransmitterAction TEMPLATABLE_VALUE(uint8_t, message_type) TEMPLATABLE_VALUE(uint8_t, message_id) TEMPLATABLE_VALUE(bool, auto_message_id) - void set_data_static(std::vector data) { data_static_ = std::move(data); } - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - has_data_func_ = true; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->static_ = false; + } + void set_data_static(const uint8_t *data, size_t len) { + this->data_.static_data.ptr = data; + this->data_.static_data.len = len; + this->static_ = true; } void encode(RemoteTransmitData *dst, Ts... x) override { ABBWelcomeData data; @@ -228,19 +232,26 @@ template class ABBWelcomeAction : public RemoteTransmitterAction data.set_message_type(this->message_type_.value(x...)); data.set_message_id(this->message_id_.value(x...)); data.auto_message_id = this->auto_message_id_.value(x...); - if (has_data_func_) { - data.set_data(this->data_func_(x...)); + std::vector data_vec; + if (this->static_) { + data_vec.assign(this->data_.static_data.ptr, this->data_.static_data.ptr + this->data_.static_data.len); } else { - data.set_data(this->data_static_); + data_vec = this->data_.func(x...); } + data.set_data(data_vec); data.finalize(); ABBWelcomeProtocol().encode(dst, data); } protected: - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; - bool has_data_func_{false}; + bool static_{true}; + union Data { + std::vector (*func)(Ts...); + struct { + const uint8_t *ptr; + size_t len; + } static_data; + } data_; }; } // namespace remote_base diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index e9593cc97ce..101d60a8932 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -1,3 +1,11 @@ +number: + - platform: template + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + button: - platform: template name: Beo4 audio mute @@ -217,6 +225,23 @@ button: command: 0xEC rc_code_1: 0x0D rc_code_2: 0x0D + - platform: template + name: ABBWelcome static + on_press: + remote_transmitter.transmit_abbwelcome: + source_address: 0x1234 + destination_address: 0x5678 + message_type: 0x01 + data: [0x10, 0x20, 0x30] + - platform: template + name: ABBWelcome lambda + on_press: + remote_transmitter.transmit_abbwelcome: + source_address: 0x1234 + destination_address: 0x5678 + message_type: 0x01 + data: !lambda |- + return {(uint8_t)id(test_number).state, 0x20, 0x30}; - platform: template name: Digital Write on_press: From d29882e4ad78d504d46cd46b44785c41adff0986 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 22:51:11 -0600 Subject: [PATCH 264/461] Add additonal abbwelcome remote_base tests --- .../remote_transmitter/common-buttons.yaml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index e9593cc97ce..101d60a8932 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -1,3 +1,11 @@ +number: + - platform: template + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + button: - platform: template name: Beo4 audio mute @@ -217,6 +225,23 @@ button: command: 0xEC rc_code_1: 0x0D rc_code_2: 0x0D + - platform: template + name: ABBWelcome static + on_press: + remote_transmitter.transmit_abbwelcome: + source_address: 0x1234 + destination_address: 0x5678 + message_type: 0x01 + data: [0x10, 0x20, 0x30] + - platform: template + name: ABBWelcome lambda + on_press: + remote_transmitter.transmit_abbwelcome: + source_address: 0x1234 + destination_address: 0x5678 + message_type: 0x01 + data: !lambda |- + return {(uint8_t)id(test_number).state, 0x20, 0x30}; - platform: template name: Digital Write on_press: From 5b8827d47acec9732a191d5b2f900610ad7fcf92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 23:07:43 -0600 Subject: [PATCH 265/461] [remote_base] Optimize raw transmit action memory usage - use function pointers --- esphome/components/remote_base/raw_protocol.h | 29 ++++++++++++------- .../remote_transmitter/common-buttons.yaml | 16 +++++++++- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/esphome/components/remote_base/raw_protocol.h b/esphome/components/remote_base/raw_protocol.h index 9b671e611f4..4f50a859088 100644 --- a/esphome/components/remote_base/raw_protocol.h +++ b/esphome/components/remote_base/raw_protocol.h @@ -42,17 +42,21 @@ class RawTrigger : public Trigger, public Component, public RemoteRe template class RawAction : public RemoteTransmitterActionBase { public: - void set_code_template(std::function func) { this->code_func_ = func; } + void set_code_template(RawTimings (*func)(Ts...)) { + this->code_.func = func; + this->static_ = false; + } void set_code_static(const int32_t *code, size_t len) { - this->code_static_ = code; - this->code_static_len_ = len; + this->code_.static_code.data = code; + this->code_.static_code.len = len; + this->static_ = true; } TEMPLATABLE_VALUE(uint32_t, carrier_frequency); void encode(RemoteTransmitData *dst, Ts... x) override { - if (this->code_static_ != nullptr) { - for (size_t i = 0; i < this->code_static_len_; i++) { - auto val = this->code_static_[i]; + if (this->static_) { + for (size_t i = 0; i < this->code_.static_code.len; i++) { + auto val = this->code_.static_code.data[i]; if (val < 0) { dst->space(static_cast(-val)); } else { @@ -60,15 +64,20 @@ template class RawAction : public RemoteTransmitterActionBaseset_data(this->code_func_(x...)); + dst->set_data(this->code_.func(x...)); } dst->set_carrier_frequency(this->carrier_frequency_.value(x...)); } protected: - std::function code_func_{nullptr}; - const int32_t *code_static_{nullptr}; - int32_t code_static_len_{0}; + bool static_{true}; + union Code { + RawTimings (*func)(Ts...); + struct { + const int32_t *data; + size_t len; + } static_code; + } code_; }; class RawDumper : public RemoteReceiverDumperBase { diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index e9593cc97ce..cab28d813bb 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -1,3 +1,11 @@ +number: + - platform: template + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + button: - platform: template name: Beo4 audio mute @@ -128,10 +136,16 @@ button: address: 0x00 command: 0x0B - platform: template - name: RC5 Raw + name: RC5 Raw static on_press: remote_transmitter.transmit_raw: code: [1000, -1000] + - platform: template + name: RC5 Raw lambda + on_press: + remote_transmitter.transmit_raw: + code: !lambda |- + return {(int32_t)id(test_number).state * 100, -1000}; - platform: template name: AEHA id: eaha_hitachi_climate_power_on From 353ea5674dd2243f4e0bfae2958c2da14d551665 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 23:09:31 -0600 Subject: [PATCH 266/461] Add additional tests for remote_transmitter raw --- .../remote_transmitter/common-buttons.yaml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index e9593cc97ce..cab28d813bb 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -1,3 +1,11 @@ +number: + - platform: template + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + button: - platform: template name: Beo4 audio mute @@ -128,10 +136,16 @@ button: address: 0x00 command: 0x0B - platform: template - name: RC5 Raw + name: RC5 Raw static on_press: remote_transmitter.transmit_raw: code: [1000, -1000] + - platform: template + name: RC5 Raw lambda + on_press: + remote_transmitter.transmit_raw: + code: !lambda |- + return {(int32_t)id(test_number).state * 100, -1000}; - platform: template name: AEHA id: eaha_hitachi_climate_power_on From 59485c1d2b9090dfe2e30ff41e8111ad0466a6e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 23:14:57 -0600 Subject: [PATCH 267/461] save 4 bytes --- esphome/components/remote_base/raw_protocol.h | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/esphome/components/remote_base/raw_protocol.h b/esphome/components/remote_base/raw_protocol.h index 4f50a859088..f59431c88ad 100644 --- a/esphome/components/remote_base/raw_protocol.h +++ b/esphome/components/remote_base/raw_protocol.h @@ -44,19 +44,18 @@ template class RawAction : public RemoteTransmitterActionBasecode_.func = func; - this->static_ = false; + this->len_ = -1; } void set_code_static(const int32_t *code, size_t len) { - this->code_.static_code.data = code; - this->code_.static_code.len = len; - this->static_ = true; + this->code_.data = code; + this->len_ = len; } TEMPLATABLE_VALUE(uint32_t, carrier_frequency); void encode(RemoteTransmitData *dst, Ts... x) override { - if (this->static_) { - for (size_t i = 0; i < this->code_.static_code.len; i++) { - auto val = this->code_.static_code.data[i]; + if (this->len_ >= 0) { + for (size_t i = 0; i < static_cast(this->len_); i++) { + auto val = this->code_.data[i]; if (val < 0) { dst->space(static_cast(-val)); } else { @@ -70,13 +69,10 @@ template class RawAction : public RemoteTransmitterActionBase Date: Sat, 8 Nov 2025 23:20:27 -0600 Subject: [PATCH 268/461] optimize --- esphome/components/uart/automation.h | 29 +++++++++++++--------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index 7a3344c2f17..c2eb308eb8d 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -12,36 +12,33 @@ template class UARTWriteAction : public Action, public Pa public: void set_data_template(std::vector (*func)(Ts...)) { // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers - this->data_.func = func; - this->static_ = false; + this->code_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } // Store pointer to static data in flash (no RAM copy) void set_data_static(const uint8_t *data, size_t len) { - // Simply set pointer and length - no construction needed for POD types - this->data_.static_data.ptr = data; - this->data_.static_data.len = len; - this->static_ = true; + this->code_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void play(const Ts &...x) override { - if (this->static_) { - this->parent_->write_array(this->data_.static_data.ptr, this->data_.static_data.len); + if (this->len_ >= 0) { + // Static mode: use pointer and length + this->parent_->write_array(this->code_.data, static_cast(this->len_)); } else { - auto val = this->data_.func(x...); + // Template mode: call function + auto val = this->code_.func(x...); this->parent_->write_array(val); } } protected: - bool static_{true}; // Default to static mode (most common case) - union Data { + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Code { std::vector (*func)(Ts...); // Function pointer (stateless lambdas) - struct { - const uint8_t *ptr; - size_t len; - } static_data; - } data_; + const uint8_t *data; // Pointer to static data in flash + } code_; }; } // namespace uart From db8b96f257685808ddd61aedebbc72c529552e17 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 23:21:57 -0600 Subject: [PATCH 269/461] tweak --- esphome/components/remote_base/raw_protocol.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/remote_base/raw_protocol.h b/esphome/components/remote_base/raw_protocol.h index f59431c88ad..941b6aab426 100644 --- a/esphome/components/remote_base/raw_protocol.h +++ b/esphome/components/remote_base/raw_protocol.h @@ -44,11 +44,11 @@ template class RawAction : public RemoteTransmitterActionBasecode_.func = func; - this->len_ = -1; + this->len_ = -1; // Sentinel value indicates template mode } void set_code_static(const int32_t *code, size_t len) { this->code_.data = code; - this->len_ = len; + this->len_ = len; // Length >= 0 indicates static mode } TEMPLATABLE_VALUE(uint32_t, carrier_frequency); @@ -69,7 +69,7 @@ template class RawAction : public RemoteTransmitterActionBase=0 = static mode with length union Code { RawTimings (*func)(Ts...); const int32_t *data; From 729304af01898a319f643417b257ad049409d1d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 23:28:23 -0600 Subject: [PATCH 270/461] optimize --- esphome/components/ble_client/automation.h | 26 ++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 46915be9edb..9c5646b3d19 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -107,15 +107,14 @@ template class BLEClientWriteAction : public Action, publ void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_value_template(std::vector (*func)(Ts...)) { - this->value_.template_func = func; - this->has_simple_value_ = false; + this->value_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } // Store pointer to static data in flash (no RAM copy) void set_value_simple(const uint8_t *data, size_t len) { - this->value_.simple_data.ptr = data; - this->value_.simple_data.len = len; - this->has_simple_value_ = true; + this->value_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void play(const Ts &...x) override {} @@ -124,10 +123,12 @@ template class BLEClientWriteAction : public Action, publ this->num_running_++; this->var_ = std::make_tuple(x...); std::vector value; - if (this->has_simple_value_) { - value.assign(this->value_.simple_data.ptr, this->value_.simple_data.ptr + this->value_.simple_data.len); + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + value.assign(this->value_.data, this->value_.data + this->len_); } else { - value = this->value_.template_func(x...); + // Template mode: call function + value = this->value_.func(x...); } // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. if (!write(value)) @@ -202,13 +203,10 @@ template class BLEClientWriteAction : public Action, publ private: BLEClient *ble_client_; - bool has_simple_value_{true}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Value { - std::vector (*template_func)(Ts...); - struct { - const uint8_t *ptr; - size_t len; - } simple_data; + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash } value_; espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID char_uuid_; From 845fae77166454c011e782967a3d9f622475be3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 23:30:53 -0600 Subject: [PATCH 271/461] optimize --- esphome/components/canbus/canbus.h | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 122ccfe39e0..f7b84111bd5 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -115,14 +115,13 @@ template class CanbusSendAction : public Action, public P void set_data_template(std::vector (*func)(Ts...)) { // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers this->data_.func = func; - this->static_ = false; + this->len_ = -1; // Sentinel value indicates template mode } // Store pointer to static data in flash (no RAM copy) void set_data_static(const uint8_t *data, size_t len) { - this->data_.static_data.ptr = data; - this->data_.static_data.len = len; - this->static_ = true; + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } @@ -138,9 +137,11 @@ template class CanbusSendAction : public Action, public P auto use_extended_id = this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; std::vector data; - if (this->static_) { - data.assign(this->data_.static_data.ptr, this->data_.static_data.ptr + this->data_.static_data.len); + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); } else { + // Template mode: call function data = this->data_.func(x...); } this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data); @@ -150,14 +151,11 @@ template class CanbusSendAction : public Action, public P optional can_id_{}; optional use_extended_id_{}; bool remote_transmission_request_{false}; - bool static_{true}; // Default to static mode (most common case) + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Data { - std::vector (*func)(Ts...); // 4 bytes on 32-bit - struct { - const uint8_t *ptr; // 4 bytes on 32-bit - size_t len; // 4 bytes on 32-bit - } static_data; // 8 bytes total on 32-bit - } data_; // Union size = 8 bytes (max of 4 and 8) + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; class CanbusTrigger : public Trigger, uint32_t, bool>, public Component { From 21d0c8b54903fdaf74f402acd25bee8d6e7795b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 23:36:06 -0600 Subject: [PATCH 272/461] optimize --- esphome/components/sx126x/automation.h | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/sx126x/automation.h b/esphome/components/sx126x/automation.h index 41f0a888e54..2282c583cbf 100644 --- a/esphome/components/sx126x/automation.h +++ b/esphome/components/sx126x/automation.h @@ -16,33 +16,31 @@ template class SendPacketAction : public Action, public P public: void set_data_template(std::vector (*func)(Ts...)) { this->data_.func = func; - this->static_ = false; + this->len_ = -1; // Sentinel value indicates template mode } void set_data_static(const uint8_t *data, size_t len) { - this->data_.static_data.ptr = data; - this->data_.static_data.len = len; - this->static_ = true; + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void play(const Ts &...x) override { std::vector data; - if (this->static_) { - data.assign(this->data_.static_data.ptr, this->data_.static_data.ptr + this->data_.static_data.len); + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); } else { + // Template mode: call function data = this->data_.func(x...); } this->parent_->transmit_packet(data); } protected: - bool static_{true}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Data { - std::vector (*func)(Ts...); - struct { - const uint8_t *ptr; - size_t len; - } static_data; + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash } data_; }; From bdaeb2cf2e1235c6a69902440853b28d45402a7d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 23:39:39 -0600 Subject: [PATCH 273/461] optimize --- esphome/components/sx127x/automation.h | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/sx127x/automation.h b/esphome/components/sx127x/automation.h index 52dbf37e09e..fb0367fcca0 100644 --- a/esphome/components/sx127x/automation.h +++ b/esphome/components/sx127x/automation.h @@ -16,33 +16,31 @@ template class SendPacketAction : public Action, public P public: void set_data_template(std::vector (*func)(Ts...)) { this->data_.func = func; - this->static_ = false; + this->len_ = -1; // Sentinel value indicates template mode } void set_data_static(const uint8_t *data, size_t len) { - this->data_.static_data.ptr = data; - this->data_.static_data.len = len; - this->static_ = true; + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void play(const Ts &...x) override { std::vector data; - if (this->static_) { - data.assign(this->data_.static_data.ptr, this->data_.static_data.ptr + this->data_.static_data.len); + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); } else { + // Template mode: call function data = this->data_.func(x...); } this->parent_->transmit_packet(data); } protected: - bool static_{true}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Data { - std::vector (*func)(Ts...); - struct { - const uint8_t *ptr; - size_t len; - } static_data; + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash } data_; }; From c16cd3bab5e2da864099dbf01bf400302d5ec676 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 23:41:24 -0600 Subject: [PATCH 274/461] optimize --- esphome/components/udp/automation.h | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/udp/automation.h b/esphome/components/udp/automation.h index a3b76fb4ea3..b66c2a9892a 100644 --- a/esphome/components/udp/automation.h +++ b/esphome/components/udp/automation.h @@ -13,32 +13,30 @@ template class UDPWriteAction : public Action, public Par public: void set_data_template(std::vector (*func)(Ts...)) { this->data_.func = func; - this->static_ = false; + this->len_ = -1; // Sentinel value indicates template mode } void set_data_static(const uint8_t *data, size_t len) { - this->data_.static_data.ptr = data; - this->data_.static_data.len = len; - this->static_ = true; + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void play(const Ts &...x) override { - if (this->static_) { - this->parent_->send_packet(this->data_.static_data.ptr, this->data_.static_data.len); + if (this->len_ >= 0) { + // Static mode: pass pointer directly to send_packet(const uint8_t *, size_t) + this->parent_->send_packet(this->data_.data, static_cast(this->len_)); } else { + // Template mode: call function and pass vector to send_packet(const std::vector &) auto val = this->data_.func(x...); this->parent_->send_packet(val); } } protected: - bool static_{true}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Data { - std::vector (*func)(Ts...); - struct { - const uint8_t *ptr; - size_t len; - } static_data; + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash } data_; }; From 9abef44ac035a0ce8865d5b77c8e52aabceb8441 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 23:44:11 -0600 Subject: [PATCH 275/461] optimize --- esphome/components/speaker/automation.h | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index cae429cf8a3..391c9e4c627 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -12,32 +12,30 @@ template class PlayAction : public Action, public Parente public: void set_data_template(std::vector (*func)(Ts...)) { this->data_.func = func; - this->static_ = false; + this->len_ = -1; // Sentinel value indicates template mode } void set_data_static(const uint8_t *data, size_t len) { - this->data_.static_data.ptr = data; - this->data_.static_data.len = len; - this->static_ = true; + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void play(const Ts &...x) override { - if (this->static_) { - this->parent_->play(this->data_.static_data.ptr, this->data_.static_data.len); + if (this->len_ >= 0) { + // Static mode: pass pointer directly to play(const uint8_t *, size_t) + this->parent_->play(this->data_.data, static_cast(this->len_)); } else { + // Template mode: call function and pass vector to play(const std::vector &) auto val = this->data_.func(x...); this->parent_->play(val); } } protected: - bool static_{true}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Data { - std::vector (*func)(Ts...); - struct { - const uint8_t *ptr; - size_t len; - } static_data; + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash } data_; }; From ff04a6da4b9acc11d6d28752508f05c266a00a77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Nov 2025 23:45:42 -0600 Subject: [PATCH 276/461] optimize --- .../remote_base/abbwelcome_protocol.h | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index 2ed90c502e6..4b922eb2f16 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -216,12 +216,11 @@ template class ABBWelcomeAction : public RemoteTransmitterAction TEMPLATABLE_VALUE(bool, auto_message_id) void set_data_template(std::vector (*func)(Ts...)) { this->data_.func = func; - this->static_ = false; + this->len_ = -1; // Sentinel value indicates template mode } void set_data_static(const uint8_t *data, size_t len) { - this->data_.static_data.ptr = data; - this->data_.static_data.len = len; - this->static_ = true; + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void encode(RemoteTransmitData *dst, Ts... x) override { ABBWelcomeData data; @@ -233,9 +232,11 @@ template class ABBWelcomeAction : public RemoteTransmitterAction data.set_message_id(this->message_id_.value(x...)); data.auto_message_id = this->auto_message_id_.value(x...); std::vector data_vec; - if (this->static_) { - data_vec.assign(this->data_.static_data.ptr, this->data_.static_data.ptr + this->data_.static_data.len); + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data_vec.assign(this->data_.data, this->data_.data + this->len_); } else { + // Template mode: call function data_vec = this->data_.func(x...); } data.set_data(data_vec); @@ -244,13 +245,10 @@ template class ABBWelcomeAction : public RemoteTransmitterAction } protected: - bool static_{true}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Data { - std::vector (*func)(Ts...); - struct { - const uint8_t *ptr; - size_t len; - } static_data; + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash } data_; }; From a290b88cd6fa86566068370b3ee7e0723ecdd967 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:09:03 -0600 Subject: [PATCH 277/461] Expand uart.write tests (#11785) --- tests/components/uart/test.esp32-idf.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml index 9744a484096..6ffd0d72824 100644 --- a/tests/components/uart/test.esp32-idf.yaml +++ b/tests/components/uart/test.esp32-idf.yaml @@ -3,6 +3,8 @@ esphome: then: - uart.write: 'Hello World' - uart.write: [0x00, 0x20, 0x42] + - uart.write: !lambda |- + return {0xAA, 0xBB, 0xCC}; uart: - id: uart_uart @@ -46,6 +48,15 @@ switch: turn_on: "TURN_ON" turn_off: "TURN_OFF" +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: # Test uart button with array data - platform: uart @@ -57,3 +68,10 @@ button: name: "UART Button String" uart_id: uart_uart data: "BUTTON_PRESS" + # Test uart button with lambda (function pointer) + - platform: template + name: "UART Lambda Test" + on_press: + - uart.write: !lambda |- + std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n"; + return std::vector(cmd.begin(), cmd.end()); From b49619d9bf17eef24f6029af9d2dfa9c5be722a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:09:25 -0600 Subject: [PATCH 278/461] Add ble_client lambda compile tests (#11787) --- tests/components/ble_client/common.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/components/ble_client/common.yaml b/tests/components/ble_client/common.yaml index aa4b6394635..4ea1dd60f38 100644 --- a/tests/components/ble_client/common.yaml +++ b/tests/components/ble_client/common.yaml @@ -52,3 +52,25 @@ sensor: name: "BLE Sensor without Lambda" service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" characteristic_uuid: "abcd1237-abcd-1234-abcd-abcd12345678" + +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + +button: + # Test ble_write with lambda that references a component (function pointer) + - platform: template + name: "BLE Write Lambda Test" + on_press: + - ble_client.ble_write: + id: test_blec + service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" + characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678" + value: !lambda |- + uint8_t val = (uint8_t)id(test_number).state; + return std::vector{0xAA, val, 0xBB}; From 783dbd1e6b6f462317ed5881df871c87903ea442 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:09:46 -0600 Subject: [PATCH 279/461] Add additional compile time tests for canbus (#11789) --- tests/components/canbus/common.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/components/canbus/common.yaml b/tests/components/canbus/common.yaml index fd146cc3a30..8bddeb74094 100644 --- a/tests/components/canbus/common.yaml +++ b/tests/components/canbus/common.yaml @@ -37,6 +37,15 @@ canbus: break; } +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + button: - platform: template name: Canbus Actions @@ -44,3 +53,7 @@ button: - canbus.send: "abc" - canbus.send: [0, 1, 2] - canbus.send: !lambda return {0, 1, 2}; + # Test canbus.send with lambda that references a component (function pointer) + - canbus.send: !lambda |- + uint8_t val = (uint8_t)id(test_number).state; + return std::vector{0xAA, val, 0xBB}; From 4c078dea2c035fec594c1abfed51c0d03e51de2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:10:31 -0600 Subject: [PATCH 280/461] Add additional sx126x lambda tests (#11791) --- tests/components/sx126x/common.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/components/sx126x/common.yaml b/tests/components/sx126x/common.yaml index 3f540a4baed..659550cc01d 100644 --- a/tests/components/sx126x/common.yaml +++ b/tests/components/sx126x/common.yaml @@ -26,6 +26,15 @@ sx126x: - lambda: |- ESP_LOGD("lambda", "packet %.2f %.2f %s", rssi, snr, format_hex(x).c_str()); +number: + - platform: template + name: "SX126x Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: - platform: template name: "SX126x Button" @@ -37,3 +46,5 @@ button: - sx126x.set_mode_rx - sx126x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] + - sx126x.send_packet: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; From e468ca48812ef2ab649ecc4f8649da6975dabc4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:11:31 -0600 Subject: [PATCH 281/461] Add additional sx127x lambda tests (#11793) --- tests/components/sx127x/common.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/components/sx127x/common.yaml b/tests/components/sx127x/common.yaml index 540381fc083..6e48952fcca 100644 --- a/tests/components/sx127x/common.yaml +++ b/tests/components/sx127x/common.yaml @@ -26,6 +26,15 @@ sx127x: - sx127x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] +number: + - platform: template + name: "SX127x Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: - platform: template name: "SX127x Button" @@ -38,3 +47,5 @@ button: - sx127x.set_mode_rx - sx127x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] + - sx127x.send_packet: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; From 55853552637893cbb4ca4e8be25f2314b6614b21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:15:50 -0600 Subject: [PATCH 282/461] Add additional speaker lambda tests (#11797) --- tests/components/speaker/common.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/components/speaker/common.yaml b/tests/components/speaker/common.yaml index c04674ee29c..fa54fa7e396 100644 --- a/tests/components/speaker/common.yaml +++ b/tests/components/speaker/common.yaml @@ -1,3 +1,12 @@ +number: + - platform: template + name: "Speaker Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + esphome: on_boot: then: @@ -14,6 +23,15 @@ esphome: - speaker.finish: - speaker.stop: +button: + - platform: template + name: "Speaker Button" + on_press: + then: + - speaker.play: [0x10, 0x20, 0x30, 0x40] + - speaker.play: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; + i2s_audio: i2s_lrclk_pin: ${i2s_bclk_pin} i2s_bclk_pin: ${i2s_lrclk_pin} From eb0558ca3fb71c10516c96f366d2880f1e325717 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:16:09 -0600 Subject: [PATCH 283/461] Add additional udp lambda tests (#11795) --- tests/components/udp/common.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml index 96224d0d1f1..98546d49ef5 100644 --- a/tests/components/udp/common.yaml +++ b/tests/components/udp/common.yaml @@ -17,3 +17,22 @@ udp: id: my_udp data: !lambda |- return std::vector{1,3,4,5,6}; + +number: + - platform: template + name: "UDP Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +button: + - platform: template + name: "UDP Button" + on_press: + then: + - udp.write: + data: [0x01, 0x02, 0x03] + - udp.write: !lambda |- + return {0x10, 0x20, (uint8_t)id(my_number).state}; From f7179d42557b926f1f30adc1c4a6acd433491715 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:16:53 -0600 Subject: [PATCH 284/461] Add additonal abbwelcome remote_base tests (#11799) --- .../remote_transmitter/common-buttons.yaml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index e9593cc97ce..101d60a8932 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -1,3 +1,11 @@ +number: + - platform: template + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + button: - platform: template name: Beo4 audio mute @@ -217,6 +225,23 @@ button: command: 0xEC rc_code_1: 0x0D rc_code_2: 0x0D + - platform: template + name: ABBWelcome static + on_press: + remote_transmitter.transmit_abbwelcome: + source_address: 0x1234 + destination_address: 0x5678 + message_type: 0x01 + data: [0x10, 0x20, 0x30] + - platform: template + name: ABBWelcome lambda + on_press: + remote_transmitter.transmit_abbwelcome: + source_address: 0x1234 + destination_address: 0x5678 + message_type: 0x01 + data: !lambda |- + return {(uint8_t)id(test_number).state, 0x20, 0x30}; - platform: template name: Digital Write on_press: From 5f9c7a70ff8721d0f80c739827810e166b870cb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:17:14 -0600 Subject: [PATCH 285/461] Add additional tests for remote_transmitter raw (#11801) --- tests/components/remote_transmitter/common-buttons.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index 101d60a8932..d48d36bd548 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -136,10 +136,16 @@ button: address: 0x00 command: 0x0B - platform: template - name: RC5 Raw + name: RC5 Raw static on_press: remote_transmitter.transmit_raw: code: [1000, -1000] + - platform: template + name: RC5 Raw lambda + on_press: + remote_transmitter.transmit_raw: + code: !lambda |- + return {(int32_t)id(test_number).state * 100, -1000}; - platform: template name: AEHA id: eaha_hitachi_climate_power_on From 870b2c4f848b6bc0ad123cb907a7012300db02e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:21:25 -0600 Subject: [PATCH 286/461] [ble_client] Optimize ble_write memory usage - store static data in flash (#11786) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ble_client/__init__.py | 8 +++- esphome/components/ble_client/automation.h | 43 +++++++++------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 768a345213d..37db181584b 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VALUE, ) +from esphome.core import ID AUTO_LOAD = ["esp32_ble_client"] CODEOWNERS = ["@buxtronix", "@clydebarrow"] @@ -198,7 +199,12 @@ async def ble_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(value, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_value_template(templ)) else: - cg.add(var.set_value_simple(value)) + # Generate static array in flash to avoid RAM copy + if isinstance(value, bytes): + value = list(value) + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*value)) + cg.add(var.set_value_simple(arr, len(value))) if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): cg.add( diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index ce534501f39..9c5646b3d19 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -96,11 +96,8 @@ template class BLEClientWriteAction : public Action, publ BLEClientWriteAction(BLEClient *ble_client) { ble_client->register_ble_node(this); ble_client_ = ble_client; - this->construct_simple_value_(); } - ~BLEClientWriteAction() { this->destroy_simple_value_(); } - void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } @@ -110,17 +107,14 @@ template class BLEClientWriteAction : public Action, publ void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_value_template(std::vector (*func)(Ts...)) { - this->destroy_simple_value_(); - this->value_.template_func = func; - this->has_simple_value_ = false; + this->value_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_value_simple(const std::vector &value) { - if (!this->has_simple_value_) { - this->construct_simple_value_(); - } - this->value_.simple = value; - this->has_simple_value_ = true; + // Store pointer to static data in flash (no RAM copy) + void set_value_simple(const uint8_t *data, size_t len) { + this->value_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void play(const Ts &...x) override {} @@ -128,7 +122,14 @@ template class BLEClientWriteAction : public Action, publ void play_complex(const Ts &...x) override { this->num_running_++; this->var_ = std::make_tuple(x...); - auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...); + std::vector value; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + value.assign(this->value_.data, this->value_.data + this->len_); + } else { + // Template mode: call function + value = this->value_.func(x...); + } // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. if (!write(value)) this->play_next_(x...); @@ -201,21 +202,11 @@ template class BLEClientWriteAction : public Action, publ } private: - void construct_simple_value_() { new (&this->value_.simple) std::vector(); } - - void destroy_simple_value_() { - if (this->has_simple_value_) { - this->value_.simple.~vector(); - } - } - BLEClient *ble_client_; - bool has_simple_value_ = true; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Value { - std::vector simple; - std::vector (*template_func)(Ts...); - Value() {} // trivial constructor - ~Value() {} // trivial destructor - we manage lifetime via discriminator + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash } value_; espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID char_uuid_; From 3bcbfe8d9756ef97a47dff0ed4e641919022b0fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:22:15 -0600 Subject: [PATCH 287/461] [canbus] Optimize canbus.send memory usage - store static data in flash (#11788) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/canbus/__init__.py | 7 ++++-- esphome/components/canbus/canbus.h | 34 +++++++++++++++++---------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index e1de1eb2f21..7b51c2c45c1 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -4,7 +4,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID -from esphome.core import CORE +from esphome.core import CORE, ID CODEOWNERS = ["@mvturnho", "@danielschramm"] IS_PLATFORM_COMPONENT = True @@ -176,5 +176,8 @@ async def canbus_action_to_code(config, action_id, template_arg, args): else: if isinstance(data, bytes): data = [int(x) for x in data] - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 029eb278c0b..f7b84111bd5 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -112,13 +112,16 @@ class Canbus : public Component { template class CanbusSendAction : public Action, public Parented { public: - void set_data_template(const std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; + void set_data_template(std::vector (*func)(Ts...)) { + // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + + // Store pointer to static data in flash (no RAM copy) + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } @@ -133,21 +136,26 @@ template class CanbusSendAction : public Action, public P auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; auto use_extended_id = this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; - if (this->static_) { - this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, this->data_static_); + std::vector data; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); } else { - auto val = this->data_func_(x...); - this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, val); + // Template mode: call function + data = this->data_.func(x...); } + this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data); } protected: optional can_id_{}; optional use_extended_id_{}; bool remote_transmission_request_{false}; - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; class CanbusTrigger : public Trigger, uint32_t, bool>, public Component { From 26a3ec41d6037f44b4479ee35983aab9f789688f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:23:33 -0600 Subject: [PATCH 288/461] [sx126x] Optimize send_packet action memory usage - store static data in flash (#11790) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/sx126x/__init__.py | 7 ++++-- esphome/components/sx126x/automation.h | 30 +++++++++++++++----------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 370cd102d49..f8f3b9d1040 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -3,7 +3,7 @@ import esphome.codegen as cg from esphome.components import spi import esphome.config_validation as cv from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID -from esphome.core import TimePeriod +from esphome.core import ID, TimePeriod MULTI_CONF = True CODEOWNERS = ["@swoboda1337"] @@ -329,5 +329,8 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/sx126x/automation.h b/esphome/components/sx126x/automation.h index 6b2371e2539..2282c583cbf 100644 --- a/esphome/components/sx126x/automation.h +++ b/esphome/components/sx126x/automation.h @@ -14,28 +14,34 @@ template class RunImageCalAction : public Action, public template class SendPacketAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void play(const Ts &...x) override { - if (this->static_) { - this->parent_->transmit_packet(this->data_static_); + std::vector data; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); } else { - this->parent_->transmit_packet(this->data_func_(x...)); + // Template mode: call function + data = this->data_.func(x...); } + this->parent_->transmit_packet(data); } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; template class SetModeTxAction : public Action, public Parented { From 77ab096b59311c49679899b3f0c3b56e02e50dd6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:25:16 -0600 Subject: [PATCH 289/461] [remote_base] Optimize raw transmit action memory usage - use function pointers (#11800) --- esphome/components/remote_base/raw_protocol.h | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/esphome/components/remote_base/raw_protocol.h b/esphome/components/remote_base/raw_protocol.h index 9b671e611f4..941b6aab426 100644 --- a/esphome/components/remote_base/raw_protocol.h +++ b/esphome/components/remote_base/raw_protocol.h @@ -42,17 +42,20 @@ class RawTrigger : public Trigger, public Component, public RemoteRe template class RawAction : public RemoteTransmitterActionBase { public: - void set_code_template(std::function func) { this->code_func_ = func; } + void set_code_template(RawTimings (*func)(Ts...)) { + this->code_.func = func; + this->len_ = -1; // Sentinel value indicates template mode + } void set_code_static(const int32_t *code, size_t len) { - this->code_static_ = code; - this->code_static_len_ = len; + this->code_.data = code; + this->len_ = len; // Length >= 0 indicates static mode } TEMPLATABLE_VALUE(uint32_t, carrier_frequency); void encode(RemoteTransmitData *dst, Ts... x) override { - if (this->code_static_ != nullptr) { - for (size_t i = 0; i < this->code_static_len_; i++) { - auto val = this->code_static_[i]; + if (this->len_ >= 0) { + for (size_t i = 0; i < static_cast(this->len_); i++) { + auto val = this->code_.data[i]; if (val < 0) { dst->space(static_cast(-val)); } else { @@ -60,15 +63,17 @@ template class RawAction : public RemoteTransmitterActionBaseset_data(this->code_func_(x...)); + dst->set_data(this->code_.func(x...)); } dst->set_carrier_frequency(this->carrier_frequency_.value(x...)); } protected: - std::function code_func_{nullptr}; - const int32_t *code_static_{nullptr}; - int32_t code_static_len_{0}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Code { + RawTimings (*func)(Ts...); + const int32_t *data; + } code_; }; class RawDumper : public RemoteReceiverDumperBase { From 7705a5de063a80b763cb81104142fb3371410f53 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:25:40 -0600 Subject: [PATCH 290/461] [sx127x] Optimize send_packet action memory usage - store static data in flash (#11792) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/sx127x/__init__.py | 6 +++++- esphome/components/sx127x/automation.h | 30 +++++++++++++++----------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index 33b556db078..77cb61f7f81 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import spi import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID +from esphome.core import ID MULTI_CONF = True CODEOWNERS = ["@swoboda1337"] @@ -321,5 +322,8 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/sx127x/automation.h b/esphome/components/sx127x/automation.h index eae16c11fa6..fb0367fcca0 100644 --- a/esphome/components/sx127x/automation.h +++ b/esphome/components/sx127x/automation.h @@ -14,28 +14,34 @@ template class RunImageCalAction : public Action, public template class SendPacketAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void play(const Ts &...x) override { - if (this->static_) { - this->parent_->transmit_packet(this->data_static_); + std::vector data; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); } else { - this->parent_->transmit_packet(this->data_func_(x...)); + // Template mode: call function + data = this->data_.func(x...); } + this->parent_->transmit_packet(data); } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; template class SetModeTxAction : public Action, public Parented { From e7ff56f1cd377128c9123a4cb4ca8c6f7f8ca8bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:27:09 -0600 Subject: [PATCH 291/461] [remote_base] Eliminate substr() allocations in Pronto dump logging (#11726) --- .../remote_base/pronto_protocol.cpp | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 35fd7822487..9fbc9e85baf 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -71,6 +71,7 @@ static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0 static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; static const uint16_t PRONTO_DEFAULT_GAP = 45000; static const uint16_t MARK_EXCESS_MICROS = 20; +static constexpr size_t PRONTO_LOG_CHUNK_SIZE = 230; static uint16_t to_frequency_k_hz(uint16_t code) { if (code == 0) @@ -225,18 +226,18 @@ optional ProntoProtocol::decode(RemoteReceiveData src) { } void ProntoProtocol::dump(const ProntoData &data) { - std::string rest; - - rest = data.data; ESP_LOGI(TAG, "Received Pronto: data="); - while (true) { - ESP_LOGI(TAG, "%s", rest.substr(0, 230).c_str()); - if (rest.size() > 230) { - rest = rest.substr(230); - } else { - break; - } - } + + const char *ptr = data.data.c_str(); + size_t remaining = data.data.size(); + + // Log in chunks, always logging at least once (even for empty string) + do { + size_t chunk_size = remaining < PRONTO_LOG_CHUNK_SIZE ? remaining : PRONTO_LOG_CHUNK_SIZE; + ESP_LOGI(TAG, "%.*s", (int) chunk_size, ptr); + ptr += chunk_size; + remaining -= chunk_size; + } while (remaining > 0); } } // namespace remote_base From cbb98c40502be70cb50ffa8ae741dc18be324f3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:27:56 -0600 Subject: [PATCH 292/461] [bl0940] Fix calibration number preference hash for multi-device configs (#11769) --- esphome/components/bl0940/number/calibration_number.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bl0940/number/calibration_number.cpp b/esphome/components/bl0940/number/calibration_number.cpp index cdb26cd2982..e83c3add1fb 100644 --- a/esphome/components/bl0940/number/calibration_number.cpp +++ b/esphome/components/bl0940/number/calibration_number.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number"; void CalibrationNumber::setup() { float value = 0.0f; if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (!this->pref_.load(&value)) { value = 0.0f; } From 8b9600b930d74339a9c47035bc641d147bf0d2e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:33:29 -0600 Subject: [PATCH 293/461] [speaker] Optimize speaker.play action memory usage - store static data in flash (#11796) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/speaker/__init__.py | 7 ++++-- esphome/components/speaker/automation.h | 29 +++++++++++++++---------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 5f1ba94ee60..18e1d9782cc 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -3,7 +3,7 @@ import esphome.codegen as cg from esphome.components import audio, audio_dac import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME -from esphome.core import CORE +from esphome.core import CORE, ID from esphome.coroutine import CoroPriority, coroutine_with_priority AUTO_LOAD = ["audio"] @@ -90,7 +90,10 @@ async def speaker_play_action(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index 80bba250300..391c9e4c627 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -10,28 +10,33 @@ namespace speaker { template class PlayAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void play(const Ts &...x) override { - if (this->static_) { - this->parent_->play(this->data_static_); + if (this->len_ >= 0) { + // Static mode: pass pointer directly to play(const uint8_t *, size_t) + this->parent_->play(this->data_.data, static_cast(this->len_)); } else { - auto val = this->data_func_(x...); + // Template mode: call function and pass vector to play(const std::vector &) + auto val = this->data_.func(x...); this->parent_->play(val); } } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; template class VolumeSetAction : public Action, public Parented { From fb1c67490ad170fd92bdeea357242fae9fb847b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:33:56 -0600 Subject: [PATCH 294/461] [udp] Optimize udp.write action memory usage - store static data in flash (#11794) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/udp/__init__.py | 7 +++++-- esphome/components/udp/automation.h | 29 +++++++++++++++++------------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 6b1e4f8ed87..69abf4b989e 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -12,7 +12,7 @@ from esphome.components.packet_transport import ( ) import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID -from esphome.core import Lambda +from esphome.core import ID, Lambda from esphome.cpp_generator import ExpressionStatement, MockObj CODEOWNERS = ["@clydebarrow"] @@ -158,5 +158,8 @@ async def udp_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/udp/automation.h b/esphome/components/udp/automation.h index c5e5e2eae8c..b66c2a9892a 100644 --- a/esphome/components/udp/automation.h +++ b/esphome/components/udp/automation.h @@ -11,28 +11,33 @@ namespace udp { template class UDPWriteAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void play(const Ts &...x) override { - if (this->static_) { - this->parent_->send_packet(this->data_static_); + if (this->len_ >= 0) { + // Static mode: pass pointer directly to send_packet(const uint8_t *, size_t) + this->parent_->send_packet(this->data_.data, static_cast(this->len_)); } else { - auto val = this->data_func_(x...); + // Template mode: call function and pass vector to send_packet(const std::vector &) + auto val = this->data_.func(x...); this->parent_->send_packet(val); } } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; } // namespace udp From d516627957778427af22ce57c2d5c1294e9948d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:37:14 -0600 Subject: [PATCH 295/461] [uart] Store static data in flash and use function pointers for lambdas (#11784) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/uart/__init__.py | 7 ++++-- esphome/components/uart/automation.h | 35 +++++++++++++++------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index eb911ed0073..cbc11d0db04 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -31,7 +31,7 @@ from esphome.const import ( PLATFORM_HOST, PlatformFramework, ) -from esphome.core import CORE +from esphome.core import CORE, ID import esphome.final_validate as fv from esphome.yaml_util import make_data_base @@ -446,7 +446,10 @@ async def uart_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(cg.ArrayInitializer(*data))) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index ad2c4d2bf1d..c2eb308eb8d 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -10,32 +10,35 @@ namespace uart { template class UARTWriteAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; + void set_data_template(std::vector (*func)(Ts...)) { + // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers + this->code_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_data_static(std::vector &&data) { - this->data_static_ = std::move(data); - this->static_ = true; - } - void set_data_static(std::initializer_list data) { - this->data_static_ = std::vector(data); - this->static_ = true; + + // Store pointer to static data in flash (no RAM copy) + void set_data_static(const uint8_t *data, size_t len) { + this->code_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void play(const Ts &...x) override { - if (this->static_) { - this->parent_->write_array(this->data_static_); + if (this->len_ >= 0) { + // Static mode: use pointer and length + this->parent_->write_array(this->code_.data, static_cast(this->len_)); } else { - auto val = this->data_func_(x...); + // Template mode: call function + auto val = this->code_.func(x...); this->parent_->write_array(val); } } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Code { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } code_; }; } // namespace uart From 7b86e1feb0f71e23dc8a7db9663f8bebb4d8075b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:39:27 -0600 Subject: [PATCH 296/461] [core] Remove deprecated EntityBase::hash_base() method (#11783) --- esphome/core/entity_base.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 1486ff53602..2b52d66f76e 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -129,9 +129,6 @@ class EntityBase { // Returns empty StringRef if object_id is dynamic (needs allocation) StringRef get_object_id_ref_for_api_() const; - /// The hash_base() function has been deprecated. It is kept in this - /// class for now, to prevent external components from not compiling. - virtual uint32_t hash_base() { return 0L; } void calc_object_id_(); /// Check if the object_id is dynamic (changes with MAC suffix) From 0d735dc259b8338b35464ddddca6c1533c331bd4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 16:46:01 -0600 Subject: [PATCH 297/461] [remote_base] Optimize abbwelcome action memory usage - store static data in flash (#11798) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/remote_base/__init__.py | 6 ++-- .../remote_base/abbwelcome_protocol.h | 29 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 8d735ea563a..d24d24b0007 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -39,7 +39,7 @@ from esphome.const import ( CONF_WAND_ID, CONF_ZERO, ) -from esphome.core import coroutine +from esphome.core import ID, coroutine from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.util import Registry, SimpleRegistry @@ -2104,7 +2104,9 @@ async def abbwelcome_action(var, config, args): ) cg.add(var.set_data_template(template_)) else: - cg.add(var.set_data_static(data_)) + arr_id = ID(f"{var.base}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data_)) + cg.add(var.set_data_static(arr, len(data_))) # Mirage diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index b258bd920b3..4b922eb2f16 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -214,10 +214,13 @@ template class ABBWelcomeAction : public RemoteTransmitterAction TEMPLATABLE_VALUE(uint8_t, message_type) TEMPLATABLE_VALUE(uint8_t, message_id) TEMPLATABLE_VALUE(bool, auto_message_id) - void set_data_static(std::vector data) { data_static_ = std::move(data); } - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - has_data_func_ = true; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode + } + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void encode(RemoteTransmitData *dst, Ts... x) override { ABBWelcomeData data; @@ -228,19 +231,25 @@ template class ABBWelcomeAction : public RemoteTransmitterAction data.set_message_type(this->message_type_.value(x...)); data.set_message_id(this->message_id_.value(x...)); data.auto_message_id = this->auto_message_id_.value(x...); - if (has_data_func_) { - data.set_data(this->data_func_(x...)); + std::vector data_vec; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data_vec.assign(this->data_.data, this->data_.data + this->len_); } else { - data.set_data(this->data_static_); + // Template mode: call function + data_vec = this->data_.func(x...); } + data.set_data(data_vec); data.finalize(); ABBWelcomeProtocol().encode(dst, data); } protected: - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; - bool has_data_func_{false}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; } // namespace remote_base From 1dabe83d048601b36d29c8b33be70acc84540a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sun, 9 Nov 2025 23:48:33 +0100 Subject: [PATCH 298/461] [nrf52] api (#11751) --- esphome/components/api/__init__.py | 1 + esphome/components/api/api_connection.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 449572c0e51..7f69a9fda12 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -227,6 +227,7 @@ CONFIG_SCHEMA = cv.All( esp32=8, # More RAM, can buffer more rp2040=5, # Limited RAM bk72xx=8, # Moderate RAM + nrf52=8, # Moderate RAM rtl87xx=8, # Moderate RAM host=16, # Abundant resources ln882x=8, # Moderate RAM diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8c293b41a29..7eb61f08b61 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1467,6 +1467,8 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { static constexpr auto MANUFACTURER = StringRef::from_lit("Beken"); #elif defined(USE_LN882X) static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning"); +#elif defined(USE_NRF52) + static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor"); #elif defined(USE_RTL87XX) static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek"); #elif defined(USE_HOST) From 7abb6d499870ef418b27bbd63e3281aff85eb11c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 17:34:08 -0600 Subject: [PATCH 299/461] [core] Implement Global Controller Registry to reduce RAM usage (#11772) --- .../alarm_control_panel.cpp | 9 +- esphome/components/api/__init__.py | 3 + esphome/components/api/api_server.cpp | 33 +-- esphome/components/api/api_server.h | 14 +- .../binary_sensor/binary_sensor.cpp | 5 + esphome/components/climate/climate.cpp | 5 + esphome/components/cover/cover.cpp | 7 + esphome/components/datetime/date_entity.cpp | 6 +- .../components/datetime/datetime_entity.cpp | 6 +- esphome/components/datetime/time_entity.cpp | 6 +- esphome/components/event/event.cpp | 6 +- esphome/components/fan/fan.cpp | 5 + esphome/components/light/light_state.cpp | 14 +- esphome/components/lock/lock.cpp | 5 + .../components/media_player/media_player.cpp | 10 +- esphome/components/number/number.cpp | 5 + esphome/components/select/select.cpp | 5 + esphome/components/sensor/sensor.cpp | 5 + esphome/components/switch/switch.cpp | 5 + esphome/components/text/text.cpp | 5 + .../components/text_sensor/text_sensor.cpp | 5 + esphome/components/update/update_entity.cpp | 6 +- esphome/components/valve/valve.cpp | 5 + esphome/components/web_server/__init__.py | 3 + esphome/components/web_server/web_server.cpp | 54 +++- esphome/components/web_server/web_server.h | 14 +- esphome/core/__init__.py | 8 + esphome/core/config.py | 17 +- esphome/core/controller.cpp | 134 ---------- esphome/core/controller.h | 15 +- esphome/core/controller_registry.cpp | 114 ++++++++ esphome/core/controller_registry.h | 245 ++++++++++++++++++ esphome/core/defines.h | 2 + 33 files changed, 583 insertions(+), 198 deletions(-) delete mode 100644 esphome/core/controller.cpp create mode 100644 esphome/core/controller_registry.cpp create mode 100644 esphome/core/controller_registry.h diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 9f1485ee905..c29e02c8efd 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -1,6 +1,8 @@ -#include - #include "alarm_control_panel.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" + +#include #include "esphome/core/application.h" #include "esphome/core/helpers.h" @@ -34,6 +36,9 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; this->state_callback_.call(); +#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_alarm_control_panel_update(this); +#endif if (state == ACP_STATE_TRIGGERED) { this->triggered_callback_.call(); } else if (state == ACP_STATE_ARMING) { diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 7f69a9fda12..a9286c531fa 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -245,6 +245,9 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + # Track controller registration for StaticVector sizing + CORE.register_controller() + cg.add(var.set_port(config[CONF_PORT])) if config[CONF_PASSWORD]: cg.add_define("USE_API_PASSWORD") diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index e5f0d9795ed..18601d74ff4 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -5,6 +5,7 @@ #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -34,7 +35,7 @@ APIServer::APIServer() { } void APIServer::setup() { - this->setup_controller(); + ControllerRegistry::register_controller(this); #ifdef USE_API_NOISE uint32_t hash = 88491486UL; @@ -269,7 +270,7 @@ bool APIServer::check_password(const uint8_t *password_data, size_t password_len void APIServer::handle_disconnect(APIConnection *conn) {} -// Macro for entities without extra parameters +// Macro for controller update dispatch #define API_DISPATCH_UPDATE(entity_type, entity_name) \ void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ if (obj->is_internal()) \ @@ -278,15 +279,6 @@ void APIServer::handle_disconnect(APIConnection *conn) {} c->send_##entity_name##_state(obj); \ } -// Macro for entities with extra parameters (but parameters not used in send) -#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \ - void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \ - if (obj->is_internal()) \ - return; \ - for (auto &c : this->clients_) \ - c->send_##entity_name##_state(obj); \ - } - #ifdef USE_BINARY_SENSOR API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) #endif @@ -304,15 +296,15 @@ API_DISPATCH_UPDATE(light::LightState, light) #endif #ifdef USE_SENSOR -API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state) +API_DISPATCH_UPDATE(sensor::Sensor, sensor) #endif #ifdef USE_SWITCH -API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state) +API_DISPATCH_UPDATE(switch_::Switch, switch) #endif #ifdef USE_TEXT_SENSOR -API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state) +API_DISPATCH_UPDATE(text_sensor::TextSensor, text_sensor) #endif #ifdef USE_CLIMATE @@ -320,7 +312,7 @@ API_DISPATCH_UPDATE(climate::Climate, climate) #endif #ifdef USE_NUMBER -API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state) +API_DISPATCH_UPDATE(number::Number, number) #endif #ifdef USE_DATETIME_DATE @@ -336,11 +328,11 @@ API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime) #endif #ifdef USE_TEXT -API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state) +API_DISPATCH_UPDATE(text::Text, text) #endif #ifdef USE_SELECT -API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index) +API_DISPATCH_UPDATE(select::Select, select) #endif #ifdef USE_LOCK @@ -356,12 +348,13 @@ API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) #endif #ifdef USE_EVENT -// Event is a special case - it's the only entity that passes extra parameters to the send method -void APIServer::on_event(event::Event *obj, const std::string &event_type) { +// Event is a special case - unlike other entities with simple state fields, +// events store their state in a member accessed via obj->get_last_event_type() +void APIServer::on_event(event::Event *obj) { if (obj->is_internal()) return; for (auto &c : this->clients_) - c->send_event(obj, event_type); + c->send_event(obj, obj->get_last_event_type()); } #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index f1f44a266d3..2d58063d6cf 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -72,19 +72,19 @@ class APIServer : public Component, public Controller { void on_light_update(light::LightState *obj) override; #endif #ifdef USE_SENSOR - void on_sensor_update(sensor::Sensor *obj, float state) override; + void on_sensor_update(sensor::Sensor *obj) override; #endif #ifdef USE_SWITCH - void on_switch_update(switch_::Switch *obj, bool state) override; + void on_switch_update(switch_::Switch *obj) override; #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override; + void on_text_sensor_update(text_sensor::TextSensor *obj) override; #endif #ifdef USE_CLIMATE void on_climate_update(climate::Climate *obj) override; #endif #ifdef USE_NUMBER - void on_number_update(number::Number *obj, float state) override; + void on_number_update(number::Number *obj) override; #endif #ifdef USE_DATETIME_DATE void on_date_update(datetime::DateEntity *obj) override; @@ -96,10 +96,10 @@ class APIServer : public Component, public Controller { void on_datetime_update(datetime::DateTimeEntity *obj) override; #endif #ifdef USE_TEXT - void on_text_update(text::Text *obj, const std::string &state) override; + void on_text_update(text::Text *obj) override; #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj, const std::string &state, size_t index) override; + void on_select_update(select::Select *obj) override; #endif #ifdef USE_LOCK void on_lock_update(lock::Lock *obj) override; @@ -141,7 +141,7 @@ class APIServer : public Component, public Controller { void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; #endif #ifdef USE_EVENT - void on_event(event::Event *obj, const std::string &event_type) override; + void on_event(event::Event *obj) override; #endif #ifdef USE_UPDATE void on_update(update::UpdateEntity *obj) override; diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 33b3de6d72b..220ed685db0 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -1,4 +1,6 @@ #include "binary_sensor.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -37,6 +39,9 @@ void BinarySensor::send_state_internal(bool new_state) { // Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed if (this->set_state_(new_state)) { ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state)); +#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_binary_sensor_update(this); +#endif } } diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 7df38758dce..82b75660bae 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -1,4 +1,6 @@ #include "climate.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/macros.h" namespace esphome { @@ -463,6 +465,9 @@ void Climate::publish_state() { // Send state to frontend this->state_callback_.call(*this); +#if defined(USE_CLIMATE) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_climate_update(this); +#endif // Save state this->save_state_(); } diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 654bb956a5c..3062dba28ad 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -1,5 +1,9 @@ #include "cover.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" + #include + #include "esphome/core/log.h" namespace esphome { @@ -169,6 +173,9 @@ void Cover::publish_state(bool save) { ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation)); this->state_callback_.call(); +#if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_cover_update(this); +#endif if (save) { CoverRestoreState restore{}; diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index c164a98b2e3..2c2775ecf40 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -1,5 +1,6 @@ #include "date_entity.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_DATE #include "esphome/core/log.h" @@ -32,6 +33,9 @@ void DateEntity::publish_state() { this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); this->state_callback_.call(); +#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_date_update(this); +#endif } DateCall DateEntity::make_call() { return DateCall(this); } diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 4e3b051eb35..8606a47fa72 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -1,5 +1,6 @@ #include "datetime_entity.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_DATETIME #include "esphome/core/log.h" @@ -48,6 +49,9 @@ void DateTimeEntity::publish_state() { ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, this->day_, this->hour_, this->minute_, this->second_); this->state_callback_.call(); +#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_datetime_update(this); +#endif } DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 9b05c2124f9..469be077eab 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -1,5 +1,6 @@ #include "time_entity.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_TIME #include "esphome/core/log.h" @@ -29,6 +30,9 @@ void TimeEntity::publish_state() { ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); this->state_callback_.call(); +#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_time_update(this); +#endif } TimeCall TimeEntity::make_call() { return TimeCall(this); } diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index a14afbd7f55..4c74a113885 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -1,5 +1,6 @@ #include "event.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -23,6 +24,9 @@ void Event::trigger(const std::string &event_type) { this->last_event_type_ = found; ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_); this->event_callback_.call(event_type); +#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_event(this); +#endif } void Event::set_event_types(const FixedVector &event_types) { diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 959572e9d94..d37825a6513 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -1,4 +1,6 @@ #include "fan.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -181,6 +183,9 @@ void Fan::publish_state() { ESP_LOGD(TAG, " Preset Mode: %s", preset); } this->state_callback_.call(); +#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_fan_update(this); +#endif this->save_state_(); } diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 7b0a698bb8f..4c253ec5a83 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,7 +1,8 @@ -#include "esphome/core/log.h" - -#include "light_output.h" #include "light_state.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" +#include "esphome/core/log.h" +#include "light_output.h" #include "transformers.h" namespace esphome { @@ -137,7 +138,12 @@ void LightState::loop() { float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } -void LightState::publish_state() { this->remote_values_callback_.call(); } +void LightState::publish_state() { + this->remote_values_callback_.call(); +#if defined(USE_LIGHT) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_light_update(this); +#endif +} LightOutput *LightState::get_output() const { return this->output_; } diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index ddc54453496..54fefe8745e 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -1,4 +1,6 @@ #include "lock.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -53,6 +55,9 @@ void Lock::publish_state(LockState state) { this->rtc_.save(&this->state); ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), lock_state_to_string(state)); this->state_callback_.call(); +#if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_lock_update(this); +#endif } void Lock::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 3f274bf73b3..b46ec39d302 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -1,5 +1,6 @@ #include "media_player.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -148,7 +149,12 @@ void MediaPlayer::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -void MediaPlayer::publish_state() { this->state_callback_.call(); } +void MediaPlayer::publish_state() { + this->state_callback_.call(); +#if defined(USE_MEDIA_PLAYER) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_media_player_update(this); +#endif +} } // namespace media_player } // namespace esphome diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index da08faf6558..f12e0e9e1e1 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -1,4 +1,6 @@ #include "number.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -32,6 +34,9 @@ void Number::publish_state(float state) { this->state = state; ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); this->state_callback_.call(state); +#if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_number_update(this); +#endif } void Number::add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 6bb01ba6e25..9fe7a524227 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -1,4 +1,6 @@ #include "select.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" #include @@ -33,6 +35,9 @@ void Select::publish_state(size_t index) { ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); // Callback signature requires std::string, create temporary for compatibility this->state_callback_.call(std::string(option), index); +#if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_select_update(this); +#endif } const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; } diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 92da4345b70..df6bd644e84 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -1,4 +1,6 @@ #include "sensor.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -131,6 +133,9 @@ void Sensor::internal_send_state_to_frontend(float state) { ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals()); this->callback_.call(state); +#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_sensor_update(this); +#endif } } // namespace sensor diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 02cee91a768..3c3a437ff36 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -1,4 +1,6 @@ #include "switch.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -62,6 +64,9 @@ void Switch::publish_state(bool state) { ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state)); this->state_callback_.call(this->state); +#if defined(USE_SWITCH) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_switch_update(this); +#endif } bool Switch::assumed_state() { return false; } diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 654893d4e49..933d82c85c1 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -1,4 +1,6 @@ #include "text.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -16,6 +18,9 @@ void Text::publish_state(const std::string &state) { ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); } this->state_callback_.call(state); +#if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_text_update(this); +#endif } void Text::add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 0294d65861c..a7bcf199672 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -1,4 +1,6 @@ #include "text_sensor.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -84,6 +86,9 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); this->callback_.call(state); +#if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_text_sensor_update(this); +#endif } } // namespace text_sensor diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index ce97fb1b77f..567fc9fc8e0 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -1,5 +1,6 @@ #include "update_entity.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -32,6 +33,9 @@ void UpdateEntity::publish_state() { this->set_has_state(true); this->state_callback_.call(); +#if defined(USE_UPDATE) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_update(this); +#endif } } // namespace update diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index b041fe84496..381d9061de3 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -1,4 +1,6 @@ #include "valve.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" #include @@ -147,6 +149,9 @@ void Valve::publish_state(bool save) { ESP_LOGD(TAG, " Current Operation: %s", valve_operation_to_str(this->current_operation)); this->state_callback_.call(); +#if defined(USE_VALVE) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_valve_update(this); +#endif if (save) { ValveRestoreState restore{}; diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index a7fdf30eef4..17ad496f30d 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -289,6 +289,9 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) + # Track controller registration for StaticVector sizing + CORE.register_controller() + version = config[CONF_VERSION] cg.add(paren.set_port(config[CONF_PORT])) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 91ca0764749..5a8128ba431 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -3,6 +3,8 @@ #include "esphome/components/json/json_util.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -294,7 +296,7 @@ std::string WebServer::get_config_json() { } void WebServer::setup() { - this->setup_controller(this->include_internal_); + ControllerRegistry::register_controller(this); this->base_->init(); #ifdef USE_LOGGER @@ -430,7 +432,9 @@ static JsonDetail get_request_detail(AsyncWebServerRequest *request) { } #ifdef USE_SENSOR -void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { +void WebServer::on_sensor_update(sensor::Sensor *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", sensor_state_json_generator); } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -473,7 +477,9 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail #endif #ifdef USE_TEXT_SENSOR -void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { +void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", text_sensor_state_json_generator); } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -513,7 +519,9 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: #endif #ifdef USE_SWITCH -void WebServer::on_switch_update(switch_::Switch *obj, bool state) { +void WebServer::on_switch_update(switch_::Switch *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", switch_state_json_generator); } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -625,6 +633,8 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) #ifdef USE_BINARY_SENSOR void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator); } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -664,6 +674,8 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool #ifdef USE_FAN void WebServer::on_fan_update(fan::Fan *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", fan_state_json_generator); } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -738,6 +750,8 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { #ifdef USE_LIGHT void WebServer::on_light_update(light::LightState *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", light_state_json_generator); } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -811,6 +825,8 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi #ifdef USE_COVER void WebServer::on_cover_update(cover::Cover *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", cover_state_json_generator); } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -895,7 +911,9 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { #endif #ifdef USE_NUMBER -void WebServer::on_number_update(number::Number *obj, float state) { +void WebServer::on_number_update(number::Number *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", number_state_json_generator); } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -961,6 +979,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail #ifdef USE_DATETIME_DATE void WebServer::on_date_update(datetime::DateEntity *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", date_state_json_generator); } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1016,6 +1036,8 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con #ifdef USE_DATETIME_TIME void WebServer::on_time_update(datetime::TimeEntity *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", time_state_json_generator); } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1070,6 +1092,8 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con #ifdef USE_DATETIME_DATETIME void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", datetime_state_json_generator); } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1124,7 +1148,9 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s #endif // USE_DATETIME_DATETIME #ifdef USE_TEXT -void WebServer::on_text_update(text::Text *obj, const std::string &state) { +void WebServer::on_text_update(text::Text *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", text_state_json_generator); } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1178,7 +1204,9 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json #endif #ifdef USE_SELECT -void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { +void WebServer::on_select_update(select::Select *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", select_state_json_generator); } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1237,6 +1265,8 @@ std::string WebServer::select_json(select::Select *obj, const char *value, JsonD #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", climate_state_json_generator); } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1378,6 +1408,8 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf #ifdef USE_LOCK void WebServer::on_lock_update(lock::Lock *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", lock_state_json_generator); } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1449,6 +1481,8 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet #ifdef USE_VALVE void WebServer::on_valve_update(valve::Valve *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", valve_state_json_generator); } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1530,6 +1564,8 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { #ifdef USE_ALARM_CONTROL_PANEL void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", alarm_control_panel_state_json_generator); } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1607,7 +1643,9 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro #endif #ifdef USE_EVENT -void WebServer::on_event(event::Event *obj, const std::string &event_type) { +void WebServer::on_event(event::Event *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", event_state_json_generator); } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 328140cfae9..7e1af886457 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -255,7 +255,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SENSOR - void on_sensor_update(sensor::Sensor *obj, float state) override; + void on_sensor_update(sensor::Sensor *obj) override; /// Handle a sensor request under '/sensor/'. void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -266,7 +266,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SWITCH - void on_switch_update(switch_::Switch *obj, bool state) override; + void on_switch_update(switch_::Switch *obj) override; /// Handle a switch request under '/switch//'. void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -324,7 +324,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override; + void on_text_sensor_update(text_sensor::TextSensor *obj) override; /// Handle a text sensor request under '/text_sensor/'. void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -348,7 +348,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_NUMBER - void on_number_update(number::Number *obj, float state) override; + void on_number_update(number::Number *obj) override; /// Handle a number request under '/number/'. void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -392,7 +392,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_TEXT - void on_text_update(text::Text *obj, const std::string &state) override; + void on_text_update(text::Text *obj) override; /// Handle a text input request under '/text/'. void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -403,7 +403,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj, const std::string &state, size_t index) override; + void on_select_update(select::Select *obj) override; /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -462,7 +462,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_EVENT - void on_event(event::Event *obj, const std::string &event_type) override; + void on_event(event::Event *obj) override; static std::string event_state_json_generator(WebServer *web_server, void *source); static std::string event_all_json_generator(WebServer *web_server, void *source); diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index fed5265d6b9..08753b0f2d7 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -48,6 +48,9 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +# Key for tracking controller count in CORE.data for ControllerRegistry StaticVector sizing +KEY_CONTROLLER_REGISTRY_COUNT = "controller_registry_count" + class EsphomeError(Exception): """General ESPHome exception occurred.""" @@ -910,6 +913,11 @@ class EsphomeCore: """ self.platform_counts[platform_name] += 1 + def register_controller(self) -> None: + """Track registration of a Controller for ControllerRegistry StaticVector sizing.""" + controller_count = self.data.setdefault(KEY_CONTROLLER_REGISTRY_COUNT, 0) + self.data[KEY_CONTROLLER_REGISTRY_COUNT] = controller_count + 1 + @property def cpp_main_section(self): from esphome.cpp_generator import statement diff --git a/esphome/core/config.py b/esphome/core/config.py index 27404538085..763f9ebd9fd 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -40,7 +40,12 @@ from esphome.const import ( PlatformFramework, __version__ as ESPHOME_VERSION, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import ( + CORE, + KEY_CONTROLLER_REGISTRY_COUNT, + CoroPriority, + coroutine_with_priority, +) from esphome.helpers import ( copy_file_if_changed, fnv1a_32bit_hash, @@ -462,6 +467,15 @@ async def _add_platform_defines() -> None: cg.add_define(f"USE_{platform_name.upper()}") +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_controller_registry_define() -> None: + # Generate StaticVector size for ControllerRegistry + controller_count = CORE.data.get(KEY_CONTROLLER_REGISTRY_COUNT, 0) + if controller_count > 0: + cg.add_define("USE_CONTROLLER_REGISTRY") + cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count) + + @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_global(cg.global_ns.namespace("esphome").using) @@ -483,6 +497,7 @@ async def to_code(config: ConfigType) -> None: cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids)) CORE.add_job(_add_platform_defines) + CORE.add_job(_add_controller_registry_define) CORE.add_job(_add_automations, config) diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp deleted file mode 100644 index f7ff5a9734a..00000000000 --- a/esphome/core/controller.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include "controller.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" - -namespace esphome { - -void Controller::setup_controller(bool include_internal) { -#ifdef USE_BINARY_SENSOR - for (auto *obj : App.get_binary_sensors()) { - if (include_internal || !obj->is_internal()) { - obj->add_full_state_callback( - [this, obj](optional previous, optional state) { this->on_binary_sensor_update(obj); }); - } - } -#endif -#ifdef USE_FAN - for (auto *obj : App.get_fans()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_fan_update(obj); }); - } -#endif -#ifdef USE_LIGHT - for (auto *obj : App.get_lights()) { - if (include_internal || !obj->is_internal()) - obj->add_new_remote_values_callback([this, obj]() { this->on_light_update(obj); }); - } -#endif -#ifdef USE_SENSOR - for (auto *obj : App.get_sensors()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](float state) { this->on_sensor_update(obj, state); }); - } -#endif -#ifdef USE_SWITCH - for (auto *obj : App.get_switches()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](bool state) { this->on_switch_update(obj, state); }); - } -#endif -#ifdef USE_COVER - for (auto *obj : App.get_covers()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_cover_update(obj); }); - } -#endif -#ifdef USE_TEXT_SENSOR - for (auto *obj : App.get_text_sensors()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_sensor_update(obj, state); }); - } -#endif -#ifdef USE_CLIMATE - for (auto *obj : App.get_climates()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](climate::Climate & /*unused*/) { this->on_climate_update(obj); }); - } -#endif -#ifdef USE_NUMBER - for (auto *obj : App.get_numbers()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); }); - } -#endif -#ifdef USE_DATETIME_DATE - for (auto *obj : App.get_dates()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_date_update(obj); }); - } -#endif -#ifdef USE_DATETIME_TIME - for (auto *obj : App.get_times()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_time_update(obj); }); - } -#endif -#ifdef USE_DATETIME_DATETIME - for (auto *obj : App.get_datetimes()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_datetime_update(obj); }); - } -#endif -#ifdef USE_TEXT - for (auto *obj : App.get_texts()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_update(obj, state); }); - } -#endif -#ifdef USE_SELECT - for (auto *obj : App.get_selects()) { - if (include_internal || !obj->is_internal()) { - obj->add_on_state_callback( - [this, obj](const std::string &state, size_t index) { this->on_select_update(obj, state, index); }); - } - } -#endif -#ifdef USE_LOCK - for (auto *obj : App.get_locks()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_lock_update(obj); }); - } -#endif -#ifdef USE_VALVE - for (auto *obj : App.get_valves()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_valve_update(obj); }); - } -#endif -#ifdef USE_MEDIA_PLAYER - for (auto *obj : App.get_media_players()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_media_player_update(obj); }); - } -#endif -#ifdef USE_ALARM_CONTROL_PANEL - for (auto *obj : App.get_alarm_control_panels()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_alarm_control_panel_update(obj); }); - } -#endif -#ifdef USE_EVENT - for (auto *obj : App.get_events()) { - if (include_internal || !obj->is_internal()) - obj->add_on_event_callback([this, obj](const std::string &event_type) { this->on_event(obj, event_type); }); - } -#endif -#ifdef USE_UPDATE - for (auto *obj : App.get_updates()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_update(obj); }); - } -#endif -} - -} // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index b475e326ee0..697017217db 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -69,7 +69,6 @@ namespace esphome { class Controller { public: - void setup_controller(bool include_internal = false); #ifdef USE_BINARY_SENSOR virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj){}; #endif @@ -80,22 +79,22 @@ class Controller { virtual void on_light_update(light::LightState *obj){}; #endif #ifdef USE_SENSOR - virtual void on_sensor_update(sensor::Sensor *obj, float state){}; + virtual void on_sensor_update(sensor::Sensor *obj){}; #endif #ifdef USE_SWITCH - virtual void on_switch_update(switch_::Switch *obj, bool state){}; + virtual void on_switch_update(switch_::Switch *obj){}; #endif #ifdef USE_COVER virtual void on_cover_update(cover::Cover *obj){}; #endif #ifdef USE_TEXT_SENSOR - virtual void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state){}; + virtual void on_text_sensor_update(text_sensor::TextSensor *obj){}; #endif #ifdef USE_CLIMATE virtual void on_climate_update(climate::Climate *obj){}; #endif #ifdef USE_NUMBER - virtual void on_number_update(number::Number *obj, float state){}; + virtual void on_number_update(number::Number *obj){}; #endif #ifdef USE_DATETIME_DATE virtual void on_date_update(datetime::DateEntity *obj){}; @@ -107,10 +106,10 @@ class Controller { virtual void on_datetime_update(datetime::DateTimeEntity *obj){}; #endif #ifdef USE_TEXT - virtual void on_text_update(text::Text *obj, const std::string &state){}; + virtual void on_text_update(text::Text *obj){}; #endif #ifdef USE_SELECT - virtual void on_select_update(select::Select *obj, const std::string &state, size_t index){}; + virtual void on_select_update(select::Select *obj){}; #endif #ifdef USE_LOCK virtual void on_lock_update(lock::Lock *obj){}; @@ -125,7 +124,7 @@ class Controller { virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){}; #endif #ifdef USE_EVENT - virtual void on_event(event::Event *obj, const std::string &event_type){}; + virtual void on_event(event::Event *obj){}; #endif #ifdef USE_UPDATE virtual void on_update(update::UpdateEntity *obj){}; diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp new file mode 100644 index 00000000000..0a84bb0d0d5 --- /dev/null +++ b/esphome/core/controller_registry.cpp @@ -0,0 +1,114 @@ +#include "esphome/core/controller_registry.h" + +#ifdef USE_CONTROLLER_REGISTRY + +#include "esphome/core/controller.h" + +namespace esphome { + +StaticVector ControllerRegistry::controllers; + +void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } + +// Macro for standard registry notification dispatch - calls on__update() +#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ + void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ + for (auto *controller : controllers) { \ + controller->on_##entity_name##_update(obj); \ + } \ + } + +// Macro for entities where controller method has no "_update" suffix (Event, Update) +#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ + void ControllerRegistry::notify_##entity_name(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ + for (auto *controller : controllers) { \ + controller->on_##entity_name(obj); \ + } \ + } + +#ifdef USE_BINARY_SENSOR +CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor) +#endif + +#ifdef USE_FAN +CONTROLLER_REGISTRY_NOTIFY(fan::Fan, fan) +#endif + +#ifdef USE_LIGHT +CONTROLLER_REGISTRY_NOTIFY(light::LightState, light) +#endif + +#ifdef USE_SENSOR +CONTROLLER_REGISTRY_NOTIFY(sensor::Sensor, sensor) +#endif + +#ifdef USE_SWITCH +CONTROLLER_REGISTRY_NOTIFY(switch_::Switch, switch) +#endif + +#ifdef USE_COVER +CONTROLLER_REGISTRY_NOTIFY(cover::Cover, cover) +#endif + +#ifdef USE_TEXT_SENSOR +CONTROLLER_REGISTRY_NOTIFY(text_sensor::TextSensor, text_sensor) +#endif + +#ifdef USE_CLIMATE +CONTROLLER_REGISTRY_NOTIFY(climate::Climate, climate) +#endif + +#ifdef USE_NUMBER +CONTROLLER_REGISTRY_NOTIFY(number::Number, number) +#endif + +#ifdef USE_DATETIME_DATE +CONTROLLER_REGISTRY_NOTIFY(datetime::DateEntity, date) +#endif + +#ifdef USE_DATETIME_TIME +CONTROLLER_REGISTRY_NOTIFY(datetime::TimeEntity, time) +#endif + +#ifdef USE_DATETIME_DATETIME +CONTROLLER_REGISTRY_NOTIFY(datetime::DateTimeEntity, datetime) +#endif + +#ifdef USE_TEXT +CONTROLLER_REGISTRY_NOTIFY(text::Text, text) +#endif + +#ifdef USE_SELECT +CONTROLLER_REGISTRY_NOTIFY(select::Select, select) +#endif + +#ifdef USE_LOCK +CONTROLLER_REGISTRY_NOTIFY(lock::Lock, lock) +#endif + +#ifdef USE_VALVE +CONTROLLER_REGISTRY_NOTIFY(valve::Valve, valve) +#endif + +#ifdef USE_MEDIA_PLAYER +CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player) +#endif + +#ifdef USE_ALARM_CONTROL_PANEL +CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel) +#endif + +#ifdef USE_EVENT +CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event) +#endif + +#ifdef USE_UPDATE +CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(update::UpdateEntity, update) +#endif + +#undef CONTROLLER_REGISTRY_NOTIFY +#undef CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX + +} // namespace esphome + +#endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h new file mode 100644 index 00000000000..640a276a0a0 --- /dev/null +++ b/esphome/core/controller_registry.h @@ -0,0 +1,245 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_CONTROLLER_REGISTRY + +#include "esphome/core/helpers.h" + +// Forward declarations +namespace esphome { + +class Controller; + +#ifdef USE_BINARY_SENSOR +namespace binary_sensor { +class BinarySensor; +} +#endif + +#ifdef USE_FAN +namespace fan { +class Fan; +} +#endif + +#ifdef USE_LIGHT +namespace light { +class LightState; +} +#endif + +#ifdef USE_SENSOR +namespace sensor { +class Sensor; +} +#endif + +#ifdef USE_SWITCH +namespace switch_ { +class Switch; +} +#endif + +#ifdef USE_COVER +namespace cover { +class Cover; +} +#endif + +#ifdef USE_TEXT_SENSOR +namespace text_sensor { +class TextSensor; +} +#endif + +#ifdef USE_CLIMATE +namespace climate { +class Climate; +} +#endif + +#ifdef USE_NUMBER +namespace number { +class Number; +} +#endif + +#ifdef USE_DATETIME_DATE +namespace datetime { +class DateEntity; +} +#endif + +#ifdef USE_DATETIME_TIME +namespace datetime { +class TimeEntity; +} +#endif + +#ifdef USE_DATETIME_DATETIME +namespace datetime { +class DateTimeEntity; +} +#endif + +#ifdef USE_TEXT +namespace text { +class Text; +} +#endif + +#ifdef USE_SELECT +namespace select { +class Select; +} +#endif + +#ifdef USE_LOCK +namespace lock { +class Lock; +} +#endif + +#ifdef USE_VALVE +namespace valve { +class Valve; +} +#endif + +#ifdef USE_MEDIA_PLAYER +namespace media_player { +class MediaPlayer; +} +#endif + +#ifdef USE_ALARM_CONTROL_PANEL +namespace alarm_control_panel { +class AlarmControlPanel; +} +#endif + +#ifdef USE_EVENT +namespace event { +class Event; +} +#endif + +#ifdef USE_UPDATE +namespace update { +class UpdateEntity; +} +#endif + +/** Global registry for Controllers to receive entity state updates. + * + * This singleton registry allows Controllers (APIServer, WebServer) to receive + * entity state change notifications without storing per-entity callbacks. + * + * Instead of each entity maintaining controller callbacks (32 bytes overhead per entity), + * entities call ControllerRegistry::notify_*_update() which iterates the small list + * of registered controllers (typically 2: API and WebServer). + * + * Controllers read state directly from entities using existing accessors (obj->state, etc.) + * rather than receiving it as callback parameters that were being ignored anyway. + * + * Memory savings: 32 bytes per entity (2 controllers × 16 bytes std::function overhead) + * Typical config (25 entities): ~780 bytes saved + * Large config (80 entities): ~2,540 bytes saved + */ +class ControllerRegistry { + public: + /** Register a controller to receive entity state updates. + * + * Controllers should call this in their setup() method. + * Typically only APIServer and WebServer register. + */ + static void register_controller(Controller *controller); + +#ifdef USE_BINARY_SENSOR + static void notify_binary_sensor_update(binary_sensor::BinarySensor *obj); +#endif + +#ifdef USE_FAN + static void notify_fan_update(fan::Fan *obj); +#endif + +#ifdef USE_LIGHT + static void notify_light_update(light::LightState *obj); +#endif + +#ifdef USE_SENSOR + static void notify_sensor_update(sensor::Sensor *obj); +#endif + +#ifdef USE_SWITCH + static void notify_switch_update(switch_::Switch *obj); +#endif + +#ifdef USE_COVER + static void notify_cover_update(cover::Cover *obj); +#endif + +#ifdef USE_TEXT_SENSOR + static void notify_text_sensor_update(text_sensor::TextSensor *obj); +#endif + +#ifdef USE_CLIMATE + static void notify_climate_update(climate::Climate *obj); +#endif + +#ifdef USE_NUMBER + static void notify_number_update(number::Number *obj); +#endif + +#ifdef USE_DATETIME_DATE + static void notify_date_update(datetime::DateEntity *obj); +#endif + +#ifdef USE_DATETIME_TIME + static void notify_time_update(datetime::TimeEntity *obj); +#endif + +#ifdef USE_DATETIME_DATETIME + static void notify_datetime_update(datetime::DateTimeEntity *obj); +#endif + +#ifdef USE_TEXT + static void notify_text_update(text::Text *obj); +#endif + +#ifdef USE_SELECT + static void notify_select_update(select::Select *obj); +#endif + +#ifdef USE_LOCK + static void notify_lock_update(lock::Lock *obj); +#endif + +#ifdef USE_VALVE + static void notify_valve_update(valve::Valve *obj); +#endif + +#ifdef USE_MEDIA_PLAYER + static void notify_media_player_update(media_player::MediaPlayer *obj); +#endif + +#ifdef USE_ALARM_CONTROL_PANEL + static void notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj); +#endif + +#ifdef USE_EVENT + static void notify_event(event::Event *obj); +#endif + +#ifdef USE_UPDATE + static void notify_update(update::UpdateEntity *obj); +#endif + + protected: + static StaticVector controllers; +}; + +} // namespace esphome + +#endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 2be32058ea0..82305180712 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -28,6 +28,7 @@ #define USE_BUTTON #define USE_CAMERA #define USE_CLIMATE +#define USE_CONTROLLER_REGISTRY #define USE_COVER #define USE_DATETIME #define USE_DATETIME_DATE @@ -296,6 +297,7 @@ #define USE_DASHBOARD_IMPORT // Default counts for static analysis +#define CONTROLLER_REGISTRY_MAX 2 #define ESPHOME_COMPONENT_COUNT 50 #define ESPHOME_DEVICE_COUNT 10 #define ESPHOME_AREA_COUNT 10 From fbbdad75f6ad8b10b6c8926abd7a82a6b1c55345 Mon Sep 17 00:00:00 2001 From: Paul Schulz Date: Mon, 10 Nov 2025 11:56:02 +1030 Subject: [PATCH 300/461] [sx126x] Change BUSY, RST, DIO1 pins to general GPIO (from internal) (#11782) --- esphome/components/sx126x/__init__.py | 6 +++--- esphome/components/sx126x/sx126x.h | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index f8f3b9d1040..1eb83b7a334 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -189,7 +189,7 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(SX126x), cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW), cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=600, max=300000), - cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_BUSY_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, cv.Optional(CONF_CRC_INVERTED, default=True): cv.boolean, @@ -201,7 +201,7 @@ CONFIG_SCHEMA = ( cv.hex_int, cv.Range(min=0, max=0xFFFF) ), cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), - cv.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema, cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), cv.Required(CONF_HW_VERSION): cv.one_of( "sx1261", "sx1262", "sx1268", "llcc68", lower=True @@ -213,7 +213,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256), cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4), cv.Optional(CONF_PREAMBLE_SIZE, default=8): cv.int_range(min=1, max=65535), - cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_RST_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RX_START, default=True): cv.boolean, cv.Required(CONF_RF_SWITCH): cv.boolean, cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING), diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h index 47d6449738a..850d7d4c77d 100644 --- a/esphome/components/sx126x/sx126x.h +++ b/esphome/components/sx126x/sx126x.h @@ -64,7 +64,7 @@ class SX126x : public Component, void dump_config() override; void set_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; } void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; } - void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; } + void set_busy_pin(GPIOPin *busy_pin) { this->busy_pin_ = busy_pin; } void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; } void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; } void set_crc_inverted(bool crc_inverted) { this->crc_inverted_ = crc_inverted; } @@ -72,7 +72,7 @@ class SX126x : public Component, void set_crc_polynomial(uint16_t crc_polynomial) { this->crc_polynomial_ = crc_polynomial; } void set_crc_initial(uint16_t crc_initial) { this->crc_initial_ = crc_initial; } void set_deviation(uint32_t deviation) { this->deviation_ = deviation; } - void set_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; } + void set_dio1_pin(GPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; } void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } void set_hw_version(const std::string &hw_version) { this->hw_version_ = hw_version; } void set_mode_rx(); @@ -85,7 +85,7 @@ class SX126x : public Component, void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; } void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; } void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; } - void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; } + void set_rst_pin(GPIOPin *rst_pin) { this->rst_pin_ = rst_pin; } void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; } void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; } void set_shaping(uint8_t shaping) { this->shaping_ = shaping; } @@ -115,9 +115,9 @@ class SX126x : public Component, std::vector listeners_; std::vector packet_; std::vector sync_value_; - InternalGPIOPin *busy_pin_{nullptr}; - InternalGPIOPin *dio1_pin_{nullptr}; - InternalGPIOPin *rst_pin_{nullptr}; + GPIOPin *busy_pin_{nullptr}; + GPIOPin *dio1_pin_{nullptr}; + GPIOPin *rst_pin_{nullptr}; std::string hw_version_; char version_[16]; SX126xBw bandwidth_{SX126X_BW_125000}; From c17a31a8f836503b742a63f87125b6532f3db4ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 19:28:49 -0600 Subject: [PATCH 301/461] Ensure event paths are enabled in api compile tests (#11776) --- tests/components/api/common-base.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml index c90fa4dfefc..fc53b8ac7e0 100644 --- a/tests/components/api/common-base.yaml +++ b/tests/components/api/common-base.yaml @@ -178,6 +178,14 @@ api: - logger.log: "Skipped loops" - logger.log: "After combined test" +event: + - platform: template + name: Test Event + id: test_event + event_types: + - single_click + - double_click + globals: - id: api_continuation_test_counter type: int From b47e89a7d593d3b38a9cadf0b75ea605c666cd0a Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 10 Nov 2025 03:21:38 +0100 Subject: [PATCH 302/461] [nrf52,watchdog] do not disable watchog if it is not nesesery (#11686) --- esphome/components/debug/__init__.py | 1 + esphome/components/zephyr/core.cpp | 10 +++++++++- esphome/core/defines.h | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index 6b4205a5450..dc032f442e1 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -59,6 +59,7 @@ async def to_code(config): zephyr_add_prj_conf("SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL", True) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + cg.add_define("USE_DEBUG") FILTER_SOURCE_FILES = filter_source_files_from_platform( diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index 365b6b8ed2d..d5427a0ebfc 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -6,6 +6,7 @@ #include #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/defines.h" namespace esphome { @@ -25,7 +26,14 @@ void arch_init() { wdt_config.window.max = 2000; wdt_channel_id = wdt_install_timeout(WDT, &wdt_config); if (wdt_channel_id >= 0) { - wdt_setup(WDT, WDT_OPT_PAUSE_HALTED_BY_DBG | WDT_OPT_PAUSE_IN_SLEEP); + uint8_t options = 0; +#ifdef USE_DEBUG + options |= WDT_OPT_PAUSE_HALTED_BY_DBG; +#endif +#ifdef USE_DEEP_SLEEP + options |= WDT_OPT_PAUSE_IN_SLEEP; +#endif + wdt_setup(WDT, options); } } } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 82305180712..ac725fbca94 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -34,6 +34,7 @@ #define USE_DATETIME_DATE #define USE_DATETIME_DATETIME #define USE_DATETIME_TIME +#define USE_DEBUG #define USE_DEEP_SLEEP #define USE_DEVICES #define USE_DISPLAY From 6feaa8dd1393deb2be259f65d04d2b98a0d85aed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 23:10:06 -0600 Subject: [PATCH 303/461] preserve order --- esphome/core/helpers.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 732a8a66af0..4588c759e89 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -889,10 +889,11 @@ template class PartitionedCallbackManager { this->callbacks_ = make_unique>>(); } - // Add to first partition: append then swap into position + // Add to first partition: append then rotate into position this->callbacks_->push_back(std::move(callback)); if (*first_count < this->callbacks_->size() - 1) { - std::swap((*this->callbacks_)[*first_count], (*this->callbacks_)[this->callbacks_->size() - 1]); + // Use std::rotate to maintain registration order in second partition + std::rotate(this->callbacks_->begin() + *first_count, this->callbacks_->end() - 1, this->callbacks_->end()); } (*first_count)++; } From 0f136a888cb8c94a4360be48569f1959ae0833e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Nov 2025 23:19:02 -0600 Subject: [PATCH 304/461] Merge branch 'dev' into parition_callbacks and address Copilot review - Resolved conflicts in sensor.cpp and text_sensor.cpp to keep the PartitionedCallbackManager approach from this branch - Fixed platform-dependent pointer size documentation (4 bytes on 32-bit, 8 bytes on 64-bit) - Fixed potential integer underflow in add_first comparison - Added documentation explaining asymmetric API design rationale --- esphome/core/helpers.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 732a8a66af0..d61edd20e26 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -876,8 +876,12 @@ template class PartitionedCallbackManager; * Uses a single vector partitioned into two sections: [first_0, ..., first_m-1, second_0, ..., second_n-1] * The partition point is tracked externally by the caller (typically stored in the entity class for optimal alignment). * - * Memory efficient: Only stores a 4-byte pointer. The partition count lives in the entity class where it can be - * packed with other small fields to avoid padding waste. + * Memory efficient: Only stores a single pointer (4 bytes on 32-bit platforms, 8 bytes on 64-bit platforms). + * The partition count lives in the entity class where it can be packed with other small fields to avoid padding waste. + * + * Design rationale: The asymmetric API (add_first takes first_count*, while call_first/call_second take it by value) + * is intentional - add_first must increment the count, while call methods only read it. This avoids storing first_count + * internally, saving memory per instance. * * @tparam Ts The arguments for the callbacks, wrapped in void(). */ @@ -891,7 +895,8 @@ template class PartitionedCallbackManager { // Add to first partition: append then swap into position this->callbacks_->push_back(std::move(callback)); - if (*first_count < this->callbacks_->size() - 1) { + // Avoid potential underflow: rewrite comparison to not subtract from size() + if (*first_count + 1 < this->callbacks_->size()) { std::swap((*this->callbacks_)[*first_count], (*this->callbacks_)[this->callbacks_->size() - 1]); } (*first_count)++; From 2a166536426c63c3ef588e76841bbbcf24ec4f6e Mon Sep 17 00:00:00 2001 From: On Freund Date: Mon, 10 Nov 2025 15:44:27 +0200 Subject: [PATCH 305/461] HLK-FM22X Face Recognition module component (#8059) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/hlk_fm22x/__init__.py | 247 +++++++++++++ esphome/components/hlk_fm22x/binary_sensor.py | 21 ++ esphome/components/hlk_fm22x/hlk_fm22x.cpp | 325 ++++++++++++++++++ esphome/components/hlk_fm22x/hlk_fm22x.h | 224 ++++++++++++ esphome/components/hlk_fm22x/sensor.py | 47 +++ esphome/components/hlk_fm22x/text_sensor.py | 42 +++ .../components/hlk_fm22x/test.esp32-idf.yaml | 47 +++ .../hlk_fm22x/test.esp8266-ard.yaml | 47 +++ .../components/hlk_fm22x/test.rp2040-ard.yaml | 47 +++ 10 files changed, 1048 insertions(+) create mode 100644 esphome/components/hlk_fm22x/__init__.py create mode 100644 esphome/components/hlk_fm22x/binary_sensor.py create mode 100644 esphome/components/hlk_fm22x/hlk_fm22x.cpp create mode 100644 esphome/components/hlk_fm22x/hlk_fm22x.h create mode 100644 esphome/components/hlk_fm22x/sensor.py create mode 100644 esphome/components/hlk_fm22x/text_sensor.py create mode 100644 tests/components/hlk_fm22x/test.esp32-idf.yaml create mode 100644 tests/components/hlk_fm22x/test.esp8266-ard.yaml create mode 100644 tests/components/hlk_fm22x/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 7e785db451f..393774372f2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -206,6 +206,7 @@ esphome/components/hdc2010/* @optimusprimespace @ssieb esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal +esphome/components/hlk_fm22x/* @OnFreund esphome/components/hm3301/* @freekode esphome/components/hmac_md5/* @dwmw2 esphome/components/homeassistant/* @esphome/core @OttoWinter diff --git a/esphome/components/hlk_fm22x/__init__.py b/esphome/components/hlk_fm22x/__init__.py new file mode 100644 index 00000000000..efd64b65139 --- /dev/null +++ b/esphome/components/hlk_fm22x/__init__.py @@ -0,0 +1,247 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import ( + CONF_DIRECTION, + CONF_ID, + CONF_NAME, + CONF_ON_ENROLLMENT_DONE, + CONF_ON_ENROLLMENT_FAILED, + CONF_TRIGGER_ID, +) + +CODEOWNERS = ["@OnFreund"] +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["binary_sensor", "sensor", "text_sensor"] +MULTI_CONF = True + +CONF_HLK_FM22X_ID = "hlk_fm22x_id" +CONF_FACE_ID = "face_id" +CONF_ON_FACE_SCAN_MATCHED = "on_face_scan_matched" +CONF_ON_FACE_SCAN_UNMATCHED = "on_face_scan_unmatched" +CONF_ON_FACE_SCAN_INVALID = "on_face_scan_invalid" +CONF_ON_FACE_INFO = "on_face_info" + +hlk_fm22x_ns = cg.esphome_ns.namespace("hlk_fm22x") +HlkFm22xComponent = hlk_fm22x_ns.class_( + "HlkFm22xComponent", cg.PollingComponent, uart.UARTDevice +) + +FaceScanMatchedTrigger = hlk_fm22x_ns.class_( + "FaceScanMatchedTrigger", automation.Trigger.template(cg.int16, cg.std_string) +) + +FaceScanUnmatchedTrigger = hlk_fm22x_ns.class_( + "FaceScanUnmatchedTrigger", automation.Trigger.template() +) + +FaceScanInvalidTrigger = hlk_fm22x_ns.class_( + "FaceScanInvalidTrigger", automation.Trigger.template(cg.uint8) +) + +FaceInfoTrigger = hlk_fm22x_ns.class_( + "FaceInfoTrigger", + automation.Trigger.template( + cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16 + ), +) + +EnrollmentDoneTrigger = hlk_fm22x_ns.class_( + "EnrollmentDoneTrigger", automation.Trigger.template(cg.int16, cg.uint8) +) + +EnrollmentFailedTrigger = hlk_fm22x_ns.class_( + "EnrollmentFailedTrigger", automation.Trigger.template(cg.uint8) +) + +EnrollmentAction = hlk_fm22x_ns.class_("EnrollmentAction", automation.Action) +DeleteAction = hlk_fm22x_ns.class_("DeleteAction", automation.Action) +DeleteAllAction = hlk_fm22x_ns.class_("DeleteAllAction", automation.Action) +ScanAction = hlk_fm22x_ns.class_("ScanAction", automation.Action) +ResetAction = hlk_fm22x_ns.class_("ResetAction", automation.Action) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HlkFm22xComponent), + cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FaceScanMatchedTrigger + ), + } + ), + cv.Optional(CONF_ON_FACE_SCAN_UNMATCHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FaceScanUnmatchedTrigger + ), + } + ), + cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FaceScanInvalidTrigger + ), + } + ), + cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FaceInfoTrigger), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentDoneTrigger + ), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentFailedTrigger + ), + } + ), + } + ) + .extend(cv.polling_component_schema("50ms")) + .extend(uart.UART_DEVICE_SCHEMA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + for conf in config.get(CONF_ON_FACE_SCAN_MATCHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.int16, "face_id"), (cg.std_string, "name")], conf + ) + + for conf in config.get(CONF_ON_FACE_SCAN_UNMATCHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_FACE_SCAN_INVALID, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.uint8, "error")], conf) + + for conf in config.get(CONF_ON_FACE_INFO, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, + [ + (cg.int16, "status"), + (cg.int16, "left"), + (cg.int16, "top"), + (cg.int16, "right"), + (cg.int16, "bottom"), + (cg.int16, "yaw"), + (cg.int16, "pitch"), + (cg.int16, "roll"), + ], + conf, + ) + + for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.int16, "face_id"), (cg.uint8, "direction")], conf + ) + + for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.uint8, "error")], conf) + + +@automation.register_action( + "hlk_fm22x.enroll", + EnrollmentAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + cv.Required(CONF_NAME): cv.templatable(cv.string), + cv.Required(CONF_DIRECTION): cv.templatable(cv.uint8_t), + }, + key=CONF_NAME, + ), +) +async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_ = await cg.templatable(config[CONF_NAME], args, cg.std_string) + cg.add(var.set_name(template_)) + template_ = await cg.templatable(config[CONF_DIRECTION], args, cg.uint8) + cg.add(var.set_direction(template_)) + return var + + +@automation.register_action( + "hlk_fm22x.delete", + DeleteAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + cv.Required(CONF_FACE_ID): cv.templatable(cv.uint16_t), + }, + key=CONF_FACE_ID, + ), +) +async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + template_ = await cg.templatable(config[CONF_FACE_ID], args, cg.int16) + cg.add(var.set_face_id(template_)) + return var + + +@automation.register_action( + "hlk_fm22x.delete_all", + DeleteAllAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + } + ), +) +async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "hlk_fm22x.scan", + ScanAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + } + ), +) +async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "hlk_fm22x.reset", + ResetAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + } + ), +) +async def hlk_fm22x_reset_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/hlk_fm22x/binary_sensor.py b/esphome/components/hlk_fm22x/binary_sensor.py new file mode 100644 index 00000000000..3620f33ac0d --- /dev/null +++ b/esphome/components/hlk_fm22x/binary_sensor.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import CONF_ICON, ICON_KEY_PLUS + +from . import CONF_HLK_FM22X_ID, HlkFm22xComponent + +DEPENDENCIES = ["hlk_fm22x"] + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend( + { + cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent), + cv.Optional(CONF_ICON, default=ICON_KEY_PLUS): cv.icon, + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_HLK_FM22X_ID]) + var = await binary_sensor.new_binary_sensor(config) + cg.add(hub.set_enrolling_binary_sensor(var)) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp new file mode 100644 index 00000000000..ab15a2340d3 --- /dev/null +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -0,0 +1,325 @@ +#include "hlk_fm22x.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include +#include + +namespace esphome::hlk_fm22x { + +static const char *const TAG = "hlk_fm22x"; + +void HlkFm22xComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); + this->set_enrolling_(false); + while (this->available()) { + this->read(); + } + this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); }); +} + +void HlkFm22xComponent::update() { + if (this->active_command_ != HlkFm22xCommand::NONE) { + if (this->wait_cycles_ > 600) { + ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_); + if (HlkFm22xCommand::RESET == this->active_command_) { + this->mark_failed(); + } else { + this->reset(); + } + } + } + this->recv_command_(); +} + +void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) { + if (name.length() > 31) { + ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str()); + return; + } + ESP_LOGI(TAG, "Starting enrollment for %s", name.c_str()); + std::array data{}; + data[0] = 0; // admin + std::copy(name.begin(), name.end(), data.begin() + 1); + // Remaining bytes are already zero-initialized + data[33] = (uint8_t) direction; + data[34] = 10; // timeout + this->send_command_(HlkFm22xCommand::ENROLL, data.data(), data.size()); + this->set_enrolling_(true); +} + +void HlkFm22xComponent::scan_face() { + ESP_LOGI(TAG, "Verify face"); + static const uint8_t DATA[] = {0, 0}; + this->send_command_(HlkFm22xCommand::VERIFY, DATA, sizeof(DATA)); +} + +void HlkFm22xComponent::delete_face(int16_t face_id) { + ESP_LOGI(TAG, "Deleting face in slot %d", face_id); + const uint8_t data[] = {(uint8_t) (face_id >> 8), (uint8_t) (face_id & 0xFF)}; + this->send_command_(HlkFm22xCommand::DELETE_FACE, data, sizeof(data)); +} + +void HlkFm22xComponent::delete_all_faces() { + ESP_LOGI(TAG, "Deleting all stored faces"); + this->send_command_(HlkFm22xCommand::DELETE_ALL_FACES); +} + +void HlkFm22xComponent::get_face_count_() { + ESP_LOGD(TAG, "Getting face count"); + this->send_command_(HlkFm22xCommand::GET_ALL_FACE_IDS); +} + +void HlkFm22xComponent::reset() { + ESP_LOGI(TAG, "Resetting module"); + this->active_command_ = HlkFm22xCommand::NONE; + this->wait_cycles_ = 0; + this->set_enrolling_(false); + this->send_command_(HlkFm22xCommand::RESET); +} + +void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *data, size_t size) { + ESP_LOGV(TAG, "Send command: 0x%.2X", command); + if (this->active_command_ != HlkFm22xCommand::NONE) { + ESP_LOGW(TAG, "Command 0x%.2X already active", this->active_command_); + return; + } + this->wait_cycles_ = 0; + this->active_command_ = command; + while (this->available()) + this->read(); + this->write((uint8_t) (START_CODE >> 8)); + this->write((uint8_t) (START_CODE & 0xFF)); + this->write((uint8_t) command); + uint16_t data_size = size; + this->write((uint8_t) (data_size >> 8)); + this->write((uint8_t) (data_size & 0xFF)); + + uint8_t checksum = 0; + checksum ^= (uint8_t) command; + checksum ^= (data_size >> 8); + checksum ^= (data_size & 0xFF); + for (size_t i = 0; i < size; i++) { + this->write(data[i]); + checksum ^= data[i]; + } + + this->write(checksum); + this->active_command_ = command; + this->wait_cycles_ = 0; +} + +void HlkFm22xComponent::recv_command_() { + uint8_t byte, checksum = 0; + uint16_t length = 0; + + if (this->available() < 7) { + ++this->wait_cycles_; + return; + } + this->wait_cycles_ = 0; + + if ((this->read() != (uint8_t) (START_CODE >> 8)) || (this->read() != (uint8_t) (START_CODE & 0xFF))) { + ESP_LOGE(TAG, "Invalid start code"); + return; + } + + byte = this->read(); + checksum ^= byte; + HlkFm22xResponseType response_type = (HlkFm22xResponseType) byte; + + byte = this->read(); + checksum ^= byte; + length = byte << 8; + byte = this->read(); + checksum ^= byte; + length |= byte; + + std::vector data; + data.reserve(length); + for (uint16_t idx = 0; idx < length; ++idx) { + byte = this->read(); + checksum ^= byte; + data.push_back(byte); + } + + ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty(data).c_str()); + + byte = this->read(); + if (byte != checksum) { + ESP_LOGE(TAG, "Invalid checksum for data. Calculated: 0x%.2X, Received: 0x%.2X", checksum, byte); + return; + } + switch (response_type) { + case HlkFm22xResponseType::NOTE: + this->handle_note_(data); + break; + case HlkFm22xResponseType::REPLY: + this->handle_reply_(data); + break; + default: + ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type); + break; + } +} + +void HlkFm22xComponent::handle_note_(const std::vector &data) { + switch (data[0]) { + case HlkFm22xNoteType::FACE_STATE: + if (data.size() < 17) { + ESP_LOGE(TAG, "Invalid face note data size: %u", data.size()); + break; + } + { + int16_t info[8]; + uint8_t offset = 1; + for (int16_t &i : info) { + i = ((int16_t) data[offset + 1] << 8) | data[offset]; + offset += 2; + } + ESP_LOGV(TAG, "Face state: status: %d, left: %d, top: %d, right: %d, bottom: %d, yaw: %d, pitch: %d, roll: %d", + info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]); + this->face_info_callback_.call(info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]); + } + break; + case HlkFm22xNoteType::READY: + ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_); + switch (this->active_command_) { + case HlkFm22xCommand::ENROLL: + this->set_enrolling_(false); + this->enrollment_failed_callback_.call(HlkFm22xResult::FAILED4_TIMEOUT); + break; + case HlkFm22xCommand::VERIFY: + this->face_scan_invalid_callback_.call(HlkFm22xResult::FAILED4_TIMEOUT); + break; + default: + break; + } + this->active_command_ = HlkFm22xCommand::NONE; + this->wait_cycles_ = 0; + break; + default: + ESP_LOGW(TAG, "Unhandled note: 0x%.2X", data[0]); + break; + } +} + +void HlkFm22xComponent::handle_reply_(const std::vector &data) { + auto expected = this->active_command_; + this->active_command_ = HlkFm22xCommand::NONE; + if (data[0] != (uint8_t) expected) { + ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]); + return; + } + + if (data[1] != HlkFm22xResult::SUCCESS) { + ESP_LOGE(TAG, "Command <0x%.2X> failed. Error: 0x%.2X", data[0], data[1]); + switch (expected) { + case HlkFm22xCommand::ENROLL: + this->set_enrolling_(false); + this->enrollment_failed_callback_.call(data[1]); + break; + case HlkFm22xCommand::VERIFY: + if (data[1] == HlkFm22xResult::REJECTED) { + this->face_scan_unmatched_callback_.call(); + } else { + this->face_scan_invalid_callback_.call(data[1]); + } + break; + default: + break; + } + return; + } + switch (expected) { + case HlkFm22xCommand::VERIFY: { + int16_t face_id = ((int16_t) data[2] << 8) | data[3]; + std::string name(data.begin() + 4, data.begin() + 36); + ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str()); + if (this->last_face_id_sensor_ != nullptr) { + this->last_face_id_sensor_->publish_state(face_id); + } + if (this->last_face_name_text_sensor_ != nullptr) { + this->last_face_name_text_sensor_->publish_state(name); + } + this->face_scan_matched_callback_.call(face_id, name); + break; + } + case HlkFm22xCommand::ENROLL: { + int16_t face_id = ((int16_t) data[2] << 8) | data[3]; + HlkFm22xFaceDirection direction = (HlkFm22xFaceDirection) data[4]; + ESP_LOGI(TAG, "Face enrolled. ID: %d, Direction: 0x%.2X", face_id, direction); + this->enrollment_done_callback_.call(face_id, (uint8_t) direction); + this->set_enrolling_(false); + this->defer([this]() { this->get_face_count_(); }); + break; + } + case HlkFm22xCommand::GET_STATUS: + if (this->status_sensor_ != nullptr) { + this->status_sensor_->publish_state(data[2]); + } + this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); }); + break; + case HlkFm22xCommand::GET_VERSION: + if (this->version_text_sensor_ != nullptr) { + std::string version(data.begin() + 2, data.end()); + this->version_text_sensor_->publish_state(version); + } + this->defer([this]() { this->get_face_count_(); }); + break; + case HlkFm22xCommand::GET_ALL_FACE_IDS: + if (this->face_count_sensor_ != nullptr) { + this->face_count_sensor_->publish_state(data[2]); + } + break; + case HlkFm22xCommand::DELETE_FACE: + ESP_LOGI(TAG, "Deleted face"); + break; + case HlkFm22xCommand::DELETE_ALL_FACES: + ESP_LOGI(TAG, "Deleted all faces"); + break; + case HlkFm22xCommand::RESET: + ESP_LOGI(TAG, "Module reset"); + this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); }); + break; + default: + ESP_LOGW(TAG, "Unhandled command: 0x%.2X", this->active_command_); + break; + } +} + +void HlkFm22xComponent::set_enrolling_(bool enrolling) { + if (this->enrolling_binary_sensor_ != nullptr) { + this->enrolling_binary_sensor_->publish_state(enrolling); + } +} + +void HlkFm22xComponent::dump_config() { + ESP_LOGCONFIG(TAG, "HLK_FM22X:"); + LOG_UPDATE_INTERVAL(this); + if (this->version_text_sensor_) { + LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %s", this->version_text_sensor_->get_state().c_str()); + } + if (this->enrolling_binary_sensor_) { + LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF"); + } + if (this->face_count_sensor_) { + LOG_SENSOR(" ", "Face Count", this->face_count_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (uint16_t) this->face_count_sensor_->get_state()); + } + if (this->status_sensor_) { + LOG_SENSOR(" ", "Status", this->status_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (uint8_t) this->status_sensor_->get_state()); + } + if (this->last_face_id_sensor_) { + LOG_SENSOR(" ", "Last Face ID", this->last_face_id_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (int16_t) this->last_face_id_sensor_->get_state()); + } + if (this->last_face_name_text_sensor_) { + LOG_TEXT_SENSOR(" ", "Last Face Name", this->last_face_name_text_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %s", this->last_face_name_text_sensor_->get_state().c_str()); + } +} + +} // namespace esphome::hlk_fm22x diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h new file mode 100644 index 00000000000..5ecc715ea17 --- /dev/null +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -0,0 +1,224 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/uart/uart.h" + +#include +#include + +namespace esphome::hlk_fm22x { + +static const uint16_t START_CODE = 0xEFAA; +enum HlkFm22xCommand { + NONE = 0x00, + RESET = 0x10, + GET_STATUS = 0x11, + VERIFY = 0x12, + ENROLL = 0x13, + DELETE_FACE = 0x20, + DELETE_ALL_FACES = 0x21, + GET_ALL_FACE_IDS = 0x24, + GET_VERSION = 0x30, + GET_SERIAL_NUMBER = 0x93, +}; + +enum HlkFm22xResponseType { + REPLY = 0x00, + NOTE = 0x01, + IMAGE = 0x02, +}; + +enum HlkFm22xNoteType { + READY = 0x00, + FACE_STATE = 0x01, +}; + +enum HlkFm22xResult { + SUCCESS = 0x00, + REJECTED = 0x01, + ABORTED = 0x02, + FAILED4_CAMERA = 0x04, + FAILED4_UNKNOWNREASON = 0x05, + FAILED4_INVALIDPARAM = 0x06, + FAILED4_NOMEMORY = 0x07, + FAILED4_UNKNOWNUSER = 0x08, + FAILED4_MAXUSER = 0x09, + FAILED4_FACEENROLLED = 0x0A, + FAILED4_LIVENESSCHECK = 0x0C, + FAILED4_TIMEOUT = 0x0D, + FAILED4_AUTHORIZATION = 0x0E, + FAILED4_READ_FILE = 0x13, + FAILED4_WRITE_FILE = 0x14, + FAILED4_NO_ENCRYPT = 0x15, + FAILED4_NO_RGBIMAGE = 0x17, + FAILED4_JPGPHOTO_LARGE = 0x18, + FAILED4_JPGPHOTO_SMALL = 0x19, +}; + +enum HlkFm22xFaceDirection { + FACE_DIRECTION_UNDEFINED = 0x00, + FACE_DIRECTION_MIDDLE = 0x01, + FACE_DIRECTION_RIGHT = 0x02, + FACE_DIRECTION_LEFT = 0x04, + FACE_DIRECTION_DOWN = 0x08, + FACE_DIRECTION_UP = 0x10, +}; + +class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + void set_face_count_sensor(sensor::Sensor *face_count_sensor) { this->face_count_sensor_ = face_count_sensor; } + void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; } + void set_last_face_id_sensor(sensor::Sensor *last_face_id_sensor) { + this->last_face_id_sensor_ = last_face_id_sensor; + } + void set_last_face_name_text_sensor(text_sensor::TextSensor *last_face_name_text_sensor) { + this->last_face_name_text_sensor_ = last_face_name_text_sensor; + } + void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) { + this->enrolling_binary_sensor_ = enrolling_binary_sensor; + } + void set_version_text_sensor(text_sensor::TextSensor *version_text_sensor) { + this->version_text_sensor_ = version_text_sensor; + } + void add_on_face_scan_matched_callback(std::function callback) { + this->face_scan_matched_callback_.add(std::move(callback)); + } + void add_on_face_scan_unmatched_callback(std::function callback) { + this->face_scan_unmatched_callback_.add(std::move(callback)); + } + void add_on_face_scan_invalid_callback(std::function callback) { + this->face_scan_invalid_callback_.add(std::move(callback)); + } + void add_on_face_info_callback( + std::function callback) { + this->face_info_callback_.add(std::move(callback)); + } + void add_on_enrollment_done_callback(std::function callback) { + this->enrollment_done_callback_.add(std::move(callback)); + } + void add_on_enrollment_failed_callback(std::function callback) { + this->enrollment_failed_callback_.add(std::move(callback)); + } + + void enroll_face(const std::string &name, HlkFm22xFaceDirection direction); + void scan_face(); + void delete_face(int16_t face_id); + void delete_all_faces(); + void reset(); + + protected: + void get_face_count_(); + void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0); + void recv_command_(); + void handle_note_(const std::vector &data); + void handle_reply_(const std::vector &data); + void set_enrolling_(bool enrolling); + + HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE; + uint16_t wait_cycles_ = 0; + sensor::Sensor *face_count_sensor_{nullptr}; + sensor::Sensor *status_sensor_{nullptr}; + sensor::Sensor *last_face_id_sensor_{nullptr}; + binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr}; + text_sensor::TextSensor *last_face_name_text_sensor_{nullptr}; + text_sensor::TextSensor *version_text_sensor_{nullptr}; + CallbackManager face_scan_invalid_callback_; + CallbackManager face_scan_matched_callback_; + CallbackManager face_scan_unmatched_callback_; + CallbackManager face_info_callback_; + CallbackManager enrollment_done_callback_; + CallbackManager enrollment_failed_callback_; +}; + +class FaceScanMatchedTrigger : public Trigger { + public: + explicit FaceScanMatchedTrigger(HlkFm22xComponent *parent) { + parent->add_on_face_scan_matched_callback( + [this](int16_t face_id, const std::string &name) { this->trigger(face_id, name); }); + } +}; + +class FaceScanUnmatchedTrigger : public Trigger<> { + public: + explicit FaceScanUnmatchedTrigger(HlkFm22xComponent *parent) { + parent->add_on_face_scan_unmatched_callback([this]() { this->trigger(); }); + } +}; + +class FaceScanInvalidTrigger : public Trigger { + public: + explicit FaceScanInvalidTrigger(HlkFm22xComponent *parent) { + parent->add_on_face_scan_invalid_callback([this](uint8_t error) { this->trigger(error); }); + } +}; + +class FaceInfoTrigger : public Trigger { + public: + explicit FaceInfoTrigger(HlkFm22xComponent *parent) { + parent->add_on_face_info_callback( + [this](int16_t status, int16_t left, int16_t top, int16_t right, int16_t bottom, int16_t yaw, int16_t pitch, + int16_t roll) { this->trigger(status, left, top, right, bottom, yaw, pitch, roll); }); + } +}; + +class EnrollmentDoneTrigger : public Trigger { + public: + explicit EnrollmentDoneTrigger(HlkFm22xComponent *parent) { + parent->add_on_enrollment_done_callback( + [this](int16_t face_id, uint8_t direction) { this->trigger(face_id, direction); }); + } +}; + +class EnrollmentFailedTrigger : public Trigger { + public: + explicit EnrollmentFailedTrigger(HlkFm22xComponent *parent) { + parent->add_on_enrollment_failed_callback([this](uint8_t error) { this->trigger(error); }); + } +}; + +template class EnrollmentAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(std::string, name) + TEMPLATABLE_VALUE(uint8_t, direction) + + void play(Ts... x) override { + auto name = this->name_.value(x...); + auto direction = (HlkFm22xFaceDirection) this->direction_.value(x...); + this->parent_->enroll_face(name, direction); + } +}; + +template class DeleteAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(int16_t, face_id) + + void play(Ts... x) override { + auto face_id = this->face_id_.value(x...); + this->parent_->delete_face(face_id); + } +}; + +template class DeleteAllAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->delete_all_faces(); } +}; + +template class ScanAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->scan_face(); } +}; + +template class ResetAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->reset(); } +}; + +} // namespace esphome::hlk_fm22x diff --git a/esphome/components/hlk_fm22x/sensor.py b/esphome/components/hlk_fm22x/sensor.py new file mode 100644 index 00000000000..e14b45599f1 --- /dev/null +++ b/esphome/components/hlk_fm22x/sensor.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import CONF_STATUS, ENTITY_CATEGORY_DIAGNOSTIC, ICON_ACCOUNT + +from . import CONF_HLK_FM22X_ID, HlkFm22xComponent + +DEPENDENCIES = ["hlk_fm22x"] + +CONF_FACE_COUNT = "face_count" +CONF_LAST_FACE_ID = "last_face_id" +ICON_FACE = "mdi:face-recognition" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent), + cv.Optional(CONF_FACE_COUNT): sensor.sensor_schema( + icon=ICON_FACE, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_STATUS): sensor.sensor_schema( + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_LAST_FACE_ID): sensor.sensor_schema( + icon=ICON_ACCOUNT, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_HLK_FM22X_ID]) + + for key in [ + CONF_FACE_COUNT, + CONF_STATUS, + CONF_LAST_FACE_ID, + ]: + if key not in config: + continue + conf = config[key] + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/hlk_fm22x/text_sensor.py b/esphome/components/hlk_fm22x/text_sensor.py new file mode 100644 index 00000000000..06da61c8b3a --- /dev/null +++ b/esphome/components/hlk_fm22x/text_sensor.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_VERSION, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_ACCOUNT, + ICON_RESTART, +) + +from . import CONF_HLK_FM22X_ID, HlkFm22xComponent + +DEPENDENCIES = ["hlk_fm22x"] + +CONF_LAST_FACE_NAME = "last_face_name" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent), + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + icon=ICON_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_LAST_FACE_NAME): text_sensor.text_sensor_schema( + icon=ICON_ACCOUNT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_HLK_FM22X_ID]) + for key in [ + CONF_VERSION, + CONF_LAST_FACE_NAME, + ]: + if key not in config: + continue + conf = config[key] + sens = await text_sensor.new_text_sensor(conf) + cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) diff --git a/tests/components/hlk_fm22x/test.esp32-idf.yaml b/tests/components/hlk_fm22x/test.esp32-idf.yaml new file mode 100644 index 00000000000..5e7cbde6649 --- /dev/null +++ b/tests/components/hlk_fm22x/test.esp32-idf.yaml @@ -0,0 +1,47 @@ +esphome: + on_boot: + then: + - hlk_fm22x.enroll: + name: "Test" + direction: 1 + - hlk_fm22x.delete_all: + +uart: + - id: uart_hlk_fm22x + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + +hlk_fm22x: + on_face_scan_matched: + - logger.log: test_hlk_22x_face_scan_matched + on_face_scan_unmatched: + - logger.log: test_hlk_22x_face_scan_unmatched + on_face_scan_invalid: + - logger.log: test_hlk_22x_face_scan_invalid + on_face_info: + - logger.log: test_hlk_22x_face_info + on_enrollment_done: + - logger.log: test_hlk_22x_enrollment_done + on_enrollment_failed: + - logger.log: test_hlk_22x_enrollment_failed + +sensor: + - platform: hlk_fm22x + face_count: + name: "Face Count" + last_face_id: + name: "Last Face ID" + status: + name: "Face Status" + +binary_sensor: + - platform: hlk_fm22x + name: "Face Enrolling" + +text_sensor: + - platform: hlk_fm22x + version: + name: "HLK Version" + last_face_name: + name: "Last Face Name" diff --git a/tests/components/hlk_fm22x/test.esp8266-ard.yaml b/tests/components/hlk_fm22x/test.esp8266-ard.yaml new file mode 100644 index 00000000000..680047834cd --- /dev/null +++ b/tests/components/hlk_fm22x/test.esp8266-ard.yaml @@ -0,0 +1,47 @@ +esphome: + on_boot: + then: + - hlk_fm22x.enroll: + name: "Test" + direction: 1 + - hlk_fm22x.delete_all: + +uart: + - id: uart_hlk_fm22x + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +hlk_fm22x: + on_face_scan_matched: + - logger.log: test_hlk_22x_face_scan_matched + on_face_scan_unmatched: + - logger.log: test_hlk_22x_face_scan_unmatched + on_face_scan_invalid: + - logger.log: test_hlk_22x_face_scan_invalid + on_face_info: + - logger.log: test_hlk_22x_face_info + on_enrollment_done: + - logger.log: test_hlk_22x_enrollment_done + on_enrollment_failed: + - logger.log: test_hlk_22x_enrollment_failed + +sensor: + - platform: hlk_fm22x + face_count: + name: "Face Count" + last_face_id: + name: "Last Face ID" + status: + name: "Face Status" + +binary_sensor: + - platform: hlk_fm22x + name: "Face Enrolling" + +text_sensor: + - platform: hlk_fm22x + version: + name: "HLK Version" + last_face_name: + name: "Last Face Name" diff --git a/tests/components/hlk_fm22x/test.rp2040-ard.yaml b/tests/components/hlk_fm22x/test.rp2040-ard.yaml new file mode 100644 index 00000000000..680047834cd --- /dev/null +++ b/tests/components/hlk_fm22x/test.rp2040-ard.yaml @@ -0,0 +1,47 @@ +esphome: + on_boot: + then: + - hlk_fm22x.enroll: + name: "Test" + direction: 1 + - hlk_fm22x.delete_all: + +uart: + - id: uart_hlk_fm22x + tx_pin: 4 + rx_pin: 5 + baud_rate: 115200 + +hlk_fm22x: + on_face_scan_matched: + - logger.log: test_hlk_22x_face_scan_matched + on_face_scan_unmatched: + - logger.log: test_hlk_22x_face_scan_unmatched + on_face_scan_invalid: + - logger.log: test_hlk_22x_face_scan_invalid + on_face_info: + - logger.log: test_hlk_22x_face_info + on_enrollment_done: + - logger.log: test_hlk_22x_enrollment_done + on_enrollment_failed: + - logger.log: test_hlk_22x_enrollment_failed + +sensor: + - platform: hlk_fm22x + face_count: + name: "Face Count" + last_face_id: + name: "Last Face ID" + status: + name: "Face Status" + +binary_sensor: + - platform: hlk_fm22x + name: "Face Enrolling" + +text_sensor: + - platform: hlk_fm22x + version: + name: "HLK Version" + last_face_name: + name: "Last Face Name" From f32b69b8f15bb0e4208854c75f8283c51b515ac1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 10:00:42 -0600 Subject: [PATCH 306/461] [tests] Add unit test coverage for web_port property (#11811) --- tests/unit_tests/test_core.py | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 92b60efd93f..e52cb248319 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -670,3 +670,51 @@ class TestEsphomeCore: os.environ.pop("ESPHOME_IS_HA_ADDON", None) os.environ.pop("ESPHOME_DATA_DIR", None) assert target.data_dir == Path(expected_default) + + def test_web_port__none(self, target): + """Test web_port returns None when web_server is not configured.""" + target.config = {} + assert target.web_port is None + + def test_web_port__explicit_web_server_default_port(self, target): + """Test web_port returns 80 when web_server is explicitly configured without port.""" + target.config = {const.CONF_WEB_SERVER: {}} + assert target.web_port == 80 + + def test_web_port__explicit_web_server_custom_port(self, target): + """Test web_port returns custom port when web_server is configured with port.""" + target.config = {const.CONF_WEB_SERVER: {const.CONF_PORT: 8080}} + assert target.web_port == 8080 + + def test_web_port__ota_web_server_platform_only(self, target): + """ + Test web_port returns None when ota.web_server platform is explicitly configured. + + This is a critical test for Dashboard Issue #766: + https://github.com/esphome/dashboard/issues/766 + + When ota: platform: web_server is explicitly configured (or auto-loaded by captive_portal): + - "web_server" appears in loaded_integrations (platform name added to integrations) + - "ota/web_server" appears in loaded_platforms + - But CONF_WEB_SERVER is NOT in config (only the platform is loaded, not the component) + - web_port MUST return None (no web UI available) + - Dashboard should NOT show VISIT button + + This test ensures web_port only checks CONF_WEB_SERVER in config, not loaded_integrations. + """ + # Simulate config with ota.web_server platform but no web_server component + # This happens when: + # 1. User explicitly configures: ota: - platform: web_server + # 2. OR captive_portal auto-loads ota.web_server + target.config = { + const.CONF_OTA: [ + { + "platform": "web_server", + # OTA web_server platform config would be here + } + ], + # Note: CONF_WEB_SERVER is NOT in config - only the OTA platform + } + # Even though "web_server" is in loaded_integrations due to the platform, + # web_port must return None because the full web_server component is not configured + assert target.web_port is None From f84cdad93cc8361bf18f9819e2f3b3acfa9ac446 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 12:50:17 -0600 Subject: [PATCH 307/461] [wifi] Add min_auth_mode configuration option --- esphome/components/wifi/__init__.py | 39 +++++++++++++++++++ esphome/components/wifi/wifi_component.h | 8 ++++ .../wifi/wifi_component_esp8266.cpp | 13 ++++++- .../wifi/wifi_component_esp_idf.cpp | 15 +++++-- tests/components/wifi/test.esp32-idf.yaml | 1 + tests/components/wifi/test.esp8266-ard.yaml | 3 ++ 6 files changed, 74 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 5f4190a9331..146e6eb78f5 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,3 +1,5 @@ +import logging + from esphome import automation from esphome.automation import Condition import esphome.codegen as cg @@ -42,6 +44,7 @@ from esphome.const import ( CONF_TTLS_PHASE_2, CONF_USE_ADDRESS, CONF_USERNAME, + Platform, PlatformFramework, ) from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority @@ -49,10 +52,13 @@ import esphome.final_validate as fv from . import wpa2_eap +_LOGGER = logging.getLogger(__name__) + AUTO_LOAD = ["network"] NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" +CONF_MIN_AUTH_MODE = "min_auth_mode" # Maximum number of WiFi networks that can be configured # Limited to 127 because selected_sta_index_ is int8_t in C++ @@ -70,6 +76,13 @@ WIFI_POWER_SAVE_MODES = { "LIGHT": WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT, "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, } + +WiFiAuthMode = wifi_ns.enum("WiFiAuthMode") +WIFI_AUTH_MODES = { + "WPA": WiFiAuthMode.WIFI_AUTH_MODE_WPA, + "WPA2": WiFiAuthMode.WIFI_AUTH_MODE_WPA2, + "WPA3": WiFiAuthMode.WIFI_AUTH_MODE_WPA3, +} WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) @@ -187,6 +200,25 @@ def validate_variant(_): raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}") +def _apply_min_auth_mode_default(config): + """Apply platform-specific default for min_auth_mode and warn ESP8266 users.""" + if CONF_MIN_AUTH_MODE not in config: + if CORE.is_esp8266: + _LOGGER.warning( + "The minimum WiFi authentication mode (min_auth_mode) is not set. " + "This controls the weakest encryption your device will accept when connecting to WiFi. " + "Currently defaults to WPA (less secure), but will change to WPA2 (more secure) in 2026.6.0. " + "WPA uses TKIP encryption which has known security vulnerabilities and should be avoided. " + "WPA2 uses AES encryption which is significantly more secure. " + "If your router supports WPA2 or WPA3, no action is needed - the new default will be more secure. " + "If your router only supports WPA, explicitly set 'min_auth_mode: WPA' to maintain compatibility." + ) + config[CONF_MIN_AUTH_MODE] = "WPA" + elif CORE.is_esp32: + config[CONF_MIN_AUTH_MODE] = "WPA2" + return config + + def final_validate(config): has_sta = bool(config.get(CONF_NETWORKS, True)) has_ap = CONF_AP in config @@ -287,6 +319,10 @@ CONFIG_SCHEMA = cv.All( ): cv.enum(WIFI_POWER_SAVE_MODES, upper=True), cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, + cv.Optional(CONF_MIN_AUTH_MODE): cv.All( + cv.enum(WIFI_AUTH_MODES, upper=True), + cv.only_on([Platform.ESP32, Platform.ESP8266]), + ), cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All( cv.decibel, cv.float_range(min=8.5, max=20.5) ), @@ -311,6 +347,7 @@ CONFIG_SCHEMA = cv.All( ), } ), + _apply_min_auth_mode_default, _validate, ) @@ -420,6 +457,8 @@ async def to_code(config): cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) + if CONF_MIN_AUTH_MODE in config: + cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE])) if config[CONF_FAST_CONNECT]: cg.add_define("USE_WIFI_FAST_CONNECT") cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN])) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index cb75edf5a03..11c6b50f0c7 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -207,6 +207,12 @@ enum WiFiPowerSaveMode : uint8_t { WIFI_POWER_SAVE_HIGH, }; +enum WiFiAuthMode : uint8_t { + WIFI_AUTH_MODE_WPA = 0, + WIFI_AUTH_MODE_WPA2, + WIFI_AUTH_MODE_WPA3, +}; + #ifdef USE_ESP32 struct IDFWiFiEvent; #endif @@ -258,6 +264,7 @@ class WiFiComponent : public Component { bool is_connected(); void set_power_save_mode(WiFiPowerSaveMode power_save); + void set_min_auth_mode(WiFiAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } void set_output_power(float output_power) { output_power_ = output_power; } void set_passive_scan(bool passive); @@ -443,6 +450,7 @@ class WiFiComponent : public Component { // Group all 8-bit values together WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; + WiFiAuthMode min_auth_mode_{WIFI_AUTH_MODE_WPA2}; uint8_t num_retried_{0}; // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 4e17c42f413..77fae1a0b05 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -258,8 +258,17 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ap.get_password().empty()) { conf.threshold.authmode = AUTH_OPEN; } else { - // Only allow auth modes with at least WPA - conf.threshold.authmode = AUTH_WPA_PSK; + // Set threshold based on configured minimum auth mode + // Note: ESP8266 doesn't support WPA3 + switch (this->min_auth_mode_) { + case WIFI_AUTH_MODE_WPA: + conf.threshold.authmode = AUTH_WPA_PSK; + break; + case WIFI_AUTH_MODE_WPA2: + case WIFI_AUTH_MODE_WPA3: // Fall back to WPA2 for ESP8266 + conf.threshold.authmode = AUTH_WPA2_PSK; + break; + } } conf.threshold.rssi = -127; #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 08ecba35987..1c10ca7399b 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -308,7 +308,18 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ap.get_password().empty()) { conf.sta.threshold.authmode = WIFI_AUTH_OPEN; } else { - conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK; + // Set threshold based on configured minimum auth mode + switch (this->min_auth_mode_) { + case WIFI_AUTH_MODE_WPA: + conf.sta.threshold.authmode = WIFI_AUTH_WPA_PSK; + break; + case WIFI_AUTH_MODE_WPA2: + conf.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; + break; + case WIFI_AUTH_MODE_WPA3: + conf.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK; + break; + } } #ifdef USE_WIFI_WPA2_EAP @@ -347,8 +358,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // The minimum rssi to accept in the fast scan mode conf.sta.threshold.rssi = -127; - conf.sta.threshold.authmode = WIFI_AUTH_OPEN; - wifi_config_t current_conf; esp_err_t err; err = esp_wifi_get_config(WIFI_IF_STA, ¤t_conf); diff --git a/tests/components/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml index 91e235b9ce2..827e4b17f72 100644 --- a/tests/components/wifi/test.esp32-idf.yaml +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -2,6 +2,7 @@ psram: wifi: use_psram: true + min_auth_mode: WPA packages: - !include common.yaml diff --git a/tests/components/wifi/test.esp8266-ard.yaml b/tests/components/wifi/test.esp8266-ard.yaml index dade44d145b..5a9b201292c 100644 --- a/tests/components/wifi/test.esp8266-ard.yaml +++ b/tests/components/wifi/test.esp8266-ard.yaml @@ -1 +1,4 @@ +wifi: + min_auth_mode: WPA2 + <<: !include common.yaml From 5a67d2b20b07ca15bdab1f910100391643ac3df9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 13:00:52 -0600 Subject: [PATCH 308/461] fixes --- tests/components/wifi/test.esp8266-ard.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/wifi/test.esp8266-ard.yaml b/tests/components/wifi/test.esp8266-ard.yaml index 5a9b201292c..9cb0e3cf48e 100644 --- a/tests/components/wifi/test.esp8266-ard.yaml +++ b/tests/components/wifi/test.esp8266-ard.yaml @@ -1,4 +1,5 @@ wifi: min_auth_mode: WPA2 -<<: !include common.yaml +packages: + - !include common.yaml From 43eafbccb3f6a397e2107a005b37b8098f1880b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:28:14 -0600 Subject: [PATCH 309/461] Bump pytest-asyncio from 1.2.0 to 1.3.0 (#11815) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 81cb711eec9..f845c47fc0c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ pre-commit pytest==8.4.2 pytest-cov==7.0.0 pytest-mock==3.15.1 -pytest-asyncio==1.2.0 +pytest-asyncio==1.3.0 pytest-xdist==3.8.0 asyncmock==0.4.2 hypothesis==6.92.1 From 8c5b9647223f0fe5a83b0820c98c0c05aa89252d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:28:25 -0600 Subject: [PATCH 310/461] Bump pyupgrade from 3.21.0 to 3.21.1 (#11816) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index f845c47fc0c..169037753b6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pylint==4.0.2 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.14.4 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests From 8d284ea90c097f911a049b33861bfde5284a492c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 13:30:36 -0600 Subject: [PATCH 311/461] fixes --- esphome/components/wifi/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 146e6eb78f5..b4a02f9f153 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -202,7 +202,8 @@ def validate_variant(_): def _apply_min_auth_mode_default(config): """Apply platform-specific default for min_auth_mode and warn ESP8266 users.""" - if CONF_MIN_AUTH_MODE not in config: + # Only apply defaults for platforms that support min_auth_mode + if CONF_MIN_AUTH_MODE not in config and (CORE.is_esp8266 or CORE.is_esp32): if CORE.is_esp8266: _LOGGER.warning( "The minimum WiFi authentication mode (min_auth_mode) is not set. " From e46300828e661b2b88bd3a568a7d46b0092c5c31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:45:56 -0600 Subject: [PATCH 312/461] Bump pytest from 8.4.2 to 9.0.0 (#11817) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 169037753b6..35010ad52fc 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==8.4.2 +pytest==9.0.0 pytest-cov==7.0.0 pytest-mock==3.15.1 pytest-asyncio==1.3.0 From 3fd5e8737976f842f6575e60de73923c865b8f73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 13:51:16 -0600 Subject: [PATCH 313/461] fix namespace conflicts --- esphome/components/wifi/__init__.py | 12 ++++++------ esphome/components/wifi/wifi_component.h | 12 ++++++------ esphome/components/wifi/wifi_component_esp8266.cpp | 6 +++--- esphome/components/wifi/wifi_component_esp_idf.cpp | 6 +++--- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b4a02f9f153..a58e673a8ca 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -77,11 +77,11 @@ WIFI_POWER_SAVE_MODES = { "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, } -WiFiAuthMode = wifi_ns.enum("WiFiAuthMode") -WIFI_AUTH_MODES = { - "WPA": WiFiAuthMode.WIFI_AUTH_MODE_WPA, - "WPA2": WiFiAuthMode.WIFI_AUTH_MODE_WPA2, - "WPA3": WiFiAuthMode.WIFI_AUTH_MODE_WPA3, +WifiMinAuthMode = wifi_ns.enum("WifiMinAuthMode") +WIFI_MIN_AUTH_MODES = { + "WPA": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA, + "WPA2": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA2, + "WPA3": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA3, } WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) @@ -321,7 +321,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_MIN_AUTH_MODE): cv.All( - cv.enum(WIFI_AUTH_MODES, upper=True), + cv.enum(WIFI_MIN_AUTH_MODES, upper=True), cv.only_on([Platform.ESP32, Platform.ESP8266]), ), cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All( diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 11c6b50f0c7..2f9591854df 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -207,10 +207,10 @@ enum WiFiPowerSaveMode : uint8_t { WIFI_POWER_SAVE_HIGH, }; -enum WiFiAuthMode : uint8_t { - WIFI_AUTH_MODE_WPA = 0, - WIFI_AUTH_MODE_WPA2, - WIFI_AUTH_MODE_WPA3, +enum WifiMinAuthMode : uint8_t { + WIFI_MIN_AUTH_MODE_WPA = 0, + WIFI_MIN_AUTH_MODE_WPA2, + WIFI_MIN_AUTH_MODE_WPA3, }; #ifdef USE_ESP32 @@ -264,7 +264,7 @@ class WiFiComponent : public Component { bool is_connected(); void set_power_save_mode(WiFiPowerSaveMode power_save); - void set_min_auth_mode(WiFiAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } + void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } void set_output_power(float output_power) { output_power_ = output_power; } void set_passive_scan(bool passive); @@ -450,7 +450,7 @@ class WiFiComponent : public Component { // Group all 8-bit values together WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; - WiFiAuthMode min_auth_mode_{WIFI_AUTH_MODE_WPA2}; + WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; uint8_t num_retried_{0}; // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 77fae1a0b05..56e071404b0 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -261,11 +261,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // Set threshold based on configured minimum auth mode // Note: ESP8266 doesn't support WPA3 switch (this->min_auth_mode_) { - case WIFI_AUTH_MODE_WPA: + case WIFI_MIN_AUTH_MODE_WPA: conf.threshold.authmode = AUTH_WPA_PSK; break; - case WIFI_AUTH_MODE_WPA2: - case WIFI_AUTH_MODE_WPA3: // Fall back to WPA2 for ESP8266 + case WIFI_MIN_AUTH_MODE_WPA2: + case WIFI_MIN_AUTH_MODE_WPA3: // Fall back to WPA2 for ESP8266 conf.threshold.authmode = AUTH_WPA2_PSK; break; } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 1c10ca7399b..d3088c9a104 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -310,13 +310,13 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } else { // Set threshold based on configured minimum auth mode switch (this->min_auth_mode_) { - case WIFI_AUTH_MODE_WPA: + case WIFI_MIN_AUTH_MODE_WPA: conf.sta.threshold.authmode = WIFI_AUTH_WPA_PSK; break; - case WIFI_AUTH_MODE_WPA2: + case WIFI_MIN_AUTH_MODE_WPA2: conf.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; break; - case WIFI_AUTH_MODE_WPA3: + case WIFI_MIN_AUTH_MODE_WPA3: conf.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK; break; } From 23b8139d24e1139497b1643021797d294f16521a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 14:31:26 -0600 Subject: [PATCH 314/461] fix defaults --- esphome/components/wifi/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index a58e673a8ca..5856442877f 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -214,9 +214,9 @@ def _apply_min_auth_mode_default(config): "If your router supports WPA2 or WPA3, no action is needed - the new default will be more secure. " "If your router only supports WPA, explicitly set 'min_auth_mode: WPA' to maintain compatibility." ) - config[CONF_MIN_AUTH_MODE] = "WPA" + config[CONF_MIN_AUTH_MODE] = WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA elif CORE.is_esp32: - config[CONF_MIN_AUTH_MODE] = "WPA2" + config[CONF_MIN_AUTH_MODE] = WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA2 return config From d7cef22ddba757f6a3374a3df2a780ebcf24c6e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 14:33:11 -0600 Subject: [PATCH 315/461] fix defaults --- esphome/components/wifi/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 5856442877f..ac31b340f71 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -214,9 +214,9 @@ def _apply_min_auth_mode_default(config): "If your router supports WPA2 or WPA3, no action is needed - the new default will be more secure. " "If your router only supports WPA, explicitly set 'min_auth_mode: WPA' to maintain compatibility." ) - config[CONF_MIN_AUTH_MODE] = WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA + config[CONF_MIN_AUTH_MODE] = "WPA" elif CORE.is_esp32: - config[CONF_MIN_AUTH_MODE] = WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA2 + config[CONF_MIN_AUTH_MODE] = "WPA2" return config @@ -459,7 +459,7 @@ async def to_code(config): cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) if CONF_MIN_AUTH_MODE in config: - cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE])) + cg.add(var.set_min_auth_mode(WIFI_MIN_AUTH_MODES[config[CONF_MIN_AUTH_MODE]])) if config[CONF_FAST_CONNECT]: cg.add_define("USE_WIFI_FAST_CONNECT") cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN])) From f275a31c3acc1e3f5b3e3647f7691abf5bcf2791 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 14:37:54 -0600 Subject: [PATCH 316/461] preen --- esphome/components/wifi/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index ac31b340f71..aaccb3ceb53 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -83,6 +83,7 @@ WIFI_MIN_AUTH_MODES = { "WPA2": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA2, "WPA3": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA3, } +VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True) WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) @@ -214,9 +215,9 @@ def _apply_min_auth_mode_default(config): "If your router supports WPA2 or WPA3, no action is needed - the new default will be more secure. " "If your router only supports WPA, explicitly set 'min_auth_mode: WPA' to maintain compatibility." ) - config[CONF_MIN_AUTH_MODE] = "WPA" + config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA") elif CORE.is_esp32: - config[CONF_MIN_AUTH_MODE] = "WPA2" + config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA2") return config @@ -321,7 +322,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_MIN_AUTH_MODE): cv.All( - cv.enum(WIFI_MIN_AUTH_MODES, upper=True), + VALIDATE_WIFI_MIN_AUTH_MODE, cv.only_on([Platform.ESP32, Platform.ESP8266]), ), cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All( @@ -459,7 +460,7 @@ async def to_code(config): cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) if CONF_MIN_AUTH_MODE in config: - cg.add(var.set_min_auth_mode(WIFI_MIN_AUTH_MODES[config[CONF_MIN_AUTH_MODE]])) + cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE])) if config[CONF_FAST_CONNECT]: cg.add_define("USE_WIFI_FAST_CONNECT") cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN])) From 4964fdc1b0ad26b03387d8fa3d97e5bce452146b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 16:45:54 -0600 Subject: [PATCH 317/461] help --- esphome/components/wifi/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index aaccb3ceb53..6ff01ca9223 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -207,13 +207,15 @@ def _apply_min_auth_mode_default(config): if CONF_MIN_AUTH_MODE not in config and (CORE.is_esp8266 or CORE.is_esp32): if CORE.is_esp8266: _LOGGER.warning( - "The minimum WiFi authentication mode (min_auth_mode) is not set. " + "The minimum WiFi authentication mode (wifi -> min_auth_mode) is not set. " "This controls the weakest encryption your device will accept when connecting to WiFi. " "Currently defaults to WPA (less secure), but will change to WPA2 (more secure) in 2026.6.0. " "WPA uses TKIP encryption which has known security vulnerabilities and should be avoided. " "WPA2 uses AES encryption which is significantly more secure. " - "If your router supports WPA2 or WPA3, no action is needed - the new default will be more secure. " - "If your router only supports WPA, explicitly set 'min_auth_mode: WPA' to maintain compatibility." + "If your router supports WPA2 or WPA3, no action is needed - " + "the new default will be more secure. " + "If your router only supports WPA, add 'min_auth_mode: WPA' under 'wifi:' " + "in your configuration to maintain compatibility." ) config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA") elif CORE.is_esp32: From 4f411dc4f23dc3282c007f80661895e501160ee1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 16:47:42 -0600 Subject: [PATCH 318/461] help --- esphome/components/wifi/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 6ff01ca9223..0bd2e158e8f 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -212,10 +212,9 @@ def _apply_min_auth_mode_default(config): "Currently defaults to WPA (less secure), but will change to WPA2 (more secure) in 2026.6.0. " "WPA uses TKIP encryption which has known security vulnerabilities and should be avoided. " "WPA2 uses AES encryption which is significantly more secure. " - "If your router supports WPA2 or WPA3, no action is needed - " - "the new default will be more secure. " - "If your router only supports WPA, add 'min_auth_mode: WPA' under 'wifi:' " - "in your configuration to maintain compatibility." + "To silence this warning, explicitly set min_auth_mode under 'wifi:'. " + "If your router supports WPA2 or WPA3, set 'min_auth_mode: WPA2'. " + "If your router only supports WPA, set 'min_auth_mode: WPA'." ) config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA") elif CORE.is_esp32: From 40e2976ba2f46a58bb0fe220ce0c312d74a6586e Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Tue, 11 Nov 2025 00:33:34 +0100 Subject: [PATCH 319/461] [ai] simplify namespace syntax (#11824) --- .ai/instructions.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.ai/instructions.md b/.ai/instructions.md index 8d81c6cf0fc..681829bae6b 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -172,8 +172,7 @@ This document provides essential context for AI models interacting with this pro * **C++ Class Pattern:** ```cpp - namespace esphome { - namespace my_component { + namespace esphome::my_component { class MyComponent : public Component { public: @@ -189,8 +188,7 @@ This document provides essential context for AI models interacting with this pro int param_{0}; }; - } // namespace my_component - } // namespace esphome + } // namespace esphome::my_component ``` * **Common Component Examples:** From 0f8332fe3cd0680e9c98a04803f6b5884fe5ae47 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Mon, 10 Nov 2025 16:04:03 -0800 Subject: [PATCH 320/461] [lvgl] Automatically register widget types (#11394) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/lvgl/__init__.py | 103 ++++++-------------- esphome/components/lvgl/schemas.py | 4 + esphome/components/lvgl/types.py | 23 ++++- esphome/components/lvgl/widgets/__init__.py | 7 +- esphome/components/lvgl/widgets/spinbox.py | 16 +-- tests/components/lvgl/lvgl-package.yaml | 4 + 6 files changed, 64 insertions(+), 93 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 861999d0b74..4df68a6386c 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -1,6 +1,8 @@ +import importlib import logging +import pkgutil -from esphome.automation import build_automation, register_action, validate_automation +from esphome.automation import build_automation, validate_automation import esphome.codegen as cg from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING from esphome.components.display import Display @@ -25,8 +27,8 @@ from esphome.cpp_generator import MockObj from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed -from . import defines as df, helpers, lv_validation as lvalid -from .automation import disp_update, focused_widgets, refreshed_widgets, update_to_code +from . import defines as df, helpers, lv_validation as lvalid, widgets +from .automation import disp_update, focused_widgets, refreshed_widgets from .defines import add_define from .encoders import ( ENCODERS_CONFIG, @@ -45,7 +47,6 @@ from .schemas import ( WIDGET_TYPES, any_widget_schema, container_schema, - create_modify_schema, obj_schema, ) from .styles import add_top_layer, styles_to_code, theme_to_code @@ -54,7 +55,6 @@ from .trigger import add_on_boot_triggers, generate_triggers from .types import ( FontEngine, IdleTrigger, - ObjUpdateAction, PlainTrigger, lv_font_t, lv_group_t, @@ -69,33 +69,23 @@ from .widgets import ( set_obj_properties, styles_used, ) -from .widgets.animimg import animimg_spec -from .widgets.arc import arc_spec -from .widgets.button import button_spec -from .widgets.buttonmatrix import buttonmatrix_spec -from .widgets.canvas import canvas_spec -from .widgets.checkbox import checkbox_spec -from .widgets.container import container_spec -from .widgets.dropdown import dropdown_spec -from .widgets.img import img_spec -from .widgets.keyboard import keyboard_spec -from .widgets.label import label_spec -from .widgets.led import led_spec -from .widgets.line import line_spec -from .widgets.lv_bar import bar_spec -from .widgets.meter import meter_spec + +# Import only what we actually use directly in this file from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code -from .widgets.obj import obj_spec -from .widgets.page import add_pages, generate_page_triggers, page_spec -from .widgets.qrcode import qr_code_spec -from .widgets.roller import roller_spec -from .widgets.slider import slider_spec -from .widgets.spinbox import spinbox_spec -from .widgets.spinner import spinner_spec -from .widgets.switch import switch_spec -from .widgets.tabview import tabview_spec -from .widgets.textarea import textarea_spec -from .widgets.tileview import tileview_spec +from .widgets.obj import obj_spec # Used in LVGL_SCHEMA +from .widgets.page import ( # page_spec used in LVGL_SCHEMA + add_pages, + generate_page_triggers, + page_spec, +) + +# Widget registration happens via WidgetType.__init__ in individual widget files +# The imports below trigger creation of the widget types +# Action registration (lvgl.{widget}.update) happens automatically +# in the WidgetType.__init__ method + +for module_info in pkgutil.iter_modules(widgets.__path__): + importlib.import_module(f".widgets.{module_info.name}", package=__package__) DOMAIN = "lvgl" DEPENDENCIES = ["display"] @@ -103,41 +93,6 @@ AUTO_LOAD = ["key_provider"] CODEOWNERS = ["@clydebarrow"] LOGGER = logging.getLogger(__name__) -for w_type in ( - label_spec, - obj_spec, - button_spec, - bar_spec, - slider_spec, - arc_spec, - line_spec, - spinner_spec, - led_spec, - animimg_spec, - checkbox_spec, - img_spec, - switch_spec, - tabview_spec, - buttonmatrix_spec, - meter_spec, - dropdown_spec, - roller_spec, - textarea_spec, - spinbox_spec, - keyboard_spec, - tileview_spec, - qr_code_spec, - canvas_spec, - container_spec, -): - WIDGET_TYPES[w_type.name] = w_type - -for w_type in WIDGET_TYPES.values(): - register_action( - f"lvgl.{w_type.name}.update", - ObjUpdateAction, - create_modify_schema(w_type), - )(update_to_code) SIMPLE_TRIGGERS = ( df.CONF_ON_PAUSE, @@ -402,6 +357,15 @@ def add_hello_world(config): return config +def _theme_schema(value): + return cv.Schema( + { + cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) + for name, w in WIDGET_TYPES.items() + } + )(value) + + FINAL_VALIDATE_SCHEMA = final_validation LVGL_SCHEMA = cv.All( @@ -454,12 +418,7 @@ LVGL_SCHEMA = cv.All( cv.Optional( df.CONF_TRANSPARENCY_KEY, default=0x000400 ): lvalid.lv_color, - cv.Optional(df.CONF_THEME): cv.Schema( - { - cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) - for name, w in WIDGET_TYPES.items() - } - ), + cv.Optional(df.CONF_THEME): _theme_schema, cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA, cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema, cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG, diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 0dcf420f249..6b77f66abb5 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -411,6 +411,10 @@ def any_widget_schema(extras=None): Dynamically generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of widget under the widgets: key. + This uses lazy evaluation - the schema is built when called during validation, + not at import time. This allows external components to register widgets + before schema validation begins. + :param extras: Additional schema to be applied to each generated one :return: A validator for the Widgets key """ diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 8c33e139344..035320b6ac3 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,8 +1,10 @@ import sys from esphome import automation, codegen as cg +from esphome.automation import register_action from esphome.config_validation import Schema from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE +from esphome.core import EsphomeError from esphome.cpp_generator import MockObj, MockObjClass from esphome.cpp_types import esphome_ns @@ -124,13 +126,16 @@ class WidgetType: schema=None, modify_schema=None, lv_name=None, + is_mock: bool = False, ): """ :param name: The widget name, e.g. "bar" :param w_type: The C type of the widget :param parts: What parts this widget supports :param schema: The config schema for defining a widget - :param modify_schema: A schema to update the widget + :param modify_schema: A schema to update the widget, defaults to the same as the schema + :param lv_name: The name of the LVGL widget in the LVGL library, if different from the name + :param is_mock: Whether this widget is a mock widget, i.e. not a real LVGL widget """ self.name = name self.lv_name = lv_name or name @@ -146,6 +151,22 @@ class WidgetType: self.modify_schema = modify_schema self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") + # Local import to avoid circular import + from .automation import update_to_code + from .schemas import WIDGET_TYPES, create_modify_schema + + if not is_mock: + if self.name in WIDGET_TYPES: + raise EsphomeError(f"Duplicate definition of widget type '{self.name}'") + WIDGET_TYPES[self.name] = self + + # Register the update action automatically + register_action( + f"lvgl.{self.name}.update", + ObjUpdateAction, + create_modify_schema(self), + )(update_to_code) + @property def animated(self): return False diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 7d9f9cb7dec..187b5828c24 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -213,17 +213,14 @@ class LvScrActType(WidgetType): """ def __init__(self): - super().__init__("lv_scr_act()", lv_obj_t, ()) + super().__init__("lv_scr_act()", lv_obj_t, (), is_mock=True) async def to_code(self, w, config: dict): return [] -lv_scr_act_spec = LvScrActType() - - def get_scr_act(lv_comp: MockObj) -> Widget: - return Widget.create(None, lv_comp.get_scr_act(), lv_scr_act_spec, {}) + return Widget.create(None, lv_comp.get_scr_act(), LvScrActType(), {}) def get_widget_generator(wid): diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index 26ad149c6f9..ac23ded723f 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE -from ..automation import action_to_code, update_to_code +from ..automation import action_to_code from ..defines import ( CONF_CURSOR, CONF_DECIMAL_PLACES, @@ -171,17 +171,3 @@ async def spinbox_decrement(config, action_id, template_arg, args): lv.spinbox_decrement(w.obj) return await action_to_code(widgets, do_increment, action_id, template_arg, args) - - -@automation.register_action( - "lvgl.spinbox.update", - ObjUpdateAction, - cv.Schema( - { - cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), - cv.Required(CONF_VALUE): lv_float, - } - ), -) -async def spinbox_update_to_code(config, action_id, template_arg, args): - return await update_to_code(config, action_id, template_arg, args) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index b122d10f041..d7c342b16e7 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -700,6 +700,10 @@ lvgl: width: 100% height: 10% align: top_mid + on_value: + - lvgl.spinbox.update: + id: spinbox_id + value: !lambda return x; - button: styles: spin_button id: spin_up From a7674cd0e8d2064e775b8ebbf8beeb7d4437d5ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 18:28:51 -0600 Subject: [PATCH 321/461] [ble_client] Write static BLE data directly from flash without allocation --- esphome/components/ble_client/automation.h | 27 +++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 9c5646b3d19..bbc2dd05e0e 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -122,16 +122,19 @@ template class BLEClientWriteAction : public Action, publ void play_complex(const Ts &...x) override { this->num_running_++; this->var_ = std::make_tuple(x...); - std::vector value; + + bool result; if (this->len_ >= 0) { - // Static mode: copy from flash to vector - value.assign(this->value_.data, this->value_.data + this->len_); + // Static mode: write directly from flash pointer + result = this->write(this->value_.data, this->len_); } else { - // Template mode: call function - value = this->value_.func(x...); + // Template mode: call function and write the vector + std::vector value = this->value_.func(x...); + result = this->write(value); } + // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. - if (!write(value)) + if (!result) this->play_next_(x...); } @@ -144,15 +147,15 @@ template class BLEClientWriteAction : public Action, publ * errors. */ // initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event. - bool write(const std::vector &value) { + bool write(const uint8_t *data, size_t len) { if (this->node_state != espbt::ClientState::ESTABLISHED) { esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected"); return false; } - esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str()); - esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), - this->char_handle_, value.size(), const_cast(value.data()), - this->write_type_, ESP_GATT_AUTH_REQ_NONE); + esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str()); + esp_err_t err = + esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len, + const_cast(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE); if (err != ESP_OK) { esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); return false; @@ -160,6 +163,8 @@ template class BLEClientWriteAction : public Action, publ return true; } + bool write(const std::vector &value) { return this->write(value.data(), value.size()); } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { switch (event) { From 855aa32f542d5296a7febf9b388819c20a19ecf9 Mon Sep 17 00:00:00 2001 From: Beormund <75735592+Beormund@users.noreply.github.com> Date: Tue, 11 Nov 2025 00:32:59 +0000 Subject: [PATCH 322/461] Add support for RX8130 RTC Chip (#10511) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/rx8130/__init__.py | 0 esphome/components/rx8130/rx8130.cpp | 127 ++++++++++++++++++ esphome/components/rx8130/rx8130.h | 35 +++++ esphome/components/rx8130/time.py | 56 ++++++++ tests/components/rx8130/common.yaml | 8 ++ tests/components/rx8130/test.esp32-idf.yaml | 4 + tests/components/rx8130/test.esp8266-ard.yaml | 4 + tests/components/rx8130/test.rp2040-ard.yaml | 4 + 9 files changed, 239 insertions(+) create mode 100644 esphome/components/rx8130/__init__.py create mode 100644 esphome/components/rx8130/rx8130.cpp create mode 100644 esphome/components/rx8130/rx8130.h create mode 100644 esphome/components/rx8130/time.py create mode 100644 tests/components/rx8130/common.yaml create mode 100644 tests/components/rx8130/test.esp32-idf.yaml create mode 100644 tests/components/rx8130/test.esp8266-ard.yaml create mode 100644 tests/components/rx8130/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 393774372f2..e6970af47cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -396,6 +396,7 @@ esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtttl/* @glmnet esphome/components/runtime_stats/* @bdraco +esphome/components/rx8130/* @beormund esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/scd4x/* @martgras @sjtrny esphome/components/script/* @esphome/core diff --git a/esphome/components/rx8130/__init__.py b/esphome/components/rx8130/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp new file mode 100644 index 00000000000..cf6ea3e6e6e --- /dev/null +++ b/esphome/components/rx8130/rx8130.cpp @@ -0,0 +1,127 @@ +#include "rx8130.h" +#include "esphome/core/log.h" + +// https://download.epsondevice.com/td/pdf/app/RX8130CE_en.pdf + +namespace esphome { +namespace rx8130 { + +static const uint8_t RX8130_REG_SEC = 0x10; +static const uint8_t RX8130_REG_MIN = 0x11; +static const uint8_t RX8130_REG_HOUR = 0x12; +static const uint8_t RX8130_REG_WDAY = 0x13; +static const uint8_t RX8130_REG_MDAY = 0x14; +static const uint8_t RX8130_REG_MONTH = 0x15; +static const uint8_t RX8130_REG_YEAR = 0x16; +static const uint8_t RX8130_REG_EXTEN = 0x1C; +static const uint8_t RX8130_REG_FLAG = 0x1D; +static const uint8_t RX8130_REG_CTRL0 = 0x1E; +static const uint8_t RX8130_REG_CTRL1 = 0x1F; +static const uint8_t RX8130_REG_DIG_OFFSET = 0x30; +static const uint8_t RX8130_BIT_CTRL_STOP = 0x40; +static const uint8_t RX8130_BAT_FLAGS = 0x30; +static const uint8_t RX8130_CLEAR_FLAGS = 0x00; + +static const char *const TAG = "rx8130"; + +constexpr uint8_t bcd2dec(uint8_t val) { return (val >> 4) * 10 + (val & 0x0f); } +constexpr uint8_t dec2bcd(uint8_t val) { return ((val / 10) << 4) + (val % 10); } + +void RX8130Component::setup() { + // Set digital offset to disabled with no offset + if (this->write_register(RX8130_REG_DIG_OFFSET, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Disable wakeup timers + if (this->write_register(RX8130_REG_EXTEN, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Clear VLF flag in case there has been data loss + if (this->write_register(RX8130_REG_FLAG, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Clear test flag and disable interrupts + if (this->write_register(RX8130_REG_CTRL0, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Enable battery charging and switching + if (this->write_register(RX8130_REG_CTRL1, &RX8130_BAT_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Clear STOP bit + this->stop_(false); +} + +void RX8130Component::update() { this->read_time(); } + +void RX8130Component::dump_config() { + ESP_LOGCONFIG(TAG, "RX8130:"); + LOG_I2C_DEVICE(this); +} + +void RX8130Component::read_time() { + uint8_t date[7]; + if (this->read_register(RX8130_REG_SEC, date, 7) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + return; + } + ESPTime rtc_time{ + .second = bcd2dec(date[0] & 0x7f), + .minute = bcd2dec(date[1] & 0x7f), + .hour = bcd2dec(date[2] & 0x3f), + .day_of_week = bcd2dec(date[3] & 0x7f), + .day_of_month = bcd2dec(date[4] & 0x3f), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = bcd2dec(date[5] & 0x1f), + .year = static_cast(bcd2dec(date[6]) + 2000), + .is_dst = false, // not used + .timestamp = 0 // overwritten by recalc_timestamp_utc(false) + }; + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + ESP_LOGD(TAG, "Read UTC time: %04d-%02d-%02d %02d:%02d:%02d", rtc_time.year, rtc_time.month, rtc_time.day_of_month, + rtc_time.hour, rtc_time.minute, rtc_time.second); + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +void RX8130Component::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + uint8_t buff[7]; + buff[0] = dec2bcd(now.second); + buff[1] = dec2bcd(now.minute); + buff[2] = dec2bcd(now.hour); + buff[3] = dec2bcd(now.day_of_week); + buff[4] = dec2bcd(now.day_of_month); + buff[5] = dec2bcd(now.month); + buff[6] = dec2bcd(now.year % 100); + this->stop_(true); + if (this->write_register(RX8130_REG_SEC, buff, 7) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + } else { + ESP_LOGD(TAG, "Wrote UTC time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour, + now.minute, now.second); + } + this->stop_(false); +} + +void RX8130Component::stop_(bool stop) { + const uint8_t data = stop ? RX8130_BIT_CTRL_STOP : RX8130_CLEAR_FLAGS; + if (this->write_register(RX8130_REG_CTRL0, &data, 1) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + } +} + +} // namespace rx8130 +} // namespace esphome diff --git a/esphome/components/rx8130/rx8130.h b/esphome/components/rx8130/rx8130.h new file mode 100644 index 00000000000..6694c763cd5 --- /dev/null +++ b/esphome/components/rx8130/rx8130.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace rx8130 { + +class RX8130Component : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + void read_time(); + void write_time(); + /// Ensure RTC is initialized at the correct time in the setup sequence + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void stop_(bool stop); +}; + +template class WriteAction : public Action, public Parented { + public: + void play(const Ts... x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(const Ts... x) override { this->parent_->read_time(); } +}; + +} // namespace rx8130 +} // namespace esphome diff --git a/esphome/components/rx8130/time.py b/esphome/components/rx8130/time.py new file mode 100644 index 00000000000..cb0402bd323 --- /dev/null +++ b/esphome/components/rx8130/time.py @@ -0,0 +1,56 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import i2c, time +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@beormund"] +DEPENDENCIES = ["i2c"] +rx8130_ns = cg.esphome_ns.namespace("rx8130") +RX8130Component = rx8130_ns.class_("RX8130Component", time.RealTimeClock, i2c.I2CDevice) +WriteAction = rx8130_ns.class_("WriteAction", automation.Action) +ReadAction = rx8130_ns.class_("ReadAction", automation.Action) + + +CONFIG_SCHEMA = time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(RX8130Component), + } +).extend(i2c.i2c_device_schema(0x32)) + + +@automation.register_action( + "rx8130.write_time", + WriteAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(RX8130Component), + } + ), +) +async def rx8130_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "rx8130.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(RX8130Component), + } + ), +) +async def rx8130_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await time.register_time(var, config) diff --git a/tests/components/rx8130/common.yaml b/tests/components/rx8130/common.yaml new file mode 100644 index 00000000000..e6b849e25bd --- /dev/null +++ b/tests/components/rx8130/common.yaml @@ -0,0 +1,8 @@ +esphome: + on_boot: + - rx8130.read_time + - rx8130.write_time + +time: + - platform: rx8130 + i2c_id: i2c_bus diff --git a/tests/components/rx8130/test.esp32-idf.yaml b/tests/components/rx8130/test.esp32-idf.yaml new file mode 100644 index 00000000000..b47e39c3898 --- /dev/null +++ b/tests/components/rx8130/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/rx8130/test.esp8266-ard.yaml b/tests/components/rx8130/test.esp8266-ard.yaml new file mode 100644 index 00000000000..4a98b9388ab --- /dev/null +++ b/tests/components/rx8130/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/rx8130/test.rp2040-ard.yaml b/tests/components/rx8130/test.rp2040-ard.yaml new file mode 100644 index 00000000000..319a7c71a65 --- /dev/null +++ b/tests/components/rx8130/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From 1cccfdd2b92ffb12c7f6a578258481f8c232351e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 18:40:23 -0600 Subject: [PATCH 323/461] [wifi] Fix mesh network failover and improve retry logic reliability (#11805) --- esphome/components/wifi/wifi_component.cpp | 847 +++++++++++++++++---- esphome/components/wifi/wifi_component.h | 70 +- 2 files changed, 776 insertions(+), 141 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 789c22bae10..885288f46a0 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -42,6 +42,258 @@ namespace wifi { static const char *const TAG = "wifi"; +/// WiFi Retry Logic - Priority-Based BSSID Selection +/// +/// The WiFi component uses a state machine with priority degradation to handle connection failures +/// and automatically cycle through different BSSIDs in mesh networks or multiple configured networks. +/// +/// Connection Flow: +/// ┌──────────────────────────────────────────────────────────────────────┐ +/// │ Fast Connect Path (Optional) │ +/// ├──────────────────────────────────────────────────────────────────────┤ +/// │ Entered if: configuration has 'fast_connect: true' │ +/// │ Optimization to skip scanning when possible: │ +/// │ │ +/// │ 1. INITIAL_CONNECT → Try one of: │ +/// │ a) Saved BSSID+channel (from previous boot) │ +/// │ b) First configured non-hidden network (any BSSID) │ +/// │ ↓ │ +/// │ [FAILED] → Check if more configured networks available │ +/// │ ↓ │ +/// │ 2. FAST_CONNECT_CYCLING_APS → Try remaining configured networks │ +/// │ (1 attempt each, any BSSID) │ +/// │ ↓ │ +/// │ [All Failed] → Fall through to explicit hidden or scanning │ +/// │ │ +/// │ Note: Fast connect data saved from previous successful connection │ +/// └──────────────────────────────────────────────────────────────────────┘ +/// ↓ +/// ┌──────────────────────────────────────────────────────────────────────┐ +/// │ Explicit Hidden Networks Path (Optional) │ +/// ├──────────────────────────────────────────────────────────────────────┤ +/// │ Entered if: first configured network has 'hidden: true' │ +/// │ │ +/// │ 1. EXPLICIT_HIDDEN → Try consecutive hidden networks (1 attempt) │ +/// │ Stop when visible network reached │ +/// │ ↓ │ +/// │ Example: Hidden1, Hidden2, Visible1, Hidden3, Visible2 │ +/// │ Try: Hidden1, Hidden2 (stop at Visible1) │ +/// │ ↓ │ +/// │ [All Failed] → Fall back to scan-based connection │ +/// │ │ +/// │ Note: Fast connect saves BSSID after first successful connection, │ +/// │ so subsequent boots use fast path instead of hidden mode │ +/// └──────────────────────────────────────────────────────────────────────┘ +/// ↓ +/// ┌──────────────────────────────────────────────────────────────────────┐ +/// │ Scan-Based Connection Path │ +/// ├──────────────────────────────────────────────────────────────────────┤ +/// │ │ +/// │ 1. SCAN → Sort by priority (highest first), then RSSI │ +/// │ ┌─────────────────────────────────────────────────┐ │ +/// │ │ scan_result_[0] = Best BSSID (highest priority) │ │ +/// │ │ scan_result_[1] = Second best │ │ +/// │ │ scan_result_[2] = Third best │ │ +/// │ └─────────────────────────────────────────────────┘ │ +/// │ ↓ │ +/// │ 2. SCAN_CONNECTING → Try scan_result_[0] (2 attempts) │ +/// │ (Visible1, Visible2 from example above) │ +/// │ ↓ │ +/// │ 3. FAILED → Decrease priority: 0.0 → -1.0 → -2.0 │ +/// │ (stored in persistent sta_priorities_) │ +/// │ ↓ │ +/// │ 4. Check for hidden networks: │ +/// │ - If found → RETRY_HIDDEN (try SSIDs not in scan, 1 attempt) │ +/// │ Skip hidden networks before first visible one │ +/// │ (Skip Hidden1/Hidden2, try Hidden3 from example) │ +/// │ - If none → Skip RETRY_HIDDEN, go to step 5 │ +/// │ ↓ │ +/// │ 5. FAILED → RESTARTING_ADAPTER (skipped if AP/improv active) │ +/// │ ↓ │ +/// │ 6. Loop back to start: │ +/// │ - If first network is hidden → EXPLICIT_HIDDEN (retry cycle) │ +/// │ - Otherwise → SCAN_CONNECTING (rescan) │ +/// │ ↓ │ +/// │ 7. RESCAN → Apply stored priorities, sort again │ +/// │ ┌─────────────────────────────────────────────────┐ │ +/// │ │ scan_result_[0] = BSSID B (priority 0.0) ← NEW │ │ +/// │ │ scan_result_[1] = BSSID C (priority 0.0) │ │ +/// │ │ scan_result_[2] = BSSID A (priority -2.0) ← OLD │ │ +/// │ └─────────────────────────────────────────────────┘ │ +/// │ ↓ │ +/// │ 8. SCAN_CONNECTING → Try scan_result_[0] (next best) │ +/// │ │ +/// │ Key: Priority system cycles through BSSIDs ACROSS scan cycles │ +/// │ Full retry cycle: EXPLICIT_HIDDEN → SCAN → RETRY_HIDDEN │ +/// │ Always try best available BSSID (scan_result_[0]) │ +/// └──────────────────────────────────────────────────────────────────────┘ +/// +/// Retry Phases: +/// - INITIAL_CONNECT: Try saved BSSID+channel (fast_connect), or fall back to normal flow +/// - FAST_CONNECT_CYCLING_APS: Cycle through remaining configured networks (1 attempt each, fast_connect only) +/// - EXPLICIT_HIDDEN: Try consecutive networks marked hidden:true before scanning (1 attempt per SSID) +/// - SCAN_CONNECTING: Connect using scan results (2 attempts per BSSID) +/// - RETRY_HIDDEN: Try networks not found in scan (1 attempt per SSID, skipped if none found) +/// - RESTARTING_ADAPTER: Restart WiFi adapter to clear stuck state +/// +/// Hidden Network Handling: +/// - Networks marked 'hidden: true' before first non-hidden → Tried in EXPLICIT_HIDDEN phase +/// - Networks marked 'hidden: true' after first non-hidden → Tried in RETRY_HIDDEN phase +/// - After successful connection, fast_connect saves BSSID → subsequent boots use fast path +/// - Networks not in scan results → Tried in RETRY_HIDDEN phase +/// - Networks visible in scan + not marked hidden → Skipped in RETRY_HIDDEN phase +/// - Networks marked 'hidden: true' always use hidden mode, even if broadcasting SSID + +static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) { + switch (phase) { + case WiFiRetryPhase::INITIAL_CONNECT: + return LOG_STR("INITIAL_CONNECT"); +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: + return LOG_STR("FAST_CONNECT_CYCLING"); +#endif + case WiFiRetryPhase::EXPLICIT_HIDDEN: + return LOG_STR("EXPLICIT_HIDDEN"); + case WiFiRetryPhase::SCAN_CONNECTING: + return LOG_STR("SCAN_CONNECTING"); + case WiFiRetryPhase::RETRY_HIDDEN: + return LOG_STR("RETRY_HIDDEN"); + case WiFiRetryPhase::RESTARTING_ADAPTER: + return LOG_STR("RESTARTING"); + default: + return LOG_STR("UNKNOWN"); + } +} + +bool WiFiComponent::went_through_explicit_hidden_phase_() const { + // If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase + // This means those networks were already tried and should be skipped in RETRY_HIDDEN + return !this->sta_.empty() && this->sta_[0].get_hidden(); +} + +int8_t WiFiComponent::find_first_non_hidden_index_() const { + // Find the first network that is NOT marked hidden:true + // This is where EXPLICIT_HIDDEN phase would have stopped + for (size_t i = 0; i < this->sta_.size(); i++) { + if (!this->sta_[i].get_hidden()) { + return static_cast(i); + } + } + return -1; // All networks are hidden +} + +// 2 attempts per BSSID in SCAN_CONNECTING phase +// Rationale: This is the ONLY phase where we decrease BSSID priority, so we must be very sure. +// Auth failures are common immediately after scan due to WiFi stack state transitions. +// Trying twice filters out false positives and prevents unnecessarily marking a good BSSID as bad. +// After 2 genuine failures, priority degradation ensures we skip this BSSID on subsequent scans. +static constexpr uint8_t WIFI_RETRY_COUNT_PER_BSSID = 2; + +// 1 attempt per SSID in RETRY_HIDDEN phase +// Rationale: Try hidden mode once, then rescan to get next best BSSID via priority system +static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1; + +// 1 attempt per AP in fast_connect mode (INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS) +// Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly +static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1; + +static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { + switch (phase) { + case WiFiRetryPhase::INITIAL_CONNECT: +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: +#endif + // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS both use 1 attempt per AP (fast_connect mode) + return WIFI_RETRY_COUNT_PER_AP; + case WiFiRetryPhase::EXPLICIT_HIDDEN: + // Explicitly hidden network: 1 attempt (user marked as hidden, try once then scan) + return WIFI_RETRY_COUNT_PER_SSID; + case WiFiRetryPhase::SCAN_CONNECTING: + // Scan-based phase: 2 attempts per BSSID (handles transient auth failures after scan) + return WIFI_RETRY_COUNT_PER_BSSID; + case WiFiRetryPhase::RETRY_HIDDEN: + // Hidden network mode: 1 attempt per SSID + return WIFI_RETRY_COUNT_PER_SSID; + default: + return WIFI_RETRY_COUNT_PER_BSSID; + } +} + +static void apply_scan_result_to_params(WiFiAP ¶ms, const WiFiScanResult &scan) { + params.set_hidden(false); + params.set_ssid(scan.get_ssid()); + params.set_bssid(scan.get_bssid()); + params.set_channel(scan.get_channel()); +} + +bool WiFiComponent::needs_scan_results_() const { + // Only SCAN_CONNECTING phase needs scan results + if (this->retry_phase_ != WiFiRetryPhase::SCAN_CONNECTING) { + return false; + } + // Need scan if we have no results or no matching networks + return this->scan_result_.empty() || !this->scan_result_[0].get_matches(); +} + +bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const { + // Check if this SSID is configured as hidden + // If explicitly marked hidden, we should always try hidden mode regardless of scan results + for (const auto &conf : this->sta_) { + if (conf.get_ssid() == ssid && conf.get_hidden()) { + return false; // Treat as not seen - force hidden mode attempt + } + } + + // Otherwise, check if we saw it in scan results + for (const auto &scan : this->scan_result_) { + if (scan.get_ssid() == ssid) { + return true; + } + } + return false; +} + +int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden) { + // Find next SSID that wasn't in scan results (might be hidden) + // Start searching from start_index + 1 + for (size_t i = start_index + 1; i < this->sta_.size(); i++) { + const auto &sta = this->sta_[i]; + + // Skip networks that were already tried in EXPLICIT_HIDDEN phase + // Those are: networks marked hidden:true that appear before the first non-hidden network + if (!include_explicit_hidden && sta.get_hidden()) { + int8_t first_non_hidden_idx = this->find_first_non_hidden_index_(); + if (first_non_hidden_idx >= 0 && static_cast(i) < first_non_hidden_idx) { + ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str()); + continue; + } + } + + if (!this->ssid_was_seen_in_scan_(sta.get_ssid())) { + ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); + return static_cast(i); + } + ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (visible in scan)", sta.get_ssid().c_str()); + } + // No hidden SSIDs found + return -1; +} + +void WiFiComponent::start_initial_connection_() { + // If first network (highest priority) is explicitly marked hidden, try it first before scanning + // This respects user's priority order when they explicitly configure hidden networks + if (!this->sta_.empty() && this->sta_[0].get_hidden()) { + ESP_LOGI(TAG, "Starting with explicit hidden network (highest priority)"); + this->selected_sta_index_ = 0; + this->retry_phase_ = WiFiRetryPhase::EXPLICIT_HIDDEN; + WiFiAP params = this->build_params_for_current_phase_(); + this->start_connecting(params, false); + } else { + ESP_LOGI(TAG, "Starting scan"); + this->start_scanning(); + } +} + #if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) { switch (type) { @@ -109,18 +361,28 @@ void WiFiComponent::start() { ESP_LOGV(TAG, "Setting Power Save Option failed"); } + this->transition_to_phase_(WiFiRetryPhase::INITIAL_CONNECT); #ifdef USE_WIFI_FAST_CONNECT WiFiAP params; - this->trying_loaded_ap_ = this->load_fast_connect_settings_(params); - if (!this->trying_loaded_ap_) { - // FAST CONNECT FALLBACK: No saved settings available - // Use first config (will use SSID from config) + bool loaded_fast_connect = this->load_fast_connect_settings_(params); + // Fast connect optimization: only use when we have saved BSSID+channel data + // Without saved data, try first configured network or use normal flow + if (loaded_fast_connect) { + ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str()); + this->start_connecting(params, false); + } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) { + // No saved data, but have configured networks - try first non-hidden network + ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str()); this->selected_sta_index_ = 0; - params = this->build_wifi_ap_from_selected_(); + params = this->build_params_for_current_phase_(); + this->start_connecting(params, false); + } else { + // No saved data and (no networks OR first is hidden) - use normal flow + this->start_initial_connection_(); } - this->start_connecting(params, false); #else - this->start_scanning(); + // Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden) + this->start_initial_connection_(); #endif #ifdef USE_WIFI_AP } else if (this->has_ap()) { @@ -150,8 +412,7 @@ void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); delay(100); // NOLINT - this->num_retried_ = 0; - this->retry_hidden_ = false; + // Don't set retry_phase_ or num_retried_ here - state machine handles transitions } void WiFiComponent::loop() { @@ -172,21 +433,19 @@ void WiFiComponent::loop() { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); if (millis() - this->action_started_ > 5000) { -#ifdef USE_WIFI_FAST_CONNECT - // Safety check: Ensure selected_sta_index_ is valid before retrying - // (should already be set by retry_connect(), but check for robustness) + // After cooldown, connect based on current retry phase this->reset_selected_ap_to_first_if_invalid_(); - WiFiAP params = this->build_wifi_ap_from_selected_(); - this->start_connecting(params, false); -#else - if (this->retry_hidden_) { - this->reset_selected_ap_to_first_if_invalid_(); - WiFiAP params = this->build_wifi_ap_from_selected_(); - this->start_connecting(params, false); - } else { + + // Check if we need to trigger a scan first + if (this->needs_scan_results_() && !this->all_networks_hidden_()) { + // Need scan results or no matching networks found - scan/rescan + ESP_LOGD(TAG, "Scanning required for phase %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); this->start_scanning(); + } else { + // Have everything we need to connect (or all networks are hidden, skip scanning) + WiFiAP params = this->build_params_for_current_phase_(); + this->start_connecting(params, false); } -#endif } break; } @@ -344,30 +603,44 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->selected_sta_index_ = 0; } -WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { - // PRECONDITION: selected_sta_index_ must be valid (ensured by all callers) +WiFiAP WiFiComponent::build_params_for_current_phase_() { const WiFiAP *config = this->get_selected_sta_(); - assert(config != nullptr); + if (config == nullptr) { + ESP_LOGE(TAG, "No valid network config (selected_sta_index_=%d, sta_.size()=%zu)", + static_cast(this->selected_sta_index_), this->sta_.size()); + // Return empty params - caller should handle this gracefully + return WiFiAP(); + } + WiFiAP params = *config; - // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync after wifi_scan_done(): - // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) - // - It then finds which sta_[i] config matches scan_result_[0] - // - Sets selected_sta_index_ = i to record that matching config - // This sync holds until scan_result_ is cleared (e.g., after connection or in reset_for_next_ap_attempt_()) - if (!this->scan_result_.empty()) { - // Override with scan data - network is visible - const WiFiScanResult &scan = this->scan_result_[0]; - params.set_hidden(false); - params.set_ssid(scan.get_ssid()); - params.set_bssid(scan.get_bssid()); - params.set_channel(scan.get_channel()); - } else if (params.get_hidden()) { - // Hidden network - clear BSSID and channel even if set in config - // There might be multiple hidden networks with same SSID but we can't know which is correct - // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - params.set_bssid(optional{}); - params.set_channel(optional{}); + switch (this->retry_phase_) { + case WiFiRetryPhase::INITIAL_CONNECT: +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: +#endif + // Fast connect phases: use config-only (no scan results) + // BSSID/channel from config if user specified them, otherwise empty + break; + + case WiFiRetryPhase::EXPLICIT_HIDDEN: + case WiFiRetryPhase::RETRY_HIDDEN: + // Hidden network mode: clear BSSID/channel to trigger probe request + // (both explicit hidden and retry hidden use same behavior) + params.set_bssid(optional{}); + params.set_channel(optional{}); + break; + + case WiFiRetryPhase::SCAN_CONNECTING: + // Scan-based phase: always use best scan result (index 0 - highest priority after sorting) + if (!this->scan_result_.empty()) { + apply_scan_result_to_params(params, this->scan_result_[0]); + } + break; + + case WiFiRetryPhase::RESTARTING_ADAPTER: + // Should not be building params during restart + break; } return params; @@ -392,7 +665,21 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa } void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { - ESP_LOGI(TAG, "Connecting to '%s'", ap.get_ssid().c_str()); + // Log connection attempt at INFO level with priority + std::string bssid_formatted; + float priority = 0.0f; + + if (ap.get_bssid().has_value()) { + bssid_formatted = format_mac_address_pretty(ap.get_bssid().value().data()); + priority = this->get_sta_priority(ap.get_bssid().value()); + } + + ESP_LOGI(TAG, + "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %.1f, attempt %u/%u in phase %s)...", + ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_formatted.c_str() : LOG_STR_LITERAL("any"), + priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), + LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); + #ifdef ESPHOME_LOG_HAS_VERBOSE ESP_LOGV(TAG, "Connection Params:"); ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str()); @@ -565,8 +852,39 @@ void WiFiComponent::start_scanning() { this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING; } -// Helper function for WiFi scan result comparison -// Returns true if 'a' should be placed before 'b' in the sorted order +/// Comparator for WiFi scan result sorting - determines which network should be tried first +/// Returns true if 'a' should be placed before 'b' in the sorted order (a is "better" than b) +/// +/// Sorting logic (in priority order): +/// 1. Matching networks always ranked before non-matching networks +/// 2. For matching networks: Priority first (CRITICAL - tracks failure history) +/// 3. RSSI as tiebreaker for equal priority or non-matching networks +/// +/// WHY PRIORITY MUST BE CHECKED FIRST: +/// The priority field tracks connection failure history via priority degradation: +/// - Initial priority: 0.0 (from config or default) +/// - Each connection failure: priority -= 1.0 (becomes -1.0, -2.0, -3.0, etc.) +/// - Failed BSSIDs sorted lower → naturally try different BSSID on next scan +/// +/// This enables automatic BSSID cycling for various real-world failure scenarios: +/// - Crashed/hung AP (visible but not responding) +/// - Misconfigured mesh node (accepts auth but no DHCP/routing) +/// - Capacity limits (AP refuses new clients) +/// - Rogue AP (same SSID, wrong password or malicious) +/// - Intermittent hardware issues (flaky radio, overheating) +/// +/// Example mesh network: 3 APs with same SSID "home", all at priority 0.0 initially +/// - Try strongest BSSID A (sorted by RSSI) → fails → priority A becomes -1.0 +/// - Next scan: BSSID B and C (priority 0.0) sorted BEFORE A (priority -1.0) +/// - Try next strongest BSSID B → succeeds or fails and gets deprioritized +/// - System naturally cycles through all BSSIDs via priority degradation +/// - Eventually finds working AP or tries all options before restarting adapter +/// +/// If we checked RSSI first (Bug in PR #9963): +/// - Same failed BSSID would keep being selected if it has strongest signal +/// - Device stuck connecting to crashed AP with -30dBm while working AP at -50dBm ignored +/// - Priority degradation would be useless +/// - Mesh networks would never recover from single AP failure [[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) { // Matching networks always come before non-matching if (a.get_matches() && !b.get_matches()) @@ -574,21 +892,13 @@ void WiFiComponent::start_scanning() { if (!a.get_matches() && b.get_matches()) return false; - if (a.get_matches() && b.get_matches()) { - // For APs with the same SSID, always prefer stronger signal - // This helps with mesh networks and multiple APs - if (a.get_ssid() == b.get_ssid()) { - return a.get_rssi() > b.get_rssi(); - } - - // For different SSIDs, check priority first - if (a.get_priority() != b.get_priority()) - return a.get_priority() > b.get_priority(); - // If priorities are equal, prefer stronger signal - return a.get_rssi() > b.get_rssi(); + // Both matching: check priority first (tracks connection failures via priority degradation) + // Priority is decreased when a BSSID fails to connect, so lower priority = previously failed + if (a.get_matches() && b.get_matches() && a.get_priority() != b.get_priority()) { + return a.get_priority() > b.get_priority(); } - // Both don't match - sort by signal strength + // Use RSSI as tiebreaker (for equal-priority matching networks or all non-matching networks) return a.get_rssi() > b.get_rssi(); } @@ -623,10 +933,8 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - ESP_LOGD(TAG, - " Channel: %u\n" - " RSSI: %d dB", - res.get_channel(), res.get_rssi()); + ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4.1f", res.get_channel(), res.get_rssi(), + res.get_priority()); } else { ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); @@ -675,34 +983,36 @@ void WiFiComponent::check_scanning_finished() { // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config // matches that network and record it in selected_sta_index_. This keeps the two indices - // synchronized so build_wifi_ap_from_selected_() can safely use both to build connection parameters. + // synchronized so build_params_for_current_phase_() can safely use both to build connection parameters. const WiFiScanResult &scan_res = this->scan_result_[0]; - if (!scan_res.get_matches()) { - ESP_LOGW(TAG, "No matching network found"); - this->retry_connect(); - return; - } - bool found_match = false; - for (size_t i = 0; i < this->sta_.size(); i++) { - if (scan_res.matches(this->sta_[i])) { - // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation - // No overflow check needed - YAML validation prevents >127 networks - this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] - found_match = true; - break; + if (scan_res.get_matches()) { + for (size_t i = 0; i < this->sta_.size(); i++) { + if (scan_res.matches(this->sta_[i])) { + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation + // No overflow check needed - YAML validation prevents >127 networks + this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] + found_match = true; + break; + } } } if (!found_match) { ESP_LOGW(TAG, "No matching network found"); - this->retry_connect(); - return; + // No scan results matched our configured networks - transition directly to hidden mode + // Don't call retry_connect() since we never attempted a connection (no BSSID to penalize) + this->transition_to_phase_(WiFiRetryPhase::RETRY_HIDDEN); + // Now start connection attempt in hidden mode + } else if (this->transition_to_phase_(WiFiRetryPhase::SCAN_CONNECTING)) { + return; // scan started, wait for next loop iteration } yield(); - WiFiAP params = this->build_wifi_ap_from_selected_(); + WiFiAP params = this->build_params_for_current_phase_(); + // Ensure we're in SCAN_CONNECTING phase when connecting with scan results + // (needed when scan was started directly without transition_to_phase_, e.g., initial scan) this->start_connecting(params, false); } @@ -724,11 +1034,14 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGI(TAG, "Connected"); // Warn if we had to retry with hidden network mode for a network that's not marked hidden // Only warn if we actually connected without scan data (SSID only), not if scan succeeded on retry - if (const WiFiAP *config = this->get_selected_sta_(); - this->retry_hidden_ && config && !config->get_hidden() && this->scan_result_.empty()) { - ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); + if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN && + config && !config->get_hidden() && + this->scan_result_.empty()) { + ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->get_ssid().c_str()); } - this->retry_hidden_ = false; + // Reset to initial phase on successful connection (don't log transition, just reset state) + this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT; + this->num_retried_ = 0; this->print_connect_params_(); @@ -796,58 +1109,334 @@ void WiFiComponent::check_connecting_finished() { this->retry_connect(); } -void WiFiComponent::retry_connect() { - if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) { - auto bssid = *config->get_bssid(); - float priority = this->get_sta_priority(bssid); - this->set_sta_priority(bssid, priority - 1.0f); +/// Determine the next retry phase based on current state and failure conditions +/// This function examines the current retry phase, number of retries, and failure reasons +/// to decide what phase to move to next. It does not modify any state - it only returns +/// the recommended next phase. +/// +/// @return The next WiFiRetryPhase to transition to (may be same as current phase if should retry) +WiFiRetryPhase WiFiComponent::determine_next_phase_() { + switch (this->retry_phase_) { + case WiFiRetryPhase::INITIAL_CONNECT: +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: + // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS: no retries, try next AP or fall back to scan + if (this->selected_sta_index_ < static_cast(this->sta_.size()) - 1) { + return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP + } +#endif + // No more APs to try, fall back to scan + return WiFiRetryPhase::SCAN_CONNECTING; + + case WiFiRetryPhase::EXPLICIT_HIDDEN: { + // Try all explicitly hidden networks before scanning + if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; // Keep retrying same SSID + } + + // Exhausted retries on current SSID - check for more explicitly hidden networks + // Stop when we reach a visible network (proceed to scanning) + size_t next_index = this->selected_sta_index_ + 1; + if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) { + // Found another explicitly hidden network + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } + + // No more consecutive explicitly hidden networks - proceed to scanning + return WiFiRetryPhase::SCAN_CONNECTING; + } + + case WiFiRetryPhase::SCAN_CONNECTING: + // If scan found no matching networks, skip to hidden network mode + if (!this->scan_result_.empty() && !this->scan_result_[0].get_matches()) { + return WiFiRetryPhase::RETRY_HIDDEN; + } + + if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_BSSID) { + return WiFiRetryPhase::SCAN_CONNECTING; // Keep retrying same BSSID + } + + // Exhausted retries on current BSSID (scan_result_[0]) + // Its priority has been decreased, so on next scan it will be sorted lower + // and we'll try the next best BSSID. + // Check if there are any potentially hidden networks to try + if (this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_()) >= 0) { + return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try + } + // No hidden networks - skip directly to restart/rescan + if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { + return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN + : WiFiRetryPhase::SCAN_CONNECTING; + } + return WiFiRetryPhase::RESTARTING_ADAPTER; + + case WiFiRetryPhase::RETRY_HIDDEN: + // If no hidden SSIDs to try (selected_sta_index_ == -1), skip directly to rescan + if (this->selected_sta_index_ >= 0) { + if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) { + return WiFiRetryPhase::RETRY_HIDDEN; // Keep retrying same SSID + } + + // Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try + if (this->selected_sta_index_ < static_cast(this->sta_.size()) - 1) { + // More SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect() + return WiFiRetryPhase::RETRY_HIDDEN; + } + } + // Exhausted all potentially hidden SSIDs - rescan to try next BSSID + // If captive portal/improv is active, skip adapter restart and go back to start + // Otherwise restart adapter to clear any stuck state + if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { + // Go back to explicit hidden if we went through it initially, otherwise scan + return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN + : WiFiRetryPhase::SCAN_CONNECTING; + } + + // Restart adapter + return WiFiRetryPhase::RESTARTING_ADAPTER; + + case WiFiRetryPhase::RESTARTING_ADAPTER: + // After restart, go back to explicit hidden if we went through it initially, otherwise scan + return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN + : WiFiRetryPhase::SCAN_CONNECTING; } - delay(10); - if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && - (this->num_retried_ > 3 || this->error_from_callback_)) { -#ifdef USE_WIFI_FAST_CONNECT - // No empty check needed - YAML validation requires at least one network for fast_connect - if (this->trying_loaded_ap_) { - this->trying_loaded_ap_ = false; - this->selected_sta_index_ = 0; // Retry from the first configured AP - this->reset_for_next_ap_attempt_(); - } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { - // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation - // Exhausted all configured APs, restart adapter and cycle back to first - // Restart clears any stuck WiFi driver state - // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) - // Typically SSID only, which triggers ESP-IDF internal scanning - ESP_LOGW(TAG, "No more APs to try"); - this->selected_sta_index_ = 0; - this->reset_for_next_ap_attempt_(); - this->restart_adapter(); - } else { - // Try next AP - this->selected_sta_index_++; - this->reset_for_next_ap_attempt_(); - } -#else - if (this->num_retried_ > 5) { - // If retry failed for more than 5 times, let's restart STA - this->restart_adapter(); - } else { - // Try hidden networks after 3 failed retries - ESP_LOGD(TAG, "Retrying with hidden networks"); - this->retry_hidden_ = true; - this->num_retried_++; - } -#endif - } else { - this->num_retried_++; + // Should never reach here + return WiFiRetryPhase::SCAN_CONNECTING; +} + +/// Transition from current retry phase to a new phase with logging and phase-specific setup +/// This function handles the actual state change, including: +/// - Logging the phase transition +/// - Resetting the retry counter +/// - Performing phase-specific initialization (e.g., advancing AP index, starting scans) +/// +/// @param new_phase The phase we're transitioning TO +/// @return true if an async scan was started (caller should wait for completion) +/// false if no scan started (caller can proceed with connection attempt) +bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { + WiFiRetryPhase old_phase = this->retry_phase_; + + // No-op if staying in same phase + if (old_phase == new_phase) { + return false; } + + ESP_LOGD(TAG, "Retry phase: %s → %s", LOG_STR_ARG(retry_phase_to_log_string(old_phase)), + LOG_STR_ARG(retry_phase_to_log_string(new_phase))); + + this->retry_phase_ = new_phase; + this->num_retried_ = 0; // Reset retry counter on phase change + + // Phase-specific setup + switch (new_phase) { +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: + // Move to next configured AP - clear old scan data so new AP is tried with config only + this->selected_sta_index_++; + this->scan_result_.clear(); + break; +#endif + + case WiFiRetryPhase::EXPLICIT_HIDDEN: + // Starting explicit hidden phase - reset to first network + this->selected_sta_index_ = 0; + break; + + case WiFiRetryPhase::SCAN_CONNECTING: + // Transitioning to scan-based connection +#ifdef USE_WIFI_FAST_CONNECT + if (old_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) { + ESP_LOGI(TAG, "Fast connect exhausted, falling back to scan"); + } +#endif + // Trigger scan if we don't have scan results OR if transitioning from phases that need fresh scan + if (this->scan_result_.empty() || old_phase == WiFiRetryPhase::EXPLICIT_HIDDEN || + old_phase == WiFiRetryPhase::RETRY_HIDDEN || old_phase == WiFiRetryPhase::RESTARTING_ADAPTER) { + this->selected_sta_index_ = -1; // Will be set after scan completes + this->start_scanning(); + return true; // Started scan, wait for completion + } + // Already have scan results - selected_sta_index_ should already be synchronized + // (set in check_scanning_finished() when scan completed) + // No need to reset it here + break; + + case WiFiRetryPhase::RETRY_HIDDEN: + // Starting hidden mode - find first SSID that wasn't in scan results + if (old_phase == WiFiRetryPhase::SCAN_CONNECTING) { + // Keep scan results so we can skip SSIDs that were visible in the scan + // Don't clear scan_result_ - we need it to know which SSIDs are NOT hidden + + // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase + // In that case, skip networks marked hidden:true (already tried) + // Otherwise, include them (they haven't been tried yet) + this->selected_sta_index_ = this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_()); + + if (this->selected_sta_index_ == -1) { + ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode"); + } + } + break; + + case WiFiRetryPhase::RESTARTING_ADAPTER: + this->restart_adapter(); + // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting + return true; + + default: + break; + } + + return false; // Did not start scan, can proceed with connection +} + +/// Log failed connection attempt and decrease BSSID priority to avoid repeated failures +/// This function identifies which BSSID was attempted (from scan results or config), +/// decreases its priority by 1.0 to discourage future attempts, and logs the change. +/// +/// The priority degradation system ensures that failed BSSIDs are automatically sorted +/// lower in subsequent scans, naturally cycling through different APs without explicit +/// BSSID tracking within a scan cycle. +/// +/// Priority sources: +/// - SCAN_CONNECTING phase: Uses BSSID from scan_result_[0] (best match after sorting) +/// - Other phases: Uses BSSID from config if explicitly specified by user or fast_connect +/// +/// If no BSSID is available (SSID-only connection), priority adjustment is skipped. +void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { + // Determine which BSSID we tried to connect to + optional failed_bssid; + + if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { + // Scan-based phase: always use best result (index 0) + failed_bssid = this->scan_result_[0].get_bssid(); + } else if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) { + // Config has specific BSSID (fast_connect or user-specified) + failed_bssid = *config->get_bssid(); + } + + if (!failed_bssid.has_value()) { + return; // No BSSID to penalize + } + + // Decrease priority to avoid repeatedly trying the same failed BSSID + float old_priority = this->get_sta_priority(failed_bssid.value()); + float new_priority = old_priority - 1.0f; + this->set_sta_priority(failed_bssid.value(), new_priority); + + // Get SSID for logging + std::string ssid; + if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { + ssid = this->scan_result_[0].get_ssid(); + } else if (const WiFiAP *config = this->get_selected_sta_()) { + ssid = config->get_ssid(); + } + + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %.1f → %.1f", ssid.c_str(), + format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority); +} + +/// Handle target advancement or retry counter increment when staying in the same phase +/// This function is called when a connection attempt fails and determine_next_phase_() indicates +/// we should stay in the current phase. It decides whether to: +/// - Advance to the next target (AP in fast_connect, SSID in hidden mode) +/// - Or increment the retry counter to try the same target again +/// +/// Phase-specific behavior: +/// - FAST_CONNECT_CYCLING_APS: Always advance to next AP (no retries per AP) +/// - RETRY_HIDDEN: Advance to next SSID after exhausting retries on current SSID +/// - Other phases: Increment retry counter (will retry same target) +void WiFiComponent::advance_to_next_target_or_increment_retry_() { + WiFiRetryPhase current_phase = this->retry_phase_; + + // Check if we need to advance to next AP/SSID within the same phase +#ifdef USE_WIFI_FAST_CONNECT + if (current_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) { + // Fast connect: always advance to next AP (no retries per AP) + this->selected_sta_index_++; + this->num_retried_ = 0; + ESP_LOGD(TAG, "Next AP in %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); + return; + } +#endif + + if (current_phase == WiFiRetryPhase::EXPLICIT_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) { + // Explicit hidden: exhausted retries on current SSID, find next explicitly hidden network + // Stop when we reach a visible network (proceed to scanning) + size_t next_index = this->selected_sta_index_ + 1; + if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) { + this->selected_sta_index_ = static_cast(next_index); + this->num_retried_ = 0; + ESP_LOGD(TAG, "Next explicit hidden network at index %d", static_cast(next_index)); + return; + } + // No more consecutive explicit hidden networks found - fall through to trigger phase change + } + + if (current_phase == WiFiRetryPhase::RETRY_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) { + // Hidden mode: exhausted retries on current SSID, find next potentially hidden SSID + // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase + // In that case, skip networks marked hidden:true (already tried) + // Otherwise, include them (they haven't been tried yet) + int8_t next_index = + this->find_next_hidden_sta_(this->selected_sta_index_, !this->went_through_explicit_hidden_phase_()); + if (next_index != -1) { + // Found another potentially hidden SSID + this->selected_sta_index_ = next_index; + this->num_retried_ = 0; + return; + } + // No more potentially hidden SSIDs - set selected_sta_index_ to -1 to trigger phase change + // This ensures determine_next_phase_() will skip the RETRY_HIDDEN logic and transition out + this->selected_sta_index_ = -1; + // Return early - phase change will happen on next wifi_loop() iteration + return; + } + + // Don't increment retry counter if we're in a scan phase with no valid targets + if (this->needs_scan_results_()) { + return; + } + + // Increment retry counter to try the same target again + this->num_retried_++; + ESP_LOGD(TAG, "Retry attempt %u/%u in phase %s", this->num_retried_ + 1, + get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); +} + +void WiFiComponent::retry_connect() { + this->log_and_adjust_priority_for_failed_connect_(); + + delay(10); + + // Determine next retry phase based on current state + WiFiRetryPhase current_phase = this->retry_phase_; + WiFiRetryPhase next_phase = this->determine_next_phase_(); + + // Handle phase transitions (transition_to_phase_ handles same-phase no-op internally) + if (this->transition_to_phase_(next_phase)) { + return; // Wait for scan to complete + } + + if (next_phase == current_phase) { + this->advance_to_next_target_or_increment_retry_(); + } + this->error_from_callback_ = false; + if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { yield(); - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; - WiFiAP params = this->build_wifi_ap_from_selected_(); - this->start_connecting(params, true); - return; + // Check if we have a valid target before building params + // After exhausting all networks in a phase, selected_sta_index_ may be -1 + // In that case, skip connection and let next wifi_loop() handle phase transition + if (this->selected_sta_index_ >= 0) { + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; + WiFiAP params = this->build_params_for_current_phase_(); + this->start_connecting(params, true); + return; + } + // No valid target - fall through to set state to allow phase transition } this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index cb75edf5a03..1cdf3234c78 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -94,6 +94,24 @@ enum class WiFiSTAConnectStatus : int { ERROR_CONNECT_FAILED, }; +/// Tracks the current retry strategy/phase for WiFi connection attempts +enum class WiFiRetryPhase : uint8_t { + /// Initial connection attempt (varies based on fast_connect setting) + INITIAL_CONNECT, +#ifdef USE_WIFI_FAST_CONNECT + /// Fast connect mode: cycling through configured APs (config-only, no scan) + FAST_CONNECT_CYCLING_APS, +#endif + /// Explicitly hidden networks (user marked as hidden, try before scanning) + EXPLICIT_HIDDEN, + /// Scan-based: connecting to best AP from scan results + SCAN_CONNECTING, + /// Retry networks not found in scan (might be hidden) + RETRY_HIDDEN, + /// Restarting WiFi adapter to clear stuck state + RESTARTING_ADAPTER, +}; + /// Struct for setting static IPs in WiFiComponent. struct ManualIP { network::IPAddress static_ip; @@ -341,8 +359,37 @@ class WiFiComponent : public Component { #endif // USE_WIFI_AP void print_connect_params_(); - WiFiAP build_wifi_ap_from_selected_() const; + WiFiAP build_params_for_current_phase_(); + /// Determine next retry phase based on current state and failure conditions + WiFiRetryPhase determine_next_phase_(); + /// Transition to a new retry phase with logging + /// Returns true if a scan was started (caller should wait), false otherwise + bool transition_to_phase_(WiFiRetryPhase new_phase); + /// Check if we need valid scan results for the current phase but don't have any + /// Returns true if the phase requires scan results but they're missing or don't match + bool needs_scan_results_() const; + /// Check if we went through EXPLICIT_HIDDEN phase (first network is marked hidden) + /// Used in RETRY_HIDDEN to determine whether to skip explicitly hidden networks + bool went_through_explicit_hidden_phase_() const; + /// Find the index of the first non-hidden network + /// Returns where EXPLICIT_HIDDEN phase would have stopped, or -1 if all networks are hidden + int8_t find_first_non_hidden_index_() const; + /// Check if an SSID was seen in the most recent scan results + /// Used to skip hidden mode for SSIDs we know are visible + bool ssid_was_seen_in_scan_(const std::string &ssid) const; + /// Find next SSID that wasn't in scan results (might be hidden) + /// Returns index of next potentially hidden SSID, or -1 if none found + /// @param start_index Start searching from index after this (-1 to start from beginning) + /// @param include_explicit_hidden If true, include SSIDs marked hidden:true. If false, only find truly hidden SSIDs. + int8_t find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden = true); + /// Log failed connection and decrease BSSID priority to avoid repeated attempts + void log_and_adjust_priority_for_failed_connect_(); + /// Advance to next target (AP/SSID) within current phase, or increment retry counter + /// Called when staying in the same phase after a failed connection attempt + void advance_to_next_target_or_increment_retry_(); + /// Start initial connection - either scan or connect directly to hidden networks + void start_initial_connection_(); const WiFiAP *get_selected_sta_() const { if (this->selected_sta_index_ >= 0 && static_cast(this->selected_sta_index_) < this->sta_.size()) { return &this->sta_[this->selected_sta_index_]; @@ -356,14 +403,15 @@ class WiFiComponent : public Component { } } -#ifdef USE_WIFI_FAST_CONNECT - // Reset state for next fast connect AP attempt - // Clears old scan data so the new AP is tried with config only (SSID without specific BSSID/channel) - void reset_for_next_ap_attempt_() { - this->num_retried_ = 0; - this->scan_result_.clear(); + bool all_networks_hidden_() const { + if (this->sta_.empty()) + return false; + for (const auto &ap : this->sta_) { + if (!ap.get_hidden()) + return false; + } + return true; } -#endif void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); @@ -443,20 +491,18 @@ class WiFiComponent : public Component { // Group all 8-bit values together WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; + WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT}; uint8_t num_retried_{0}; // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) int8_t selected_sta_index_{-1}; + #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ // Group all boolean values together -#ifdef USE_WIFI_FAST_CONNECT - bool trying_loaded_ap_{false}; -#endif - bool retry_hidden_{false}; bool has_ap_{false}; bool handled_connected_state_{false}; bool error_from_callback_{false}; From 82692d7053ec4713a4eadb802ec33619d91ce57f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 19:00:54 -0600 Subject: [PATCH 324/461] [tests] Migrate components to shared packages and fix ID ambiguity (#11819) --- script/analyze_component_buses.py | 1 + tests/components/chsc6x/test.rp2040-ard.yaml | 1 + tests/components/hlk_fm22x/common.yaml | 41 ++++++++++++++++ .../components/hlk_fm22x/test.esp32-idf.yaml | 49 ++----------------- .../hlk_fm22x/test.esp8266-ard.yaml | 49 ++----------------- .../components/hlk_fm22x/test.rp2040-ard.yaml | 49 ++----------------- tests/components/speaker/common.yaml | 30 +++++++++--- tests/components/toshiba/common_ras2819t.yaml | 8 --- .../toshiba/test_ras2819t.esp32-ard.yaml | 6 +-- .../toshiba/test_ras2819t.esp32-c3-ard.yaml | 6 +-- .../toshiba/test_ras2819t.esp32-idf.yaml | 6 +-- .../toshiba/test_ras2819t.esp8266-ard.yaml | 6 +-- .../common/remote_receiver/esp32-ard.yaml | 12 +++++ .../common/remote_receiver/esp32-c3-ard.yaml | 12 +++++ 14 files changed, 111 insertions(+), 165 deletions(-) create mode 100644 tests/components/hlk_fm22x/common.yaml create mode 100644 tests/test_build_components/common/remote_receiver/esp32-ard.yaml create mode 100644 tests/test_build_components/common/remote_receiver/esp32-c3-ard.yaml diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index 38d1f8c2b72..27a36f889f4 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -86,6 +86,7 @@ ISOLATED_COMPONENTS = { "modbus_controller": "Defines multiple modbus buses for testing client/server functionality - conflicts with package modbus bus", "neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)", "packages": "cannot merge packages", + "tinyusb": "Conflicts with usb_host component - cannot be used together", } diff --git a/tests/components/chsc6x/test.rp2040-ard.yaml b/tests/components/chsc6x/test.rp2040-ard.yaml index 89cc1b74772..2e3613a4a36 100644 --- a/tests/components/chsc6x/test.rp2040-ard.yaml +++ b/tests/components/chsc6x/test.rp2040-ard.yaml @@ -16,5 +16,6 @@ display: touchscreen: - platform: chsc6x + i2c_id: i2c_bus display: ili9xxx_display interrupt_pin: 22 diff --git a/tests/components/hlk_fm22x/common.yaml b/tests/components/hlk_fm22x/common.yaml new file mode 100644 index 00000000000..6fcd9af5949 --- /dev/null +++ b/tests/components/hlk_fm22x/common.yaml @@ -0,0 +1,41 @@ +esphome: + on_boot: + then: + - hlk_fm22x.enroll: + name: "Test" + direction: 1 + - hlk_fm22x.delete_all: + +hlk_fm22x: + on_face_scan_matched: + - logger.log: test_hlk_22x_face_scan_matched + on_face_scan_unmatched: + - logger.log: test_hlk_22x_face_scan_unmatched + on_face_scan_invalid: + - logger.log: test_hlk_22x_face_scan_invalid + on_face_info: + - logger.log: test_hlk_22x_face_info + on_enrollment_done: + - logger.log: test_hlk_22x_enrollment_done + on_enrollment_failed: + - logger.log: test_hlk_22x_enrollment_failed + +sensor: + - platform: hlk_fm22x + face_count: + name: "Face Count" + last_face_id: + name: "Last Face ID" + status: + name: "Face Status" + +binary_sensor: + - platform: hlk_fm22x + name: "Face Enrolling" + +text_sensor: + - platform: hlk_fm22x + version: + name: "HLK Version" + last_face_name: + name: "Last Face Name" diff --git a/tests/components/hlk_fm22x/test.esp32-idf.yaml b/tests/components/hlk_fm22x/test.esp32-idf.yaml index 5e7cbde6649..2d29656c94a 100644 --- a/tests/components/hlk_fm22x/test.esp32-idf.yaml +++ b/tests/components/hlk_fm22x/test.esp32-idf.yaml @@ -1,47 +1,4 @@ -esphome: - on_boot: - then: - - hlk_fm22x.enroll: - name: "Test" - direction: 1 - - hlk_fm22x.delete_all: +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml -uart: - - id: uart_hlk_fm22x - tx_pin: 17 - rx_pin: 16 - baud_rate: 115200 - -hlk_fm22x: - on_face_scan_matched: - - logger.log: test_hlk_22x_face_scan_matched - on_face_scan_unmatched: - - logger.log: test_hlk_22x_face_scan_unmatched - on_face_scan_invalid: - - logger.log: test_hlk_22x_face_scan_invalid - on_face_info: - - logger.log: test_hlk_22x_face_info - on_enrollment_done: - - logger.log: test_hlk_22x_enrollment_done - on_enrollment_failed: - - logger.log: test_hlk_22x_enrollment_failed - -sensor: - - platform: hlk_fm22x - face_count: - name: "Face Count" - last_face_id: - name: "Last Face ID" - status: - name: "Face Status" - -binary_sensor: - - platform: hlk_fm22x - name: "Face Enrolling" - -text_sensor: - - platform: hlk_fm22x - version: - name: "HLK Version" - last_face_name: - name: "Last Face Name" +<<: !include common.yaml diff --git a/tests/components/hlk_fm22x/test.esp8266-ard.yaml b/tests/components/hlk_fm22x/test.esp8266-ard.yaml index 680047834cd..5a05efa259b 100644 --- a/tests/components/hlk_fm22x/test.esp8266-ard.yaml +++ b/tests/components/hlk_fm22x/test.esp8266-ard.yaml @@ -1,47 +1,4 @@ -esphome: - on_boot: - then: - - hlk_fm22x.enroll: - name: "Test" - direction: 1 - - hlk_fm22x.delete_all: +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml -uart: - - id: uart_hlk_fm22x - tx_pin: 4 - rx_pin: 5 - baud_rate: 115200 - -hlk_fm22x: - on_face_scan_matched: - - logger.log: test_hlk_22x_face_scan_matched - on_face_scan_unmatched: - - logger.log: test_hlk_22x_face_scan_unmatched - on_face_scan_invalid: - - logger.log: test_hlk_22x_face_scan_invalid - on_face_info: - - logger.log: test_hlk_22x_face_info - on_enrollment_done: - - logger.log: test_hlk_22x_enrollment_done - on_enrollment_failed: - - logger.log: test_hlk_22x_enrollment_failed - -sensor: - - platform: hlk_fm22x - face_count: - name: "Face Count" - last_face_id: - name: "Last Face ID" - status: - name: "Face Status" - -binary_sensor: - - platform: hlk_fm22x - name: "Face Enrolling" - -text_sensor: - - platform: hlk_fm22x - version: - name: "HLK Version" - last_face_name: - name: "Last Face Name" +<<: !include common.yaml diff --git a/tests/components/hlk_fm22x/test.rp2040-ard.yaml b/tests/components/hlk_fm22x/test.rp2040-ard.yaml index 680047834cd..f1df2daf83a 100644 --- a/tests/components/hlk_fm22x/test.rp2040-ard.yaml +++ b/tests/components/hlk_fm22x/test.rp2040-ard.yaml @@ -1,47 +1,4 @@ -esphome: - on_boot: - then: - - hlk_fm22x.enroll: - name: "Test" - direction: 1 - - hlk_fm22x.delete_all: +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml -uart: - - id: uart_hlk_fm22x - tx_pin: 4 - rx_pin: 5 - baud_rate: 115200 - -hlk_fm22x: - on_face_scan_matched: - - logger.log: test_hlk_22x_face_scan_matched - on_face_scan_unmatched: - - logger.log: test_hlk_22x_face_scan_unmatched - on_face_scan_invalid: - - logger.log: test_hlk_22x_face_scan_invalid - on_face_info: - - logger.log: test_hlk_22x_face_info - on_enrollment_done: - - logger.log: test_hlk_22x_enrollment_done - on_enrollment_failed: - - logger.log: test_hlk_22x_enrollment_failed - -sensor: - - platform: hlk_fm22x - face_count: - name: "Face Count" - last_face_id: - name: "Last Face ID" - status: - name: "Face Status" - -binary_sensor: - - platform: hlk_fm22x - name: "Face Enrolling" - -text_sensor: - - platform: hlk_fm22x - version: - name: "HLK Version" - last_face_name: - name: "Last Face Name" +<<: !include common.yaml diff --git a/tests/components/speaker/common.yaml b/tests/components/speaker/common.yaml index fa54fa7e396..9aaf639162b 100644 --- a/tests/components/speaker/common.yaml +++ b/tests/components/speaker/common.yaml @@ -11,26 +11,42 @@ esphome: on_boot: then: - speaker.mute_on: + id: speaker_id - speaker.mute_off: + id: speaker_id - if: - condition: speaker.is_stopped + condition: + speaker.is_stopped: + id: speaker_id then: - - speaker.play: [0, 1, 2, 3] - - speaker.volume_set: 0.9 + - speaker.play: + id: speaker_id + data: [0, 1, 2, 3] + - speaker.volume_set: + id: speaker_id + volume: 0.9 - if: - condition: speaker.is_playing + condition: + speaker.is_playing: + id: speaker_id then: - speaker.finish: + id: speaker_id - speaker.stop: + id: speaker_id button: - platform: template name: "Speaker Button" on_press: then: - - speaker.play: [0x10, 0x20, 0x30, 0x40] - - speaker.play: !lambda |- - return {0x01, 0x02, (uint8_t)id(my_number).state}; + - speaker.play: + id: speaker_id + data: [0x10, 0x20, 0x30, 0x40] + - speaker.play: + id: speaker_id + data: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; i2s_audio: i2s_lrclk_pin: ${i2s_bclk_pin} diff --git a/tests/components/toshiba/common_ras2819t.yaml b/tests/components/toshiba/common_ras2819t.yaml index 32081fca98e..157456ba81b 100644 --- a/tests/components/toshiba/common_ras2819t.yaml +++ b/tests/components/toshiba/common_ras2819t.yaml @@ -1,11 +1,3 @@ -remote_transmitter: - pin: ${tx_pin} - carrier_duty_percent: 50% - -remote_receiver: - id: rcvr - pin: ${rx_pin} - climate: - platform: toshiba name: "RAS-2819T Climate" diff --git a/tests/components/toshiba/test_ras2819t.esp32-ard.yaml b/tests/components/toshiba/test_ras2819t.esp32-ard.yaml index 00805baa011..d82ba54897c 100644 --- a/tests/components/toshiba/test_ras2819t.esp32-ard.yaml +++ b/tests/components/toshiba/test_ras2819t.esp32-ard.yaml @@ -1,5 +1,5 @@ -substitutions: - tx_pin: GPIO5 - rx_pin: GPIO4 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-ard.yaml <<: !include common_ras2819t.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml b/tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml index 00805baa011..6858dd587f4 100644 --- a/tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml +++ b/tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml @@ -1,5 +1,5 @@ -substitutions: - tx_pin: GPIO5 - rx_pin: GPIO4 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-c3-ard.yaml <<: !include common_ras2819t.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp32-idf.yaml b/tests/components/toshiba/test_ras2819t.esp32-idf.yaml index 00805baa011..3facc5bbb34 100644 --- a/tests/components/toshiba/test_ras2819t.esp32-idf.yaml +++ b/tests/components/toshiba/test_ras2819t.esp32-idf.yaml @@ -1,5 +1,5 @@ -substitutions: - tx_pin: GPIO5 - rx_pin: GPIO4 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml <<: !include common_ras2819t.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp8266-ard.yaml b/tests/components/toshiba/test_ras2819t.esp8266-ard.yaml index 00805baa011..3976dcc739e 100644 --- a/tests/components/toshiba/test_ras2819t.esp8266-ard.yaml +++ b/tests/components/toshiba/test_ras2819t.esp8266-ard.yaml @@ -1,5 +1,5 @@ -substitutions: - tx_pin: GPIO5 - rx_pin: GPIO4 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml <<: !include common_ras2819t.yaml diff --git a/tests/test_build_components/common/remote_receiver/esp32-ard.yaml b/tests/test_build_components/common/remote_receiver/esp32-ard.yaml new file mode 100644 index 00000000000..af5c2f24097 --- /dev/null +++ b/tests/test_build_components/common/remote_receiver/esp32-ard.yaml @@ -0,0 +1,12 @@ +# Common remote_receiver configuration for ESP32 Arduino tests +# Provides a shared remote receiver that all components can use +# Components will auto-use this receiver if they don't specify receiver_id + +substitutions: + remote_receiver_pin: GPIO32 + +remote_receiver: + - id: rcvr + pin: ${remote_receiver_pin} + dump: all + tolerance: 25% diff --git a/tests/test_build_components/common/remote_receiver/esp32-c3-ard.yaml b/tests/test_build_components/common/remote_receiver/esp32-c3-ard.yaml new file mode 100644 index 00000000000..26b288b4274 --- /dev/null +++ b/tests/test_build_components/common/remote_receiver/esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +# Common remote_receiver configuration for ESP32-C3 Arduino tests +# Provides a shared remote receiver that all components can use +# Components will auto-use this receiver if they don't specify receiver_id + +substitutions: + remote_receiver_pin: GPIO10 + +remote_receiver: + - id: rcvr + pin: ${remote_receiver_pin} + dump: all + tolerance: 25% From 463a00b1aca9fd7a44bb91a50418ce5a0bef48a3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:10:29 +1300 Subject: [PATCH 325/461] [CI] Don't request codeowners review in forks (#11827) --- .github/workflows/codeowner-review-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index 563d55f42b3..6f4351b2984 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -21,7 +21,7 @@ permissions: jobs: request-codeowner-reviews: name: Run - if: ${{ !github.event.pull_request.draft }} + if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: - name: Request reviews from component codeowners From 1539b4307469b59fe654b940920a2863dad44d8a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:17:16 +1000 Subject: [PATCH 326/461] [wifi][ethernet] Don't block setup until connected (#9823) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/ethernet/ethernet_component.cpp | 2 -- esphome/components/ethernet/ethernet_component.h | 1 - esphome/components/wifi/wifi_component.cpp | 6 ------ esphome/components/wifi/wifi_component.h | 2 -- 4 files changed, 11 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 893d0285be2..5888ddce603 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -418,8 +418,6 @@ void EthernetComponent::dump_config() { float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } -bool EthernetComponent::can_proceed() { return this->is_connected(); } - network::IPAddresses EthernetComponent::get_ip_addresses() { network::IPAddresses addresses; esp_netif_ip_info_t ip; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 31f9fa360a5..f1f0ac9cb8f 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -58,7 +58,6 @@ class EthernetComponent : public Component { void loop() override; void dump_config() override; float get_setup_priority() const override; - bool can_proceed() override; void on_powerdown() override { powerdown(); } bool is_connected(); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 885288f46a0..7279e0c7838 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1443,12 +1443,6 @@ void WiFiComponent::retry_connect() { this->action_started_ = millis(); } -bool WiFiComponent::can_proceed() { - if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { - return true; - } - return this->is_connected(); -} void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool WiFiComponent::is_connected() { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 1cdf3234c78..ed049544cfd 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -269,8 +269,6 @@ class WiFiComponent : public Component { void retry_connect(); - bool can_proceed() override; - void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected(); From 0f02c75f66bb56318edae341830c3c71819f6b6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 20:03:50 -0600 Subject: [PATCH 327/461] [wifi] Change priority type from float to int8_t --- esphome/components/wifi/__init__.py | 2 +- esphome/components/wifi/wifi_component.cpp | 42 ++++++++++++++++++---- esphome/components/wifi/wifi_component.h | 24 +++++++------ 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 5f4190a9331..358f920c2cc 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -174,7 +174,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( { cv.Optional(CONF_BSSID): cv.mac_address, cv.Optional(CONF_HIDDEN): cv.boolean, - cv.Optional(CONF_PRIORITY, default=0.0): cv.float_, + cv.Optional(CONF_PRIORITY, default=0): cv.int_range(min=-128, max=127), cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, } ) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7279e0c7838..11969021179 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -675,7 +675,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { } ESP_LOGI(TAG, - "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %.1f, attempt %u/%u in phase %s)...", + "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...", ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_formatted.c_str() : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); @@ -812,7 +812,7 @@ void WiFiComponent::print_connect_params_() { wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) { - ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config->get_bssid())); + ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid())); } #endif #ifdef USE_WIFI_11KV_SUPPORT @@ -933,8 +933,7 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4.1f", res.get_channel(), res.get_rssi(), - res.get_priority()); + ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority()); } else { ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); @@ -1063,6 +1062,9 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; + // Reset all priorities if they're all the same (can't differentiate) + this->reset_priorities_if_all_same_(); + #ifdef USE_WIFI_FAST_CONNECT this->save_fast_connect_settings_(); #endif @@ -1291,6 +1293,27 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { return false; // Did not start scan, can proceed with connection } +/// Reset all BSSID priorities to 0 if they're all identical (can't differentiate) +/// Called when starting a fresh connection attempt or after successful connection +void WiFiComponent::reset_priorities_if_all_same_() { + if (this->sta_priorities_.empty()) { + return; + } + + int8_t first_priority = this->sta_priorities_[0].priority; + for (const auto &pri : this->sta_priorities_) { + if (pri.priority != first_priority) { + return; // Not all same, nothing to do + } + } + + // All priorities are identical, reset to 0 + ESP_LOGD(TAG, "Resetting all BSSID priorities (all identical)"); + for (auto &pri : this->sta_priorities_) { + pri.priority = 0; + } +} + /// Log failed connection attempt and decrease BSSID priority to avoid repeated failures /// This function identifies which BSSID was attempted (from scan results or config), /// decreases its priority by 1.0 to discourage future attempts, and logs the change. @@ -1321,8 +1344,9 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { } // Decrease priority to avoid repeatedly trying the same failed BSSID - float old_priority = this->get_sta_priority(failed_bssid.value()); - float new_priority = old_priority - 1.0f; + int8_t old_priority = this->get_sta_priority(failed_bssid.value()); + int8_t new_priority = + (old_priority > std::numeric_limits::min()) ? (old_priority - 1) : std::numeric_limits::min(); this->set_sta_priority(failed_bssid.value(), new_priority); // Get SSID for logging @@ -1333,8 +1357,12 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { ssid = config->get_ssid(); } - ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %.1f → %.1f", ssid.c_str(), + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority); + + // After adjusting priority, check if all priorities are now identical + // If so, reset them all to 0 to start fresh + this->reset_priorities_if_all_same_(); } /// Handle target advancement or retry counter increment when staying in the same phase diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ed049544cfd..c0616c20768 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -157,7 +157,7 @@ class WiFiAP { void set_eap(optional eap_auth); #endif // USE_WIFI_WPA2_EAP void set_channel(optional channel); - void set_priority(float priority) { priority_ = priority; } + void set_priority(int8_t priority) { priority_ = priority; } void set_manual_ip(optional manual_ip); void set_hidden(bool hidden); const std::string &get_ssid() const; @@ -167,7 +167,7 @@ class WiFiAP { const optional &get_eap() const; #endif // USE_WIFI_WPA2_EAP const optional &get_channel() const; - float get_priority() const { return priority_; } + int8_t get_priority() const { return priority_; } const optional &get_manual_ip() const; bool get_hidden() const; @@ -179,8 +179,8 @@ class WiFiAP { optional eap_; #endif // USE_WIFI_WPA2_EAP optional manual_ip_; - float priority_{0}; optional channel_; + int8_t priority_{0}; bool hidden_{false}; }; @@ -198,17 +198,17 @@ class WiFiScanResult { int8_t get_rssi() const; bool get_with_auth() const; bool get_is_hidden() const; - float get_priority() const { return priority_; } - void set_priority(float priority) { priority_ = priority; } + int8_t get_priority() const { return priority_; } + void set_priority(int8_t priority) { priority_ = priority; } bool operator==(const WiFiScanResult &rhs) const; protected: bssid_t bssid_; - std::string ssid_; - float priority_{0.0f}; uint8_t channel_; int8_t rssi_; + std::string ssid_; + int8_t priority_{0}; bool matches_{false}; bool with_auth_; bool is_hidden_; @@ -216,7 +216,7 @@ class WiFiScanResult { struct WiFiSTAPriority { bssid_t bssid; - float priority; + int8_t priority; }; enum WiFiPowerSaveMode : uint8_t { @@ -317,14 +317,14 @@ class WiFiComponent : public Component { } return false; } - float get_sta_priority(const bssid_t bssid) { + int8_t get_sta_priority(const bssid_t bssid) { for (auto &it : this->sta_priorities_) { if (it.bssid == bssid) return it.priority; } - return 0.0f; + return 0; } - void set_sta_priority(const bssid_t bssid, float priority) { + void set_sta_priority(const bssid_t bssid, int8_t priority) { for (auto &it : this->sta_priorities_) { if (it.bssid == bssid) { it.priority = priority; @@ -383,6 +383,8 @@ class WiFiComponent : public Component { int8_t find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden = true); /// Log failed connection and decrease BSSID priority to avoid repeated attempts void log_and_adjust_priority_for_failed_connect_(); + /// Reset all BSSID priorities to 0 if they're all identical (can't differentiate) + void reset_priorities_if_all_same_(); /// Advance to next target (AP/SSID) within current phase, or increment retry counter /// Called when staying in the same phase after a failed connection attempt void advance_to_next_target_or_increment_retry_(); From 130a8b853dfca25b34ffe93f3f8c7a15e4a46329 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 20:14:40 -0600 Subject: [PATCH 328/461] missed one --- esphome/components/wifi/wifi_component.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 11969021179..8c9deee552d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -667,7 +667,7 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { // Log connection attempt at INFO level with priority std::string bssid_formatted; - float priority = 0.0f; + int8_t priority = 0; if (ap.get_bssid().has_value()) { bssid_formatted = format_mac_address_pretty(ap.get_bssid().value().data()); @@ -1575,9 +1575,9 @@ bool WiFiAP::get_hidden() const { return this->hidden_; } WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden) : bssid_(bssid), - ssid_(std::move(ssid)), channel_(channel), rssi_(rssi), + ssid_(std::move(ssid)), with_auth_(with_auth), is_hidden_(is_hidden) {} bool WiFiScanResult::matches(const WiFiAP &config) const { From 7a700ca0779367b6558489af364915f7bfd4eb9f Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:15:44 +1000 Subject: [PATCH 329/461] [core] Update clamp functions to allow mixed but comparable types (#11828) --- esphome/core/helpers.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 48af7f674a4..52a0746057e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1174,12 +1174,18 @@ template using ExternalRAMAllocator = RAMAllocator; * Functions to constrain the range of arithmetic values. */ -template T clamp_at_least(T value, T min) { +template +concept comparable_with = requires(T a, U b) { + { a > b } -> std::convertible_to; + { a < b } -> std::convertible_to; +}; + +template U> T clamp_at_least(T value, U min) { if (value < min) return min; return value; } -template T clamp_at_most(T value, T max) { +template U> T clamp_at_most(T value, U max) { if (value > max) return max; return value; From b80b0eb864281158112990eac22800bb0fe0a0d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 20:17:03 -0600 Subject: [PATCH 330/461] save more --- esphome/components/wifi/wifi_component.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8c9deee552d..629d8c12386 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1293,7 +1293,7 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { return false; // Did not start scan, can proceed with connection } -/// Reset all BSSID priorities to 0 if they're all identical (can't differentiate) +/// Reset all BSSID priorities if they're all identical (can't differentiate) /// Called when starting a fresh connection attempt or after successful connection void WiFiComponent::reset_priorities_if_all_same_() { if (this->sta_priorities_.empty()) { @@ -1307,11 +1307,10 @@ void WiFiComponent::reset_priorities_if_all_same_() { } } - // All priorities are identical, reset to 0 - ESP_LOGD(TAG, "Resetting all BSSID priorities (all identical)"); - for (auto &pri : this->sta_priorities_) { - pri.priority = 0; - } + // All priorities are identical - clear the vector to save memory + ESP_LOGD(TAG, "Clearing BSSID priorities (all identical)"); + this->sta_priorities_.clear(); + this->sta_priorities_.shrink_to_fit(); } /// Log failed connection attempt and decrease BSSID priority to avoid repeated failures From 6631e2ffb278ef3c66f2967c35fdd942e150510c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 20:22:24 -0600 Subject: [PATCH 331/461] tweaks --- esphome/components/wifi/wifi_component.cpp | 12 ++++++------ esphome/components/wifi/wifi_component.h | 4 ++-- tests/components/wifi/common.yaml | 5 +++++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 629d8c12386..b7cac68cd78 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1062,8 +1062,8 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; - // Reset all priorities if they're all the same (can't differentiate) - this->reset_priorities_if_all_same_(); + // Clear priority tracking if all priorities are identical + this->clear_priorities_if_all_same_(); #ifdef USE_WIFI_FAST_CONNECT this->save_fast_connect_settings_(); @@ -1293,9 +1293,9 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { return false; // Did not start scan, can proceed with connection } -/// Reset all BSSID priorities if they're all identical (can't differentiate) +/// Clear BSSID priority tracking if all priorities are identical (can't differentiate, saves memory) /// Called when starting a fresh connection attempt or after successful connection -void WiFiComponent::reset_priorities_if_all_same_() { +void WiFiComponent::clear_priorities_if_all_same_() { if (this->sta_priorities_.empty()) { return; } @@ -1360,8 +1360,8 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority); // After adjusting priority, check if all priorities are now identical - // If so, reset them all to 0 to start fresh - this->reset_priorities_if_all_same_(); + // If so, clear the vector to save memory + this->clear_priorities_if_all_same_(); } /// Handle target advancement or retry counter increment when staying in the same phase diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index c0616c20768..84dfa57ab13 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -383,8 +383,8 @@ class WiFiComponent : public Component { int8_t find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden = true); /// Log failed connection and decrease BSSID priority to avoid repeated attempts void log_and_adjust_priority_for_failed_connect_(); - /// Reset all BSSID priorities to 0 if they're all identical (can't differentiate) - void reset_priorities_if_all_same_(); + /// Clear BSSID priority tracking if all priorities are identical (saves memory) + void clear_priorities_if_all_same_(); /// Advance to next target (AP/SSID) within current phase, or increment retry counter /// Called when staying in the same phase after a failed connection attempt void advance_to_next_target_or_increment_retry_(); diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml index af27f850923..5d9973cbc80 100644 --- a/tests/components/wifi/common.yaml +++ b/tests/components/wifi/common.yaml @@ -15,5 +15,10 @@ wifi: networks: - ssid: MySSID password: password1 + priority: 10 - ssid: MySSID2 password: password2 + priority: 5 + - ssid: MySSID3 + password: password3 + priority: 0 From 48a33611a1e207abe190484077bcf7db4c220a40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 20:43:32 -0600 Subject: [PATCH 332/461] [wifi] Fix infinite retry loop when no hidden networks and captive portal active --- esphome/components/wifi/wifi_component.cpp | 28 ++++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7279e0c7838..49e433b4682 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1163,11 +1163,9 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { if (this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_()) >= 0) { return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try } - // No hidden networks - skip directly to restart/rescan - if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { - return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN - : WiFiRetryPhase::SCAN_CONNECTING; - } + // No hidden networks - always go through RESTARTING_ADAPTER phase + // This ensures num_retried_ gets reset and a fresh scan is triggered + // The actual adapter restart will be skipped if captive portal/improv is active return WiFiRetryPhase::RESTARTING_ADAPTER; case WiFiRetryPhase::RETRY_HIDDEN: @@ -1183,16 +1181,9 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::RETRY_HIDDEN; } } - // Exhausted all potentially hidden SSIDs - rescan to try next BSSID - // If captive portal/improv is active, skip adapter restart and go back to start - // Otherwise restart adapter to clear any stuck state - if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { - // Go back to explicit hidden if we went through it initially, otherwise scan - return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN - : WiFiRetryPhase::SCAN_CONNECTING; - } - - // Restart adapter + // Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER + // This ensures num_retried_ gets reset and a fresh scan is triggered + // The actual adapter restart will be skipped if captive portal/improv is active return WiFiRetryPhase::RESTARTING_ADAPTER; case WiFiRetryPhase::RESTARTING_ADAPTER: @@ -1280,7 +1271,12 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { break; case WiFiRetryPhase::RESTARTING_ADAPTER: - this->restart_adapter(); + // Skip actual adapter restart if captive portal/improv is active + // This allows state machine to reset num_retried_ and trigger fresh scan + // without disrupting the captive portal/improv connection + if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { + this->restart_adapter(); + } // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting return true; From d87063865cfa1ebd323dd22a70317cb515fa5fb0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 21:57:52 -0600 Subject: [PATCH 333/461] [ethernet] Conditionally compile manual_ip to save 24 bytes RAM --- esphome/components/ethernet/__init__.py | 1 + esphome/components/ethernet/ethernet_component.cpp | 12 ++++++++++-- esphome/components/ethernet/ethernet_component.h | 4 ++++ esphome/core/defines.h | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 2f02d227d71..b4d67635c18 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -383,6 +383,7 @@ async def to_code(config): cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) if CONF_MANUAL_IP in config: + cg.add_define("USE_ETHERNET_MANUAL_IP") cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) # Add compile-time define for PHY types with specific code diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 5888ddce603..f3266c851db 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -550,11 +550,14 @@ void EthernetComponent::start_connect_() { } esp_netif_ip_info_t info; +#ifdef USE_ETHERNET_MANUAL_IP if (this->manual_ip_.has_value()) { info.ip = this->manual_ip_->static_ip; info.gw = this->manual_ip_->gateway; info.netmask = this->manual_ip_->subnet; - } else { + } else +#endif + { info.ip.addr = 0; info.gw.addr = 0; info.netmask.addr = 0; @@ -575,6 +578,7 @@ void EthernetComponent::start_connect_() { err = esp_netif_set_ip_info(this->eth_netif_, &info); ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); +#ifdef USE_ETHERNET_MANUAL_IP if (this->manual_ip_.has_value()) { LwIPLock lock; if (this->manual_ip_->dns1.is_set()) { @@ -587,7 +591,9 @@ void EthernetComponent::start_connect_() { d = this->manual_ip_->dns2; dns_setserver(1, &d); } - } else { + } else +#endif + { err = esp_netif_dhcpc_start(this->eth_netif_); if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { ESPHL_ERROR_CHECK(err, "DHCPC start error"); @@ -685,7 +691,9 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } #endif void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } +#ifdef USE_ETHERNET_MANUAL_IP void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } +#endif // set_use_address() is guaranteed to be called during component setup by Python code generation, // so use_address_ will always be valid when get_use_address() is called - no fallback needed. diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index f1f0ac9cb8f..bffed4dc4a3 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -82,7 +82,9 @@ class EthernetComponent : public Component { void add_phy_register(PHYRegister register_value); #endif void set_type(EthernetType type); +#ifdef USE_ETHERNET_MANUAL_IP void set_manual_ip(const ManualIP &manual_ip); +#endif void set_fixed_mac(const std::array &mac) { this->fixed_mac_ = mac; } network::IPAddresses get_ip_addresses(); @@ -137,7 +139,9 @@ class EthernetComponent : public Component { uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; #endif +#ifdef USE_ETHERNET_MANUAL_IP optional manual_ip_{}; +#endif uint32_t connect_begin_; // Group all uint8_t types together (enums and bools) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ac725fbca94..92b5d89cacc 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -215,6 +215,7 @@ #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2) #define USE_ETHERNET #define USE_ETHERNET_KSZ8081 +#define USE_ETHERNET_MANUAL_IP #endif #ifdef USE_ESP_IDF From c38df0af85565eddb41e834922c197505b9bafb1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 22:09:01 -0600 Subject: [PATCH 334/461] [wifi] Conditionally compile manual_ip to save 24-120 bytes RAM --- esphome/components/wifi/__init__.py | 10 ++++++++++ esphome/components/wifi/wifi_component.cpp | 11 ++++++++++- esphome/components/wifi/wifi_component.h | 6 ++++++ esphome/components/wifi/wifi_component_esp8266.cpp | 13 +++++++++++++ esphome/components/wifi/wifi_component_esp_idf.cpp | 13 +++++++++++++ .../components/wifi/wifi_component_libretiny.cpp | 13 +++++++++++++ esphome/components/wifi/wifi_component_pico_w.cpp | 12 ++++++++++++ esphome/core/defines.h | 1 + 8 files changed, 78 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 5f4190a9331..6cdcd2fd069 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -385,6 +385,8 @@ async def to_code(config): # Track if any network uses Enterprise authentication has_eap = False + # Track if any network uses manual IP + has_manual_ip = False # Initialize FixedVector with the count of networks networks = config.get(CONF_NETWORKS, []) @@ -398,11 +400,15 @@ async def to_code(config): for network in networks: if CONF_EAP in network: has_eap = True + if network.get(CONF_MANUAL_IP) or config.get(CONF_MANUAL_IP): + has_manual_ip = True cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network) if CONF_AP in config: conf = config[CONF_AP] ip_config = conf.get(CONF_MANUAL_IP) + if ip_config: + has_manual_ip = True cg.with_local_variable( conf[CONF_ID], WiFiAP(), @@ -418,6 +424,10 @@ async def to_code(config): if CORE.is_esp32: add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap) + # Only define USE_WIFI_MANUAL_IP if any AP uses manual IP + if has_manual_ip: + cg.add_define("USE_WIFI_MANUAL_IP") + cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) if config[CONF_FAST_CONNECT]: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7279e0c7838..66db813ab97 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -569,6 +569,7 @@ void WiFiComponent::setup_ap_config_() { " IP Address: %s", this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str()); +#ifdef USE_WIFI_MANUAL_IP auto manual_ip = this->ap_.get_manual_ip(); if (manual_ip.has_value()) { ESP_LOGCONFIG(TAG, @@ -578,6 +579,7 @@ void WiFiComponent::setup_ap_config_() { manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(), manual_ip->subnet.str().c_str()); } +#endif if (!this->has_sta()) { this->state_ = WIFI_COMPONENT_STATE_AP; @@ -716,11 +718,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { } else { ESP_LOGV(TAG, " Channel not set"); } +#ifdef USE_WIFI_MANUAL_IP if (ap.get_manual_ip().has_value()) { ManualIP m = *ap.get_manual_ip(); ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str().c_str(), m.gateway.str().c_str(), m.subnet.str().c_str(), m.dns1.str().c_str(), m.dns2.str().c_str()); - } else { + } else +#endif + { ESP_LOGV(TAG, " Using DHCP IP"); } ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden())); @@ -1532,7 +1537,9 @@ void WiFiAP::set_password(const std::string &password) { this->password_ = passw void WiFiAP::set_eap(optional eap_auth) { this->eap_ = std::move(eap_auth); } #endif void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } +#ifdef USE_WIFI_MANUAL_IP void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } +#endif void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const optional &WiFiAP::get_bssid() const { return this->bssid_; } @@ -1541,7 +1548,9 @@ const std::string &WiFiAP::get_password() const { return this->password_; } const optional &WiFiAP::get_eap() const { return this->eap_; } #endif const optional &WiFiAP::get_channel() const { return this->channel_; } +#ifdef USE_WIFI_MANUAL_IP const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } +#endif bool WiFiAP::get_hidden() const { return this->hidden_; } WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ed049544cfd..a37a5f76a74 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -158,7 +158,9 @@ class WiFiAP { #endif // USE_WIFI_WPA2_EAP void set_channel(optional channel); void set_priority(float priority) { priority_ = priority; } +#ifdef USE_WIFI_MANUAL_IP void set_manual_ip(optional manual_ip); +#endif void set_hidden(bool hidden); const std::string &get_ssid() const; const optional &get_bssid() const; @@ -168,7 +170,9 @@ class WiFiAP { #endif // USE_WIFI_WPA2_EAP const optional &get_channel() const; float get_priority() const { return priority_; } +#ifdef USE_WIFI_MANUAL_IP const optional &get_manual_ip() const; +#endif bool get_hidden() const; protected: @@ -178,7 +182,9 @@ class WiFiAP { #ifdef USE_WIFI_WPA2_EAP optional eap_; #endif // USE_WIFI_WPA2_EAP +#ifdef USE_WIFI_MANUAL_IP optional manual_ip_; +#endif float priority_{0}; optional channel_; bool hidden_{false}; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 4e17c42f413..79d9a381ecf 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -273,9 +273,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { return false; } +#else + if (!this->wifi_sta_ip_config_({})) { + return false; + } +#endif // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP @@ -823,10 +829,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); + return false; + } +#endif return true; } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 08ecba35987..016e6a8043b 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -371,9 +371,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { return false; } +#else + if (!this->wifi_sta_ip_config_({})) { + return false; + } +#endif // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP @@ -985,10 +991,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:"); + return false; + } +#endif return true; } diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 45e2fba82a7..2946b9e8310 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -112,9 +112,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { WiFi.disconnect(); } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { return false; } +#else + if (!this->wifi_sta_ip_config_({})) { + return false; + } +#endif this->wifi_apply_hostname_(); @@ -445,10 +451,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { if (!this->wifi_mode_({}, true)) return false; +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); + return false; + } +#endif yield(); diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index bf15892cd5e..7025ba16bda 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -55,8 +55,13 @@ bool WiFiComponent::wifi_apply_power_save_() { bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; } bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) return false; +#else + if (!this->wifi_sta_ip_config_({})) + return false; +#endif auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str()); if (ret != WL_CONNECTED) @@ -161,10 +166,17 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { if (!this->wifi_mode_({}, true)) return false; +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); + return false; + } +#endif WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1)); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ac725fbca94..f9a07f69480 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -144,6 +144,7 @@ #define USE_TIME_TIMEZONE #define USE_WIFI #define USE_WIFI_AP +#define USE_WIFI_MANUAL_IP #define USE_WIREGUARD #endif From d4d44a5c0813658ce94d294224455a5a7c7f1e8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 22:23:02 -0600 Subject: [PATCH 335/461] manual_ip test --- tests/components/wifi/test.esp32-idf.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/components/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml index 91e235b9ce2..6b3ef20963c 100644 --- a/tests/components/wifi/test.esp32-idf.yaml +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -2,6 +2,22 @@ psram: wifi: use_psram: true + min_auth_mode: WPA + manual_ip: + static_ip: 192.168.1.100 + gateway: 192.168.1.1 + subnet: 255.255.255.0 + dns1: 1.1.1.1 + dns2: 8.8.8.8 + ap: + ssid: Fallback AP + password: fallback_password + manual_ip: + static_ip: 192.168.4.1 + gateway: 192.168.4.1 + subnet: 255.255.255.0 + +captive_portal: packages: - !include common.yaml From 89abd9c817916c42f5a9d9d7065dc1e03f98dc40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 22:24:22 -0600 Subject: [PATCH 336/461] fix conflict --- tests/components/wifi/test.esp32-idf.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml index 6b3ef20963c..fff2116e9c1 100644 --- a/tests/components/wifi/test.esp32-idf.yaml +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -2,7 +2,6 @@ psram: wifi: use_psram: true - min_auth_mode: WPA manual_ip: static_ip: 192.168.1.100 gateway: 192.168.1.1 From 4160157457c86b01ae52b37bf7edf9ac535d4807 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 08:26:15 -0600 Subject: [PATCH 337/461] [wifi] Restore two-attempt BSSID filtering for mesh networks --- esphome/components/wifi/wifi_component.cpp | 24 +++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7279e0c7838..cba7267aaec 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1304,6 +1304,11 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { /// - Other phases: Uses BSSID from config if explicitly specified by user or fast_connect /// /// If no BSSID is available (SSID-only connection), priority adjustment is skipped. +/// +/// IMPORTANT: Priority is only decreased on the LAST attempt for a BSSID in SCAN_CONNECTING phase. +/// This prevents false positives from transient WiFi stack state issues after scanning. +/// Single failures don't necessarily mean the AP is bad - two genuine failures provide +/// higher confidence before degrading priority and skipping the BSSID in future scans. void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { // Determine which BSSID we tried to connect to optional failed_bssid; @@ -1320,11 +1325,6 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { return; // No BSSID to penalize } - // Decrease priority to avoid repeatedly trying the same failed BSSID - float old_priority = this->get_sta_priority(failed_bssid.value()); - float new_priority = old_priority - 1.0f; - this->set_sta_priority(failed_bssid.value(), new_priority); - // Get SSID for logging std::string ssid; if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { @@ -1333,6 +1333,20 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { ssid = config->get_ssid(); } + // Only decrease priority on the last attempt for this phase + // This prevents false positives from transient WiFi stack issues + uint8_t max_retries = get_max_retries_for_phase(this->retry_phase_); + bool is_last_attempt = (this->num_retried_ + 1 >= max_retries); + + // Decrease priority only on last attempt to avoid false positives from transient failures + float old_priority = this->get_sta_priority(failed_bssid.value()); + float new_priority = old_priority; + + if (is_last_attempt) { + new_priority -= 1.0f; // Decrease priority on failure + this->set_sta_priority(failed_bssid.value(), new_priority); + } + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %.1f → %.1f", ssid.c_str(), format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority); } From 55cf0adb185ee041a829ddcff3896065a585105b Mon Sep 17 00:00:00 2001 From: Tomasz Duda Date: Tue, 11 Nov 2025 15:38:19 +0100 Subject: [PATCH 338/461] [nrf52,pcf8563] fix build error --- esphome/components/pcf8563/pcf8563.cpp | 2 ++ tests/components/pcf8563/test.nrf52-adafruit.yaml | 4 ++++ 2 files changed, 6 insertions(+) create mode 100644 tests/components/pcf8563/test.nrf52-adafruit.yaml diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index 27020378a6a..d6f37f44e6e 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -23,7 +23,9 @@ void PCF8563Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } +#ifndef USE_ZEPHYR ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); +#endif } float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/tests/components/pcf8563/test.nrf52-adafruit.yaml b/tests/components/pcf8563/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/pcf8563/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml From bf312ad9ec0b4e3f76aa6398a35e93a930b18490 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 08:50:54 -0600 Subject: [PATCH 339/461] fixes --- esphome/components/wifi/wifi_component.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index b7cac68cd78..83e8aa90cb2 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1301,14 +1301,21 @@ void WiFiComponent::clear_priorities_if_all_same_() { } int8_t first_priority = this->sta_priorities_[0].priority; + + // Only clear if all priorities have been decremented to the minimum value + // At this point, all BSSIDs have been equally penalized and priority info is useless + if (first_priority != std::numeric_limits::min()) { + return; + } + for (const auto &pri : this->sta_priorities_) { if (pri.priority != first_priority) { return; // Not all same, nothing to do } } - // All priorities are identical - clear the vector to save memory - ESP_LOGD(TAG, "Clearing BSSID priorities (all identical)"); + // All priorities are at minimum - clear the vector to save memory and reset + ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)"); this->sta_priorities_.clear(); this->sta_priorities_.shrink_to_fit(); } From bee174150b0d4a31357173c99b07608497c0937b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 08:52:12 -0600 Subject: [PATCH 340/461] fixes --- esphome/components/wifi/wifi_component.cpp | 17 +++++++++-------- esphome/components/wifi/wifi_component.h | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 83e8aa90cb2..4a5a4cbe556 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1062,8 +1062,8 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; - // Clear priority tracking if all priorities are identical - this->clear_priorities_if_all_same_(); + // Clear priority tracking if all priorities are at minimum + this->clear_priorities_if_all_min_(); #ifdef USE_WIFI_FAST_CONNECT this->save_fast_connect_settings_(); @@ -1293,9 +1293,10 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { return false; // Did not start scan, can proceed with connection } -/// Clear BSSID priority tracking if all priorities are identical (can't differentiate, saves memory) -/// Called when starting a fresh connection attempt or after successful connection -void WiFiComponent::clear_priorities_if_all_same_() { +/// Clear BSSID priority tracking if all priorities are at minimum (saves memory) +/// At minimum priority, all BSSIDs are equally bad, so priority tracking is useless +/// Called after successful connection or after failed connection attempts +void WiFiComponent::clear_priorities_if_all_min_() { if (this->sta_priorities_.empty()) { return; } @@ -1366,9 +1367,9 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority); - // After adjusting priority, check if all priorities are now identical - // If so, clear the vector to save memory - this->clear_priorities_if_all_same_(); + // After adjusting priority, check if all priorities are now at minimum + // If so, clear the vector to save memory and reset for fresh start + this->clear_priorities_if_all_min_(); } /// Handle target advancement or retry counter increment when staying in the same phase diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 84dfa57ab13..b8223e8dc89 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -383,8 +383,8 @@ class WiFiComponent : public Component { int8_t find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden = true); /// Log failed connection and decrease BSSID priority to avoid repeated attempts void log_and_adjust_priority_for_failed_connect_(); - /// Clear BSSID priority tracking if all priorities are identical (saves memory) - void clear_priorities_if_all_same_(); + /// Clear BSSID priority tracking if all priorities are at minimum (saves memory) + void clear_priorities_if_all_min_(); /// Advance to next target (AP/SSID) within current phase, or increment retry counter /// Called when staying in the same phase after a failed connection attempt void advance_to_next_target_or_increment_retry_(); From 72a6051f0d5cf7581e8bc3d3a4776eefc48a9c2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 09:16:31 -0600 Subject: [PATCH 341/461] [wifi] Fix infinite loop in RETRY_HIDDEN when remaining networks are visible --- esphome/components/wifi/wifi_component.cpp | 19 ++++++++++++------- esphome/components/wifi/wifi_component.h | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7279e0c7838..0fef3fe6616 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -253,8 +253,9 @@ bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const { return false; } -int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden) { +int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { // Find next SSID that wasn't in scan results (might be hidden) + bool include_explicit_hidden = !this->went_through_explicit_hidden_phase_(); // Start searching from start_index + 1 for (size_t i = start_index + 1; i < this->sta_.size(); i++) { const auto &sta = this->sta_[i]; @@ -1160,7 +1161,7 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { // Its priority has been decreased, so on next scan it will be sorted lower // and we'll try the next best BSSID. // Check if there are any potentially hidden networks to try - if (this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_()) >= 0) { + if (this->find_next_hidden_sta_(-1) >= 0) { return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try } // No hidden networks - skip directly to restart/rescan @@ -1179,8 +1180,13 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { // Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try if (this->selected_sta_index_ < static_cast(this->sta_.size()) - 1) { - // More SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect() - return WiFiRetryPhase::RETRY_HIDDEN; + // Check if find_next_hidden_sta_() would actually find another hidden SSID + // as it might have been seen in the scan results and we want to skip those + // otherwise we will get stuck in RETRY_HIDDEN phase + if (this->find_next_hidden_sta_(this->selected_sta_index_) != -1) { + // More hidden SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect() + return WiFiRetryPhase::RETRY_HIDDEN; + } } } // Exhausted all potentially hidden SSIDs - rescan to try next BSSID @@ -1271,7 +1277,7 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase // In that case, skip networks marked hidden:true (already tried) // Otherwise, include them (they haven't been tried yet) - this->selected_sta_index_ = this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_()); + this->selected_sta_index_ = this->find_next_hidden_sta_(-1); if (this->selected_sta_index_ == -1) { ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode"); @@ -1379,8 +1385,7 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() { // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase // In that case, skip networks marked hidden:true (already tried) // Otherwise, include them (they haven't been tried yet) - int8_t next_index = - this->find_next_hidden_sta_(this->selected_sta_index_, !this->went_through_explicit_hidden_phase_()); + int8_t next_index = this->find_next_hidden_sta_(this->selected_sta_index_); if (next_index != -1) { // Found another potentially hidden SSID this->selected_sta_index_ = next_index; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ed049544cfd..df8bac57283 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -380,7 +380,7 @@ class WiFiComponent : public Component { /// Returns index of next potentially hidden SSID, or -1 if none found /// @param start_index Start searching from index after this (-1 to start from beginning) /// @param include_explicit_hidden If true, include SSIDs marked hidden:true. If false, only find truly hidden SSIDs. - int8_t find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden = true); + int8_t find_next_hidden_sta_(int8_t start_index); /// Log failed connection and decrease BSSID priority to avoid repeated attempts void log_and_adjust_priority_for_failed_connect_(); /// Advance to next target (AP/SSID) within current phase, or increment retry counter From a6b7c1f18c933a8f9d0dc2b94ea492cfff70ef39 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 11 Nov 2025 16:17:25 +0100 Subject: [PATCH 342/461] [nrf52,gpio] add gpio levels for high voltage mode (#9858) Co-authored-by: J. Nick Koston --- esphome/components/nrf52/__init__.py | 26 +++++ esphome/components/nrf52/uicr.cpp | 110 ++++++++++++++++++ esphome/core/defines.h | 2 + .../components/nrf52/test.nrf52-adafruit.yaml | 3 + tests/components/nrf52/test.nrf52-mcumgr.yaml | 4 + .../components/nrf52/test.nrf52-xiao-ble.yaml | 2 + 6 files changed, 147 insertions(+) create mode 100644 esphome/components/nrf52/uicr.cpp diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index ace324c1f56..9566263c7c7 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -25,6 +25,7 @@ from esphome.const import ( CONF_FRAMEWORK, CONF_ID, CONF_RESET_PIN, + CONF_VOLTAGE, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, @@ -102,6 +103,11 @@ nrf52_ns = cg.esphome_ns.namespace("nrf52") DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component) CONF_DFU = "dfu" +CONF_REG0 = "reg0" +CONF_UICR_ERASE = "uicr_erase" + +VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3] +DEFAULT_VOLTAGE_LEVEL = "default" CONFIG_SCHEMA = cv.All( _detect_bootloader, @@ -116,6 +122,18 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, } ), + cv.Optional(CONF_REG0): cv.Schema( + { + cv.Required(CONF_VOLTAGE): cv.Any( + cv.All( + cv.voltage, + cv.one_of(*VOLTAGE_LEVELS, float=True), + ), + cv.one_of(*[DEFAULT_VOLTAGE_LEVEL], lower=True), + ), + cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean, + } + ), } ), ) @@ -183,6 +201,14 @@ async def to_code(config: ConfigType) -> None: if dfu_config := config.get(CONF_DFU): CORE.add_job(_dfu_to_code, dfu_config) + if reg0_config := config.get(CONF_REG0): + value = 7 # DEFAULT_VOLTAGE_LEVEL + if reg0_config[CONF_VOLTAGE] in VOLTAGE_LEVELS: + value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE]) + cg.add_define("USE_NRF52_REG0_VOUT", value) + if reg0_config[CONF_UICR_ERASE]: + cg.add_define("USE_NRF52_UICR_ERASE") + @coroutine_with_priority(CoroPriority.DIAGNOSTICS) async def _dfu_to_code(dfu_config): diff --git a/esphome/components/nrf52/uicr.cpp b/esphome/components/nrf52/uicr.cpp new file mode 100644 index 00000000000..22714b7e503 --- /dev/null +++ b/esphome/components/nrf52/uicr.cpp @@ -0,0 +1,110 @@ +#include "esphome/core/defines.h" + +#ifdef USE_NRF52_REG0_VOUT +#include +#include +#include + +extern "C" { +void nvmc_config(uint32_t mode); +void nvmc_wait(); +nrfx_err_t nrfx_nvmc_uicr_erase(); +} + +namespace esphome::nrf52 { + +enum class StatusFlags : uint8_t { + OK = 0x00, + NEED_RESET = 0x01, + NEED_ERASE = 0x02, +}; + +constexpr StatusFlags &operator|=(StatusFlags &a, StatusFlags b) { + a = static_cast(static_cast(a) | static_cast(b)); + return a; +} + +constexpr bool operator&(StatusFlags a, StatusFlags b) { + return (static_cast(a) & static_cast(b)) != 0; +} + +static bool regout0_ok() { + return (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) == (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos); +} + +static StatusFlags set_regout0() { + /* If the board is powered from USB (high voltage mode), + * GPIO output voltage is set to 1.8 volts by default. + */ + if (!regout0_ok()) { + nvmc_config(NVMC_CONFIG_WEN_Wen); + NRF_UICR->REGOUT0 = + (NRF_UICR->REGOUT0 & ~((uint32_t) UICR_REGOUT0_VOUT_Msk)) | (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos); + nvmc_wait(); + nvmc_config(NVMC_CONFIG_WEN_Ren); + return regout0_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE; + } + return StatusFlags::OK; +} + +#ifndef USE_BOOTLOADER_MCUBOOT +// https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/6a9a6a3e6d0f86918e9286188426a279976645bd/lib/sdk11/components/libraries/bootloader_dfu/dfu_types.h#L61 +constexpr uint32_t BOOTLOADER_REGION_START = 0x000F4000; +constexpr uint32_t BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS = 0x000FE000; + +static bool bootloader_ok() { + return NRF_UICR->NRFFW[0] == BOOTLOADER_REGION_START && NRF_UICR->NRFFW[1] == BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS; +} + +static StatusFlags fix_bootloader() { + if (!bootloader_ok()) { + nvmc_config(NVMC_CONFIG_WEN_Wen); + NRF_UICR->NRFFW[0] = BOOTLOADER_REGION_START; + NRF_UICR->NRFFW[1] = BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS; + nvmc_wait(); + nvmc_config(NVMC_CONFIG_WEN_Ren); + return bootloader_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE; + } + return StatusFlags::OK; +} +#endif + +static StatusFlags set_uicr() { + StatusFlags status = StatusFlags::OK; + status |= set_regout0(); +#ifndef USE_BOOTLOADER_MCUBOOT + status |= fix_bootloader(); +#endif + return status; +} + +static int board_esphome_init() { + StatusFlags status = set_uicr(); + +#ifdef USE_NRF52_UICR_ERASE + if (status & StatusFlags::NEED_ERASE) { + nrfx_err_t ret = nrfx_nvmc_uicr_erase(); + if (ret != NRFX_SUCCESS) { +#ifdef CONFIG_PRINTK + printk("nrfx_nvmc_uicr_erase failed %d\n", ret); +#endif + } else { + status |= set_uicr(); + } + } +#endif + + if (status & StatusFlags::NEED_RESET) { + /* a reset is required for changes to take effect */ + NVIC_SystemReset(); + } + + return 0; +} +} // namespace esphome::nrf52 + +static int board_esphome_init() { return esphome::nrf52::board_esphome_init(); } + +SYS_INIT(board_esphome_init, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); + +#endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ac725fbca94..c522a8ec62a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -287,6 +287,8 @@ #ifdef USE_NRF52 #define USE_NRF52_DFU +#define USE_NRF52_REG0_VOUT 5 +#define USE_NRF52_UICR_ERASE #define USE_SOFTDEVICE_ID 7 #define USE_SOFTDEVICE_VERSION 1 #endif diff --git a/tests/components/nrf52/test.nrf52-adafruit.yaml b/tests/components/nrf52/test.nrf52-adafruit.yaml index cf704ecceb2..72fd015953e 100644 --- a/tests/components/nrf52/test.nrf52-adafruit.yaml +++ b/tests/components/nrf52/test.nrf52-adafruit.yaml @@ -15,3 +15,6 @@ nrf52: inverted: true mode: output: true + reg0: + voltage: 2.1V + uicr_erase: true diff --git a/tests/components/nrf52/test.nrf52-mcumgr.yaml b/tests/components/nrf52/test.nrf52-mcumgr.yaml index e69de29bb2d..89ec637db6a 100644 --- a/tests/components/nrf52/test.nrf52-mcumgr.yaml +++ b/tests/components/nrf52/test.nrf52-mcumgr.yaml @@ -0,0 +1,4 @@ +nrf52: + reg0: + voltage: 3.3V + uicr_erase: true diff --git a/tests/components/nrf52/test.nrf52-xiao-ble.yaml b/tests/components/nrf52/test.nrf52-xiao-ble.yaml index 3fe80209b62..c3c44902f05 100644 --- a/tests/components/nrf52/test.nrf52-xiao-ble.yaml +++ b/tests/components/nrf52/test.nrf52-xiao-ble.yaml @@ -5,3 +5,5 @@ nrf52: inverted: true mode: output: true + reg0: + voltage: default From 75c220eeb609e0ee5c1c874d3268de1df5c0d251 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 09:42:09 -0600 Subject: [PATCH 343/461] more tweaks for corner cases --- esphome/components/wifi/wifi_component.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0fef3fe6616..74543613192 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -262,9 +262,10 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { // Skip networks that were already tried in EXPLICIT_HIDDEN phase // Those are: networks marked hidden:true that appear before the first non-hidden network + // If all networks are hidden (first_non_hidden_idx == -1), skip all of them if (!include_explicit_hidden && sta.get_hidden()) { int8_t first_non_hidden_idx = this->find_first_non_hidden_index_(); - if (first_non_hidden_idx >= 0 && static_cast(i) < first_non_hidden_idx) { + if (first_non_hidden_idx < 0 || static_cast(i) < first_non_hidden_idx) { ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str()); continue; } @@ -1004,6 +1005,12 @@ void WiFiComponent::check_scanning_finished() { // No scan results matched our configured networks - transition directly to hidden mode // Don't call retry_connect() since we never attempted a connection (no BSSID to penalize) this->transition_to_phase_(WiFiRetryPhase::RETRY_HIDDEN); + // If no hidden networks to try, skip connection attempt (will be handled on next loop) + if (this->selected_sta_index_ == -1) { + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); + return; + } // Now start connection attempt in hidden mode } else if (this->transition_to_phase_(WiFiRetryPhase::SCAN_CONNECTING)) { return; // scan started, wait for next loop iteration @@ -1143,7 +1150,12 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::EXPLICIT_HIDDEN; } - // No more consecutive explicitly hidden networks - proceed to scanning + // No more consecutive explicitly hidden networks + // If ALL networks are hidden, skip scanning and go directly to restart + if (this->find_first_non_hidden_index_() < 0) { + return WiFiRetryPhase::RESTARTING_ADAPTER; + } + // Otherwise proceed to scanning for non-hidden networks return WiFiRetryPhase::SCAN_CONNECTING; } @@ -1218,8 +1230,8 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { /// - Performing phase-specific initialization (e.g., advancing AP index, starting scans) /// /// @param new_phase The phase we're transitioning TO -/// @return true if an async scan was started (caller should wait for completion) -/// false if no scan started (caller can proceed with connection attempt) +/// @return true if connection attempt should be skipped (scan started or no networks to try) +/// false if caller can proceed with connection attempt bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { WiFiRetryPhase old_phase = this->retry_phase_; From 8e29ae416e1819d50f72691b894fa910d619b553 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 09:44:34 -0600 Subject: [PATCH 344/461] Update esphome/components/wifi/wifi_component.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/wifi/wifi_component.h | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index df8bac57283..a8fc7df37ce 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -379,7 +379,6 @@ class WiFiComponent : public Component { /// Find next SSID that wasn't in scan results (might be hidden) /// Returns index of next potentially hidden SSID, or -1 if none found /// @param start_index Start searching from index after this (-1 to start from beginning) - /// @param include_explicit_hidden If true, include SSIDs marked hidden:true. If false, only find truly hidden SSIDs. int8_t find_next_hidden_sta_(int8_t start_index); /// Log failed connection and decrease BSSID priority to avoid repeated attempts void log_and_adjust_priority_for_failed_connect_(); From 1b30346c1e6dd0aa5b057ceec2f54828e9ae7c3e Mon Sep 17 00:00:00 2001 From: Tomasz Duda Date: Tue, 11 Nov 2025 18:08:10 +0100 Subject: [PATCH 345/461] fix --- esphome/components/ds1307/ds1307.cpp | 2 +- esphome/components/pcf85063/pcf85063.cpp | 2 +- esphome/components/pcf8563/pcf8563.cpp | 4 +--- esphome/components/time/real_time_clock.cpp | 7 +++++++ esphome/components/time/real_time_clock.h | 2 ++ tests/components/ds1307/test.nrf52-adafruit.yaml | 4 ++++ tests/components/pcf85063/test.nrf52-adafruit.yaml | 4 ++++ 7 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 tests/components/ds1307/test.nrf52-adafruit.yaml create mode 100644 tests/components/pcf85063/test.nrf52-adafruit.yaml diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index 077db497b1e..adbd7b5487a 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -23,7 +23,7 @@ void DS1307Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + RealTimeClock::dump_config(); } float DS1307Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index cb987c6129e..f38b60b55d4 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -23,7 +23,7 @@ void PCF85063Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + RealTimeClock::dump_config(); } float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index d6f37f44e6e..2090936bb69 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -23,9 +23,7 @@ void PCF8563Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } -#ifndef USE_ZEPHYR - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); -#endif + RealTimeClock::dump_config(); } float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 42c564659f7..f8888380bc8 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -23,6 +23,13 @@ namespace time { static const char *const TAG = "time"; RealTimeClock::RealTimeClock() = default; + +void RealTimeClock::dump_config() { +#ifdef USE_TIME_TIMEZONE + ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); +#endif +} + void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); // Update UTC epoch time. diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index bbcecaa6284..2f17bd86d6f 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -52,6 +52,8 @@ class RealTimeClock : public PollingComponent { this->time_sync_callback_.add(std::move(callback)); }; + void dump_config() override; + protected: /// Report a unix epoch as current time. void synchronize_epoch_(uint32_t epoch); diff --git a/tests/components/ds1307/test.nrf52-adafruit.yaml b/tests/components/ds1307/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/ds1307/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/pcf85063/test.nrf52-adafruit.yaml b/tests/components/pcf85063/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/pcf85063/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml From d389ed585e47b0ae01131924f7f6b359b3c48c67 Mon Sep 17 00:00:00 2001 From: Tomasz Duda Date: Tue, 11 Nov 2025 18:13:02 +0100 Subject: [PATCH 346/461] fix --- esphome/components/rx8130/rx8130.cpp | 1 + tests/components/rx8130/test.nrf52-adafruit.yaml | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 tests/components/rx8130/test.nrf52-adafruit.yaml diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index cf6ea3e6e6e..ba092a48340 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -62,6 +62,7 @@ void RX8130Component::update() { this->read_time(); } void RX8130Component::dump_config() { ESP_LOGCONFIG(TAG, "RX8130:"); LOG_I2C_DEVICE(this); + RealTimeClock::dump_config(); } void RX8130Component::read_time() { diff --git a/tests/components/rx8130/test.nrf52-adafruit.yaml b/tests/components/rx8130/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/rx8130/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml From b58b706bd6529a30f6168f472254417cc5643292 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 11:17:05 -0600 Subject: [PATCH 347/461] fix --- .../components/homeassistant/time/homeassistant_time.cpp | 6 ++---- esphome/components/rx8130/rx8130.cpp | 1 + esphome/components/sntp/sntp_component.cpp | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index 0a91a2f63db..1715a7e24d4 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -7,10 +7,8 @@ namespace homeassistant { static const char *const TAG = "homeassistant.time"; void HomeassistantTime::dump_config() { - ESP_LOGCONFIG(TAG, - "Home Assistant Time:\n" - " Timezone: '%s'", - this->timezone_.c_str()); + ESP_LOGCONFIG(TAG, "Home Assistant Time:"); + RealTimeClock::dump_config(); } float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index cf6ea3e6e6e..ba092a48340 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -62,6 +62,7 @@ void RX8130Component::update() { this->read_time(); } void RX8130Component::dump_config() { ESP_LOGCONFIG(TAG, "RX8130:"); LOG_I2C_DEVICE(this); + RealTimeClock::dump_config(); } void RX8130Component::read_time() { diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 331a9b35099..c4d78b6e0b1 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -61,6 +61,7 @@ void SNTPComponent::dump_config() { for (auto &server : this->servers_) { ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server); } + RealTimeClock::dump_config(); } void SNTPComponent::update() { #if !defined(USE_ESP32) From a14e2d4d087ed3f18023e7891cf066a1a9292254 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 11:22:33 -0600 Subject: [PATCH 348/461] Update esphome/components/time/real_time_clock.cpp --- esphome/components/time/real_time_clock.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index f8888380bc8..175cee0c1ff 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -26,7 +26,7 @@ RealTimeClock::RealTimeClock() = default; void RealTimeClock::dump_config() { #ifdef USE_TIME_TIMEZONE - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str()); #endif } From d74fc6347bef4587c4ce6118b4ea02382bfd4fff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 11:24:41 -0600 Subject: [PATCH 349/461] Update esphome/components/homeassistant/time/homeassistant_time.cpp --- esphome/components/homeassistant/time/homeassistant_time.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index 1715a7e24d4..e72c5a21f5b 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -7,7 +7,7 @@ namespace homeassistant { static const char *const TAG = "homeassistant.time"; void HomeassistantTime::dump_config() { - ESP_LOGCONFIG(TAG, "Home Assistant Time:"); + ESP_LOGCONFIG(TAG, "Home Assistant Time"); RealTimeClock::dump_config(); } From a6b905e1488fb5bd9eefef426d45ddfd2b38700c Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 11 Nov 2025 18:50:07 +0100 Subject: [PATCH 350/461] [nrf52,pcf8563] fix build error (#11846) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/ds1307/ds1307.cpp | 2 +- .../components/homeassistant/time/homeassistant_time.cpp | 6 ++---- esphome/components/pcf85063/pcf85063.cpp | 2 +- esphome/components/pcf8563/pcf8563.cpp | 2 +- esphome/components/rx8130/rx8130.cpp | 1 + esphome/components/sntp/sntp_component.cpp | 1 + esphome/components/time/real_time_clock.cpp | 7 +++++++ esphome/components/time/real_time_clock.h | 2 ++ tests/components/ds1307/test.nrf52-adafruit.yaml | 4 ++++ tests/components/pcf85063/test.nrf52-adafruit.yaml | 4 ++++ tests/components/pcf8563/test.nrf52-adafruit.yaml | 4 ++++ tests/components/rx8130/test.nrf52-adafruit.yaml | 4 ++++ 12 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 tests/components/ds1307/test.nrf52-adafruit.yaml create mode 100644 tests/components/pcf85063/test.nrf52-adafruit.yaml create mode 100644 tests/components/pcf8563/test.nrf52-adafruit.yaml create mode 100644 tests/components/rx8130/test.nrf52-adafruit.yaml diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index 077db497b1e..adbd7b5487a 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -23,7 +23,7 @@ void DS1307Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + RealTimeClock::dump_config(); } float DS1307Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index 0a91a2f63db..e72c5a21f5b 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -7,10 +7,8 @@ namespace homeassistant { static const char *const TAG = "homeassistant.time"; void HomeassistantTime::dump_config() { - ESP_LOGCONFIG(TAG, - "Home Assistant Time:\n" - " Timezone: '%s'", - this->timezone_.c_str()); + ESP_LOGCONFIG(TAG, "Home Assistant Time"); + RealTimeClock::dump_config(); } float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index cb987c6129e..f38b60b55d4 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -23,7 +23,7 @@ void PCF85063Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + RealTimeClock::dump_config(); } float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index 27020378a6a..2090936bb69 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -23,7 +23,7 @@ void PCF8563Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + RealTimeClock::dump_config(); } float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index cf6ea3e6e6e..ba092a48340 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -62,6 +62,7 @@ void RX8130Component::update() { this->read_time(); } void RX8130Component::dump_config() { ESP_LOGCONFIG(TAG, "RX8130:"); LOG_I2C_DEVICE(this); + RealTimeClock::dump_config(); } void RX8130Component::read_time() { diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 331a9b35099..c4d78b6e0b1 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -61,6 +61,7 @@ void SNTPComponent::dump_config() { for (auto &server : this->servers_) { ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server); } + RealTimeClock::dump_config(); } void SNTPComponent::update() { #if !defined(USE_ESP32) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 42c564659f7..175cee0c1ff 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -23,6 +23,13 @@ namespace time { static const char *const TAG = "time"; RealTimeClock::RealTimeClock() = default; + +void RealTimeClock::dump_config() { +#ifdef USE_TIME_TIMEZONE + ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str()); +#endif +} + void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); // Update UTC epoch time. diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index bbcecaa6284..2f17bd86d6f 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -52,6 +52,8 @@ class RealTimeClock : public PollingComponent { this->time_sync_callback_.add(std::move(callback)); }; + void dump_config() override; + protected: /// Report a unix epoch as current time. void synchronize_epoch_(uint32_t epoch); diff --git a/tests/components/ds1307/test.nrf52-adafruit.yaml b/tests/components/ds1307/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/ds1307/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/pcf85063/test.nrf52-adafruit.yaml b/tests/components/pcf85063/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/pcf85063/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/pcf8563/test.nrf52-adafruit.yaml b/tests/components/pcf8563/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/pcf8563/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/rx8130/test.nrf52-adafruit.yaml b/tests/components/rx8130/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/rx8130/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml From 661920c51edcaaf3c8c62d86499f79388a063d8d Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 11 Nov 2025 19:18:17 +0100 Subject: [PATCH 351/461] [nrf52,ssd1306_i2c] fix build error (#11847) --- esphome/core/macros.h | 4 ++++ tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml diff --git a/esphome/core/macros.h b/esphome/core/macros.h index 8b2383321b2..2e47453c406 100644 --- a/esphome/core/macros.h +++ b/esphome/core/macros.h @@ -6,3 +6,7 @@ #ifdef USE_ARDUINO #include #endif + +#ifdef USE_ZEPHYR +#define M_PI 3.14159265358979323846 +#endif diff --git a/tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml b/tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml new file mode 100644 index 00000000000..28254e4af59 --- /dev/null +++ b/tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml @@ -0,0 +1,7 @@ +substitutions: + reset_pin: P0.10 + +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml From 93f8e40111da66b8d1e9c6561c612c92e7c99d35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 13:12:10 -0600 Subject: [PATCH 352/461] Fix scan failing after restart --- esphome/components/wifi/wifi_component.cpp | 34 ++++++---------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 74543613192..88e60c45237 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -413,8 +413,10 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); - delay(100); // NOLINT + // Enter cooldown state to allow WiFi hardware to stabilize after restart // Don't set retry_phase_ or num_retried_ here - state machine handles transitions + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); } void WiFiComponent::loop() { @@ -435,19 +437,8 @@ void WiFiComponent::loop() { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); if (millis() - this->action_started_ > 5000) { - // After cooldown, connect based on current retry phase - this->reset_selected_ap_to_first_if_invalid_(); - - // Check if we need to trigger a scan first - if (this->needs_scan_results_() && !this->all_networks_hidden_()) { - // Need scan results or no matching networks found - scan/rescan - ESP_LOGD(TAG, "Scanning required for phase %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); - this->start_scanning(); - } else { - // Have everything we need to connect (or all networks are hidden, skip scanning) - WiFiAP params = this->build_params_for_current_phase_(); - this->start_connecting(params, false); - } + // After cooldown, let retry_connect handle phase transitions and connection logic + this->retry_connect(); } break; } @@ -1007,8 +998,6 @@ void WiFiComponent::check_scanning_finished() { this->transition_to_phase_(WiFiRetryPhase::RETRY_HIDDEN); // If no hidden networks to try, skip connection attempt (will be handled on next loop) if (this->selected_sta_index_ == -1) { - this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; - this->action_started_ = millis(); return; } // Now start connection attempt in hidden mode @@ -1425,15 +1414,13 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() { void WiFiComponent::retry_connect() { this->log_and_adjust_priority_for_failed_connect_(); - delay(10); - // Determine next retry phase based on current state WiFiRetryPhase current_phase = this->retry_phase_; WiFiRetryPhase next_phase = this->determine_next_phase_(); // Handle phase transitions (transition_to_phase_ handles same-phase no-op internally) if (this->transition_to_phase_(next_phase)) { - return; // Wait for scan to complete + return; // Scan started or adapter restarted (which sets its own state) } if (next_phase == current_phase) { @@ -1442,7 +1429,7 @@ void WiFiComponent::retry_connect() { this->error_from_callback_ = false; - if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { + if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING || this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING_2) { yield(); // Check if we have a valid target before building params // After exhausting all networks in a phase, selected_sta_index_ may be -1 @@ -1451,13 +1438,10 @@ void WiFiComponent::retry_connect() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; WiFiAP params = this->build_params_for_current_phase_(); this->start_connecting(params, true); - return; } - // No valid target - fall through to set state to allow phase transition + } else { + ESP_LOGW(TAG, "Retry called in invalid state %d", (int) this->state_); } - - this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; - this->action_started_ = millis(); } void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } From 7a92565a0c4d0290f55a63f3cfdad9da9ec5fe17 Mon Sep 17 00:00:00 2001 From: CzBiX Date: Wed, 12 Nov 2025 03:24:52 +0800 Subject: [PATCH 353/461] [lvgl] Fix compile when using transform_zoom (#11845) --- esphome/components/lvgl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 4df68a6386c..2a24f343c33 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -331,7 +331,7 @@ async def to_code(configs): # This must be done after all widgets are created for comp in helpers.lvgl_components_required: cg.add_define(f"USE_LVGL_{comp.upper()}") - if "transform_angle" in styles_used: + if {"transform_angle", "transform_zoom"} & styles_used: add_define("LV_COLOR_SCREEN_TRANSP", "1") for use in helpers.lv_uses: add_define(f"LV_USE_{use.upper()}") From f6ac916bb2904f461fc1e4ce30ab547b0e6c926b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 13:55:29 -0600 Subject: [PATCH 354/461] cleanups --- esphome/components/wifi/wifi_component.cpp | 31 +++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 88e60c45237..3b68d299b0e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -437,8 +437,14 @@ void WiFiComponent::loop() { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); if (millis() - this->action_started_ > 5000) { - // After cooldown, let retry_connect handle phase transitions and connection logic - this->retry_connect(); + // After cooldown we either restarted the adapter because of + // a failure, or something tried to connect over and over + // so we entered cooldown. In both cases we call + // check_connecting_finished to continue the state machine. + // If we just restarted the adapter because we failed to connect, + // this->error_from_callback_ will be true and we will move to the + // next retry phase. + this->check_connecting_finished(); } break; } @@ -1076,12 +1082,18 @@ void WiFiComponent::check_connecting_finished() { uint32_t now = millis(); if (now - this->action_started_ > 30000) { ESP_LOGW(TAG, "Connection timeout"); + // Move from STA_CONNECTING_2 back to STA_CONNECTING state + // since we know the connection attempt has failed + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); return; } if (this->error_from_callback_) { - ESP_LOGW(TAG, "Connecting to network failed"); + ESP_LOGW(TAG, "Connecting to network failed (callback)"); + // Move from STA_CONNECTING_2 back to STA_CONNECTING state + // since we know the connection attempt is finished + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); return; } @@ -1090,6 +1102,9 @@ void WiFiComponent::check_connecting_finished() { return; } + // Move from STA_CONNECTING_2 back to STA_CONNECTING state + // since we know the connection attempt is finished + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; if (status == WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND) { ESP_LOGW(TAG, "Network no longer found"); this->retry_connect(); @@ -1429,7 +1444,7 @@ void WiFiComponent::retry_connect() { this->error_from_callback_ = false; - if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING || this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING_2) { + if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { yield(); // Check if we have a valid target before building params // After exhausting all networks in a phase, selected_sta_index_ may be -1 @@ -1438,10 +1453,14 @@ void WiFiComponent::retry_connect() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; WiFiAP params = this->build_params_for_current_phase_(); this->start_connecting(params, true); + return; } - } else { - ESP_LOGW(TAG, "Retry called in invalid state %d", (int) this->state_); } + + ESP_LOGD(TAG, "Entering cooldown from state %d and phase %s", this->state_, + LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); } void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } From db9af4a8623e3b5db8fb9bab8f60c970020c98a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 14:00:25 -0600 Subject: [PATCH 355/461] cleanup --- esphome/components/wifi/wifi_component.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 3b68d299b0e..e6560e86708 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -441,9 +441,6 @@ void WiFiComponent::loop() { // a failure, or something tried to connect over and over // so we entered cooldown. In both cases we call // check_connecting_finished to continue the state machine. - // If we just restarted the adapter because we failed to connect, - // this->error_from_callback_ will be true and we will move to the - // next retry phase. this->check_connecting_finished(); } break; @@ -1455,6 +1452,7 @@ void WiFiComponent::retry_connect() { this->start_connecting(params, true); return; } + // No valid target - fall through to set state to allow phase transition } ESP_LOGD(TAG, "Entering cooldown from state %d and phase %s", this->state_, From 1bde521380bef0f4c4a50f5bb144e7eb51bc7738 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 14:05:44 -0600 Subject: [PATCH 356/461] cleanups --- esphome/components/wifi/wifi_component.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e6560e86708..bd94d9392c3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1450,11 +1450,14 @@ void WiFiComponent::retry_connect() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; WiFiAP params = this->build_params_for_current_phase_(); this->start_connecting(params, true); - return; } - // No valid target - fall through to set state to allow phase transition + return; } + // If we can't progress forward its likely because scanning failed + // or the stack is in a bad state after restart so we cooldown first + // and once it finishes, cooldown will call check_connecting_finished() + // which will progress the state machine ESP_LOGD(TAG, "Entering cooldown from state %d and phase %s", this->state_, LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; From 80a7c6d3c31b172a34ba0203785b128b146d92a6 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 11 Nov 2025 21:52:41 +0100 Subject: [PATCH 357/461] [nrf52,debug] add partition dump (#11839) Co-authored-by: J. Nick Koston --- esphome/components/debug/debug_component.cpp | 6 ++-- esphome/components/debug/debug_component.h | 8 ++--- esphome/components/debug/debug_zephyr.cpp | 32 +++++++++++++++++++ .../components/debug/test.nrf52-xiao-ble.yaml | 1 + 4 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 tests/components/debug/test.nrf52-xiao-ble.yaml diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index ade0968e08f..f54bf82eae0 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -49,9 +49,9 @@ void DebugComponent::dump_config() { } #endif // USE_TEXT_SENSOR -#ifdef USE_ESP32 - this->log_partition_info_(); // Log partition information for ESP32 -#endif // USE_ESP32 +#if defined(USE_ESP32) || defined(USE_ZEPHYR) + this->log_partition_info_(); // Log partition information +#endif } void DebugComponent::loop() { diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index efd0dafab0c..96306f7cdfe 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -62,19 +62,19 @@ class DebugComponent : public PollingComponent { sensor::Sensor *cpu_frequency_sensor_{nullptr}; #endif // USE_SENSOR -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_ZEPHYR) /** * @brief Logs information about the device's partition table. * - * This function iterates through the ESP32's partition table and logs details + * This function iterates through the partition table and logs details * about each partition, including its name, type, subtype, starting address, * and size. The information is useful for diagnosing issues related to flash * memory or verifying the partition configuration dynamically at runtime. * - * Only available when compiled for ESP32 platforms. + * Only available when compiled for ESP32 and ZEPHYR platforms. */ void log_partition_info_(); -#endif // USE_ESP32 +#endif #ifdef USE_TEXT_SENSOR text_sensor::TextSensor *device_info_{nullptr}; diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 62fa391e5fa..c888c41a782 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0] @@ -86,6 +87,37 @@ std::string DebugComponent::get_reset_reason_() { uint32_t DebugComponent::get_free_heap_() { return INT_MAX; } +static void fa_cb(const struct flash_area *fa, void *user_data) { +#if CONFIG_FLASH_MAP_LABELS + const char *fa_label = flash_area_label(fa); + + if (fa_label == nullptr) { + fa_label = "-"; + } + ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s %-24.24s 0x%-10x 0x%-12x", (int) fa->fa_id, + sizeof(uintptr_t) * 2, (uintptr_t) fa->fa_dev, fa->fa_dev->name, fa_label, (uint32_t) fa->fa_off, + fa->fa_size); +#else + ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s 0x%-10x 0x%-12x", (int) fa->fa_id, sizeof(uintptr_t) * 2, + (uintptr_t) fa->fa_dev, fa->fa_dev->name, (uint32_t) fa->fa_off, fa->fa_size); +#endif +} + +void DebugComponent::log_partition_info_() { +#if CONFIG_FLASH_MAP_LABELS + ESP_LOGCONFIG(TAG, "ID | Device | Device Name " + "| Label | Offset | Size"); + ESP_LOGCONFIG(TAG, "--------------------------------------------" + "-----------------------------------------------"); +#else + ESP_LOGCONFIG(TAG, "ID | Device | Device Name " + "| Offset | Size"); + ESP_LOGCONFIG(TAG, "-----------------------------------------" + "------------------------------"); +#endif + flash_area_foreach(fa_cb, nullptr); +} + void DebugComponent::get_device_info_(std::string &device_info) { std::string supply = "Main supply status: "; if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) { diff --git a/tests/components/debug/test.nrf52-xiao-ble.yaml b/tests/components/debug/test.nrf52-xiao-ble.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/debug/test.nrf52-xiao-ble.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 191cf1b03ce3b27c87a6f03e1573251905f557c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 14:59:12 -0600 Subject: [PATCH 358/461] preen --- esphome/components/wifi/wifi_component.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index bd94d9392c3..dd4285c6e2a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -197,6 +197,10 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1; // Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1; +/// Cooldown duration in milliseconds after adapter restart or repeated failures +/// Allows WiFi hardware to stabilize before next connection attempt +static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 2500; + static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { switch (phase) { case WiFiRetryPhase::INITIAL_CONNECT: @@ -436,7 +440,7 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); - if (millis() - this->action_started_ > 5000) { + if (millis() - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) { // After cooldown we either restarted the adapter because of // a failure, or something tried to connect over and over // so we entered cooldown. In both cases we call From c3967df6ce1d5bd954b8c7be0478a7f4e31d01fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 15:15:41 -0600 Subject: [PATCH 359/461] tweak --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index dd4285c6e2a..375e1083168 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -199,7 +199,7 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1; /// Cooldown duration in milliseconds after adapter restart or repeated failures /// Allows WiFi hardware to stabilize before next connection attempt -static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 2500; +static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 1000; static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { switch (phase) { From e589542bd3e3403fb75f35efad18514b5453906c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 15:21:51 -0600 Subject: [PATCH 360/461] make message more sane --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 375e1083168..136d5443d70 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -279,7 +279,7 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); return static_cast(i); } - ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (visible in scan)", sta.get_ssid().c_str()); + ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str()); } // No hidden SSIDs found return -1; From 2f91e7bd47e96a9542e73764b67d4b77ad48f29e Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 11 Nov 2025 22:33:53 +0100 Subject: [PATCH 361/461] [nrf52] fix boot loop (#11854) --- esphome/components/nrf52/__init__.py | 14 ++++---------- esphome/components/nrf52/uicr.cpp | 13 ++++++++++++- tests/components/nrf52/test.nrf52-xiao-ble.yaml | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 9566263c7c7..a3b79bf1392 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -107,7 +107,6 @@ CONF_REG0 = "reg0" CONF_UICR_ERASE = "uicr_erase" VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3] -DEFAULT_VOLTAGE_LEVEL = "default" CONFIG_SCHEMA = cv.All( _detect_bootloader, @@ -124,12 +123,9 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_REG0): cv.Schema( { - cv.Required(CONF_VOLTAGE): cv.Any( - cv.All( - cv.voltage, - cv.one_of(*VOLTAGE_LEVELS, float=True), - ), - cv.one_of(*[DEFAULT_VOLTAGE_LEVEL], lower=True), + cv.Required(CONF_VOLTAGE): cv.All( + cv.voltage, + cv.one_of(*VOLTAGE_LEVELS, float=True), ), cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean, } @@ -202,9 +198,7 @@ async def to_code(config: ConfigType) -> None: CORE.add_job(_dfu_to_code, dfu_config) if reg0_config := config.get(CONF_REG0): - value = 7 # DEFAULT_VOLTAGE_LEVEL - if reg0_config[CONF_VOLTAGE] in VOLTAGE_LEVELS: - value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE]) + value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE]) cg.add_define("USE_NRF52_REG0_VOUT", value) if reg0_config[CONF_UICR_ERASE]: cg.add_define("USE_NRF52_UICR_ERASE") diff --git a/esphome/components/nrf52/uicr.cpp b/esphome/components/nrf52/uicr.cpp index 22714b7e503..4c0beeb503b 100644 --- a/esphome/components/nrf52/uicr.cpp +++ b/esphome/components/nrf52/uicr.cpp @@ -69,9 +69,20 @@ static StatusFlags fix_bootloader() { } #endif +#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0] + static StatusFlags set_uicr() { StatusFlags status = StatusFlags::OK; - status |= set_regout0(); +#ifndef USE_BOOTLOADER_MCUBOOT + if (BOOTLOADER_VERSION_REGISTER <= 0x902) { +#ifdef CONFIG_PRINTK + printk("cannot control regout0 for %#x\n", BOOTLOADER_VERSION_REGISTER); +#endif + } else +#endif + { + status |= set_regout0(); + } #ifndef USE_BOOTLOADER_MCUBOOT status |= fix_bootloader(); #endif diff --git a/tests/components/nrf52/test.nrf52-xiao-ble.yaml b/tests/components/nrf52/test.nrf52-xiao-ble.yaml index c3c44902f05..d53c6920017 100644 --- a/tests/components/nrf52/test.nrf52-xiao-ble.yaml +++ b/tests/components/nrf52/test.nrf52-xiao-ble.yaml @@ -6,4 +6,4 @@ nrf52: mode: output: true reg0: - voltage: default + voltage: 1.8V From 42fa0b61a74c998ce9995e762b7b019b320b7dac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 15:52:51 -0600 Subject: [PATCH 362/461] cleanup conflicting logic --- esphome/components/wifi/wifi_component.cpp | 42 +++++++--------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 136d5443d70..770a50e9092 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -464,7 +464,6 @@ void WiFiComponent::loop() { case WIFI_COMPONENT_STATE_STA_CONNECTED: { if (!this->is_connected()) { ESP_LOGW(TAG, "Connection lost; reconnecting"); - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); } else { this->status_clear_warning(); @@ -1083,18 +1082,12 @@ void WiFiComponent::check_connecting_finished() { uint32_t now = millis(); if (now - this->action_started_ > 30000) { ESP_LOGW(TAG, "Connection timeout"); - // Move from STA_CONNECTING_2 back to STA_CONNECTING state - // since we know the connection attempt has failed - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); return; } if (this->error_from_callback_) { ESP_LOGW(TAG, "Connecting to network failed (callback)"); - // Move from STA_CONNECTING_2 back to STA_CONNECTING state - // since we know the connection attempt is finished - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); return; } @@ -1103,9 +1096,6 @@ void WiFiComponent::check_connecting_finished() { return; } - // Move from STA_CONNECTING_2 back to STA_CONNECTING state - // since we know the connection attempt is finished - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; if (status == WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND) { ESP_LOGW(TAG, "Network no longer found"); this->retry_connect(); @@ -1428,6 +1418,10 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() { } void WiFiComponent::retry_connect() { + // We always need to be in STA_CONNECTING state to start a connection attempt + // If we start a scan here, we will set state to SCANNING + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; + this->log_and_adjust_priority_for_failed_connect_(); // Determine next retry phase based on current state @@ -1445,27 +1439,15 @@ void WiFiComponent::retry_connect() { this->error_from_callback_ = false; - if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { - yield(); - // Check if we have a valid target before building params - // After exhausting all networks in a phase, selected_sta_index_ may be -1 - // In that case, skip connection and let next wifi_loop() handle phase transition - if (this->selected_sta_index_ >= 0) { - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; - WiFiAP params = this->build_params_for_current_phase_(); - this->start_connecting(params, true); - } - return; + yield(); + // Check if we have a valid target before building params + // After exhausting all networks in a phase, selected_sta_index_ may be -1 + // In that case, skip connection and let next wifi_loop() handle phase transition + if (this->selected_sta_index_ >= 0) { + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; + WiFiAP params = this->build_params_for_current_phase_(); + this->start_connecting(params, true); } - - // If we can't progress forward its likely because scanning failed - // or the stack is in a bad state after restart so we cooldown first - // and once it finishes, cooldown will call check_connecting_finished() - // which will progress the state machine - ESP_LOGD(TAG, "Entering cooldown from state %d and phase %s", this->state_, - LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); - this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; - this->action_started_ = millis(); } void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } From 7b60a8a21ae1f62f62b16c3785ccea16e4c15680 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 15:55:09 -0600 Subject: [PATCH 363/461] cleanup conflicting logic --- esphome/components/wifi/wifi_component.cpp | 22 ++++++++-------------- esphome/components/wifi/wifi_component.h | 8 +------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 770a50e9092..90a0186a705 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -293,7 +293,7 @@ void WiFiComponent::start_initial_connection_() { this->selected_sta_index_ = 0; this->retry_phase_ = WiFiRetryPhase::EXPLICIT_HIDDEN; WiFiAP params = this->build_params_for_current_phase_(); - this->start_connecting(params, false); + this->start_connecting(params); } else { ESP_LOGI(TAG, "Starting scan"); this->start_scanning(); @@ -375,13 +375,13 @@ void WiFiComponent::start() { // Without saved data, try first configured network or use normal flow if (loaded_fast_connect) { ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str()); - this->start_connecting(params, false); + this->start_connecting(params); } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) { // No saved data, but have configured networks - try first non-hidden network ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str()); this->selected_sta_index_ = 0; params = this->build_params_for_current_phase_(); - this->start_connecting(params, false); + this->start_connecting(params); } else { // No saved data and (no networks OR first is hidden) - use normal flow this->start_initial_connection_(); @@ -454,8 +454,7 @@ void WiFiComponent::loop() { this->check_scanning_finished(); break; } - case WIFI_COMPONENT_STATE_STA_CONNECTING: - case WIFI_COMPONENT_STATE_STA_CONNECTING_2: { + case WIFI_COMPONENT_STATE_STA_CONNECTING: { this->status_set_warning(LOG_STR("associating to network")); this->check_connecting_finished(); break; @@ -663,7 +662,7 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa this->set_sta(sta); } -void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { +void WiFiComponent::start_connecting(const WiFiAP &ap) { // Log connection attempt at INFO level with priority std::string bssid_formatted; float priority = 0.0f; @@ -731,11 +730,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { return; } - if (!two) { - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; - } else { - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; - } + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->action_started_ = millis(); } @@ -1016,7 +1011,7 @@ void WiFiComponent::check_scanning_finished() { WiFiAP params = this->build_params_for_current_phase_(); // Ensure we're in SCAN_CONNECTING phase when connecting with scan results // (needed when scan was started directly without transition_to_phase_, e.g., initial scan) - this->start_connecting(params, false); + this->start_connecting(params); } void WiFiComponent::dump_config() { @@ -1444,9 +1439,8 @@ void WiFiComponent::retry_connect() { // After exhausting all networks in a phase, selected_sta_index_ may be -1 // In that case, skip connection and let next wifi_loop() handle phase transition if (this->selected_sta_index_ >= 0) { - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; WiFiAP params = this->build_params_for_current_phase_(); - this->start_connecting(params, true); + this->start_connecting(params); } } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index a8fc7df37ce..c4b5673d3f9 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -74,12 +74,6 @@ enum WiFiComponentState : uint8_t { WIFI_COMPONENT_STATE_STA_SCANNING, /** WiFi is in STA(+AP) mode and currently connecting to an AP. */ WIFI_COMPONENT_STATE_STA_CONNECTING, - /** WiFi is in STA(+AP) mode and currently connecting to an AP a second time. - * - * This is required because for some reason ESPs don't like to connect to WiFi APs directly after - * a scan. - * */ - WIFI_COMPONENT_STATE_STA_CONNECTING_2, /** WiFi is in STA(+AP) mode and successfully connected. */ WIFI_COMPONENT_STATE_STA_CONNECTED, /** WiFi is in AP-only mode and internal AP is already enabled. */ @@ -263,7 +257,7 @@ class WiFiComponent : public Component { bool is_disabled(); void start_scanning(); void check_scanning_finished(); - void start_connecting(const WiFiAP &ap, bool two); + void start_connecting(const WiFiAP &ap); void check_connecting_finished(); From a8f253eecf8e62815e8fc05362f498694ef6ad30 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 16:02:01 -0600 Subject: [PATCH 364/461] tweaks on failure paths --- esphome/components/wifi/wifi_component.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 90a0186a705..0b98e07c1b8 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -726,11 +726,12 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { if (!this->wifi_sta_connect_(ap)) { ESP_LOGE(TAG, "wifi_sta_connect_ failed"); - this->retry_connect(); - return; + // Enter cooldown to allow WiFi hardware to stabilize + // (immediate failure suggests hardware not ready, different from connection timeout) + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + } else { + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; } - - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->action_started_ = millis(); } @@ -1413,10 +1414,6 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() { } void WiFiComponent::retry_connect() { - // We always need to be in STA_CONNECTING state to start a connection attempt - // If we start a scan here, we will set state to SCANNING - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; - this->log_and_adjust_priority_for_failed_connect_(); // Determine next retry phase based on current state From a2ec7f622c861f7e21843d26a10b573515be43f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 16:04:37 -0600 Subject: [PATCH 365/461] [wifi] Fix infinite retry loop when no hidden networks and captive portal active (#11831) --- esphome/components/wifi/wifi_component.cpp | 28 ++++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7279e0c7838..49e433b4682 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1163,11 +1163,9 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { if (this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_()) >= 0) { return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try } - // No hidden networks - skip directly to restart/rescan - if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { - return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN - : WiFiRetryPhase::SCAN_CONNECTING; - } + // No hidden networks - always go through RESTARTING_ADAPTER phase + // This ensures num_retried_ gets reset and a fresh scan is triggered + // The actual adapter restart will be skipped if captive portal/improv is active return WiFiRetryPhase::RESTARTING_ADAPTER; case WiFiRetryPhase::RETRY_HIDDEN: @@ -1183,16 +1181,9 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::RETRY_HIDDEN; } } - // Exhausted all potentially hidden SSIDs - rescan to try next BSSID - // If captive portal/improv is active, skip adapter restart and go back to start - // Otherwise restart adapter to clear any stuck state - if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { - // Go back to explicit hidden if we went through it initially, otherwise scan - return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN - : WiFiRetryPhase::SCAN_CONNECTING; - } - - // Restart adapter + // Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER + // This ensures num_retried_ gets reset and a fresh scan is triggered + // The actual adapter restart will be skipped if captive portal/improv is active return WiFiRetryPhase::RESTARTING_ADAPTER; case WiFiRetryPhase::RESTARTING_ADAPTER: @@ -1280,7 +1271,12 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { break; case WiFiRetryPhase::RESTARTING_ADAPTER: - this->restart_adapter(); + // Skip actual adapter restart if captive portal/improv is active + // This allows state machine to reset num_retried_ and trigger fresh scan + // without disrupting the captive portal/improv connection + if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { + this->restart_adapter(); + } // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting return true; From fb5b37c17a376ac775cfab1728c159eb1dc1e129 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 16:09:33 -0600 Subject: [PATCH 366/461] avoid breaking change --- esphome/components/esp32_improv/esp32_improv_component.cpp | 2 +- esphome/components/improv_serial/improv_serial_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 2fa9d8f5234..398b1d42519 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -336,7 +336,7 @@ void ESP32ImprovComponent::process_incoming_data_() { this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); - wifi::global_wifi_component->start_connecting(sta, false); + wifi::global_wifi_component->start_connecting(sta); this->set_state_(improv::STATE_PROVISIONING); ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 9d080ea98e3..70260eeab3b 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -231,7 +231,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); - wifi::global_wifi_component->start_connecting(sta, false); + wifi::global_wifi_component->start_connecting(sta); this->set_state_(improv::STATE_PROVISIONING); ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index c4b5673d3f9..282ac5756a4 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -258,6 +258,8 @@ class WiFiComponent : public Component { void start_scanning(); void check_scanning_finished(); void start_connecting(const WiFiAP &ap); + // Backward compatibility overload - ignores 'two' parameter + void start_connecting(const WiFiAP &ap, bool /* two */) { this->start_connecting(ap); } void check_connecting_finished(); From ef04903a7a5e4665c13a2b4208e99ef976db1ab6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 16:10:17 -0600 Subject: [PATCH 367/461] [wifi] Change priority type from float to int8_t (#11830) --- esphome/components/wifi/__init__.py | 2 +- esphome/components/wifi/wifi_component.cpp | 53 ++++++++++++++++++---- esphome/components/wifi/wifi_component.h | 24 +++++----- tests/components/wifi/common.yaml | 5 ++ 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 5f4190a9331..358f920c2cc 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -174,7 +174,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( { cv.Optional(CONF_BSSID): cv.mac_address, cv.Optional(CONF_HIDDEN): cv.boolean, - cv.Optional(CONF_PRIORITY, default=0.0): cv.float_, + cv.Optional(CONF_PRIORITY, default=0): cv.int_range(min=-128, max=127), cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, } ) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 49e433b4682..681555431ef 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -667,7 +667,7 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { // Log connection attempt at INFO level with priority std::string bssid_formatted; - float priority = 0.0f; + int8_t priority = 0; if (ap.get_bssid().has_value()) { bssid_formatted = format_mac_address_pretty(ap.get_bssid().value().data()); @@ -675,7 +675,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { } ESP_LOGI(TAG, - "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %.1f, attempt %u/%u in phase %s)...", + "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...", ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_formatted.c_str() : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); @@ -812,7 +812,7 @@ void WiFiComponent::print_connect_params_() { wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) { - ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config->get_bssid())); + ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid())); } #endif #ifdef USE_WIFI_11KV_SUPPORT @@ -933,8 +933,7 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4.1f", res.get_channel(), res.get_rssi(), - res.get_priority()); + ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority()); } else { ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); @@ -1063,6 +1062,9 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; + // Clear priority tracking if all priorities are at minimum + this->clear_priorities_if_all_min_(); + #ifdef USE_WIFI_FAST_CONNECT this->save_fast_connect_settings_(); #endif @@ -1287,6 +1289,34 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { return false; // Did not start scan, can proceed with connection } +/// Clear BSSID priority tracking if all priorities are at minimum (saves memory) +/// At minimum priority, all BSSIDs are equally bad, so priority tracking is useless +/// Called after successful connection or after failed connection attempts +void WiFiComponent::clear_priorities_if_all_min_() { + if (this->sta_priorities_.empty()) { + return; + } + + int8_t first_priority = this->sta_priorities_[0].priority; + + // Only clear if all priorities have been decremented to the minimum value + // At this point, all BSSIDs have been equally penalized and priority info is useless + if (first_priority != std::numeric_limits::min()) { + return; + } + + for (const auto &pri : this->sta_priorities_) { + if (pri.priority != first_priority) { + return; // Not all same, nothing to do + } + } + + // All priorities are at minimum - clear the vector to save memory and reset + ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)"); + this->sta_priorities_.clear(); + this->sta_priorities_.shrink_to_fit(); +} + /// Log failed connection attempt and decrease BSSID priority to avoid repeated failures /// This function identifies which BSSID was attempted (from scan results or config), /// decreases its priority by 1.0 to discourage future attempts, and logs the change. @@ -1317,8 +1347,9 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { } // Decrease priority to avoid repeatedly trying the same failed BSSID - float old_priority = this->get_sta_priority(failed_bssid.value()); - float new_priority = old_priority - 1.0f; + int8_t old_priority = this->get_sta_priority(failed_bssid.value()); + int8_t new_priority = + (old_priority > std::numeric_limits::min()) ? (old_priority - 1) : std::numeric_limits::min(); this->set_sta_priority(failed_bssid.value(), new_priority); // Get SSID for logging @@ -1329,8 +1360,12 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { ssid = config->get_ssid(); } - ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %.1f → %.1f", ssid.c_str(), + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority); + + // After adjusting priority, check if all priorities are now at minimum + // If so, clear the vector to save memory and reset for fresh start + this->clear_priorities_if_all_min_(); } /// Handle target advancement or retry counter increment when staying in the same phase @@ -1543,9 +1578,9 @@ bool WiFiAP::get_hidden() const { return this->hidden_; } WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden) : bssid_(bssid), - ssid_(std::move(ssid)), channel_(channel), rssi_(rssi), + ssid_(std::move(ssid)), with_auth_(with_auth), is_hidden_(is_hidden) {} bool WiFiScanResult::matches(const WiFiAP &config) const { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ed049544cfd..b8223e8dc89 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -157,7 +157,7 @@ class WiFiAP { void set_eap(optional eap_auth); #endif // USE_WIFI_WPA2_EAP void set_channel(optional channel); - void set_priority(float priority) { priority_ = priority; } + void set_priority(int8_t priority) { priority_ = priority; } void set_manual_ip(optional manual_ip); void set_hidden(bool hidden); const std::string &get_ssid() const; @@ -167,7 +167,7 @@ class WiFiAP { const optional &get_eap() const; #endif // USE_WIFI_WPA2_EAP const optional &get_channel() const; - float get_priority() const { return priority_; } + int8_t get_priority() const { return priority_; } const optional &get_manual_ip() const; bool get_hidden() const; @@ -179,8 +179,8 @@ class WiFiAP { optional eap_; #endif // USE_WIFI_WPA2_EAP optional manual_ip_; - float priority_{0}; optional channel_; + int8_t priority_{0}; bool hidden_{false}; }; @@ -198,17 +198,17 @@ class WiFiScanResult { int8_t get_rssi() const; bool get_with_auth() const; bool get_is_hidden() const; - float get_priority() const { return priority_; } - void set_priority(float priority) { priority_ = priority; } + int8_t get_priority() const { return priority_; } + void set_priority(int8_t priority) { priority_ = priority; } bool operator==(const WiFiScanResult &rhs) const; protected: bssid_t bssid_; - std::string ssid_; - float priority_{0.0f}; uint8_t channel_; int8_t rssi_; + std::string ssid_; + int8_t priority_{0}; bool matches_{false}; bool with_auth_; bool is_hidden_; @@ -216,7 +216,7 @@ class WiFiScanResult { struct WiFiSTAPriority { bssid_t bssid; - float priority; + int8_t priority; }; enum WiFiPowerSaveMode : uint8_t { @@ -317,14 +317,14 @@ class WiFiComponent : public Component { } return false; } - float get_sta_priority(const bssid_t bssid) { + int8_t get_sta_priority(const bssid_t bssid) { for (auto &it : this->sta_priorities_) { if (it.bssid == bssid) return it.priority; } - return 0.0f; + return 0; } - void set_sta_priority(const bssid_t bssid, float priority) { + void set_sta_priority(const bssid_t bssid, int8_t priority) { for (auto &it : this->sta_priorities_) { if (it.bssid == bssid) { it.priority = priority; @@ -383,6 +383,8 @@ class WiFiComponent : public Component { int8_t find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden = true); /// Log failed connection and decrease BSSID priority to avoid repeated attempts void log_and_adjust_priority_for_failed_connect_(); + /// Clear BSSID priority tracking if all priorities are at minimum (saves memory) + void clear_priorities_if_all_min_(); /// Advance to next target (AP/SSID) within current phase, or increment retry counter /// Called when staying in the same phase after a failed connection attempt void advance_to_next_target_or_increment_retry_(); diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml index af27f850923..5d9973cbc80 100644 --- a/tests/components/wifi/common.yaml +++ b/tests/components/wifi/common.yaml @@ -15,5 +15,10 @@ wifi: networks: - ssid: MySSID password: password1 + priority: 10 - ssid: MySSID2 password: password2 + priority: 5 + - ssid: MySSID3 + password: password3 + priority: 0 From efe6e5840486acb937c818d7e1c3dff0d46ceae0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 16:15:12 -0600 Subject: [PATCH 368/461] clear failure on restart --- esphome/components/wifi/wifi_component.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c30692bb9b5..afb69454e5b 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -421,6 +421,7 @@ void WiFiComponent::restart_adapter() { // Don't set retry_phase_ or num_retried_ here - state machine handles transitions this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; this->action_started_ = millis(); + this->error_from_callback_ = false; } void WiFiComponent::loop() { From f0a9ee871b801e5296da827b07ab8bdb8f9d0dda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 16:19:47 -0600 Subject: [PATCH 369/461] keep --- esphome/components/wifi/wifi_component.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index afb69454e5b..20f66eb9f01 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -464,6 +464,7 @@ void WiFiComponent::loop() { case WIFI_COMPONENT_STATE_STA_CONNECTED: { if (!this->is_connected()) { ESP_LOGW(TAG, "Connection lost; reconnecting"); + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); } else { this->status_clear_warning(); From 1c7c559b6914b72ee1b45117fae5a855b7e3de47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 16:22:24 -0600 Subject: [PATCH 370/461] touch ups --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 20f66eb9f01..409e923f131 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -441,7 +441,7 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); - if (millis() - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) { + if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) { // After cooldown we either restarted the adapter because of // a failure, or something tried to connect over and over // so we entered cooldown. In both cases we call From 00c71b7236a1ce891f8cc6721240b72897757a46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 16:33:37 -0600 Subject: [PATCH 371/461] [wifi] Fix all-hidden networks duplicate attempts and scan skipping (#11848) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/wifi/wifi_component.cpp | 39 ++++++++++++++++------ esphome/components/wifi/wifi_component.h | 3 +- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 681555431ef..d75ac971ebe 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -253,17 +253,19 @@ bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const { return false; } -int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden) { +int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { // Find next SSID that wasn't in scan results (might be hidden) + bool include_explicit_hidden = !this->went_through_explicit_hidden_phase_(); // Start searching from start_index + 1 for (size_t i = start_index + 1; i < this->sta_.size(); i++) { const auto &sta = this->sta_[i]; // Skip networks that were already tried in EXPLICIT_HIDDEN phase // Those are: networks marked hidden:true that appear before the first non-hidden network + // If all networks are hidden (first_non_hidden_idx == -1), skip all of them if (!include_explicit_hidden && sta.get_hidden()) { int8_t first_non_hidden_idx = this->find_first_non_hidden_index_(); - if (first_non_hidden_idx >= 0 && static_cast(i) < first_non_hidden_idx) { + if (first_non_hidden_idx < 0 || static_cast(i) < first_non_hidden_idx) { ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str()); continue; } @@ -1002,6 +1004,12 @@ void WiFiComponent::check_scanning_finished() { // No scan results matched our configured networks - transition directly to hidden mode // Don't call retry_connect() since we never attempted a connection (no BSSID to penalize) this->transition_to_phase_(WiFiRetryPhase::RETRY_HIDDEN); + // If no hidden networks to try, skip connection attempt (will be handled on next loop) + if (this->selected_sta_index_ == -1) { + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); + return; + } // Now start connection attempt in hidden mode } else if (this->transition_to_phase_(WiFiRetryPhase::SCAN_CONNECTING)) { return; // scan started, wait for next loop iteration @@ -1144,7 +1152,12 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::EXPLICIT_HIDDEN; } - // No more consecutive explicitly hidden networks - proceed to scanning + // No more consecutive explicitly hidden networks + // If ALL networks are hidden, skip scanning and go directly to restart + if (this->find_first_non_hidden_index_() < 0) { + return WiFiRetryPhase::RESTARTING_ADAPTER; + } + // Otherwise proceed to scanning for non-hidden networks return WiFiRetryPhase::SCAN_CONNECTING; } @@ -1162,7 +1175,7 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { // Its priority has been decreased, so on next scan it will be sorted lower // and we'll try the next best BSSID. // Check if there are any potentially hidden networks to try - if (this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_()) >= 0) { + if (this->find_next_hidden_sta_(-1) >= 0) { return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try } // No hidden networks - always go through RESTARTING_ADAPTER phase @@ -1179,8 +1192,13 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { // Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try if (this->selected_sta_index_ < static_cast(this->sta_.size()) - 1) { - // More SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect() - return WiFiRetryPhase::RETRY_HIDDEN; + // Check if find_next_hidden_sta_() would actually find another hidden SSID + // as it might have been seen in the scan results and we want to skip those + // otherwise we will get stuck in RETRY_HIDDEN phase + if (this->find_next_hidden_sta_(this->selected_sta_index_) != -1) { + // More hidden SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect() + return WiFiRetryPhase::RETRY_HIDDEN; + } } } // Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER @@ -1205,8 +1223,8 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { /// - Performing phase-specific initialization (e.g., advancing AP index, starting scans) /// /// @param new_phase The phase we're transitioning TO -/// @return true if an async scan was started (caller should wait for completion) -/// false if no scan started (caller can proceed with connection attempt) +/// @return true if connection attempt should be skipped (scan started or no networks to try) +/// false if caller can proceed with connection attempt bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { WiFiRetryPhase old_phase = this->retry_phase_; @@ -1264,7 +1282,7 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase // In that case, skip networks marked hidden:true (already tried) // Otherwise, include them (they haven't been tried yet) - this->selected_sta_index_ = this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_()); + this->selected_sta_index_ = this->find_next_hidden_sta_(-1); if (this->selected_sta_index_ == -1) { ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode"); @@ -1410,8 +1428,7 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() { // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase // In that case, skip networks marked hidden:true (already tried) // Otherwise, include them (they haven't been tried yet) - int8_t next_index = - this->find_next_hidden_sta_(this->selected_sta_index_, !this->went_through_explicit_hidden_phase_()); + int8_t next_index = this->find_next_hidden_sta_(this->selected_sta_index_); if (next_index != -1) { // Found another potentially hidden SSID this->selected_sta_index_ = next_index; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index b8223e8dc89..e786708b08b 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -379,8 +379,7 @@ class WiFiComponent : public Component { /// Find next SSID that wasn't in scan results (might be hidden) /// Returns index of next potentially hidden SSID, or -1 if none found /// @param start_index Start searching from index after this (-1 to start from beginning) - /// @param include_explicit_hidden If true, include SSIDs marked hidden:true. If false, only find truly hidden SSIDs. - int8_t find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden = true); + int8_t find_next_hidden_sta_(int8_t start_index); /// Log failed connection and decrease BSSID priority to avoid repeated attempts void log_and_adjust_priority_for_failed_connect_(); /// Clear BSSID priority tracking if all priorities are at minimum (saves memory) From 65a303d48f92c1706dc6f464f88c41fbc9aa5c9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 16:39:55 -0600 Subject: [PATCH 372/461] [wifi] Add min_auth_mode configuration option (#11814) --- esphome/components/wifi/__init__.py | 42 +++++++++++++++++++ esphome/components/wifi/wifi_component.h | 8 ++++ .../wifi/wifi_component_esp8266.cpp | 13 +++++- .../wifi/wifi_component_esp_idf.cpp | 15 +++++-- tests/components/wifi/test.esp32-idf.yaml | 1 + tests/components/wifi/test.esp8266-ard.yaml | 6 ++- 6 files changed, 79 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 358f920c2cc..c42af232520 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,3 +1,5 @@ +import logging + from esphome import automation from esphome.automation import Condition import esphome.codegen as cg @@ -42,6 +44,7 @@ from esphome.const import ( CONF_TTLS_PHASE_2, CONF_USE_ADDRESS, CONF_USERNAME, + Platform, PlatformFramework, ) from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority @@ -49,10 +52,13 @@ import esphome.final_validate as fv from . import wpa2_eap +_LOGGER = logging.getLogger(__name__) + AUTO_LOAD = ["network"] NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" +CONF_MIN_AUTH_MODE = "min_auth_mode" # Maximum number of WiFi networks that can be configured # Limited to 127 because selected_sta_index_ is int8_t in C++ @@ -70,6 +76,14 @@ WIFI_POWER_SAVE_MODES = { "LIGHT": WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT, "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, } + +WifiMinAuthMode = wifi_ns.enum("WifiMinAuthMode") +WIFI_MIN_AUTH_MODES = { + "WPA": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA, + "WPA2": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA2, + "WPA3": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA3, +} +VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True) WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) @@ -187,6 +201,27 @@ def validate_variant(_): raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}") +def _apply_min_auth_mode_default(config): + """Apply platform-specific default for min_auth_mode and warn ESP8266 users.""" + # Only apply defaults for platforms that support min_auth_mode + if CONF_MIN_AUTH_MODE not in config and (CORE.is_esp8266 or CORE.is_esp32): + if CORE.is_esp8266: + _LOGGER.warning( + "The minimum WiFi authentication mode (wifi -> min_auth_mode) is not set. " + "This controls the weakest encryption your device will accept when connecting to WiFi. " + "Currently defaults to WPA (less secure), but will change to WPA2 (more secure) in 2026.6.0. " + "WPA uses TKIP encryption which has known security vulnerabilities and should be avoided. " + "WPA2 uses AES encryption which is significantly more secure. " + "To silence this warning, explicitly set min_auth_mode under 'wifi:'. " + "If your router supports WPA2 or WPA3, set 'min_auth_mode: WPA2'. " + "If your router only supports WPA, set 'min_auth_mode: WPA'." + ) + config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA") + elif CORE.is_esp32: + config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA2") + return config + + def final_validate(config): has_sta = bool(config.get(CONF_NETWORKS, True)) has_ap = CONF_AP in config @@ -287,6 +322,10 @@ CONFIG_SCHEMA = cv.All( ): cv.enum(WIFI_POWER_SAVE_MODES, upper=True), cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, + cv.Optional(CONF_MIN_AUTH_MODE): cv.All( + VALIDATE_WIFI_MIN_AUTH_MODE, + cv.only_on([Platform.ESP32, Platform.ESP8266]), + ), cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All( cv.decibel, cv.float_range(min=8.5, max=20.5) ), @@ -311,6 +350,7 @@ CONFIG_SCHEMA = cv.All( ), } ), + _apply_min_auth_mode_default, _validate, ) @@ -420,6 +460,8 @@ async def to_code(config): cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) + if CONF_MIN_AUTH_MODE in config: + cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE])) if config[CONF_FAST_CONNECT]: cg.add_define("USE_WIFI_FAST_CONNECT") cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN])) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index e786708b08b..02d6d984f1b 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -225,6 +225,12 @@ enum WiFiPowerSaveMode : uint8_t { WIFI_POWER_SAVE_HIGH, }; +enum WifiMinAuthMode : uint8_t { + WIFI_MIN_AUTH_MODE_WPA = 0, + WIFI_MIN_AUTH_MODE_WPA2, + WIFI_MIN_AUTH_MODE_WPA3, +}; + #ifdef USE_ESP32 struct IDFWiFiEvent; #endif @@ -274,6 +280,7 @@ class WiFiComponent : public Component { bool is_connected(); void set_power_save_mode(WiFiPowerSaveMode power_save); + void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } void set_output_power(float output_power) { output_power_ = output_power; } void set_passive_scan(bool passive); @@ -490,6 +497,7 @@ class WiFiComponent : public Component { // Group all 8-bit values together WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; + WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT}; uint8_t num_retried_{0}; // Index into sta_ array for the currently selected AP configuration (-1 = none selected) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 4e17c42f413..56e071404b0 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -258,8 +258,17 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ap.get_password().empty()) { conf.threshold.authmode = AUTH_OPEN; } else { - // Only allow auth modes with at least WPA - conf.threshold.authmode = AUTH_WPA_PSK; + // Set threshold based on configured minimum auth mode + // Note: ESP8266 doesn't support WPA3 + switch (this->min_auth_mode_) { + case WIFI_MIN_AUTH_MODE_WPA: + conf.threshold.authmode = AUTH_WPA_PSK; + break; + case WIFI_MIN_AUTH_MODE_WPA2: + case WIFI_MIN_AUTH_MODE_WPA3: // Fall back to WPA2 for ESP8266 + conf.threshold.authmode = AUTH_WPA2_PSK; + break; + } } conf.threshold.rssi = -127; #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 08ecba35987..d3088c9a104 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -308,7 +308,18 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ap.get_password().empty()) { conf.sta.threshold.authmode = WIFI_AUTH_OPEN; } else { - conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK; + // Set threshold based on configured minimum auth mode + switch (this->min_auth_mode_) { + case WIFI_MIN_AUTH_MODE_WPA: + conf.sta.threshold.authmode = WIFI_AUTH_WPA_PSK; + break; + case WIFI_MIN_AUTH_MODE_WPA2: + conf.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; + break; + case WIFI_MIN_AUTH_MODE_WPA3: + conf.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK; + break; + } } #ifdef USE_WIFI_WPA2_EAP @@ -347,8 +358,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // The minimum rssi to accept in the fast scan mode conf.sta.threshold.rssi = -127; - conf.sta.threshold.authmode = WIFI_AUTH_OPEN; - wifi_config_t current_conf; esp_err_t err; err = esp_wifi_get_config(WIFI_IF_STA, ¤t_conf); diff --git a/tests/components/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml index 91e235b9ce2..827e4b17f72 100644 --- a/tests/components/wifi/test.esp32-idf.yaml +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -2,6 +2,7 @@ psram: wifi: use_psram: true + min_auth_mode: WPA packages: - !include common.yaml diff --git a/tests/components/wifi/test.esp8266-ard.yaml b/tests/components/wifi/test.esp8266-ard.yaml index dade44d145b..9cb0e3cf48e 100644 --- a/tests/components/wifi/test.esp8266-ard.yaml +++ b/tests/components/wifi/test.esp8266-ard.yaml @@ -1 +1,5 @@ -<<: !include common.yaml +wifi: + min_auth_mode: WPA2 + +packages: + - !include common.yaml From 5dafaaced465413fb363f1896d79ea392ff0abc4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 17:12:10 -0600 Subject: [PATCH 373/461] [wifi] Fix scan and connection failures after adapter restart (#11851) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../esp32_improv/esp32_improv_component.cpp | 2 +- .../improv_serial/improv_serial_component.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 85 ++++++++----------- esphome/components/wifi/wifi_component.h | 10 +-- 4 files changed, 39 insertions(+), 60 deletions(-) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 2fa9d8f5234..398b1d42519 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -336,7 +336,7 @@ void ESP32ImprovComponent::process_incoming_data_() { this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); - wifi::global_wifi_component->start_connecting(sta, false); + wifi::global_wifi_component->start_connecting(sta); this->set_state_(improv::STATE_PROVISIONING); ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 9d080ea98e3..70260eeab3b 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -231,7 +231,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); - wifi::global_wifi_component->start_connecting(sta, false); + wifi::global_wifi_component->start_connecting(sta); this->set_state_(improv::STATE_PROVISIONING); ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d75ac971ebe..ddba0558b45 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -197,6 +197,10 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1; // Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1; +/// Cooldown duration in milliseconds after adapter restart or repeated failures +/// Allows WiFi hardware to stabilize before next connection attempt +static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 1000; + static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { switch (phase) { case WiFiRetryPhase::INITIAL_CONNECT: @@ -275,7 +279,7 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); return static_cast(i); } - ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (visible in scan)", sta.get_ssid().c_str()); + ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str()); } // No hidden SSIDs found return -1; @@ -289,7 +293,7 @@ void WiFiComponent::start_initial_connection_() { this->selected_sta_index_ = 0; this->retry_phase_ = WiFiRetryPhase::EXPLICIT_HIDDEN; WiFiAP params = this->build_params_for_current_phase_(); - this->start_connecting(params, false); + this->start_connecting(params); } else { ESP_LOGI(TAG, "Starting scan"); this->start_scanning(); @@ -371,13 +375,13 @@ void WiFiComponent::start() { // Without saved data, try first configured network or use normal flow if (loaded_fast_connect) { ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str()); - this->start_connecting(params, false); + this->start_connecting(params); } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) { // No saved data, but have configured networks - try first non-hidden network ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str()); this->selected_sta_index_ = 0; params = this->build_params_for_current_phase_(); - this->start_connecting(params, false); + this->start_connecting(params); } else { // No saved data and (no networks OR first is hidden) - use normal flow this->start_initial_connection_(); @@ -413,8 +417,11 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); - delay(100); // NOLINT + // Enter cooldown state to allow WiFi hardware to stabilize after restart // Don't set retry_phase_ or num_retried_ here - state machine handles transitions + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); + this->error_from_callback_ = false; } void WiFiComponent::loop() { @@ -434,20 +441,12 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); - if (millis() - this->action_started_ > 5000) { - // After cooldown, connect based on current retry phase - this->reset_selected_ap_to_first_if_invalid_(); - - // Check if we need to trigger a scan first - if (this->needs_scan_results_() && !this->all_networks_hidden_()) { - // Need scan results or no matching networks found - scan/rescan - ESP_LOGD(TAG, "Scanning required for phase %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); - this->start_scanning(); - } else { - // Have everything we need to connect (or all networks are hidden, skip scanning) - WiFiAP params = this->build_params_for_current_phase_(); - this->start_connecting(params, false); - } + if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) { + // After cooldown we either restarted the adapter because of + // a failure, or something tried to connect over and over + // so we entered cooldown. In both cases we call + // check_connecting_finished to continue the state machine. + this->check_connecting_finished(); } break; } @@ -456,8 +455,7 @@ void WiFiComponent::loop() { this->check_scanning_finished(); break; } - case WIFI_COMPONENT_STATE_STA_CONNECTING: - case WIFI_COMPONENT_STATE_STA_CONNECTING_2: { + case WIFI_COMPONENT_STATE_STA_CONNECTING: { this->status_set_warning(LOG_STR("associating to network")); this->check_connecting_finished(); break; @@ -666,7 +664,7 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa this->set_sta(sta); } -void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { +void WiFiComponent::start_connecting(const WiFiAP &ap) { // Log connection attempt at INFO level with priority std::string bssid_formatted; int8_t priority = 0; @@ -730,14 +728,11 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { if (!this->wifi_sta_connect_(ap)) { ESP_LOGE(TAG, "wifi_sta_connect_ failed"); - this->retry_connect(); - return; - } - - if (!two) { - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; + // Enter cooldown to allow WiFi hardware to stabilize + // (immediate failure suggests hardware not ready, different from connection timeout) + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; } else { - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; } this->action_started_ = millis(); } @@ -1006,8 +1001,6 @@ void WiFiComponent::check_scanning_finished() { this->transition_to_phase_(WiFiRetryPhase::RETRY_HIDDEN); // If no hidden networks to try, skip connection attempt (will be handled on next loop) if (this->selected_sta_index_ == -1) { - this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; - this->action_started_ = millis(); return; } // Now start connection attempt in hidden mode @@ -1020,7 +1013,7 @@ void WiFiComponent::check_scanning_finished() { WiFiAP params = this->build_params_for_current_phase_(); // Ensure we're in SCAN_CONNECTING phase when connecting with scan results // (needed when scan was started directly without transition_to_phase_, e.g., initial scan) - this->start_connecting(params, false); + this->start_connecting(params); } void WiFiComponent::dump_config() { @@ -1094,7 +1087,7 @@ void WiFiComponent::check_connecting_finished() { } if (this->error_from_callback_) { - ESP_LOGW(TAG, "Connecting to network failed"); + ESP_LOGW(TAG, "Connecting to network failed (callback)"); this->retry_connect(); return; } @@ -1456,15 +1449,13 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() { void WiFiComponent::retry_connect() { this->log_and_adjust_priority_for_failed_connect_(); - delay(10); - // Determine next retry phase based on current state WiFiRetryPhase current_phase = this->retry_phase_; WiFiRetryPhase next_phase = this->determine_next_phase_(); // Handle phase transitions (transition_to_phase_ handles same-phase no-op internally) if (this->transition_to_phase_(next_phase)) { - return; // Wait for scan to complete + return; // Scan started or adapter restarted (which sets its own state) } if (next_phase == current_phase) { @@ -1473,22 +1464,14 @@ void WiFiComponent::retry_connect() { this->error_from_callback_ = false; - if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { - yield(); - // Check if we have a valid target before building params - // After exhausting all networks in a phase, selected_sta_index_ may be -1 - // In that case, skip connection and let next wifi_loop() handle phase transition - if (this->selected_sta_index_ >= 0) { - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; - WiFiAP params = this->build_params_for_current_phase_(); - this->start_connecting(params, true); - return; - } - // No valid target - fall through to set state to allow phase transition + yield(); + // Check if we have a valid target before building params + // After exhausting all networks in a phase, selected_sta_index_ may be -1 + // In that case, skip connection and let next wifi_loop() handle phase transition + if (this->selected_sta_index_ >= 0) { + WiFiAP params = this->build_params_for_current_phase_(); + this->start_connecting(params); } - - this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; - this->action_started_ = millis(); } void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 02d6d984f1b..ef0372535ae 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -74,12 +74,6 @@ enum WiFiComponentState : uint8_t { WIFI_COMPONENT_STATE_STA_SCANNING, /** WiFi is in STA(+AP) mode and currently connecting to an AP. */ WIFI_COMPONENT_STATE_STA_CONNECTING, - /** WiFi is in STA(+AP) mode and currently connecting to an AP a second time. - * - * This is required because for some reason ESPs don't like to connect to WiFi APs directly after - * a scan. - * */ - WIFI_COMPONENT_STATE_STA_CONNECTING_2, /** WiFi is in STA(+AP) mode and successfully connected. */ WIFI_COMPONENT_STATE_STA_CONNECTED, /** WiFi is in AP-only mode and internal AP is already enabled. */ @@ -269,7 +263,9 @@ class WiFiComponent : public Component { bool is_disabled(); void start_scanning(); void check_scanning_finished(); - void start_connecting(const WiFiAP &ap, bool two); + void start_connecting(const WiFiAP &ap); + // Backward compatibility overload - ignores 'two' parameter + void start_connecting(const WiFiAP &ap, bool /* two */) { this->start_connecting(ap); } void check_connecting_finished(); From 572fae5c7d49c479f0006420e8bb656d89948480 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 17:12:53 -0600 Subject: [PATCH 374/461] [wifi] Restore two-attempt BSSID filtering for mesh networks (#11844) --- esphome/components/wifi/wifi_component.cpp | 26 +++++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index ddba0558b45..e79d821ba7b 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1341,6 +1341,11 @@ void WiFiComponent::clear_priorities_if_all_min_() { /// - Other phases: Uses BSSID from config if explicitly specified by user or fast_connect /// /// If no BSSID is available (SSID-only connection), priority adjustment is skipped. +/// +/// IMPORTANT: Priority is only decreased on the LAST attempt for a BSSID in SCAN_CONNECTING phase. +/// This prevents false positives from transient WiFi stack state issues after scanning. +/// Single failures don't necessarily mean the AP is bad - two genuine failures provide +/// higher confidence before degrading priority and skipping the BSSID in future scans. void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { // Determine which BSSID we tried to connect to optional failed_bssid; @@ -1357,12 +1362,6 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { return; // No BSSID to penalize } - // Decrease priority to avoid repeatedly trying the same failed BSSID - int8_t old_priority = this->get_sta_priority(failed_bssid.value()); - int8_t new_priority = - (old_priority > std::numeric_limits::min()) ? (old_priority - 1) : std::numeric_limits::min(); - this->set_sta_priority(failed_bssid.value(), new_priority); - // Get SSID for logging std::string ssid; if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { @@ -1371,6 +1370,21 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { ssid = config->get_ssid(); } + // Only decrease priority on the last attempt for this phase + // This prevents false positives from transient WiFi stack issues + uint8_t max_retries = get_max_retries_for_phase(this->retry_phase_); + bool is_last_attempt = (this->num_retried_ + 1 >= max_retries); + + // Decrease priority only on last attempt to avoid false positives from transient failures + int8_t old_priority = this->get_sta_priority(failed_bssid.value()); + int8_t new_priority = old_priority; + + if (is_last_attempt) { + // Decrease priority, but clamp to int8_t::min to prevent overflow + new_priority = + (old_priority > std::numeric_limits::min()) ? (old_priority - 1) : std::numeric_limits::min(); + this->set_sta_priority(failed_bssid.value(), new_priority); + } ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority); From 79a44449283407456694354e7dd3f4c8d22773ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 17:27:08 -0600 Subject: [PATCH 375/461] [wifi] Conditionally compile manual_ip to save 24-72 bytes RAM (#11833) --- esphome/components/wifi/__init__.py | 10 ++++++++++ esphome/components/wifi/wifi_component.cpp | 11 ++++++++++- esphome/components/wifi/wifi_component.h | 6 ++++++ .../components/wifi/wifi_component_esp8266.cpp | 13 +++++++++++++ .../components/wifi/wifi_component_esp_idf.cpp | 13 +++++++++++++ .../components/wifi/wifi_component_libretiny.cpp | 13 +++++++++++++ esphome/components/wifi/wifi_component_pico_w.cpp | 12 ++++++++++++ esphome/core/defines.h | 1 + tests/components/wifi/test.esp32-idf.yaml | 15 +++++++++++++++ 9 files changed, 93 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c42af232520..28db698a43f 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -425,6 +425,8 @@ async def to_code(config): # Track if any network uses Enterprise authentication has_eap = False + # Track if any network uses manual IP + has_manual_ip = False # Initialize FixedVector with the count of networks networks = config.get(CONF_NETWORKS, []) @@ -438,11 +440,15 @@ async def to_code(config): for network in networks: if CONF_EAP in network: has_eap = True + if network.get(CONF_MANUAL_IP) or config.get(CONF_MANUAL_IP): + has_manual_ip = True cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network) if CONF_AP in config: conf = config[CONF_AP] ip_config = conf.get(CONF_MANUAL_IP) + if ip_config: + has_manual_ip = True cg.with_local_variable( conf[CONF_ID], WiFiAP(), @@ -458,6 +464,10 @@ async def to_code(config): if CORE.is_esp32: add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap) + # Only define USE_WIFI_MANUAL_IP if any AP uses manual IP + if has_manual_ip: + cg.add_define("USE_WIFI_MANUAL_IP") + cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) if CONF_MIN_AUTH_MODE in config: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e79d821ba7b..817419107f2 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -569,6 +569,7 @@ void WiFiComponent::setup_ap_config_() { " IP Address: %s", this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str()); +#ifdef USE_WIFI_MANUAL_IP auto manual_ip = this->ap_.get_manual_ip(); if (manual_ip.has_value()) { ESP_LOGCONFIG(TAG, @@ -578,6 +579,7 @@ void WiFiComponent::setup_ap_config_() { manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(), manual_ip->subnet.str().c_str()); } +#endif if (!this->has_sta()) { this->state_ = WIFI_COMPONENT_STATE_AP; @@ -716,11 +718,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { } else { ESP_LOGV(TAG, " Channel not set"); } +#ifdef USE_WIFI_MANUAL_IP if (ap.get_manual_ip().has_value()) { ManualIP m = *ap.get_manual_ip(); ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str().c_str(), m.gateway.str().c_str(), m.subnet.str().c_str(), m.dns1.str().c_str(), m.dns2.str().c_str()); - } else { + } else +#endif + { ESP_LOGV(TAG, " Using DHCP IP"); } ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden())); @@ -1577,7 +1582,9 @@ void WiFiAP::set_password(const std::string &password) { this->password_ = passw void WiFiAP::set_eap(optional eap_auth) { this->eap_ = std::move(eap_auth); } #endif void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } +#ifdef USE_WIFI_MANUAL_IP void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } +#endif void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const optional &WiFiAP::get_bssid() const { return this->bssid_; } @@ -1586,7 +1593,9 @@ const std::string &WiFiAP::get_password() const { return this->password_; } const optional &WiFiAP::get_eap() const { return this->eap_; } #endif const optional &WiFiAP::get_channel() const { return this->channel_; } +#ifdef USE_WIFI_MANUAL_IP const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } +#endif bool WiFiAP::get_hidden() const { return this->hidden_; } WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ef0372535ae..713e6f223f0 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -152,7 +152,9 @@ class WiFiAP { #endif // USE_WIFI_WPA2_EAP void set_channel(optional channel); void set_priority(int8_t priority) { priority_ = priority; } +#ifdef USE_WIFI_MANUAL_IP void set_manual_ip(optional manual_ip); +#endif void set_hidden(bool hidden); const std::string &get_ssid() const; const optional &get_bssid() const; @@ -162,7 +164,9 @@ class WiFiAP { #endif // USE_WIFI_WPA2_EAP const optional &get_channel() const; int8_t get_priority() const { return priority_; } +#ifdef USE_WIFI_MANUAL_IP const optional &get_manual_ip() const; +#endif bool get_hidden() const; protected: @@ -172,7 +176,9 @@ class WiFiAP { #ifdef USE_WIFI_WPA2_EAP optional eap_; #endif // USE_WIFI_WPA2_EAP +#ifdef USE_WIFI_MANUAL_IP optional manual_ip_; +#endif optional channel_; int8_t priority_{0}; bool hidden_{false}; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 56e071404b0..bcb5dc4cf7f 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -282,9 +282,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { return false; } +#else + if (!this->wifi_sta_ip_config_({})) { + return false; + } +#endif // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP @@ -832,10 +838,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); + return false; + } +#endif return true; } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index d3088c9a104..fd7e85fb6be 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -380,9 +380,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { return false; } +#else + if (!this->wifi_sta_ip_config_({})) { + return false; + } +#endif // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP @@ -994,10 +1000,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:"); + return false; + } +#endif return true; } diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 45e2fba82a7..2946b9e8310 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -112,9 +112,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { WiFi.disconnect(); } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { return false; } +#else + if (!this->wifi_sta_ip_config_({})) { + return false; + } +#endif this->wifi_apply_hostname_(); @@ -445,10 +451,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { if (!this->wifi_mode_({}, true)) return false; +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); + return false; + } +#endif yield(); diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index bf15892cd5e..7025ba16bda 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -55,8 +55,13 @@ bool WiFiComponent::wifi_apply_power_save_() { bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; } bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) return false; +#else + if (!this->wifi_sta_ip_config_({})) + return false; +#endif auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str()); if (ret != WL_CONNECTED) @@ -161,10 +166,17 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { if (!this->wifi_mode_({}, true)) return false; +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); + return false; + } +#endif WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1)); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c522a8ec62a..41f4b28cd5e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -144,6 +144,7 @@ #define USE_TIME_TIMEZONE #define USE_WIFI #define USE_WIFI_AP +#define USE_WIFI_MANUAL_IP #define USE_WIREGUARD #endif diff --git a/tests/components/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml index 827e4b17f72..6b3ef20963c 100644 --- a/tests/components/wifi/test.esp32-idf.yaml +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -3,6 +3,21 @@ psram: wifi: use_psram: true min_auth_mode: WPA + manual_ip: + static_ip: 192.168.1.100 + gateway: 192.168.1.1 + subnet: 255.255.255.0 + dns1: 1.1.1.1 + dns2: 8.8.8.8 + ap: + ssid: Fallback AP + password: fallback_password + manual_ip: + static_ip: 192.168.4.1 + gateway: 192.168.4.1 + subnet: 255.255.255.0 + +captive_portal: packages: - !include common.yaml From 037620d75a56b92c4723627f9a482b18443cb085 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 18:36:35 -0600 Subject: [PATCH 376/461] [captive_portal] Warn when enabled without WiFi AP configured --- esphome/components/captive_portal/__init__.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 99acb76bcf3..9bd3ef8a058 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -1,9 +1,12 @@ +import logging + import esphome.codegen as cg from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( + CONF_AP, CONF_ID, PLATFORM_BK72XX, PLATFORM_ESP32, @@ -14,6 +17,10 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) def AUTO_LOAD() -> list[str]: @@ -50,6 +57,27 @@ CONFIG_SCHEMA = cv.All( ) +def _final_validate(config: ConfigType) -> ConfigType: + full_config = fv.full_config.get() + wifi_conf = full_config.get("wifi") + + if wifi_conf is None: + # This shouldn't happen due to DEPENDENCIES = ["wifi"], but check anyway + raise cv.Invalid("Captive portal requires the wifi component to be configured") + + if CONF_AP not in wifi_conf: + _LOGGER.warning( + "Captive portal is enabled but no WiFi AP is configured. " + "The captive portal will not be accessible. " + "Add 'ap:' to your WiFi configuration to enable the captive portal." + ) + + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL) async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) From d7fa131a8a31b3dab42acfe9685532b4b72ec141 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 11 Nov 2025 19:43:06 -0500 Subject: [PATCH 377/461] [network, psram, speaker wifi] Use CORE.data to enable high performance networking (#11812) --- esphome/components/network/__init__.py | 123 ++++++++++++++++++ esphome/components/psram/__init__.py | 37 +++++- .../speaker/media_player/__init__.py | 36 +++-- esphome/components/wifi/__init__.py | 58 ++++++++- tests/components/network/test.esp32-idf.yaml | 3 + 5 files changed, 235 insertions(+), 22 deletions(-) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 1d62b661caa..d7a51fb0c6c 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,7 +1,9 @@ import ipaddress +import logging import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.psram import is_guaranteed as psram_is_guaranteed import esphome.config_validation as cv from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -9,6 +11,13 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] +_LOGGER = logging.getLogger(__name__) + +# High performance networking tracking infrastructure +# Components can request high performance networking and this configures lwip and WiFi settings +KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking" +CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance" + network_ns = cg.esphome_ns.namespace("network") IPAddress = network_ns.class_("IPAddress") @@ -47,6 +56,55 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj: return IPAddress(str(ip)) +def require_high_performance_networking() -> None: + """Request high performance networking for network and WiFi. + + Call this from components that need optimized network performance for streaming + or high-throughput data transfer. This enables high performance mode which + configures both lwip TCP settings and WiFi driver settings for improved + network performance. + + Settings applied (ESP-IDF only): + - lwip: Larger TCP buffers, windows, and mailbox sizes + - WiFi: Increased RX/TX buffers, AMPDU aggregation, PSRAM allocation (set by wifi component) + + Configuration is PSRAM-aware: + - With PSRAM guaranteed: Aggressive settings (512 RX buffers, 512KB TCP windows) + - Without PSRAM: Conservative optimized settings (64 buffers, 65KB TCP windows) + + Example: + from esphome.components import network + + def _request_high_performance_networking(config): + network.require_high_performance_networking() + return config + + CONFIG_SCHEMA = cv.All( + ..., + _request_high_performance_networking, + ) + """ + # Only set up once (idempotent - multiple components can call this) + if not CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False): + CORE.data[KEY_HIGH_PERFORMANCE_NETWORKING] = True + + +def has_high_performance_networking() -> bool: + """Check if high performance networking mode is enabled. + + Returns True when high performance networking has been requested by a + component or explicitly enabled in the network configuration. This indicates + that lwip and WiFi will use optimized buffer sizes and settings. + + This function should be called during code generation (to_code phase) by + components that need to apply performance-related settings. + + Returns: + bool: True if high performance networking is enabled, False otherwise + """ + return CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False) + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( @@ -71,6 +129,7 @@ CONFIG_SCHEMA = cv.Schema( ), ), cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, + cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All(cv.boolean, cv.only_on_esp32), } ) @@ -80,6 +139,70 @@ async def to_code(config): cg.add_define("USE_NETWORK") if CORE.using_arduino and CORE.is_esp32: cg.add_library("Networking", None) + + # Apply high performance networking settings + # Config can explicitly enable/disable, or default to component-driven behavior + enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE) + component_requested = CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False) + + # Explicit config overrides component request + should_enable = ( + enable_high_perf if enable_high_perf is not None else component_requested + ) + + # Log when user explicitly disables but a component requested it + if enable_high_perf is False and component_requested: + _LOGGER.info( + "High performance networking disabled by user configuration (overriding component request)" + ) + + if CORE.is_esp32 and CORE.using_esp_idf and should_enable: + # Check if PSRAM is guaranteed (set by psram component during final validation) + psram_guaranteed = psram_is_guaranteed() + + if psram_guaranteed: + _LOGGER.info( + "Applying high-performance lwip settings (PSRAM guaranteed): 512KB TCP windows, 512 mailbox sizes" + ) + # PSRAM is guaranteed - use aggressive settings + # Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true + # CONFIG_LWIP_WND_SCALE can only be enabled if CONFIG_SPIRAM_IGNORE_NOTFOUND isn't set + # Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 + + # Enable window scaling for much larger TCP windows + add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RCV_SCALE", 3) + + # Large TCP buffers and windows (requires PSRAM) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 512000) + + # Large mailboxes for high throughput + add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 512) + + # TCP connection limits + add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16) + add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16) + + # TCP optimizations + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MAXRTX", 12) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SYNMAXRTX", 6) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSS", 1436) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSL", 60000) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_OVERSIZE_MSS", True) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_QUEUE_OOSEQ", True) + else: + _LOGGER.info( + "Applying optimized lwip settings: 65KB TCP windows, 64 mailbox sizes" + ) + # PSRAM not guaranteed - use more conservative, but still optimized settings + # Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32 + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64) + add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64) + if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None: cg.add_define("USE_NETWORK_IPV6", enable_ipv6) if enable_ipv6: diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index 11c238c1bfb..c50c5998550 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -35,6 +35,9 @@ DOMAIN = "psram" DEPENDENCIES = [PLATFORM_ESP32] +# PSRAM availability tracking for cross-component coordination +KEY_PSRAM_GUARANTEED = "psram_guaranteed" + _LOGGER = logging.getLogger(__name__) psram_ns = cg.esphome_ns.namespace(DOMAIN) @@ -71,6 +74,23 @@ def supported() -> bool: return variant in SPIRAM_MODES +def is_guaranteed() -> bool: + """Check if PSRAM is guaranteed to be available. + + Returns True when PSRAM is configured with both 'disabled: false' and + 'ignore_not_found: false', meaning the device will fail to boot if PSRAM + is not found. This ensures safe use of high buffer configurations that + depend on PSRAM. + + This function should be called during code generation (to_code phase) by + components that need to know PSRAM availability for configuration decisions. + + Returns: + bool: True if PSRAM is guaranteed, False otherwise + """ + return CORE.data.get(KEY_PSRAM_GUARANTEED, False) + + def validate_psram_mode(config): esp32_config = fv.full_config.get()[PLATFORM_ESP32] if config[CONF_SPEED] == "120MHZ": @@ -131,7 +151,22 @@ def get_config_schema(config): CONFIG_SCHEMA = get_config_schema -FINAL_VALIDATE_SCHEMA = validate_psram_mode + +def _store_psram_guaranteed(config): + """Store PSRAM guaranteed status in CORE.data for other components. + + PSRAM is "guaranteed" when it will fail if not found, ensuring safe use + of high buffer configurations in network/wifi components. + + Called during final validation to ensure the flag is available + before any to_code() functions run. + """ + psram_guaranteed = not config[CONF_DISABLED] and not config[CONF_IGNORE_NOT_FOUND] + CORE.data[KEY_PSRAM_GUARANTEED] = psram_guaranteed + return config + + +FINAL_VALIDATE_SCHEMA = cv.All(validate_psram_mode, _store_psram_guaranteed) async def to_code(config): diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index e50656e723e..062bff92f8b 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -6,7 +6,7 @@ from pathlib import Path from esphome import automation, external_files import esphome.codegen as cg -from esphome.components import audio, esp32, media_player, psram, speaker +from esphome.components import audio, esp32, media_player, network, psram, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["audio"] +DEPENDENCIES = ["network"] CODEOWNERS = ["@kahrendt", "@synesthesiam"] DOMAIN = "media_player" @@ -280,6 +281,18 @@ PIPELINE_SCHEMA = cv.Schema( } ) + +def _request_high_performance_networking(config): + """Request high performance networking for streaming media. + + Speaker media player streams audio data, so it always benefits from + optimized WiFi and lwip settings regardless of codec support. + Called during config validation to ensure flags are set before to_code(). + """ + network.require_high_performance_networking() + return config + + CONFIG_SCHEMA = cv.All( media_player.media_player_schema(SpeakerMediaPlayer).extend( { @@ -304,6 +317,7 @@ CONFIG_SCHEMA = cv.All( ), cv.only_with_esp_idf, _validate_repeated_speaker, + _request_high_performance_networking, ) @@ -321,28 +335,10 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]: - # Compile all supported audio codecs and optimize the wifi settings - + # Compile all supported audio codecs cg.add_define("USE_AUDIO_FLAC_SUPPORT", True) cg.add_define("USE_AUDIO_MP3_SUPPORT", True) - # Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32 - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32) - - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64) - - # Allocate wifi buffers in PSRAM - esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) - var = await media_player.new_media_player(config) await cg.register_component(var, config) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 28db698a43f..4dbb425e4bd 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -5,7 +5,11 @@ from esphome.automation import Condition import esphome.codegen as cg from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant -from esphome.components.network import ip_address_literal +from esphome.components.network import ( + has_high_performance_networking, + ip_address_literal, +) +from esphome.components.psram import is_guaranteed as psram_is_guaranteed from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.config_validation import only_with_esp_idf @@ -56,6 +60,8 @@ _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["network"] +_LOGGER = logging.getLogger(__name__) + NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" CONF_MIN_AUTH_MODE = "min_auth_mode" @@ -496,6 +502,56 @@ async def to_code(config): if config.get(CONF_USE_PSRAM): add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) + + # Apply high performance WiFi settings if high performance networking is enabled + if CORE.is_esp32 and CORE.using_esp_idf and has_high_performance_networking(): + # Check if PSRAM is guaranteed (set by psram component during final validation) + psram_guaranteed = psram_is_guaranteed() + + # Always allocate WiFi buffers in PSRAM if available + add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) + + if psram_guaranteed: + _LOGGER.info( + "Applying high-performance WiFi settings (PSRAM guaranteed): 512 RX buffers, 32 TX buffers" + ) + # PSRAM is guaranteed - use aggressive settings + # Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true in networking component + # Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 + + # Large dynamic RX buffers (requires PSRAM) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 512) + + # Static TX buffers for better performance + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BUFFER_TYPE", 0) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM", 32) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM", 8) + + # AMPDU settings optimized for PSRAM + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 16) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32) + else: + _LOGGER.info( + "Applying optimized WiFi settings: 64 RX buffers, 64 TX buffers" + ) + # PSRAM not guaranteed - use more conservative, but still optimized settings + # Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32 + + # Standard buffer counts + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64) + + # Standard AMPDU settings + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32) + cg.add_define("USE_WIFI") # must register before OTA safe mode check diff --git a/tests/components/network/test.esp32-idf.yaml b/tests/components/network/test.esp32-idf.yaml index dade44d145b..7c01bafa0df 100644 --- a/tests/components/network/test.esp32-idf.yaml +++ b/tests/components/network/test.esp32-idf.yaml @@ -1 +1,4 @@ <<: !include common.yaml + +network: + enable_high_performance: true From a93887a79041b7f66806e27e944c90c8a2a0a860 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Tue, 14 Oct 2025 21:29:41 -0700 Subject: [PATCH 378/461] [const] Add CONF_ROWS (#11249) --- esphome/components/lvgl/defines.py | 1 - esphome/components/lvgl/widgets/buttonmatrix.py | 3 +-- esphome/components/matrix_keypad/__init__.py | 3 +-- esphome/const.py | 1 + 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index d2b0977e89d..7fbb6de0710 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -504,7 +504,6 @@ CONF_RESUME_ON_INPUT = "resume_on_input" CONF_RIGHT_BUTTON = "right_button" CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" -CONF_ROWS = "rows" CONF_SCALE_LINES = "scale_lines" CONF_SCROLLBAR_MODE = "scrollbar_mode" CONF_SELECTED_INDEX = "selected_index" diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index c6b6d2440f3..baeb1c8e3ef 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH +from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROWS, CONF_TEXT, CONF_WIDTH from esphome.cpp_generator import MockObj from ..automation import action_to_code @@ -15,7 +15,6 @@ from ..defines import ( CONF_ONE_CHECKED, CONF_PAD_COLUMN, CONF_PAD_ROW, - CONF_ROWS, CONF_SELECTED, ) from ..helpers import lvgl_components_required diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index f7a1d622a1c..2e123323a08 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -2,7 +2,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import key_provider import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_ROWS, CONF_TRIGGER_ID CODEOWNERS = ["@ssieb"] @@ -19,7 +19,6 @@ MatrixKeyTrigger = matrix_keypad_ns.class_( ) CONF_KEYPAD_ID = "keypad_id" -CONF_ROWS = "rows" CONF_COLUMNS = "columns" CONF_KEYS = "keys" CONF_DEBOUNCE_TIME = "debounce_time" diff --git a/esphome/const.py b/esphome/const.py index 9e8ec487b52..bfd772c5395 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -837,6 +837,7 @@ CONF_RMT_CHANNEL = "rmt_channel" CONF_RMT_SYMBOLS = "rmt_symbols" CONF_ROTATION = "rotation" CONF_ROW = "row" +CONF_ROWS = "rows" CONF_RS_PIN = "rs_pin" CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance" CONF_RTD_WIRES = "rtd_wires" From 9326d78439b4ceec645ba85423abafab610d5e19 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 26 Oct 2025 09:00:08 -0400 Subject: [PATCH 379/461] [core] Don't allow python 3.14 (#11527) --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7b4a48d7eb..49598d434dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,9 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Home Automation", ] -requires-python = ">=3.11.0" + +# Python 3.14 is currently not supported by IDF <= 5.5.1, see https://github.com/esphome/esphome/issues/11502 +requires-python = ">=3.11.0,<3.14" dynamic = ["dependencies", "optional-dependencies", "version"] From 87f79290ba2fea8d0c58de18d7a44c2adf6ee389 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:23:45 +1000 Subject: [PATCH 380/461] [usb_uart] Fixes for transfer queue allocation (#11548) --- esphome/components/usb_host/usb_host.h | 12 ++--- .../components/usb_host/usb_host_client.cpp | 54 +++++++++---------- esphome/components/usb_uart/usb_uart.cpp | 22 +++++--- tests/components/usb_uart/common.yaml | 3 ++ 4 files changed, 51 insertions(+), 40 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index 43b24a54a50..31bdde2df8c 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -55,7 +55,7 @@ static const uint8_t USB_DIR_IN = 1 << 7; static const uint8_t USB_DIR_OUT = 0; static const size_t SETUP_PACKET_SIZE = 8; -static const size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. +static constexpr size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32"); // Select appropriate bitmask type for tracking allocation of TransferRequest slots. @@ -65,6 +65,7 @@ static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be bet // This is tied to the static_assert above, which enforces MAX_REQUESTS is between 1 and 32. // If MAX_REQUESTS is increased above 32, this logic and the static_assert must be updated. using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type; +static constexpr trq_bitmask_t ALL_REQUESTS_IN_USE = MAX_REQUESTS == 32 ? ~0 : (1 << MAX_REQUESTS) - 1; static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples) @@ -133,11 +134,11 @@ class USBClient : public Component { float get_setup_priority() const override { return setup_priority::IO; } void on_opened(uint8_t addr); void on_removed(usb_device_handle_t handle); - void control_transfer_callback(const usb_transfer_t *xfer) const; - void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); - void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); + bool transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); + bool transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); void dump_config() override; void release_trq(TransferRequest *trq); + trq_bitmask_t get_trq_in_use() const { return trq_in_use_; } bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, const std::vector &data = {}); @@ -147,7 +148,6 @@ class USBClient : public Component { EventPool event_pool; protected: - bool register_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); virtual void on_connected() {} @@ -158,7 +158,7 @@ class USBClient : public Component { // USB task management static void usb_task_fn(void *arg); - void usb_task_loop(); + [[noreturn]] void usb_task_loop() const; TaskHandle_t usb_task_handle_{nullptr}; diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 2139ed869a0..dc216a209db 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -188,9 +188,9 @@ void USBClient::setup() { } // Pre-allocate USB transfer buffers for all slots at startup // This avoids any dynamic allocation during runtime - for (size_t i = 0; i < MAX_REQUESTS; i++) { - usb_host_transfer_alloc(64, 0, &this->requests_[i].transfer); - this->requests_[i].client = this; // Set once, never changes + for (auto &request : this->requests_) { + usb_host_transfer_alloc(64, 0, &request.transfer); + request.client = this; // Set once, never changes } // Create and start USB task @@ -210,8 +210,7 @@ void USBClient::usb_task_fn(void *arg) { auto *client = static_cast(arg); client->usb_task_loop(); } - -void USBClient::usb_task_loop() { +void USBClient::usb_task_loop() const { while (true) { usb_host_client_handle_events(this->handle_, portMAX_DELAY); } @@ -334,22 +333,23 @@ static void control_callback(const usb_transfer_t *xfer) { // This multi-threaded access is intentional for performance - USB task can // immediately restart transfers without waiting for main loop scheduling. TransferRequest *USBClient::get_trq_() { - trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed); + trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire); // Find first available slot (bit = 0) and try to claim it atomically // We use a while loop to allow retrying the same slot after CAS failure - size_t i = 0; - while (i != MAX_REQUESTS) { - if (mask & (static_cast(1) << i)) { - // Slot is in use, move to next slot - i++; - continue; + for (;;) { + if (mask == ALL_REQUESTS_IN_USE) { + ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); + return nullptr; } + // find the least significant zero bit + trq_bitmask_t lsb = ~mask & (mask + 1); // Slot i appears available, try to claim it atomically - trq_bitmask_t desired = mask | (static_cast(1) << i); // Set bit i to mark as in-use + trq_bitmask_t desired = mask | lsb; - if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order_acquire, std::memory_order_relaxed)) { + if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order::acquire)) { + auto i = __builtin_ctz(lsb); // count trailing zeroes // Successfully claimed slot i - prepare the TransferRequest auto *trq = &this->requests_[i]; trq->transfer->context = trq; @@ -358,13 +358,9 @@ TransferRequest *USBClient::get_trq_() { } // CAS failed - another thread modified the bitmask // mask was already updated by compare_exchange_weak with the current value - // No need to reload - the CAS already did that for us - i = 0; } - - ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); - return nullptr; } + void USBClient::disconnect() { this->on_disconnected(); auto err = usb_host_device_close(this->handle_, this->device_handle_); @@ -446,11 +442,11 @@ static void transfer_callback(usb_transfer_t *xfer) { * * @throws None. */ -void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { +bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return; + return false; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -460,7 +456,9 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); + return false; } + return true; } /** @@ -476,11 +474,11 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u * * @throws None. */ -void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { +bool USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return; + return false; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -491,7 +489,9 @@ void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); + return false; } + return true; } void USBClient::dump_config() { ESP_LOGCONFIG(TAG, @@ -505,7 +505,7 @@ void USBClient::dump_config() { // - Main loop: When transfer submission fails // // THREAD SAFETY: Lock-free using atomic AND to clear bit -// Thread-safe atomic operation allows multi-threaded deallocation +// Thread-safe atomic operation allows multithreaded deallocation void USBClient::release_trq(TransferRequest *trq) { if (trq == nullptr) return; @@ -517,10 +517,10 @@ void USBClient::release_trq(TransferRequest *trq) { return; } - // Atomically clear bit i to mark slot as available + // Atomically clear the bit to mark slot as available // fetch_and with inverted bitmask clears the bit atomically - trq_bitmask_t bit = static_cast(1) << index; - this->trq_in_use_.fetch_and(static_cast(~bit), std::memory_order_release); + trq_bitmask_t mask = ~(static_cast(1) << index); + this->trq_in_use_.fetch_and(mask, std::memory_order_release); } } // namespace usb_host diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 29003e071ef..c24fffb11de 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -214,7 +214,7 @@ void USBUartComponent::dump_config() { } } void USBUartComponent::start_input(USBUartChannel *channel) { - if (!channel->initialised_.load() || channel->input_started_.load()) + if (!channel->initialised_.load()) return; // THREAD CONTEXT: Called from both USB task and main loop threads // - USB task: Immediate restart after successful transfer for continuous data flow @@ -226,12 +226,18 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // // The underlying transfer_in() uses lock-free atomic allocation from the // TransferRequest pool, making this multi-threaded access safe + + // if already started, don't restart. A spurious failure in compare_exchange_weak + // is not a problem, as it will be retried on the next read_array() + auto started = false; + if (!channel->input_started_.compare_exchange_weak(started, true)) + return; const auto *ep = channel->cdc_dev_.in_ep; // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback auto callback = [this, channel](const usb_host::TransferStatus &status) { ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); if (!status.success) { - ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); + ESP_LOGE(TAG, "Input transfer failed, status=%s", esp_err_to_name(status.error_code)); // On failure, don't restart - let next read_array() trigger it channel->input_started_.store(false); return; @@ -263,8 +269,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { channel->input_started_.store(false); this->start_input(channel); }; - channel->input_started_.store(true); - this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); + if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) { + channel->input_started_.store(false); + } } void USBUartComponent::start_output(USBUartChannel *channel) { @@ -357,11 +364,12 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); } usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); - channel->initialised_.store(false); - channel->input_started_.store(false); - channel->output_started_.store(false); + // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts + channel->input_started_.store(true); + channel->output_started_.store(true); channel->input_buffer_.clear(); channel->output_buffer_.clear(); + channel->initialised_.store(false); } USBClient::on_disconnected(); } diff --git a/tests/components/usb_uart/common.yaml b/tests/components/usb_uart/common.yaml index 46ad6291f93..474c3f5c8de 100644 --- a/tests/components/usb_uart/common.yaml +++ b/tests/components/usb_uart/common.yaml @@ -1,3 +1,6 @@ +usb_host: + max_transfer_requests: 32 + usb_uart: - id: uart_0 type: cdc_acm From 58ad4759f0b9f0e924bd093eba77e7d0db7b27a3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:43:27 +1000 Subject: [PATCH 381/461] [lvgl] Fix rotation with unusual width (#11680) --- esphome/components/const/__init__.py | 1 + esphome/components/lvgl/lvgl_esphome.cpp | 24 ++++++++++--------- .../components/lvgl/widgets/buttonmatrix.py | 3 ++- esphome/components/matrix_keypad/__init__.py | 3 ++- esphome/const.py | 1 - 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 19924f0da77..2b88bb43a8d 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -11,4 +11,5 @@ CONF_DRAW_ROUNDING = "draw_rounding" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" +CONF_ROWS = "rows" CONF_USE_PSRAM = "use_psram" diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 7a32691b532..6e18adccfe3 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -156,6 +156,7 @@ bool LvPageType::is_showing() const { return this->parent_->get_current_page() = void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); + auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto x1 = area->x1; auto y1 = area->y1; lv_color_t *dst = this->rotate_buf_; @@ -163,13 +164,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { case display::DISPLAY_ROTATION_90_DEGREES: for (lv_coord_t x = height; x-- != 0;) { for (lv_coord_t y = 0; y != width; y++) { - dst[y * height + x] = *ptr++; + dst[y * height_rounded + x] = *ptr++; } } y1 = x1; x1 = this->disp_drv_.ver_res - area->y1 - height; - width = height; - height = lv_area_get_width(area); + height = width; + width = height_rounded; break; case display::DISPLAY_ROTATION_180_DEGREES: @@ -185,13 +186,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { case display::DISPLAY_ROTATION_270_DEGREES: for (lv_coord_t x = 0; x != height; x++) { for (lv_coord_t y = width; y-- != 0;) { - dst[y * height + x] = *ptr++; + dst[y * height_rounded + x] = *ptr++; } } x1 = y1; y1 = this->disp_drv_.hor_res - area->x1 - width; - width = height; - height = lv_area_get_width(area); + height = width; + width = height_rounded; break; default: @@ -435,8 +436,10 @@ LvglComponent::LvglComponent(std::vector displays, float buf void LvglComponent::setup() { auto *display = this->displays_[0]; - auto width = display->get_width(); - auto height = display->get_height(); + auto rounding = this->draw_rounding; + // cater for displays with dimensions that don't divide by the required rounding + auto width = (display->get_width() + rounding - 1) / rounding * rounding; + auto height = (display->get_height() + rounding - 1) / rounding * rounding; auto frac = this->buffer_frac_; if (frac == 0) frac = 1; @@ -461,9 +464,8 @@ void LvglComponent::setup() { } this->buffer_frac_ = frac; lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels); - this->disp_drv_.hor_res = width; - this->disp_drv_.ver_res = height; - // this->setup_driver_(display->get_width(), display->get_height()); + this->disp_drv_.hor_res = display->get_width(); + this->disp_drv_.ver_res = display->get_height(); lv_disp_drv_update(this->disp_, &this->disp_drv_); this->rotation = display->get_rotation(); if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index baeb1c8e3ef..fe421aa4770 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -1,8 +1,9 @@ from esphome import automation import esphome.codegen as cg +from esphome.components.const import CONF_ROWS from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROWS, CONF_TEXT, CONF_WIDTH +from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH from esphome.cpp_generator import MockObj from ..automation import action_to_code diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index 2e123323a08..868b1492115 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -1,8 +1,9 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import key_provider +from esphome.components.const import CONF_ROWS import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_ROWS, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID CODEOWNERS = ["@ssieb"] diff --git a/esphome/const.py b/esphome/const.py index bfd772c5395..9e8ec487b52 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -837,7 +837,6 @@ CONF_RMT_CHANNEL = "rmt_channel" CONF_RMT_SYMBOLS = "rmt_symbols" CONF_ROTATION = "rotation" CONF_ROW = "row" -CONF_ROWS = "rows" CONF_RS_PIN = "rs_pin" CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance" CONF_RTD_WIRES = "rtd_wires" From a59888224c62ad1c0435f147baf360ba7f2269f9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:44:37 -0500 Subject: [PATCH 382/461] Bump version to 2025.10.5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 4f72970e248..1390761da53 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.10.4 +PROJECT_NUMBER = 2025.10.5 # 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 9e8ec487b52..ddf9f28618c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.10.4" +__version__ = "2025.10.5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 7806eb980f3caff0c1a7d73d8ec7bce4b0e2286d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:50:47 -0500 Subject: [PATCH 383/461] Bump version to 2025.12.0-dev --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 034fa3fa379..a19120b9da3 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.11.0-dev +PROJECT_NUMBER = 2025.12.0-dev # 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 d0d94ed283f..a25114d80e8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0-dev" +__version__ = "2025.12.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 7c6f6acf60e5f0a08acff75aeac7193872eea60c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 19:01:11 -0600 Subject: [PATCH 384/461] [wifi] Pass ManualIP by const reference to reduce stack usage --- esphome/components/network/ip_address.h | 8 ++++---- esphome/components/wifi/wifi_component.h | 4 ++-- esphome/components/wifi/wifi_component_esp8266.cpp | 4 ++-- esphome/components/wifi/wifi_component_esp_idf.cpp | 4 ++-- esphome/components/wifi/wifi_component_libretiny.cpp | 4 ++-- esphome/components/wifi/wifi_component_pico_w.cpp | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 5e6b0dbd960..5ec6450cced 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -118,10 +118,10 @@ struct IPAddress { operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); } #endif - bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) - bool is_ip4() { return IP_IS_V4(&ip_addr_); } - bool is_ip6() { return IP_IS_V6(&ip_addr_); } - bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); } + bool is_set() const { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) + bool is_ip4() const { return IP_IS_V4(&ip_addr_); } + bool is_ip6() const { return IP_IS_V6(&ip_addr_); } + bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); } bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); } bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 713e6f223f0..d37367b88c2 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -426,7 +426,7 @@ class WiFiComponent : public Component { bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); bool wifi_apply_power_save_(); - bool wifi_sta_ip_config_(optional manual_ip); + bool wifi_sta_ip_config_(const optional &manual_ip); bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); @@ -434,7 +434,7 @@ class WiFiComponent : public Component { bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP - bool wifi_ap_ip_config_(optional manual_ip); + bool wifi_ap_ip_config_(const optional &manual_ip); bool wifi_start_ap_(const WiFiAP &ap); #endif // USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index bcb5dc4cf7f..b787446a397 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -117,7 +117,7 @@ void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t }; #endif -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -730,7 +730,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) return false; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index fd7e85fb6be..824adb5cf57 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -487,7 +487,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return true; } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -884,7 +884,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { esp_err_t err; // enable AP diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2946b9e8310..eea7a7e9333 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -68,7 +68,7 @@ bool WiFiComponent::wifi_sta_pre_setup_() { return true; } bool WiFiComponent::wifi_apply_power_save_() { return WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -434,7 +434,7 @@ void WiFiComponent::wifi_scan_done_callback_() { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) return false; diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 7025ba16bda..54f03f803d5 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -72,7 +72,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { if (!manual_ip.has_value()) { return true; } @@ -146,7 +146,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { esphome::network::IPAddress ip_address, gateway, subnet, dns; if (manual_ip.has_value()) { ip_address = manual_ip->static_ip; From dd65e39d16c6cba2133358e0b6fa873949f1f486 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 20:25:18 -0600 Subject: [PATCH 385/461] [wifi] Use stack allocation for BSSID formatting in start_connecting --- esphome/components/wifi/wifi_component.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 817419107f2..e2cd22870fd 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -668,25 +668,25 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa void WiFiComponent::start_connecting(const WiFiAP &ap) { // Log connection attempt at INFO level with priority - std::string bssid_formatted; + char bssid_s[18]; int8_t priority = 0; if (ap.get_bssid().has_value()) { - bssid_formatted = format_mac_address_pretty(ap.get_bssid().value().data()); + format_mac_addr_upper(ap.get_bssid().value().data(), bssid_s); priority = this->get_sta_priority(ap.get_bssid().value()); } ESP_LOGI(TAG, "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...", - ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_formatted.c_str() : LOG_STR_LITERAL("any"), - priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), + ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_s : LOG_STR_LITERAL("any"), priority, + this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); #ifdef ESPHOME_LOG_HAS_VERBOSE ESP_LOGV(TAG, "Connection Params:"); ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str()); if (ap.get_bssid().has_value()) { - ESP_LOGV(TAG, " BSSID: %s", format_mac_address_pretty(ap.get_bssid()->data()).c_str()); + ESP_LOGV(TAG, " BSSID: %s", bssid_s); } else { ESP_LOGV(TAG, " BSSID: Not Set"); } From e104103366ccba2e6b0bce232c914a31f5aa862d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 20:33:20 -0600 Subject: [PATCH 386/461] two more --- esphome/components/wifi/wifi_component.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e2cd22870fd..e33cd7cf2d7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -787,6 +787,8 @@ const LogString *get_signal_bars(int8_t rssi) { void WiFiComponent::print_connect_params_() { bssid_t bssid = wifi_bssid(); + char bssid_s[18]; + format_mac_addr_upper(bssid.data(), bssid_s); ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); if (this->is_disabled()) { @@ -809,9 +811,9 @@ void WiFiComponent::print_connect_params_() { " Gateway: %s\n" " DNS1: %s\n" " DNS2: %s", - wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), App.get_name().c_str(), rssi, - LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), - wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); + wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), + get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), + wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) { ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid())); @@ -1390,8 +1392,10 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { (old_priority > std::numeric_limits::min()) ? (old_priority - 1) : std::numeric_limits::min(); this->set_sta_priority(failed_bssid.value(), new_priority); } - ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), - format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority); + char bssid_s[18]; + format_mac_addr_upper(failed_bssid.value().data(), bssid_s); + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s, + old_priority, new_priority); // After adjusting priority, check if all priorities are now at minimum // If so, clear the vector to save memory and reset for fresh start From 7cefb8d92c44bc2291b3b1f905fb8f77d249480a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 20:38:19 -0600 Subject: [PATCH 387/461] a few more --- esphome/components/wifi/wifi_component_esp8266.cpp | 6 ++++-- esphome/components/wifi/wifi_component_esp_idf.cpp | 6 ++++-- esphome/components/wifi/wifi_component_libretiny.cpp | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index bcb5dc4cf7f..db4fc261ebe 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -525,8 +525,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); s_sta_connect_not_found = true; } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + LOG_STR_ARG(get_disconnect_reason_str(it.reason))); s_sta_connect_error = true; } s_sta_connected = false; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index fd7e85fb6be..74d5d8fbc39 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -746,8 +746,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf); return; } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + get_disconnect_reason_str(it.reason)); s_sta_connect_error = true; } s_sta_connected = false; diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2946b9e8310..d10b95dcf69 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -299,8 +299,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ if (it.reason == WIFI_REASON_NO_AP_FOUND) { 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_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + get_disconnect_reason_str(it.reason)); } uint8_t reason = it.reason; From 0d46bc57d6f363489298561098c3c42f91e0692f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 20:42:14 -0600 Subject: [PATCH 388/461] [esp32_ble] Use stack allocation for MAC formatting in dump_config --- esphome/components/esp32_ble/ble.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 8bbb21e3ca2..d0bfb6f8439 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -634,11 +634,13 @@ void ESP32BLE::dump_config() { io_capability_s = "invalid"; break; } + char mac_s[18]; + format_mac_addr_upper(mac_address, mac_s); ESP_LOGCONFIG(TAG, "BLE:\n" " MAC address: %s\n" " IO Capability: %s", - format_mac_address_pretty(mac_address).c_str(), io_capability_s); + mac_s, io_capability_s); } else { ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } From 298813d4fab29c05767ec15914bbafb6f53b32ee Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 11 Nov 2025 22:14:22 -0500 Subject: [PATCH 389/461] Bump version to 2025.11.0b1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index a19120b9da3..8025c71c198 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.12.0-dev +PROJECT_NUMBER = 2025.11.0b1 # 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 a25114d80e8..00975753c24 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.12.0-dev" +__version__ = "2025.11.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 398dba4fc871efed35873a98a5aa44cb29217be0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 21:44:19 -0600 Subject: [PATCH 390/461] [ci] Reduce release time by removing 21 redundant ESP32-S3 IDF tests (#11850) --- .../binary_sensor/test.esp32-s3-idf.yaml | 2 - .../bme68x_bsec2_i2c/test.esp32-s3-idf.yaml | 4 -- tests/components/debug/test.esp32-s3-idf.yaml | 1 - .../matrix_keypad/test.esp32-s3-idf.yaml | 15 ------ .../components/mcp3221/test.esp32-s3-idf.yaml | 4 -- .../mlx90393/test.esp32-s3-idf.yaml | 4 -- tests/components/npi19/test.esp32-s3-idf.yaml | 4 -- tests/components/ntc/test.esp32-s3-idf.yaml | 4 -- .../resistance/test.esp32-s3-idf.yaml | 4 -- .../components/switch/test.esp32-s3-idf.yaml | 2 - .../components/tem3200/test.esp32-s3-idf.yaml | 8 ---- .../template/test.esp32-s3-idf.yaml | 2 - ...max_with_usb_serial_jtag.esp32-s3-idf.yaml | 48 ------------------- .../wk2132_i2c/test.esp32-s3-idf.yaml | 9 ---- .../wk2132_spi/test.esp32-s3-idf.yaml | 11 ----- .../wk2168_i2c/test.esp32-s3-idf.yaml | 9 ---- .../wk2168_spi/test.esp32-s3-idf.yaml | 11 ----- .../wk2204_i2c/test.esp32-s3-idf.yaml | 9 ---- .../wk2204_spi/test.esp32-s3-idf.yaml | 11 ----- .../wk2212_i2c/test.esp32-s3-idf.yaml | 9 ---- .../wk2212_spi/test.esp32-s3-idf.yaml | 11 ----- 21 files changed, 182 deletions(-) delete mode 100644 tests/components/binary_sensor/test.esp32-s3-idf.yaml delete mode 100644 tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml delete mode 100644 tests/components/debug/test.esp32-s3-idf.yaml delete mode 100644 tests/components/matrix_keypad/test.esp32-s3-idf.yaml delete mode 100644 tests/components/mcp3221/test.esp32-s3-idf.yaml delete mode 100644 tests/components/mlx90393/test.esp32-s3-idf.yaml delete mode 100644 tests/components/npi19/test.esp32-s3-idf.yaml delete mode 100644 tests/components/ntc/test.esp32-s3-idf.yaml delete mode 100644 tests/components/resistance/test.esp32-s3-idf.yaml delete mode 100644 tests/components/switch/test.esp32-s3-idf.yaml delete mode 100644 tests/components/tem3200/test.esp32-s3-idf.yaml delete mode 100644 tests/components/template/test.esp32-s3-idf.yaml delete mode 100644 tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2132_i2c/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2132_spi/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2168_i2c/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2168_spi/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2204_i2c/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2204_spi/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2212_i2c/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2212_spi/test.esp32-s3-idf.yaml diff --git a/tests/components/binary_sensor/test.esp32-s3-idf.yaml b/tests/components/binary_sensor/test.esp32-s3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/binary_sensor/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 0fd8684a2c0..00000000000 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s3-idf.yaml b/tests/components/debug/test.esp32-s3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/debug/test.esp32-s3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/matrix_keypad/test.esp32-s3-idf.yaml b/tests/components/matrix_keypad/test.esp32-s3-idf.yaml deleted file mode 100644 index a491f2ed59c..00000000000 --- a/tests/components/matrix_keypad/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,15 +0,0 @@ -packages: - common: !include common.yaml - -matrix_keypad: - id: keypad - rows: - - pin: 10 - - pin: 11 - columns: - - pin: 12 - - pin: 13 - keys: "1234" - has_pulldowns: true - on_key: - - lambda: ESP_LOGI("KEY", "key %d pressed", x); diff --git a/tests/components/mcp3221/test.esp32-s3-idf.yaml b/tests/components/mcp3221/test.esp32-s3-idf.yaml deleted file mode 100644 index 0fd8684a2c0..00000000000 --- a/tests/components/mcp3221/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mlx90393/test.esp32-s3-idf.yaml b/tests/components/mlx90393/test.esp32-s3-idf.yaml deleted file mode 100644 index 0fd8684a2c0..00000000000 --- a/tests/components/mlx90393/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/npi19/test.esp32-s3-idf.yaml b/tests/components/npi19/test.esp32-s3-idf.yaml deleted file mode 100644 index 0fd8684a2c0..00000000000 --- a/tests/components/npi19/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ntc/test.esp32-s3-idf.yaml b/tests/components/ntc/test.esp32-s3-idf.yaml deleted file mode 100644 index 37fb325f4a5..00000000000 --- a/tests/components/ntc/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/resistance/test.esp32-s3-idf.yaml b/tests/components/resistance/test.esp32-s3-idf.yaml deleted file mode 100644 index 1910f325ae5..00000000000 --- a/tests/components/resistance/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/switch/test.esp32-s3-idf.yaml b/tests/components/switch/test.esp32-s3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/switch/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/tem3200/test.esp32-s3-idf.yaml b/tests/components/tem3200/test.esp32-s3-idf.yaml deleted file mode 100644 index e9d826aa7c7..00000000000 --- a/tests/components/tem3200/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/template/test.esp32-s3-idf.yaml b/tests/components/template/test.esp32-s3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/template/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml b/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml deleted file mode 100644 index 88a806eb926..00000000000 --- a/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml +++ /dev/null @@ -1,48 +0,0 @@ -<<: !include ../logger/common-usb_serial_jtag.yaml - -esphome: - on_boot: - then: - - uart.write: - id: uart_1 - data: 'Hello World' - - uart.write: - id: uart_1 - data: [0x00, 0x20, 0x42] - -uart: - - id: uart_1 - tx_pin: 4 - rx_pin: 5 - flow_control_pin: 6 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - rx_full_threshold: 10 - rx_timeout: 1 - parity: EVEN - stop_bits: 2 - - - id: uart_2 - tx_pin: 7 - rx_pin: 8 - flow_control_pin: 9 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - rx_full_threshold: 10 - rx_timeout: 1 - parity: EVEN - stop_bits: 2 - - - id: uart_3 - tx_pin: 10 - rx_pin: 11 - flow_control_pin: 12 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - rx_full_threshold: 10 - rx_timeout: 1 - parity: EVEN - stop_bits: 2 diff --git a/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index d7b149a6fdc..00000000000 --- a/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2132_spi/test.esp32-s3-idf.yaml b/tests/components/wk2132_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index 9c7d36996e2..00000000000 --- a/tests/components/wk2132_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml - uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 115812be973..00000000000 --- a/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2168_spi/test.esp32-s3-idf.yaml b/tests/components/wk2168_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index 374fe64d166..00000000000 --- a/tests/components/wk2168_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 115812be973..00000000000 --- a/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2204_spi/test.esp32-s3-idf.yaml b/tests/components/wk2204_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index 374fe64d166..00000000000 --- a/tests/components/wk2204_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 115812be973..00000000000 --- a/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2212_spi/test.esp32-s3-idf.yaml b/tests/components/wk2212_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index 374fe64d166..00000000000 --- a/tests/components/wk2212_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml From 1b487988c9f6516ccf6b509feba9ac5da6b17f6d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 09:29:40 -0600 Subject: [PATCH 391/461] [mqtt] Fix crash with empty broker during upload/logs --- esphome/mqtt.py | 13 +++++++-- tests/unit_tests/test_main.py | 50 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 093ee64df4d..18526209f76 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -6,6 +6,7 @@ import logging import ssl import tempfile import time +from typing import Any import paho.mqtt.client as mqtt @@ -154,8 +155,12 @@ def show_discover(config, username=None, password=None, client_id=None): def get_esphome_device_ip( - config, username=None, password=None, client_id=None, timeout=25 -): + config: dict[str, Any], + username: str | None = None, + password: str | None = None, + client_id: str | None = None, + timeout=25, +) -> list[str]: if CONF_MQTT not in config: raise EsphomeError( "Cannot discover IP via MQTT as the config does not include the mqtt: " @@ -166,6 +171,10 @@ def get_esphome_device_ip( "Cannot discover IP via MQTT as the config does not include the device name: " "component" ) + if not config[CONF_MQTT].get(CONF_BROKER): + raise EsphomeError( + "Cannot discover IP via MQTT as the broker is not configured" + ) dev_name = config[CONF_ESPHOME][CONF_NAME] dev_ip = None diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 9e5f3993815..ccbc5a1306f 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -1166,6 +1166,56 @@ def test_upload_program_ota_with_mqtt_resolution( ) +def test_upload_program_ota_with_mqtt_empty_broker( + mock_mqtt_get_ip: Mock, + mock_is_ip_address: Mock, + mock_run_ota: Mock, + tmp_path: Path, + caplog: CaptureFixture, +) -> None: + """Test upload_program with OTA when MQTT broker is empty (issue #11653).""" + setup_core(address="192.168.1.50", platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_is_ip_address.return_value = True + mock_mqtt_get_ip.side_effect = EsphomeError( + "Cannot discover IP via MQTT as the broker is not configured" + ) + mock_run_ota.return_value = (0, "192.168.1.50") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ], + CONF_MQTT: { + CONF_BROKER: "", + }, + CONF_MDNS: { + CONF_DISABLED: True, + }, + } + args = MockArgs(username="user", password="pass", client_id="client") + devices = ["MQTTIP", "192.168.1.50"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.50" + # Verify MQTT was attempted but failed gracefully + mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client") + # Verify we fell back to the IP address + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.1.50"], 3232, None, expected_firmware + ) + # Verify warning was logged + assert "MQTT IP discovery failed" in caplog.text + + @patch("esphome.__main__.importlib.import_module") def test_upload_program_platform_specific_handler( mock_import: Mock, From fb00f75192a172402af7a2e516d7a2713140b47f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 09:30:46 -0600 Subject: [PATCH 392/461] [mqtt] Fix crash with empty broker during upload/logs --- esphome/mqtt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 18526209f76..d24418cc8a4 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -6,7 +6,6 @@ import logging import ssl import tempfile import time -from typing import Any import paho.mqtt.client as mqtt @@ -31,6 +30,7 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import get_int_env, get_str_env from esphome.log import AnsiFore, color +from esphome.types import ConfigType from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -155,7 +155,7 @@ def show_discover(config, username=None, password=None, client_id=None): def get_esphome_device_ip( - config: dict[str, Any], + config: ConfigType, username: str | None = None, password: str | None = None, client_id: str | None = None, From d8454e7c0a42e9d81b336164e842baad520dc442 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 09:33:29 -0600 Subject: [PATCH 393/461] Update esphome/mqtt.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/mqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index d24418cc8a4..0d50edbc2c7 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -159,7 +159,7 @@ def get_esphome_device_ip( username: str | None = None, password: str | None = None, client_id: str | None = None, - timeout=25, + timeout: int | float = 25, ) -> list[str]: if CONF_MQTT not in config: raise EsphomeError( From 748aee584a819dcab58dc07645f18af1d615cdfe Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:41:22 -0500 Subject: [PATCH 394/461] [esp32] Update the recommended platform to 55.03.31-2 (#11865) --- esphome/components/esp32/__init__.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6981662d77e..61511cba0ce 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -334,12 +334,14 @@ def _is_framework_url(source: str) -> str: # - https://github.com/espressif/arduino-esp32/releases ARDUINO_FRAMEWORK_VERSION_LOOKUP = { "recommended": cv.Version(3, 3, 2), - "latest": cv.Version(3, 3, 2), - "dev": cv.Version(3, 3, 2), + "latest": cv.Version(3, 3, 4), + "dev": cv.Version(3, 3, 4), } ARDUINO_PLATFORM_VERSION_LOOKUP = { - cv.Version(3, 3, 2): cv.Version(55, 3, 31, "1"), - cv.Version(3, 3, 1): cv.Version(55, 3, 31, "1"), + cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 1): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"), cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"), cv.Version(3, 2, 0): cv.Version(54, 3, 20), @@ -357,8 +359,8 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { "dev": cv.Version(5, 5, 1), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { - cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"), - cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"), + cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), + cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), cv.Version(5, 4, 3): cv.Version(55, 3, 32), cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), @@ -373,9 +375,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 31, "1"), - "latest": cv.Version(55, 3, 31, "1"), - "dev": cv.Version(55, 3, 31, "1"), + "recommended": cv.Version(55, 3, 31, "2"), + "latest": cv.Version(55, 3, 31, "2"), + "dev": cv.Version(55, 3, 31, "2"), } From c1fb8dae37a3ee137a442de424ea67243e32e793 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 10:33:19 -0600 Subject: [PATCH 395/461] [light] Fix dangling reference in compute_color_mode causing memory corruption --- esphome/components/light/light_call.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index df17f53adce..b81ecc57cb4 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -406,7 +406,11 @@ void LightCall::transform_parameters_() { } } ColorMode LightCall::compute_color_mode_() { - const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes(); + // Store traits locally to avoid dangling reference: get_traits() returns by value, so + // calling get_traits().get_supported_color_modes() would create a temporary LightTraits + // object, return a reference to its member, then destroy the temporary, leaving a dangling reference. + auto traits = this->parent_->get_traits(); + const auto &supported_modes = traits.get_supported_color_modes(); int supported_count = supported_modes.size(); // Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown. From 8ce4d5cd4fb21b6a1c5cfd642f59f374b8221eea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 10:39:18 -0600 Subject: [PATCH 396/461] by value --- esphome/components/api/api_connection.cpp | 3 ++- esphome/components/light/light_call.cpp | 6 +----- esphome/components/light/light_traits.h | 3 ++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7eb61f08b61..b754ec24d65 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -476,8 +476,9 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c auto *light = static_cast(entity); ListEntitiesLightResponse msg; auto traits = light->get_traits(); + auto supported_modes = traits.get_supported_color_modes(); // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values - msg.supported_color_modes = &traits.get_supported_color_modes(); + msg.supported_color_modes = &supported_modes; if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { msg.min_mireds = traits.get_min_mireds(); diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index b81ecc57cb4..8365ac77cd9 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -406,11 +406,7 @@ void LightCall::transform_parameters_() { } } ColorMode LightCall::compute_color_mode_() { - // Store traits locally to avoid dangling reference: get_traits() returns by value, so - // calling get_traits().get_supported_color_modes() would create a temporary LightTraits - // object, return a reference to its member, then destroy the temporary, leaving a dangling reference. - auto traits = this->parent_->get_traits(); - const auto &supported_modes = traits.get_supported_color_modes(); + auto supported_modes = this->parent_->get_traits().get_supported_color_modes(); int supported_count = supported_modes.size(); // Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown. diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 294b0cad1de..c3bb27a9647 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -18,7 +18,8 @@ class LightTraits { public: LightTraits() = default; - const ColorModeMask &get_supported_color_modes() const { return this->supported_color_modes_; } + // Return by value to avoid dangling reference when get_traits() returns a temporary + ColorModeMask get_supported_color_modes() const { return this->supported_color_modes_; } void set_supported_color_modes(ColorModeMask supported_color_modes) { this->supported_color_modes_ = supported_color_modes; } From 4b3d3c4ca2aefa8990cfe255ab94b2e8b8e69c5b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 11:51:35 -0600 Subject: [PATCH 397/461] some basic tests --- .coveragerc | 5 -- tests/unit_tests/test_mqtt.py | 91 +++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) delete mode 100644 .coveragerc create mode 100644 tests/unit_tests/test_mqtt.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index c15e79a31b4..00000000000 --- a/.coveragerc +++ /dev/null @@ -1,5 +0,0 @@ -[run] -omit = - esphome/components/* - esphome/analyze_memory/* - tests/integration/* diff --git a/tests/unit_tests/test_mqtt.py b/tests/unit_tests/test_mqtt.py new file mode 100644 index 00000000000..4c2c34dff12 --- /dev/null +++ b/tests/unit_tests/test_mqtt.py @@ -0,0 +1,91 @@ +"""Unit tests for esphome.mqtt module.""" + +from __future__ import annotations + +import pytest + +from esphome.const import CONF_BROKER, CONF_ESPHOME, CONF_MQTT, CONF_NAME +from esphome.core import EsphomeError +from esphome.mqtt import get_esphome_device_ip + + +def test_get_esphome_device_ip_empty_broker() -> None: + """Test that get_esphome_device_ip raises EsphomeError when broker is empty.""" + config = { + CONF_MQTT: { + CONF_BROKER: "", + }, + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the broker is not configured", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_none_broker() -> None: + """Test that get_esphome_device_ip raises EsphomeError when broker is None.""" + config = { + CONF_MQTT: { + CONF_BROKER: None, + }, + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the broker is not configured", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_mqtt() -> None: + """Test that get_esphome_device_ip raises EsphomeError when mqtt config is missing.""" + config = { + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the mqtt:", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_esphome() -> None: + """Test that get_esphome_device_ip raises EsphomeError when esphome config is missing.""" + config = { + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the device name:", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_name() -> None: + """Test that get_esphome_device_ip raises EsphomeError when device name is missing.""" + config = { + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + CONF_ESPHOME: {}, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the device name:", + ): + get_esphome_device_ip(config) From c299361753657ccc089d84e1c358f755f06a7ec8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 11:51:56 -0600 Subject: [PATCH 398/461] some basic tests --- .coveragerc | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000000..c15e79a31b4 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +omit = + esphome/components/* + esphome/analyze_memory/* + tests/integration/* From 9de80b635a9c127488557604fc693d34549f8e64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 11:56:19 -0600 Subject: [PATCH 399/461] [core] Fix wait_until hanging when used in on_boot automations (#11869) --- esphome/core/base_automation.h | 7 +- .../fixtures/wait_until_on_boot.yaml | 47 ++++++++++ tests/integration/test_wait_until_on_boot.py | 91 +++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 tests/integration/fixtures/wait_until_on_boot.yaml create mode 100644 tests/integration/test_wait_until_on_boot.py diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 6f392c8959e..a5e6139182b 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -412,7 +412,12 @@ template class WaitUntilAction : public Action, public Co void setup() override { // Start with loop disabled - only enable when there's work to do - this->disable_loop(); + // IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already + // called before our setup() (e.g., from on_boot trigger at same priority level) + // and we must not undo its enable_loop() call + if (this->num_running_ == 0) { + this->disable_loop(); + } } void play_complex(const Ts &...x) override { diff --git a/tests/integration/fixtures/wait_until_on_boot.yaml b/tests/integration/fixtures/wait_until_on_boot.yaml new file mode 100644 index 00000000000..358bef971b7 --- /dev/null +++ b/tests/integration/fixtures/wait_until_on_boot.yaml @@ -0,0 +1,47 @@ +# Test for wait_until in on_boot automation +# Reproduces bug where wait_until in on_boot would hang forever +# because WaitUntilAction::setup() would disable_loop() after +# play_complex() had already enabled it. + +esphome: + name: wait-until-on-boot + on_boot: + then: + - logger.log: "on_boot: Starting wait_until test" + - globals.set: + id: on_boot_started + value: 'true' + - wait_until: + condition: + lambda: return id(test_flag); + timeout: 5s + - logger.log: "on_boot: wait_until completed successfully" + +host: + +logger: + level: DEBUG + +globals: + - id: on_boot_started + type: bool + initial_value: 'false' + - id: test_flag + type: bool + initial_value: 'false' + +api: + actions: + - action: set_test_flag + then: + - globals.set: + id: test_flag + value: 'true' + - action: check_on_boot_started + then: + - lambda: |- + if (id(on_boot_started)) { + ESP_LOGI("test", "on_boot has started"); + } else { + ESP_LOGI("test", "on_boot has NOT started"); + } diff --git a/tests/integration/test_wait_until_on_boot.py b/tests/integration/test_wait_until_on_boot.py new file mode 100644 index 00000000000..b42c530c540 --- /dev/null +++ b/tests/integration/test_wait_until_on_boot.py @@ -0,0 +1,91 @@ +"""Integration test for wait_until in on_boot automation. + +This test validates that wait_until works correctly when triggered from on_boot, +which runs at the same setup priority as WaitUntilAction itself. This was broken +before the fix because WaitUntilAction::setup() would unconditionally disable_loop(), +even if play_complex() had already been called and enabled the loop. + +The bug: on_boot fires during StartupTrigger::setup(), which calls WaitUntilAction::play_complex() +before WaitUntilAction::setup() has run. Then when WaitUntilAction::setup() runs, it calls +disable_loop(), undoing the enable_loop() from play_complex(), causing wait_until to hang forever. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_on_boot( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that wait_until works in on_boot automation with a condition that becomes true later.""" + loop = asyncio.get_running_loop() + + on_boot_started = False + on_boot_completed = False + + on_boot_started_pattern = re.compile(r"on_boot: Starting wait_until test") + on_boot_complete_pattern = re.compile(r"on_boot: wait_until completed successfully") + + on_boot_started_future = loop.create_future() + on_boot_complete_future = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for test progress.""" + nonlocal on_boot_started, on_boot_completed + + if on_boot_started_pattern.search(line): + on_boot_started = True + if not on_boot_started_future.done(): + on_boot_started_future.set_result(True) + + if on_boot_complete_pattern.search(line): + on_boot_completed = True + if not on_boot_complete_future.done(): + on_boot_complete_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Wait for on_boot to start + await asyncio.wait_for(on_boot_started_future, timeout=10.0) + assert on_boot_started, "on_boot did not start" + + # At this point, on_boot is blocked in wait_until waiting for test_flag to become true + # If the bug exists, wait_until's loop is disabled and it will never complete + # even after we set the flag + + # Give a moment for setup to complete + await asyncio.sleep(0.5) + + # Now set the flag that wait_until is waiting for + _, services = await client.list_entities_services() + set_flag_service = next( + (s for s in services if s.name == "set_test_flag"), None + ) + assert set_flag_service is not None, "set_test_flag service not found" + + client.execute_service(set_flag_service, {}) + + # If the fix works, wait_until's loop() will check the condition and proceed + # If the bug exists, wait_until is stuck with disabled loop and will timeout + try: + await asyncio.wait_for(on_boot_complete_future, timeout=2.0) + assert on_boot_completed, ( + "on_boot wait_until did not complete after flag was set" + ) + except TimeoutError: + pytest.fail( + "wait_until in on_boot did not complete within 2s after condition became true. " + "This indicates the bug where WaitUntilAction::setup() disables the loop " + "after play_complex() has already enabled it." + ) From 97d2f5ee25b4c330c245971203d54d913778c3a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 12:17:47 -0600 Subject: [PATCH 400/461] [wifi][ethernet] Fix spurious warnings and unclear status after PR #9823 --- esphome/components/ethernet/ethernet_component.cpp | 5 ++++- esphome/components/wifi/wifi_component.cpp | 13 ++++++++++++- esphome/components/wifi/wifi_component.h | 3 +++ esphome/components/wifi/wifi_component_esp8266.cpp | 2 +- esphome/components/wifi/wifi_component_esp_idf.cpp | 11 +++++++---- .../components/wifi/wifi_component_libretiny.cpp | 2 +- esphome/components/wifi/wifi_component_pico_w.cpp | 2 +- 7 files changed, 29 insertions(+), 9 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 5888ddce603..cad963b2999 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -381,7 +381,10 @@ void EthernetComponent::dump_config() { break; } - ESP_LOGCONFIG(TAG, "Ethernet:"); + ESP_LOGCONFIG(TAG, + "Ethernet:\n" + " Connected: %s", + YESNO(this->is_connected())); this->dump_connect_params_(); #ifdef USE_ETHERNET_SPI ESP_LOGCONFIG(TAG, diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 817419107f2..33aa6c81397 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -743,6 +743,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { } const LogString *get_signal_bars(int8_t rssi) { + // Check for disconnected sentinel value first + if (rssi == WIFI_RSSI_DISCONNECTED) { + // MULTIPLICATION SIGN + // Unicode: U+00D7, UTF-8: C3 97 + return LOG_STR("\033[0;31m" // red + "\xc3\x97\xc3\x97\xc3\x97\xc3\x97" + "\033[0m"); + } // LOWER ONE QUARTER BLOCK // Unicode: U+2582, UTF-8: E2 96 82 // LOWER HALF BLOCK @@ -1022,7 +1030,10 @@ void WiFiComponent::check_scanning_finished() { } void WiFiComponent::dump_config() { - ESP_LOGCONFIG(TAG, "WiFi:"); + ESP_LOGCONFIG(TAG, + "WiFi:\n" + " Connected: %s", + YESNO(this->is_connected())); this->print_connect_params_(); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 713e6f223f0..5023cf34284 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -52,6 +52,9 @@ extern "C" { namespace esphome { namespace wifi { +/// Sentinel value for RSSI when WiFi is not connected +static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; + struct SavedWifiSettings { char ssid[33]; char password[65]; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index bcb5dc4cf7f..bdaae5382ac 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -870,7 +870,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index fd7e85fb6be..8c27fe92db9 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1029,7 +1029,8 @@ bssid_t WiFiComponent::wifi_bssid() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); return bssid; } std::copy(info.bssid, info.bssid + 6, bssid.begin()); @@ -1039,7 +1040,8 @@ std::string WiFiComponent::wifi_ssid() { wifi_ap_record_t info{}; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); return ""; } auto *ssid_s = reinterpret_cast(info.ssid); @@ -1050,8 +1052,9 @@ int8_t WiFiComponent::wifi_rssi() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); - return 0; + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + return WIFI_RSSI_DISCONNECTED; } return info.rssi; } diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2946b9e8310..8c6c28ac753 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -484,7 +484,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 7025ba16bda..073b7528861 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -200,7 +200,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { From 08127d02250dfa689994850d4e53a1a1510acd28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 12:48:44 -0600 Subject: [PATCH 401/461] [wifi] Fix phase transition and error state on reconnection --- esphome/components/wifi/wifi_component.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 817419107f2..f0392d0a647 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -465,6 +465,8 @@ void WiFiComponent::loop() { if (!this->is_connected()) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; + // Clear error flag before reconnecting so first attempt is not seen as immediate failure + this->error_from_callback_ = false; this->retry_connect(); } else { this->status_clear_warning(); @@ -1047,6 +1049,10 @@ void WiFiComponent::check_connecting_finished() { // Reset to initial phase on successful connection (don't log transition, just reset state) this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT; this->num_retried_ = 0; + // Ensure next connection attempt does not inherit error state + // so when WiFi disconnects later we start fresh we don't see + // the first connection as a failure. + this->error_from_callback_ = false; this->print_connect_params_(); @@ -1133,6 +1139,11 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP } #endif + // Check if we should try explicit hidden networks before scanning + // This handles reconnection after connection loss where first network is hidden + if (!this->sta_.empty() && this->sta_[0].get_hidden()) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } // No more APs to try, fall back to scan return WiFiRetryPhase::SCAN_CONNECTING; From 5d613ada8319e95d0d233beccdd322745d42e176 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:00:50 -0600 Subject: [PATCH 402/461] Bump pytest from 9.0.0 to 9.0.1 (#11874) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 35010ad52fc..5c7cccaf25b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==9.0.0 +pytest==9.0.1 pytest-cov==7.0.0 pytest-mock==3.15.1 pytest-asyncio==1.3.0 From 3872a2fd919fb8b33fe0829001d0dce50a54001d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:01:07 -0600 Subject: [PATCH 403/461] [captive_portal] Warn when enabled without WiFi AP configured (#11856) --- esphome/components/captive_portal/__init__.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 99acb76bcf3..9bd3ef8a058 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -1,9 +1,12 @@ +import logging + import esphome.codegen as cg from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( + CONF_AP, CONF_ID, PLATFORM_BK72XX, PLATFORM_ESP32, @@ -14,6 +17,10 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) def AUTO_LOAD() -> list[str]: @@ -50,6 +57,27 @@ CONFIG_SCHEMA = cv.All( ) +def _final_validate(config: ConfigType) -> ConfigType: + full_config = fv.full_config.get() + wifi_conf = full_config.get("wifi") + + if wifi_conf is None: + # This shouldn't happen due to DEPENDENCIES = ["wifi"], but check anyway + raise cv.Invalid("Captive portal requires the wifi component to be configured") + + if CONF_AP not in wifi_conf: + _LOGGER.warning( + "Captive portal is enabled but no WiFi AP is configured. " + "The captive portal will not be accessible. " + "Add 'ap:' to your WiFi configuration to enable the captive portal." + ) + + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL) async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) From 4b58cb4ce61cb07a3b8e1d97c7274e82e98b8db5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:01:19 -0600 Subject: [PATCH 404/461] [wifi] Pass ManualIP by const reference to reduce stack usage (#11858) --- esphome/components/network/ip_address.h | 8 ++++---- esphome/components/wifi/wifi_component.h | 4 ++-- esphome/components/wifi/wifi_component_esp8266.cpp | 4 ++-- esphome/components/wifi/wifi_component_esp_idf.cpp | 4 ++-- esphome/components/wifi/wifi_component_libretiny.cpp | 4 ++-- esphome/components/wifi/wifi_component_pico_w.cpp | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 5e6b0dbd960..5ec6450cced 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -118,10 +118,10 @@ struct IPAddress { operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); } #endif - bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) - bool is_ip4() { return IP_IS_V4(&ip_addr_); } - bool is_ip6() { return IP_IS_V6(&ip_addr_); } - bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); } + bool is_set() const { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) + bool is_ip4() const { return IP_IS_V4(&ip_addr_); } + bool is_ip6() const { return IP_IS_V6(&ip_addr_); } + bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); } bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); } bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 713e6f223f0..d37367b88c2 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -426,7 +426,7 @@ class WiFiComponent : public Component { bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); bool wifi_apply_power_save_(); - bool wifi_sta_ip_config_(optional manual_ip); + bool wifi_sta_ip_config_(const optional &manual_ip); bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); @@ -434,7 +434,7 @@ class WiFiComponent : public Component { bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP - bool wifi_ap_ip_config_(optional manual_ip); + bool wifi_ap_ip_config_(const optional &manual_ip); bool wifi_start_ap_(const WiFiAP &ap); #endif // USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index bcb5dc4cf7f..b787446a397 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -117,7 +117,7 @@ void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t }; #endif -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -730,7 +730,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) return false; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index fd7e85fb6be..824adb5cf57 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -487,7 +487,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return true; } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -884,7 +884,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { esp_err_t err; // enable AP diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2946b9e8310..eea7a7e9333 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -68,7 +68,7 @@ bool WiFiComponent::wifi_sta_pre_setup_() { return true; } bool WiFiComponent::wifi_apply_power_save_() { return WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -434,7 +434,7 @@ void WiFiComponent::wifi_scan_done_callback_() { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) return false; diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 7025ba16bda..54f03f803d5 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -72,7 +72,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { if (!manual_ip.has_value()) { return true; } @@ -146,7 +146,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { esphome::network::IPAddress ip_address, gateway, subnet, dns; if (manual_ip.has_value()) { ip_address = manual_ip->static_ip; From 5a550cc579bee3f7c07ef7c9ccdc707ac776d836 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:26:36 -0600 Subject: [PATCH 405/461] [api] Eliminate heap allocations when transmitting Event types (#11773) --- esphome/components/api/api_connection.cpp | 14 +++---- esphome/components/api/api_connection.h | 48 ++++------------------- 2 files changed, 14 insertions(+), 48 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7eb61f08b61..ca9ddaedf4a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1294,11 +1294,11 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #endif #ifdef USE_EVENT -void APIConnection::send_event(event::Event *event, const std::string &event_type) { +void APIConnection::send_event(event::Event *event, const char *event_type) { this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, +uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { EventResponse resp; resp.set_event_type(StringRef(event_type)); @@ -1650,9 +1650,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c // O(n) but optimized for RAM and not performance. for (auto &item : items) { if (item.entity == entity && item.message_type == message_type) { - // Clean up old creator before replacing - item.creator.cleanup(message_type); - // Move assign the new creator + // Replace with new creator item.creator = std::move(creator); return; } @@ -1822,7 +1820,7 @@ void APIConnection::process_batch_() { // Handle remaining items more efficiently if (items_processed < this->deferred_batch_.size()) { - // Remove processed items from the beginning with proper cleanup + // Remove processed items from the beginning this->deferred_batch_.remove_front(items_processed); // Reschedule for remaining items this->schedule_batch_(); @@ -1835,10 +1833,10 @@ void APIConnection::process_batch_() { uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const { #ifdef USE_EVENT - // Special case: EventResponse uses string pointer + // Special case: EventResponse uses const char * pointer if (message_type == EventResponse::MESSAGE_TYPE) { auto *e = static_cast(entity); - return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); + return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single); } #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 284fa11a954..a77c93a2d56 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -177,7 +177,7 @@ class APIConnection final : public APIServerConnection { #endif #ifdef USE_EVENT - void send_event(event::Event *event, const std::string &event_type); + void send_event(event::Event *event, const char *event_type); #endif #ifdef USE_UPDATE @@ -450,7 +450,7 @@ class APIConnection final : public APIServerConnection { bool is_single); #endif #ifdef USE_EVENT - static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, + static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single); static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); #endif @@ -508,10 +508,8 @@ class APIConnection final : public APIServerConnection { // Constructor for function pointer MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; } - // Constructor for string state capture - explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); } - - // No destructor - cleanup must be called explicitly with message_type + // Constructor for const char * (Event types - no allocation needed) + explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; } // Delete copy operations - MessageCreator should only be moved MessageCreator(const MessageCreator &other) = delete; @@ -523,8 +521,6 @@ class APIConnection final : public APIServerConnection { // Move assignment MessageCreator &operator=(MessageCreator &&other) noexcept { if (this != &other) { - // IMPORTANT: Caller must ensure cleanup() was called if this contains a string! - // In our usage, this happens in add_item() deduplication and vector::erase() data_ = other.data_; other.data_.function_ptr = nullptr; } @@ -535,20 +531,10 @@ class APIConnection final : public APIServerConnection { uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const; - // Manual cleanup method - must be called before destruction for string types - void cleanup(uint8_t message_type) { -#ifdef USE_EVENT - if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) { - delete data_.string_ptr; - data_.string_ptr = nullptr; - } -#endif - } - private: union Data { MessageCreatorPtr function_ptr; - std::string *string_ptr; + const char *const_char_ptr; } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before }; @@ -568,42 +554,24 @@ class APIConnection final : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - private: - // Helper to cleanup items from the beginning - void cleanup_items_(size_t count) { - for (size_t i = 0; i < count; i++) { - items[i].creator.cleanup(items[i].message_type); - } - } - - public: DeferredBatch() { // Pre-allocate capacity for typical batch sizes to avoid reallocation items.reserve(8); } - ~DeferredBatch() { - // Ensure cleanup of any remaining items - clear(); - } - // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); // Add item to the front of the batch (for high priority messages like ping) void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); - // Clear all items with proper cleanup + // Clear all items void clear() { - cleanup_items_(items.size()); items.clear(); batch_start_time = 0; } - // Remove processed items from the front with proper cleanup - void remove_front(size_t count) { - cleanup_items_(count); - items.erase(items.begin(), items.begin() + count); - } + // Remove processed items from the front + void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } bool empty() const { return items.empty(); } size_t size() const { return items.size(); } From 2f39b10baa9348c68d54413ad60177a6d0050995 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:26:46 -0600 Subject: [PATCH 406/461] [esp32_ble_tracker] Use initializer_list to eliminate compiler warning and reduce flash usage (#11861) --- esphome/components/esp32_ble_tracker/automation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index 054cbaa7df3..bbf7992fa45 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -10,7 +10,7 @@ namespace esphome::esp32_ble_tracker { class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { public: explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } - void set_addresses(const std::vector &addresses) { this->address_vec_ = addresses; } + void set_addresses(std::initializer_list addresses) { this->address_vec_ = addresses; } bool parse_device(const ESPBTDevice &device) override { uint64_t u64_addr = device.address_uint64(); From 5f0fa68d732cd672fc83a249148bcde130bb812a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:26:57 -0600 Subject: [PATCH 407/461] [esp32_ble] Use stack allocation for MAC formatting in dump_config (#11860) --- esphome/components/esp32_ble/ble.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 8bbb21e3ca2..d0bfb6f8439 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -634,11 +634,13 @@ void ESP32BLE::dump_config() { io_capability_s = "invalid"; break; } + char mac_s[18]; + format_mac_addr_upper(mac_address, mac_s); ESP_LOGCONFIG(TAG, "BLE:\n" " MAC address: %s\n" " IO Capability: %s", - format_mac_address_pretty(mac_address).c_str(), io_capability_s); + mac_s, io_capability_s); } else { ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } From 29a50da6355b2ad936dc78a19efb4cc4eeedc57e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:27:06 -0600 Subject: [PATCH 408/461] [wifi] Use stack allocation for BSSID formatting in logging (#11859) --- esphome/components/wifi/wifi_component.cpp | 24 +++++++++++-------- .../wifi/wifi_component_esp8266.cpp | 6 +++-- .../wifi/wifi_component_esp_idf.cpp | 6 +++-- .../wifi/wifi_component_libretiny.cpp | 6 +++-- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 817419107f2..e33cd7cf2d7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -668,25 +668,25 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa void WiFiComponent::start_connecting(const WiFiAP &ap) { // Log connection attempt at INFO level with priority - std::string bssid_formatted; + char bssid_s[18]; int8_t priority = 0; if (ap.get_bssid().has_value()) { - bssid_formatted = format_mac_address_pretty(ap.get_bssid().value().data()); + format_mac_addr_upper(ap.get_bssid().value().data(), bssid_s); priority = this->get_sta_priority(ap.get_bssid().value()); } ESP_LOGI(TAG, "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...", - ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_formatted.c_str() : LOG_STR_LITERAL("any"), - priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), + ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_s : LOG_STR_LITERAL("any"), priority, + this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); #ifdef ESPHOME_LOG_HAS_VERBOSE ESP_LOGV(TAG, "Connection Params:"); ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str()); if (ap.get_bssid().has_value()) { - ESP_LOGV(TAG, " BSSID: %s", format_mac_address_pretty(ap.get_bssid()->data()).c_str()); + ESP_LOGV(TAG, " BSSID: %s", bssid_s); } else { ESP_LOGV(TAG, " BSSID: Not Set"); } @@ -787,6 +787,8 @@ const LogString *get_signal_bars(int8_t rssi) { void WiFiComponent::print_connect_params_() { bssid_t bssid = wifi_bssid(); + char bssid_s[18]; + format_mac_addr_upper(bssid.data(), bssid_s); ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); if (this->is_disabled()) { @@ -809,9 +811,9 @@ void WiFiComponent::print_connect_params_() { " Gateway: %s\n" " DNS1: %s\n" " DNS2: %s", - wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), App.get_name().c_str(), rssi, - LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), - wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); + wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), + get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), + wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) { ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid())); @@ -1390,8 +1392,10 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { (old_priority > std::numeric_limits::min()) ? (old_priority - 1) : std::numeric_limits::min(); this->set_sta_priority(failed_bssid.value(), new_priority); } - ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), - format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority); + char bssid_s[18]; + format_mac_addr_upper(failed_bssid.value().data(), bssid_s); + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s, + old_priority, new_priority); // After adjusting priority, check if all priorities are now at minimum // If so, clear the vector to save memory and reset for fresh start diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index b787446a397..78f336ab154 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -525,8 +525,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); s_sta_connect_not_found = true; } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + LOG_STR_ARG(get_disconnect_reason_str(it.reason))); s_sta_connect_error = true; } s_sta_connected = false; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 824adb5cf57..df295655541 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -746,8 +746,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf); return; } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + get_disconnect_reason_str(it.reason)); s_sta_connect_error = true; } s_sta_connected = false; diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index eea7a7e9333..7f0c35c8c89 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -299,8 +299,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ if (it.reason == WIFI_REASON_NO_AP_FOUND) { 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_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + get_disconnect_reason_str(it.reason)); } uint8_t reason = it.reason; From 20388ce84856255cbc03fb720a1af532a18ac56c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:54:23 -0600 Subject: [PATCH 409/461] [thermostat] Replace std::map with FixedVector, reduce flash usage --- esphome/components/thermostat/climate.py | 38 ++++++++- .../thermostat/thermostat_climate.cpp | 82 +++++++++++------- .../thermostat/thermostat_climate.h | 33 +++++-- .../climate_custom_fan_modes_and_presets.yaml | 1 + .../integration/test_climate_custom_modes.py | 85 ++++++++++++++++++- 5 files changed, 195 insertions(+), 44 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index a928d208f3a..a3c155aac06 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -945,6 +945,10 @@ async def to_code(config): cg.add(var.set_humidity_hysteresis(config[CONF_HUMIDITY_HYSTERESIS])) if CONF_PRESET in config: + # Separate standard and custom presets, and build preset config variables + standard_presets: list[tuple[cg.MockObj, cg.MockObj]] = [] + custom_presets: list[tuple[str, cg.MockObj]] = [] + for preset_config in config[CONF_PRESET]: name = preset_config[CONF_NAME] standard_preset = None @@ -987,9 +991,39 @@ async def to_code(config): ) if standard_preset is not None: - cg.add(var.set_preset_config(standard_preset, preset_target_variable)) + standard_presets.append((standard_preset, preset_target_variable)) else: - cg.add(var.set_custom_preset_config(name, preset_target_variable)) + custom_presets.append((name, preset_target_variable)) + + # Build initializer list for standard presets + if standard_presets: + cg.add( + var.set_preset_config( + [ + cg.StructInitializer( + thermostat_ns.struct("ThermostatPresetEntry"), + ("preset", preset), + ("config", preset_var), + ) + for preset, preset_var in standard_presets + ] + ) + ) + + # Build initializer list for custom presets + if custom_presets: + cg.add( + var.set_custom_preset_config( + [ + cg.StructInitializer( + thermostat_ns.struct("ThermostatCustomPresetEntry"), + ("name", cg.RawExpression(f'"{name}"')), + ("config", preset_var), + ) + for name, preset_var in custom_presets + ] + ) + ) if CONF_DEFAULT_PRESET in config: default_preset_name = config[CONF_DEFAULT_PRESET] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d533ef93eca..2b51f58f4ff 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -53,8 +53,8 @@ void ThermostatClimate::setup() { if (use_default_preset) { if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) { this->change_preset_(this->default_preset_); - } else if (!this->default_custom_preset_.empty()) { - this->change_custom_preset_(this->default_custom_preset_.c_str()); + } else if (this->default_custom_preset_ != nullptr) { + this->change_custom_preset_(this->default_custom_preset_); } } @@ -319,16 +319,16 @@ climate::ClimateTraits ThermostatClimate::traits() { if (this->supports_swing_mode_vertical_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); - for (auto &it : this->preset_config_) { - traits.add_supported_preset(it.first); + for (const auto &entry : this->preset_config_) { + traits.add_supported_preset(entry.preset); } - // Extract custom preset names from the custom_preset_config_ map + // Extract custom preset names from the custom_preset_config_ vector if (!this->custom_preset_config_.empty()) { std::vector custom_preset_names; custom_preset_names.reserve(this->custom_preset_config_.size()); - for (const auto &it : this->custom_preset_config_) { - custom_preset_names.push_back(it.first.c_str()); + for (const auto &entry : this->custom_preset_config_) { + custom_preset_names.push_back(entry.name); } traits.set_supported_custom_presets(custom_preset_names); } @@ -1154,12 +1154,18 @@ void ThermostatClimate::dump_preset_config_(const char *preset_name, const Therm } void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { - auto config = this->preset_config_.find(preset); + // Linear search through preset configurations + const ThermostatClimateTargetTempConfig *config = nullptr; + for (const auto &entry : this->preset_config_) { + if (entry.preset == preset) { + config = &entry.config; + break; + } + } - if (config != this->preset_config_.end()) { + if (config != nullptr) { ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); - if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) || - this->preset.value() != preset) { + if (this->change_preset_internal_(*config) || (!this->preset.has_value()) || this->preset.value() != preset) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; this->set_preset_(preset); @@ -1178,11 +1184,18 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } void ThermostatClimate::change_custom_preset_(const char *custom_preset) { - auto config = this->custom_preset_config_.find(custom_preset); + // Linear search through custom preset configurations + const ThermostatClimateTargetTempConfig *config = nullptr; + for (const auto &entry : this->custom_preset_config_) { + if (strcmp(entry.name, custom_preset) == 0) { + config = &entry.config; + break; + } + } - if (config != this->custom_preset_config_.end()) { + if (config != nullptr) { ESP_LOGV(TAG, "Custom preset %s requested", custom_preset); - if (this->change_preset_internal_(config->second) || !this->has_custom_preset() || + if (this->change_preset_internal_(*config) || !this->has_custom_preset() || strcmp(this->get_custom_preset(), custom_preset) != 0) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; @@ -1247,14 +1260,12 @@ bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTem return something_changed; } -void ThermostatClimate::set_preset_config(climate::ClimatePreset preset, - const ThermostatClimateTargetTempConfig &config) { - this->preset_config_[preset] = config; +void ThermostatClimate::set_preset_config(std::initializer_list presets) { + this->preset_config_ = presets; } -void ThermostatClimate::set_custom_preset_config(const std::string &name, - const ThermostatClimateTargetTempConfig &config) { - this->custom_preset_config_[name] = config; +void ThermostatClimate::set_custom_preset_config(std::initializer_list presets) { + this->custom_preset_config_ = presets; } ThermostatClimate::ThermostatClimate() @@ -1293,8 +1304,16 @@ ThermostatClimate::ThermostatClimate() humidity_control_humidify_action_trigger_(new Trigger<>()), humidity_control_off_action_trigger_(new Trigger<>()) {} -void ThermostatClimate::set_default_preset(const std::string &custom_preset) { - this->default_custom_preset_ = custom_preset; +void ThermostatClimate::set_default_preset(const char *custom_preset) { + // Find the preset in custom_preset_config_ and store pointer from there + for (const auto &entry : this->custom_preset_config_) { + if (strcmp(entry.name, custom_preset) == 0) { + this->default_custom_preset_ = entry.name; + return; + } + } + // If not found, it will be caught during validation + this->default_custom_preset_ = nullptr; } void ThermostatClimate::set_default_preset(climate::ClimatePreset preset) { this->default_preset_ = preset; } @@ -1605,19 +1624,22 @@ void ThermostatClimate::dump_config() { if (!this->preset_config_.empty()) { ESP_LOGCONFIG(TAG, " Supported PRESETS:"); - for (auto &it : this->preset_config_) { - const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); - ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_preset_ ? " (default)" : ""); - this->dump_preset_config_(preset_name, it.second); + for (const auto &entry : this->preset_config_) { + const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(entry.preset)); + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, entry.preset == this->default_preset_ ? " (default)" : ""); + this->dump_preset_config_(preset_name, entry.config); } } if (!this->custom_preset_config_.empty()) { ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS:"); - for (auto &it : this->custom_preset_config_) { - const auto *preset_name = it.first.c_str(); - ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_custom_preset_ ? " (default)" : ""); - this->dump_preset_config_(preset_name, it.second); + for (const auto &entry : this->custom_preset_config_) { + const auto *preset_name = entry.name; + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, + (this->default_custom_preset_ != nullptr && strcmp(entry.name, this->default_custom_preset_) == 0) + ? " (default)" + : ""); + this->dump_preset_config_(preset_name, entry.config); } } } diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index c9795d9666c..3fe6ef0f7c0 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -3,12 +3,12 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/climate/climate.h" #include "esphome/components/sensor/sensor.h" #include #include -#include namespace esphome { namespace thermostat { @@ -72,14 +72,30 @@ struct ThermostatClimateTargetTempConfig { optional mode_{}; }; +/// Entry for standard preset lookup +struct ThermostatPresetEntry { + climate::ClimatePreset preset; + ThermostatClimateTargetTempConfig config; +}; + +/// Entry for custom preset lookup +struct ThermostatCustomPresetEntry { + const char *name; + ThermostatClimateTargetTempConfig config; +}; + class ThermostatClimate : public climate::Climate, public Component { + public: + using PresetEntry = ThermostatPresetEntry; + using CustomPresetEntry = ThermostatCustomPresetEntry; + public: ThermostatClimate(); void setup() override; void dump_config() override; void loop() override; - void set_default_preset(const std::string &custom_preset); + void set_default_preset(const char *custom_preset); void set_default_preset(climate::ClimatePreset preset); void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from); void set_set_point_minimum_differential(float differential); @@ -131,8 +147,8 @@ class ThermostatClimate : public climate::Climate, public Component { void set_supports_humidification(bool supports_humidification); void set_supports_two_points(bool supports_two_points); - void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config); - void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config); + void set_preset_config(std::initializer_list presets); + void set_custom_preset_config(std::initializer_list presets); Trigger<> *get_cool_action_trigger() const; Trigger<> *get_supplemental_cool_action_trigger() const; @@ -516,9 +532,6 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_swing_mode_trigger_{nullptr}; Trigger<> *prev_humidity_control_trigger_{nullptr}; - /// Default custom preset to use on start up - std::string default_custom_preset_{}; - /// Climate action timers std::array timer_{ ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)), @@ -534,9 +547,11 @@ class ThermostatClimate : public climate::Climate, public Component { }; /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) - std::map preset_config_{}; + FixedVector preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") - std::map custom_preset_config_{}; + FixedVector custom_preset_config_{}; + /// Default custom preset to use on start up (pointer to entry in custom_preset_config_) + const char *default_custom_preset_{nullptr}; }; } // namespace thermostat diff --git a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml index bf4ef9eafd5..3996d0f169a 100644 --- a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml +++ b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml @@ -14,6 +14,7 @@ climate: id: test_thermostat name: Test Thermostat Custom Modes sensor: thermostat_sensor + default_preset: "Eco Plus" preset: - name: Away default_target_temperature_low: 16°C diff --git a/tests/integration/test_climate_custom_modes.py b/tests/integration/test_climate_custom_modes.py index ce34959d88f..67a7b0581a4 100644 --- a/tests/integration/test_climate_custom_modes.py +++ b/tests/integration/test_climate_custom_modes.py @@ -2,9 +2,13 @@ from __future__ import annotations -from aioesphomeapi import ClimateInfo, ClimatePreset +import asyncio + +import aioesphomeapi +from aioesphomeapi import ClimateInfo, ClimatePreset, EntityState import pytest +from .state_utils import InitialStateHelper from .types import APIClientConnectedFactory, RunCompiledFunction @@ -14,15 +18,50 @@ async def test_climate_custom_fan_modes_and_presets( run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, ) -> None: - """Test that custom presets are properly exposed via API.""" + """Test that custom presets are properly exposed and can be changed.""" + loop = asyncio.get_running_loop() async with run_compiled(yaml_config), api_client_connected() as client: - # Get entities and services + states: dict[int, EntityState] = {} + super_saver_future: asyncio.Future[EntityState] = loop.create_future() + vacation_future: asyncio.Future[EntityState] = loop.create_future() + + def on_state(state: EntityState) -> None: + states[state.key] = state + if isinstance(state, aioesphomeapi.ClimateState): + # Wait for Super Saver preset + if ( + state.custom_preset == "Super Saver" + and state.target_temperature_low == 20.0 + and state.target_temperature_high == 24.0 + and not super_saver_future.done() + ): + super_saver_future.set_result(state) + # Wait for Vacation Mode preset + elif ( + state.custom_preset == "Vacation Mode" + and state.target_temperature_low == 15.0 + and state.target_temperature_high == 18.0 + and not vacation_future.done() + ): + vacation_future.set_result(state) + + # Get entities and set up state synchronization entities, services = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] assert len(climate_infos) == 1, "Expected exactly 1 climate entity" test_climate = climate_infos[0] + # Subscribe with the wrapper that filters initial states + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states to be broadcast + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + # Verify enum presets are exposed (from preset: config map) assert ClimatePreset.AWAY in test_climate.supported_presets, ( "Expected AWAY in enum presets" @@ -40,3 +79,43 @@ async def test_climate_custom_fan_modes_and_presets( assert "Vacation Mode" in custom_presets, ( "Expected 'Vacation Mode' in custom presets" ) + + # Get initial state and verify default preset + initial_state = initial_state_helper.initial_states.get(test_climate.key) + assert initial_state is not None, "Climate initial state not found" + assert isinstance(initial_state, aioesphomeapi.ClimateState) + assert initial_state.custom_preset == "Eco Plus", ( + f"Expected default preset 'Eco Plus', got '{initial_state.custom_preset}'" + ) + assert initial_state.target_temperature_low == 18.0, ( + f"Expected low temp 18.0, got {initial_state.target_temperature_low}" + ) + assert initial_state.target_temperature_high == 22.0, ( + f"Expected high temp 22.0, got {initial_state.target_temperature_high}" + ) + + # Test changing to "Super Saver" custom preset + client.climate_command(test_climate.key, custom_preset="Super Saver") + + try: + super_saver_state = await asyncio.wait_for(super_saver_future, timeout=5.0) + except TimeoutError: + pytest.fail("Super Saver preset change not received within 5 seconds") + + assert isinstance(super_saver_state, aioesphomeapi.ClimateState) + assert super_saver_state.custom_preset == "Super Saver" + assert super_saver_state.target_temperature_low == 20.0 + assert super_saver_state.target_temperature_high == 24.0 + + # Test changing to "Vacation Mode" custom preset + client.climate_command(test_climate.key, custom_preset="Vacation Mode") + + try: + vacation_state = await asyncio.wait_for(vacation_future, timeout=5.0) + except TimeoutError: + pytest.fail("Vacation Mode preset change not received within 5 seconds") + + assert isinstance(vacation_state, aioesphomeapi.ClimateState) + assert vacation_state.custom_preset == "Vacation Mode" + assert vacation_state.target_temperature_low == 15.0 + assert vacation_state.target_temperature_high == 18.0 From 4eb471b3164fe5e6a1d4d049cc2da55405bff410 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 15:00:52 -0600 Subject: [PATCH 410/461] tweaks --- esphome/components/thermostat/thermostat_climate.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 3fe6ef0f7c0..76391f800cb 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -89,7 +89,6 @@ class ThermostatClimate : public climate::Climate, public Component { using PresetEntry = ThermostatPresetEntry; using CustomPresetEntry = ThermostatCustomPresetEntry; - public: ThermostatClimate(); void setup() override; void dump_config() override; @@ -551,6 +550,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") FixedVector custom_preset_config_{}; /// Default custom preset to use on start up (pointer to entry in custom_preset_config_) + private: const char *default_custom_preset_{nullptr}; }; From 859101ddc9fa55db7f31dfbb4a9411f1d4c736b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 17:42:50 -0600 Subject: [PATCH 411/461] [api][event] Send events immediately to prevent loss during rapid triggers (#11777) --- esphome/components/api/api_connection.cpp | 4 +- esphome/components/api/api_connection.h | 54 ++++++++++++++++++----- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ca9ddaedf4a..b4230576de8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1295,8 +1295,8 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #ifdef USE_EVENT void APIConnection::send_event(event::Event *event, const char *event_type) { - this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, - EventResponse::ESTIMATED_SIZE); + this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, + EventResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index a77c93a2d56..6cfd1089272 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -650,21 +650,30 @@ class APIConnection final : public APIServerConnection { } #endif + // Helper to check if a message type should bypass batching + // Returns true if: + // 1. It's an UpdateStateResponse (always send immediately to handle cases where + // the main loop is blocked, e.g., during OTA updates) + // 2. It's an EventResponse (events are edge-triggered - every occurrence matters) + // 3. OR: User has opted into immediate sending (should_try_send_immediately = true + // AND batch_delay = 0) + inline bool should_send_immediately_(uint8_t message_type) const { + return ( +#ifdef USE_UPDATE + message_type == UpdateStateResponse::MESSAGE_TYPE || +#endif +#ifdef USE_EVENT + message_type == EventResponse::MESSAGE_TYPE || +#endif + (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)); + } + // Helper method to send a message either immediately or via batching + // Tries immediate send if should_send_immediately_() returns true and buffer has space + // Falls back to batching if immediate send fails or isn't applicable bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, uint8_t estimated_size) { - // Try to send immediately if: - // 1. It's an UpdateStateResponse (always send immediately to handle cases where - // the main loop is blocked, e.g., during OTA updates) - // 2. OR: We should try to send immediately (should_try_send_immediately = true) - // AND Batch delay is 0 (user has opted in to immediate sending) - // 3. AND: Buffer has space available - if (( -#ifdef USE_UPDATE - message_type == UpdateStateResponse::MESSAGE_TYPE || -#endif - (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) && - this->helper_->can_write_without_blocking()) { + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { // Now actually encode and send if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { @@ -682,6 +691,27 @@ class APIConnection final : public APIServerConnection { return this->schedule_message_(entity, creator, message_type, estimated_size); } + // Overload for MessageCreator (used by events which need to capture event_type) + bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { + // Try to send immediately if message type should bypass batching and buffer has space + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { + // Now actually encode and send + if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) && + this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log the message in verbose mode + this->log_proto_message_(entity, creator, message_type); +#endif + return true; + } + + // If immediate send failed, fall through to batching + } + + // Fall back to scheduled batching + return this->schedule_message_(entity, std::move(creator), message_type, estimated_size); + } + // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size); From 3a5b3ad77d35f9b1b1cceade282038100fb00603 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 17:55:06 -0600 Subject: [PATCH 412/461] [thermostat] Replace std::map with FixedVector, reduce flash usage (#11875) --- esphome/components/thermostat/climate.py | 38 ++++++++- .../thermostat/thermostat_climate.cpp | 82 +++++++++++------- .../thermostat/thermostat_climate.h | 33 +++++-- .../climate_custom_fan_modes_and_presets.yaml | 1 + .../integration/test_climate_custom_modes.py | 85 ++++++++++++++++++- 5 files changed, 195 insertions(+), 44 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index a928d208f3a..a3c155aac06 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -945,6 +945,10 @@ async def to_code(config): cg.add(var.set_humidity_hysteresis(config[CONF_HUMIDITY_HYSTERESIS])) if CONF_PRESET in config: + # Separate standard and custom presets, and build preset config variables + standard_presets: list[tuple[cg.MockObj, cg.MockObj]] = [] + custom_presets: list[tuple[str, cg.MockObj]] = [] + for preset_config in config[CONF_PRESET]: name = preset_config[CONF_NAME] standard_preset = None @@ -987,9 +991,39 @@ async def to_code(config): ) if standard_preset is not None: - cg.add(var.set_preset_config(standard_preset, preset_target_variable)) + standard_presets.append((standard_preset, preset_target_variable)) else: - cg.add(var.set_custom_preset_config(name, preset_target_variable)) + custom_presets.append((name, preset_target_variable)) + + # Build initializer list for standard presets + if standard_presets: + cg.add( + var.set_preset_config( + [ + cg.StructInitializer( + thermostat_ns.struct("ThermostatPresetEntry"), + ("preset", preset), + ("config", preset_var), + ) + for preset, preset_var in standard_presets + ] + ) + ) + + # Build initializer list for custom presets + if custom_presets: + cg.add( + var.set_custom_preset_config( + [ + cg.StructInitializer( + thermostat_ns.struct("ThermostatCustomPresetEntry"), + ("name", cg.RawExpression(f'"{name}"')), + ("config", preset_var), + ) + for name, preset_var in custom_presets + ] + ) + ) if CONF_DEFAULT_PRESET in config: default_preset_name = config[CONF_DEFAULT_PRESET] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d533ef93eca..2b51f58f4ff 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -53,8 +53,8 @@ void ThermostatClimate::setup() { if (use_default_preset) { if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) { this->change_preset_(this->default_preset_); - } else if (!this->default_custom_preset_.empty()) { - this->change_custom_preset_(this->default_custom_preset_.c_str()); + } else if (this->default_custom_preset_ != nullptr) { + this->change_custom_preset_(this->default_custom_preset_); } } @@ -319,16 +319,16 @@ climate::ClimateTraits ThermostatClimate::traits() { if (this->supports_swing_mode_vertical_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); - for (auto &it : this->preset_config_) { - traits.add_supported_preset(it.first); + for (const auto &entry : this->preset_config_) { + traits.add_supported_preset(entry.preset); } - // Extract custom preset names from the custom_preset_config_ map + // Extract custom preset names from the custom_preset_config_ vector if (!this->custom_preset_config_.empty()) { std::vector custom_preset_names; custom_preset_names.reserve(this->custom_preset_config_.size()); - for (const auto &it : this->custom_preset_config_) { - custom_preset_names.push_back(it.first.c_str()); + for (const auto &entry : this->custom_preset_config_) { + custom_preset_names.push_back(entry.name); } traits.set_supported_custom_presets(custom_preset_names); } @@ -1154,12 +1154,18 @@ void ThermostatClimate::dump_preset_config_(const char *preset_name, const Therm } void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { - auto config = this->preset_config_.find(preset); + // Linear search through preset configurations + const ThermostatClimateTargetTempConfig *config = nullptr; + for (const auto &entry : this->preset_config_) { + if (entry.preset == preset) { + config = &entry.config; + break; + } + } - if (config != this->preset_config_.end()) { + if (config != nullptr) { ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); - if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) || - this->preset.value() != preset) { + if (this->change_preset_internal_(*config) || (!this->preset.has_value()) || this->preset.value() != preset) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; this->set_preset_(preset); @@ -1178,11 +1184,18 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } void ThermostatClimate::change_custom_preset_(const char *custom_preset) { - auto config = this->custom_preset_config_.find(custom_preset); + // Linear search through custom preset configurations + const ThermostatClimateTargetTempConfig *config = nullptr; + for (const auto &entry : this->custom_preset_config_) { + if (strcmp(entry.name, custom_preset) == 0) { + config = &entry.config; + break; + } + } - if (config != this->custom_preset_config_.end()) { + if (config != nullptr) { ESP_LOGV(TAG, "Custom preset %s requested", custom_preset); - if (this->change_preset_internal_(config->second) || !this->has_custom_preset() || + if (this->change_preset_internal_(*config) || !this->has_custom_preset() || strcmp(this->get_custom_preset(), custom_preset) != 0) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; @@ -1247,14 +1260,12 @@ bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTem return something_changed; } -void ThermostatClimate::set_preset_config(climate::ClimatePreset preset, - const ThermostatClimateTargetTempConfig &config) { - this->preset_config_[preset] = config; +void ThermostatClimate::set_preset_config(std::initializer_list presets) { + this->preset_config_ = presets; } -void ThermostatClimate::set_custom_preset_config(const std::string &name, - const ThermostatClimateTargetTempConfig &config) { - this->custom_preset_config_[name] = config; +void ThermostatClimate::set_custom_preset_config(std::initializer_list presets) { + this->custom_preset_config_ = presets; } ThermostatClimate::ThermostatClimate() @@ -1293,8 +1304,16 @@ ThermostatClimate::ThermostatClimate() humidity_control_humidify_action_trigger_(new Trigger<>()), humidity_control_off_action_trigger_(new Trigger<>()) {} -void ThermostatClimate::set_default_preset(const std::string &custom_preset) { - this->default_custom_preset_ = custom_preset; +void ThermostatClimate::set_default_preset(const char *custom_preset) { + // Find the preset in custom_preset_config_ and store pointer from there + for (const auto &entry : this->custom_preset_config_) { + if (strcmp(entry.name, custom_preset) == 0) { + this->default_custom_preset_ = entry.name; + return; + } + } + // If not found, it will be caught during validation + this->default_custom_preset_ = nullptr; } void ThermostatClimate::set_default_preset(climate::ClimatePreset preset) { this->default_preset_ = preset; } @@ -1605,19 +1624,22 @@ void ThermostatClimate::dump_config() { if (!this->preset_config_.empty()) { ESP_LOGCONFIG(TAG, " Supported PRESETS:"); - for (auto &it : this->preset_config_) { - const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); - ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_preset_ ? " (default)" : ""); - this->dump_preset_config_(preset_name, it.second); + for (const auto &entry : this->preset_config_) { + const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(entry.preset)); + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, entry.preset == this->default_preset_ ? " (default)" : ""); + this->dump_preset_config_(preset_name, entry.config); } } if (!this->custom_preset_config_.empty()) { ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS:"); - for (auto &it : this->custom_preset_config_) { - const auto *preset_name = it.first.c_str(); - ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_custom_preset_ ? " (default)" : ""); - this->dump_preset_config_(preset_name, it.second); + for (const auto &entry : this->custom_preset_config_) { + const auto *preset_name = entry.name; + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, + (this->default_custom_preset_ != nullptr && strcmp(entry.name, this->default_custom_preset_) == 0) + ? " (default)" + : ""); + this->dump_preset_config_(preset_name, entry.config); } } } diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index c9795d9666c..76391f800cb 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -3,12 +3,12 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/climate/climate.h" #include "esphome/components/sensor/sensor.h" #include #include -#include namespace esphome { namespace thermostat { @@ -72,14 +72,29 @@ struct ThermostatClimateTargetTempConfig { optional mode_{}; }; +/// Entry for standard preset lookup +struct ThermostatPresetEntry { + climate::ClimatePreset preset; + ThermostatClimateTargetTempConfig config; +}; + +/// Entry for custom preset lookup +struct ThermostatCustomPresetEntry { + const char *name; + ThermostatClimateTargetTempConfig config; +}; + class ThermostatClimate : public climate::Climate, public Component { public: + using PresetEntry = ThermostatPresetEntry; + using CustomPresetEntry = ThermostatCustomPresetEntry; + ThermostatClimate(); void setup() override; void dump_config() override; void loop() override; - void set_default_preset(const std::string &custom_preset); + void set_default_preset(const char *custom_preset); void set_default_preset(climate::ClimatePreset preset); void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from); void set_set_point_minimum_differential(float differential); @@ -131,8 +146,8 @@ class ThermostatClimate : public climate::Climate, public Component { void set_supports_humidification(bool supports_humidification); void set_supports_two_points(bool supports_two_points); - void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config); - void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config); + void set_preset_config(std::initializer_list presets); + void set_custom_preset_config(std::initializer_list presets); Trigger<> *get_cool_action_trigger() const; Trigger<> *get_supplemental_cool_action_trigger() const; @@ -516,9 +531,6 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_swing_mode_trigger_{nullptr}; Trigger<> *prev_humidity_control_trigger_{nullptr}; - /// Default custom preset to use on start up - std::string default_custom_preset_{}; - /// Climate action timers std::array timer_{ ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)), @@ -534,9 +546,12 @@ class ThermostatClimate : public climate::Climate, public Component { }; /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) - std::map preset_config_{}; + FixedVector preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") - std::map custom_preset_config_{}; + FixedVector custom_preset_config_{}; + /// Default custom preset to use on start up (pointer to entry in custom_preset_config_) + private: + const char *default_custom_preset_{nullptr}; }; } // namespace thermostat diff --git a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml index bf4ef9eafd5..3996d0f169a 100644 --- a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml +++ b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml @@ -14,6 +14,7 @@ climate: id: test_thermostat name: Test Thermostat Custom Modes sensor: thermostat_sensor + default_preset: "Eco Plus" preset: - name: Away default_target_temperature_low: 16°C diff --git a/tests/integration/test_climate_custom_modes.py b/tests/integration/test_climate_custom_modes.py index ce34959d88f..67a7b0581a4 100644 --- a/tests/integration/test_climate_custom_modes.py +++ b/tests/integration/test_climate_custom_modes.py @@ -2,9 +2,13 @@ from __future__ import annotations -from aioesphomeapi import ClimateInfo, ClimatePreset +import asyncio + +import aioesphomeapi +from aioesphomeapi import ClimateInfo, ClimatePreset, EntityState import pytest +from .state_utils import InitialStateHelper from .types import APIClientConnectedFactory, RunCompiledFunction @@ -14,15 +18,50 @@ async def test_climate_custom_fan_modes_and_presets( run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, ) -> None: - """Test that custom presets are properly exposed via API.""" + """Test that custom presets are properly exposed and can be changed.""" + loop = asyncio.get_running_loop() async with run_compiled(yaml_config), api_client_connected() as client: - # Get entities and services + states: dict[int, EntityState] = {} + super_saver_future: asyncio.Future[EntityState] = loop.create_future() + vacation_future: asyncio.Future[EntityState] = loop.create_future() + + def on_state(state: EntityState) -> None: + states[state.key] = state + if isinstance(state, aioesphomeapi.ClimateState): + # Wait for Super Saver preset + if ( + state.custom_preset == "Super Saver" + and state.target_temperature_low == 20.0 + and state.target_temperature_high == 24.0 + and not super_saver_future.done() + ): + super_saver_future.set_result(state) + # Wait for Vacation Mode preset + elif ( + state.custom_preset == "Vacation Mode" + and state.target_temperature_low == 15.0 + and state.target_temperature_high == 18.0 + and not vacation_future.done() + ): + vacation_future.set_result(state) + + # Get entities and set up state synchronization entities, services = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] assert len(climate_infos) == 1, "Expected exactly 1 climate entity" test_climate = climate_infos[0] + # Subscribe with the wrapper that filters initial states + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states to be broadcast + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + # Verify enum presets are exposed (from preset: config map) assert ClimatePreset.AWAY in test_climate.supported_presets, ( "Expected AWAY in enum presets" @@ -40,3 +79,43 @@ async def test_climate_custom_fan_modes_and_presets( assert "Vacation Mode" in custom_presets, ( "Expected 'Vacation Mode' in custom presets" ) + + # Get initial state and verify default preset + initial_state = initial_state_helper.initial_states.get(test_climate.key) + assert initial_state is not None, "Climate initial state not found" + assert isinstance(initial_state, aioesphomeapi.ClimateState) + assert initial_state.custom_preset == "Eco Plus", ( + f"Expected default preset 'Eco Plus', got '{initial_state.custom_preset}'" + ) + assert initial_state.target_temperature_low == 18.0, ( + f"Expected low temp 18.0, got {initial_state.target_temperature_low}" + ) + assert initial_state.target_temperature_high == 22.0, ( + f"Expected high temp 22.0, got {initial_state.target_temperature_high}" + ) + + # Test changing to "Super Saver" custom preset + client.climate_command(test_climate.key, custom_preset="Super Saver") + + try: + super_saver_state = await asyncio.wait_for(super_saver_future, timeout=5.0) + except TimeoutError: + pytest.fail("Super Saver preset change not received within 5 seconds") + + assert isinstance(super_saver_state, aioesphomeapi.ClimateState) + assert super_saver_state.custom_preset == "Super Saver" + assert super_saver_state.target_temperature_low == 20.0 + assert super_saver_state.target_temperature_high == 24.0 + + # Test changing to "Vacation Mode" custom preset + client.climate_command(test_climate.key, custom_preset="Vacation Mode") + + try: + vacation_state = await asyncio.wait_for(vacation_future, timeout=5.0) + except TimeoutError: + pytest.fail("Vacation Mode preset change not received within 5 seconds") + + assert isinstance(vacation_state, aioesphomeapi.ClimateState) + assert vacation_state.custom_preset == "Vacation Mode" + assert vacation_state.target_temperature_low == 15.0 + assert vacation_state.target_temperature_high == 18.0 From 769137fc09873de801f2a3e90ce38dc35573c2be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 18:40:26 -0600 Subject: [PATCH 413/461] [mqtt] Fix crash with empty broker during upload/logs (#11866) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/mqtt.py | 13 ++++- tests/unit_tests/test_main.py | 50 +++++++++++++++++++ tests/unit_tests/test_mqtt.py | 91 +++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 tests/unit_tests/test_mqtt.py diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 093ee64df4d..0d50edbc2c7 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -30,6 +30,7 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import get_int_env, get_str_env from esphome.log import AnsiFore, color +from esphome.types import ConfigType from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -154,8 +155,12 @@ def show_discover(config, username=None, password=None, client_id=None): def get_esphome_device_ip( - config, username=None, password=None, client_id=None, timeout=25 -): + config: ConfigType, + username: str | None = None, + password: str | None = None, + client_id: str | None = None, + timeout: int | float = 25, +) -> list[str]: if CONF_MQTT not in config: raise EsphomeError( "Cannot discover IP via MQTT as the config does not include the mqtt: " @@ -166,6 +171,10 @@ def get_esphome_device_ip( "Cannot discover IP via MQTT as the config does not include the device name: " "component" ) + if not config[CONF_MQTT].get(CONF_BROKER): + raise EsphomeError( + "Cannot discover IP via MQTT as the broker is not configured" + ) dev_name = config[CONF_ESPHOME][CONF_NAME] dev_ip = None diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 9e5f3993815..ccbc5a1306f 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -1166,6 +1166,56 @@ def test_upload_program_ota_with_mqtt_resolution( ) +def test_upload_program_ota_with_mqtt_empty_broker( + mock_mqtt_get_ip: Mock, + mock_is_ip_address: Mock, + mock_run_ota: Mock, + tmp_path: Path, + caplog: CaptureFixture, +) -> None: + """Test upload_program with OTA when MQTT broker is empty (issue #11653).""" + setup_core(address="192.168.1.50", platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_is_ip_address.return_value = True + mock_mqtt_get_ip.side_effect = EsphomeError( + "Cannot discover IP via MQTT as the broker is not configured" + ) + mock_run_ota.return_value = (0, "192.168.1.50") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ], + CONF_MQTT: { + CONF_BROKER: "", + }, + CONF_MDNS: { + CONF_DISABLED: True, + }, + } + args = MockArgs(username="user", password="pass", client_id="client") + devices = ["MQTTIP", "192.168.1.50"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.50" + # Verify MQTT was attempted but failed gracefully + mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client") + # Verify we fell back to the IP address + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.1.50"], 3232, None, expected_firmware + ) + # Verify warning was logged + assert "MQTT IP discovery failed" in caplog.text + + @patch("esphome.__main__.importlib.import_module") def test_upload_program_platform_specific_handler( mock_import: Mock, diff --git a/tests/unit_tests/test_mqtt.py b/tests/unit_tests/test_mqtt.py new file mode 100644 index 00000000000..4c2c34dff12 --- /dev/null +++ b/tests/unit_tests/test_mqtt.py @@ -0,0 +1,91 @@ +"""Unit tests for esphome.mqtt module.""" + +from __future__ import annotations + +import pytest + +from esphome.const import CONF_BROKER, CONF_ESPHOME, CONF_MQTT, CONF_NAME +from esphome.core import EsphomeError +from esphome.mqtt import get_esphome_device_ip + + +def test_get_esphome_device_ip_empty_broker() -> None: + """Test that get_esphome_device_ip raises EsphomeError when broker is empty.""" + config = { + CONF_MQTT: { + CONF_BROKER: "", + }, + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the broker is not configured", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_none_broker() -> None: + """Test that get_esphome_device_ip raises EsphomeError when broker is None.""" + config = { + CONF_MQTT: { + CONF_BROKER: None, + }, + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the broker is not configured", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_mqtt() -> None: + """Test that get_esphome_device_ip raises EsphomeError when mqtt config is missing.""" + config = { + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the mqtt:", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_esphome() -> None: + """Test that get_esphome_device_ip raises EsphomeError when esphome config is missing.""" + config = { + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the device name:", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_name() -> None: + """Test that get_esphome_device_ip raises EsphomeError when device name is missing.""" + config = { + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + CONF_ESPHOME: {}, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the device name:", + ): + get_esphome_device_ip(config) From 735bf9930aa12335bfef6d939439521150b82109 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 18:41:28 -0600 Subject: [PATCH 414/461] [light] Fix dangling reference in compute_color_mode causing memory corruption (#11868) --- esphome/components/api/api_connection.cpp | 3 ++- esphome/components/light/light_call.cpp | 2 +- esphome/components/light/light_traits.h | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b4230576de8..4acd2fc15c5 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -476,8 +476,9 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c auto *light = static_cast(entity); ListEntitiesLightResponse msg; auto traits = light->get_traits(); + auto supported_modes = traits.get_supported_color_modes(); // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values - msg.supported_color_modes = &traits.get_supported_color_modes(); + msg.supported_color_modes = &supported_modes; if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { msg.min_mireds = traits.get_min_mireds(); diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index df17f53adce..8365ac77cd9 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -406,7 +406,7 @@ void LightCall::transform_parameters_() { } } ColorMode LightCall::compute_color_mode_() { - const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes(); + auto supported_modes = this->parent_->get_traits().get_supported_color_modes(); int supported_count = supported_modes.size(); // Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown. diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 294b0cad1de..c3bb27a9647 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -18,7 +18,8 @@ class LightTraits { public: LightTraits() = default; - const ColorModeMask &get_supported_color_modes() const { return this->supported_color_modes_; } + // Return by value to avoid dangling reference when get_traits() returns a temporary + ColorModeMask get_supported_color_modes() const { return this->supported_color_modes_; } void set_supported_color_modes(ColorModeMask supported_color_modes) { this->supported_color_modes_ = supported_color_modes; } From 47fe84e92208f3afab6fb871ec9842423caf3584 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 18:43:51 -0600 Subject: [PATCH 415/461] [wifi][ethernet] Fix spurious warnings and unclear status after PR #9823 (#11871) --- esphome/components/ethernet/ethernet_component.cpp | 5 ++++- esphome/components/wifi/wifi_component.cpp | 13 ++++++++++++- esphome/components/wifi/wifi_component.h | 3 +++ esphome/components/wifi/wifi_component_esp8266.cpp | 2 +- esphome/components/wifi/wifi_component_esp_idf.cpp | 11 +++++++---- .../components/wifi/wifi_component_libretiny.cpp | 2 +- esphome/components/wifi/wifi_component_pico_w.cpp | 2 +- 7 files changed, 29 insertions(+), 9 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 5888ddce603..cad963b2999 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -381,7 +381,10 @@ void EthernetComponent::dump_config() { break; } - ESP_LOGCONFIG(TAG, "Ethernet:"); + ESP_LOGCONFIG(TAG, + "Ethernet:\n" + " Connected: %s", + YESNO(this->is_connected())); this->dump_connect_params_(); #ifdef USE_ETHERNET_SPI ESP_LOGCONFIG(TAG, diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e33cd7cf2d7..7d239349ff4 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -743,6 +743,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { } const LogString *get_signal_bars(int8_t rssi) { + // Check for disconnected sentinel value first + if (rssi == WIFI_RSSI_DISCONNECTED) { + // MULTIPLICATION SIGN + // Unicode: U+00D7, UTF-8: C3 97 + return LOG_STR("\033[0;31m" // red + "\xc3\x97\xc3\x97\xc3\x97\xc3\x97" + "\033[0m"); + } // LOWER ONE QUARTER BLOCK // Unicode: U+2582, UTF-8: E2 96 82 // LOWER HALF BLOCK @@ -1024,7 +1032,10 @@ void WiFiComponent::check_scanning_finished() { } void WiFiComponent::dump_config() { - ESP_LOGCONFIG(TAG, "WiFi:"); + ESP_LOGCONFIG(TAG, + "WiFi:\n" + " Connected: %s", + YESNO(this->is_connected())); this->print_connect_params_(); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d37367b88c2..2fd7fa6cd43 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -52,6 +52,9 @@ extern "C" { namespace esphome { namespace wifi { +/// Sentinel value for RSSI when WiFi is not connected +static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; + struct SavedWifiSettings { char ssid[33]; char password[65]; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 78f336ab154..a543628e278 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -872,7 +872,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index df295655541..4aac03885ae 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1031,7 +1031,8 @@ bssid_t WiFiComponent::wifi_bssid() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); return bssid; } std::copy(info.bssid, info.bssid + 6, bssid.begin()); @@ -1041,7 +1042,8 @@ std::string WiFiComponent::wifi_ssid() { wifi_ap_record_t info{}; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); return ""; } auto *ssid_s = reinterpret_cast(info.ssid); @@ -1052,8 +1054,9 @@ int8_t WiFiComponent::wifi_rssi() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); - return 0; + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + return WIFI_RSSI_DISCONNECTED; } return info.rssi; } diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 7f0c35c8c89..98cbfddb1da 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -486,7 +486,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 54f03f803d5..91766e8ab55 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -200,7 +200,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { From 2d6618da3c2f1410a8a66f19ab15f8529412e9d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 18:44:22 -0600 Subject: [PATCH 416/461] [wifi] Fix slow reconnection after connection loss for all network types (#11873) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/wifi/wifi_component.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7d239349ff4..51a5a47323e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -465,6 +465,8 @@ void WiFiComponent::loop() { if (!this->is_connected()) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; + // Clear error flag before reconnecting so first attempt is not seen as immediate failure + this->error_from_callback_ = false; this->retry_connect(); } else { this->status_clear_warning(); @@ -1060,6 +1062,10 @@ void WiFiComponent::check_connecting_finished() { // Reset to initial phase on successful connection (don't log transition, just reset state) this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT; this->num_retried_ = 0; + // Ensure next connection attempt does not inherit error state + // so when WiFi disconnects later we start fresh and don't see + // the first connection as a failure. + this->error_from_callback_ = false; this->print_connect_params_(); @@ -1146,6 +1152,11 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP } #endif + // Check if we should try explicit hidden networks before scanning + // This handles reconnection after connection loss where first network is hidden + if (!this->sta_.empty() && this->sta_[0].get_hidden()) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } // No more APs to try, fall back to scan return WiFiRetryPhase::SCAN_CONNECTING; From d869108416ed030caa93729b419256ac45e4667b Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 13 Nov 2025 03:06:20 +0100 Subject: [PATCH 417/461] [nrf52] add settings for dcdc converter (#11841) --- esphome/components/nrf52/__init__.py | 3 +++ tests/components/nrf52/test.nrf52-adafruit.yaml | 1 + 2 files changed, 4 insertions(+) diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index a3b79bf1392..03927e8ea27 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -103,6 +103,7 @@ nrf52_ns = cg.esphome_ns.namespace("nrf52") DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component) CONF_DFU = "dfu" +CONF_DCDC = "dcdc" CONF_REG0 = "reg0" CONF_UICR_ERASE = "uicr_erase" @@ -121,6 +122,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, } ), + cv.Optional(CONF_DCDC, default=True): cv.boolean, cv.Optional(CONF_REG0): cv.Schema( { cv.Required(CONF_VOLTAGE): cv.All( @@ -196,6 +198,7 @@ async def to_code(config: ConfigType) -> None: if dfu_config := config.get(CONF_DFU): CORE.add_job(_dfu_to_code, dfu_config) + zephyr_add_prj_conf("BOARD_ENABLE_DCDC", config[CONF_DCDC]) if reg0_config := config.get(CONF_REG0): value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE]) diff --git a/tests/components/nrf52/test.nrf52-adafruit.yaml b/tests/components/nrf52/test.nrf52-adafruit.yaml index 72fd015953e..5fa0d6e88fb 100644 --- a/tests/components/nrf52/test.nrf52-adafruit.yaml +++ b/tests/components/nrf52/test.nrf52-adafruit.yaml @@ -15,6 +15,7 @@ nrf52: inverted: true mode: output: true + dcdc: False reg0: voltage: 2.1V uicr_erase: true From a1ab19d127f3aeb28a22fbc401beff00c2799529 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 21:44:19 -0600 Subject: [PATCH 418/461] [ci] Reduce release time by removing 21 redundant ESP32-S3 IDF tests (#11850) --- .../binary_sensor/test.esp32-s3-idf.yaml | 2 - .../bme68x_bsec2_i2c/test.esp32-s3-idf.yaml | 4 -- tests/components/debug/test.esp32-s3-idf.yaml | 1 - .../matrix_keypad/test.esp32-s3-idf.yaml | 15 ------ .../components/mcp3221/test.esp32-s3-idf.yaml | 4 -- .../mlx90393/test.esp32-s3-idf.yaml | 4 -- tests/components/npi19/test.esp32-s3-idf.yaml | 4 -- tests/components/ntc/test.esp32-s3-idf.yaml | 4 -- .../resistance/test.esp32-s3-idf.yaml | 4 -- .../components/switch/test.esp32-s3-idf.yaml | 2 - .../components/tem3200/test.esp32-s3-idf.yaml | 8 ---- .../template/test.esp32-s3-idf.yaml | 2 - ...max_with_usb_serial_jtag.esp32-s3-idf.yaml | 48 ------------------- .../wk2132_i2c/test.esp32-s3-idf.yaml | 9 ---- .../wk2132_spi/test.esp32-s3-idf.yaml | 11 ----- .../wk2168_i2c/test.esp32-s3-idf.yaml | 9 ---- .../wk2168_spi/test.esp32-s3-idf.yaml | 11 ----- .../wk2204_i2c/test.esp32-s3-idf.yaml | 9 ---- .../wk2204_spi/test.esp32-s3-idf.yaml | 11 ----- .../wk2212_i2c/test.esp32-s3-idf.yaml | 9 ---- .../wk2212_spi/test.esp32-s3-idf.yaml | 11 ----- 21 files changed, 182 deletions(-) delete mode 100644 tests/components/binary_sensor/test.esp32-s3-idf.yaml delete mode 100644 tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml delete mode 100644 tests/components/debug/test.esp32-s3-idf.yaml delete mode 100644 tests/components/matrix_keypad/test.esp32-s3-idf.yaml delete mode 100644 tests/components/mcp3221/test.esp32-s3-idf.yaml delete mode 100644 tests/components/mlx90393/test.esp32-s3-idf.yaml delete mode 100644 tests/components/npi19/test.esp32-s3-idf.yaml delete mode 100644 tests/components/ntc/test.esp32-s3-idf.yaml delete mode 100644 tests/components/resistance/test.esp32-s3-idf.yaml delete mode 100644 tests/components/switch/test.esp32-s3-idf.yaml delete mode 100644 tests/components/tem3200/test.esp32-s3-idf.yaml delete mode 100644 tests/components/template/test.esp32-s3-idf.yaml delete mode 100644 tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2132_i2c/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2132_spi/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2168_i2c/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2168_spi/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2204_i2c/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2204_spi/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2212_i2c/test.esp32-s3-idf.yaml delete mode 100644 tests/components/wk2212_spi/test.esp32-s3-idf.yaml diff --git a/tests/components/binary_sensor/test.esp32-s3-idf.yaml b/tests/components/binary_sensor/test.esp32-s3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/binary_sensor/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 0fd8684a2c0..00000000000 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s3-idf.yaml b/tests/components/debug/test.esp32-s3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/debug/test.esp32-s3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/matrix_keypad/test.esp32-s3-idf.yaml b/tests/components/matrix_keypad/test.esp32-s3-idf.yaml deleted file mode 100644 index a491f2ed59c..00000000000 --- a/tests/components/matrix_keypad/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,15 +0,0 @@ -packages: - common: !include common.yaml - -matrix_keypad: - id: keypad - rows: - - pin: 10 - - pin: 11 - columns: - - pin: 12 - - pin: 13 - keys: "1234" - has_pulldowns: true - on_key: - - lambda: ESP_LOGI("KEY", "key %d pressed", x); diff --git a/tests/components/mcp3221/test.esp32-s3-idf.yaml b/tests/components/mcp3221/test.esp32-s3-idf.yaml deleted file mode 100644 index 0fd8684a2c0..00000000000 --- a/tests/components/mcp3221/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mlx90393/test.esp32-s3-idf.yaml b/tests/components/mlx90393/test.esp32-s3-idf.yaml deleted file mode 100644 index 0fd8684a2c0..00000000000 --- a/tests/components/mlx90393/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/npi19/test.esp32-s3-idf.yaml b/tests/components/npi19/test.esp32-s3-idf.yaml deleted file mode 100644 index 0fd8684a2c0..00000000000 --- a/tests/components/npi19/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ntc/test.esp32-s3-idf.yaml b/tests/components/ntc/test.esp32-s3-idf.yaml deleted file mode 100644 index 37fb325f4a5..00000000000 --- a/tests/components/ntc/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/resistance/test.esp32-s3-idf.yaml b/tests/components/resistance/test.esp32-s3-idf.yaml deleted file mode 100644 index 1910f325ae5..00000000000 --- a/tests/components/resistance/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/switch/test.esp32-s3-idf.yaml b/tests/components/switch/test.esp32-s3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/switch/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/tem3200/test.esp32-s3-idf.yaml b/tests/components/tem3200/test.esp32-s3-idf.yaml deleted file mode 100644 index e9d826aa7c7..00000000000 --- a/tests/components/tem3200/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/template/test.esp32-s3-idf.yaml b/tests/components/template/test.esp32-s3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/template/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml b/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml deleted file mode 100644 index 88a806eb926..00000000000 --- a/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml +++ /dev/null @@ -1,48 +0,0 @@ -<<: !include ../logger/common-usb_serial_jtag.yaml - -esphome: - on_boot: - then: - - uart.write: - id: uart_1 - data: 'Hello World' - - uart.write: - id: uart_1 - data: [0x00, 0x20, 0x42] - -uart: - - id: uart_1 - tx_pin: 4 - rx_pin: 5 - flow_control_pin: 6 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - rx_full_threshold: 10 - rx_timeout: 1 - parity: EVEN - stop_bits: 2 - - - id: uart_2 - tx_pin: 7 - rx_pin: 8 - flow_control_pin: 9 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - rx_full_threshold: 10 - rx_timeout: 1 - parity: EVEN - stop_bits: 2 - - - id: uart_3 - tx_pin: 10 - rx_pin: 11 - flow_control_pin: 12 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - rx_full_threshold: 10 - rx_timeout: 1 - parity: EVEN - stop_bits: 2 diff --git a/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index d7b149a6fdc..00000000000 --- a/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2132_spi/test.esp32-s3-idf.yaml b/tests/components/wk2132_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index 9c7d36996e2..00000000000 --- a/tests/components/wk2132_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml - uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 115812be973..00000000000 --- a/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2168_spi/test.esp32-s3-idf.yaml b/tests/components/wk2168_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index 374fe64d166..00000000000 --- a/tests/components/wk2168_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 115812be973..00000000000 --- a/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2204_spi/test.esp32-s3-idf.yaml b/tests/components/wk2204_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index 374fe64d166..00000000000 --- a/tests/components/wk2204_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 115812be973..00000000000 --- a/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2212_spi/test.esp32-s3-idf.yaml b/tests/components/wk2212_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index 374fe64d166..00000000000 --- a/tests/components/wk2212_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml From 4f088c93c9f9135cb948dd6317b8024eeeeb888a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:41:22 -0500 Subject: [PATCH 419/461] [esp32] Update the recommended platform to 55.03.31-2 (#11865) --- esphome/components/esp32/__init__.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6981662d77e..61511cba0ce 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -334,12 +334,14 @@ def _is_framework_url(source: str) -> str: # - https://github.com/espressif/arduino-esp32/releases ARDUINO_FRAMEWORK_VERSION_LOOKUP = { "recommended": cv.Version(3, 3, 2), - "latest": cv.Version(3, 3, 2), - "dev": cv.Version(3, 3, 2), + "latest": cv.Version(3, 3, 4), + "dev": cv.Version(3, 3, 4), } ARDUINO_PLATFORM_VERSION_LOOKUP = { - cv.Version(3, 3, 2): cv.Version(55, 3, 31, "1"), - cv.Version(3, 3, 1): cv.Version(55, 3, 31, "1"), + cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 1): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"), cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"), cv.Version(3, 2, 0): cv.Version(54, 3, 20), @@ -357,8 +359,8 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { "dev": cv.Version(5, 5, 1), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { - cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"), - cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"), + cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), + cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), cv.Version(5, 4, 3): cv.Version(55, 3, 32), cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), @@ -373,9 +375,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 31, "1"), - "latest": cv.Version(55, 3, 31, "1"), - "dev": cv.Version(55, 3, 31, "1"), + "recommended": cv.Version(55, 3, 31, "2"), + "latest": cv.Version(55, 3, 31, "2"), + "dev": cv.Version(55, 3, 31, "2"), } From a859ecaad1cb64d7648c5293771c93109f58c2c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 11:56:19 -0600 Subject: [PATCH 420/461] [core] Fix wait_until hanging when used in on_boot automations (#11869) --- esphome/core/base_automation.h | 7 +- .../fixtures/wait_until_on_boot.yaml | 47 ++++++++++ tests/integration/test_wait_until_on_boot.py | 91 +++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 tests/integration/fixtures/wait_until_on_boot.yaml create mode 100644 tests/integration/test_wait_until_on_boot.py diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 6f392c8959e..a5e6139182b 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -412,7 +412,12 @@ template class WaitUntilAction : public Action, public Co void setup() override { // Start with loop disabled - only enable when there's work to do - this->disable_loop(); + // IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already + // called before our setup() (e.g., from on_boot trigger at same priority level) + // and we must not undo its enable_loop() call + if (this->num_running_ == 0) { + this->disable_loop(); + } } void play_complex(const Ts &...x) override { diff --git a/tests/integration/fixtures/wait_until_on_boot.yaml b/tests/integration/fixtures/wait_until_on_boot.yaml new file mode 100644 index 00000000000..358bef971b7 --- /dev/null +++ b/tests/integration/fixtures/wait_until_on_boot.yaml @@ -0,0 +1,47 @@ +# Test for wait_until in on_boot automation +# Reproduces bug where wait_until in on_boot would hang forever +# because WaitUntilAction::setup() would disable_loop() after +# play_complex() had already enabled it. + +esphome: + name: wait-until-on-boot + on_boot: + then: + - logger.log: "on_boot: Starting wait_until test" + - globals.set: + id: on_boot_started + value: 'true' + - wait_until: + condition: + lambda: return id(test_flag); + timeout: 5s + - logger.log: "on_boot: wait_until completed successfully" + +host: + +logger: + level: DEBUG + +globals: + - id: on_boot_started + type: bool + initial_value: 'false' + - id: test_flag + type: bool + initial_value: 'false' + +api: + actions: + - action: set_test_flag + then: + - globals.set: + id: test_flag + value: 'true' + - action: check_on_boot_started + then: + - lambda: |- + if (id(on_boot_started)) { + ESP_LOGI("test", "on_boot has started"); + } else { + ESP_LOGI("test", "on_boot has NOT started"); + } diff --git a/tests/integration/test_wait_until_on_boot.py b/tests/integration/test_wait_until_on_boot.py new file mode 100644 index 00000000000..b42c530c540 --- /dev/null +++ b/tests/integration/test_wait_until_on_boot.py @@ -0,0 +1,91 @@ +"""Integration test for wait_until in on_boot automation. + +This test validates that wait_until works correctly when triggered from on_boot, +which runs at the same setup priority as WaitUntilAction itself. This was broken +before the fix because WaitUntilAction::setup() would unconditionally disable_loop(), +even if play_complex() had already been called and enabled the loop. + +The bug: on_boot fires during StartupTrigger::setup(), which calls WaitUntilAction::play_complex() +before WaitUntilAction::setup() has run. Then when WaitUntilAction::setup() runs, it calls +disable_loop(), undoing the enable_loop() from play_complex(), causing wait_until to hang forever. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_on_boot( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that wait_until works in on_boot automation with a condition that becomes true later.""" + loop = asyncio.get_running_loop() + + on_boot_started = False + on_boot_completed = False + + on_boot_started_pattern = re.compile(r"on_boot: Starting wait_until test") + on_boot_complete_pattern = re.compile(r"on_boot: wait_until completed successfully") + + on_boot_started_future = loop.create_future() + on_boot_complete_future = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for test progress.""" + nonlocal on_boot_started, on_boot_completed + + if on_boot_started_pattern.search(line): + on_boot_started = True + if not on_boot_started_future.done(): + on_boot_started_future.set_result(True) + + if on_boot_complete_pattern.search(line): + on_boot_completed = True + if not on_boot_complete_future.done(): + on_boot_complete_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Wait for on_boot to start + await asyncio.wait_for(on_boot_started_future, timeout=10.0) + assert on_boot_started, "on_boot did not start" + + # At this point, on_boot is blocked in wait_until waiting for test_flag to become true + # If the bug exists, wait_until's loop is disabled and it will never complete + # even after we set the flag + + # Give a moment for setup to complete + await asyncio.sleep(0.5) + + # Now set the flag that wait_until is waiting for + _, services = await client.list_entities_services() + set_flag_service = next( + (s for s in services if s.name == "set_test_flag"), None + ) + assert set_flag_service is not None, "set_test_flag service not found" + + client.execute_service(set_flag_service, {}) + + # If the fix works, wait_until's loop() will check the condition and proceed + # If the bug exists, wait_until is stuck with disabled loop and will timeout + try: + await asyncio.wait_for(on_boot_complete_future, timeout=2.0) + assert on_boot_completed, ( + "on_boot wait_until did not complete after flag was set" + ) + except TimeoutError: + pytest.fail( + "wait_until in on_boot did not complete within 2s after condition became true. " + "This indicates the bug where WaitUntilAction::setup() disables the loop " + "after play_complex() has already enabled it." + ) From 6df0264d51d7b0be1cecb2870cf076b55e0b9092 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:26:36 -0600 Subject: [PATCH 421/461] [api] Eliminate heap allocations when transmitting Event types (#11773) --- esphome/components/api/api_connection.cpp | 14 +++---- esphome/components/api/api_connection.h | 48 ++++------------------- 2 files changed, 14 insertions(+), 48 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7eb61f08b61..ca9ddaedf4a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1294,11 +1294,11 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #endif #ifdef USE_EVENT -void APIConnection::send_event(event::Event *event, const std::string &event_type) { +void APIConnection::send_event(event::Event *event, const char *event_type) { this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, +uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { EventResponse resp; resp.set_event_type(StringRef(event_type)); @@ -1650,9 +1650,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c // O(n) but optimized for RAM and not performance. for (auto &item : items) { if (item.entity == entity && item.message_type == message_type) { - // Clean up old creator before replacing - item.creator.cleanup(message_type); - // Move assign the new creator + // Replace with new creator item.creator = std::move(creator); return; } @@ -1822,7 +1820,7 @@ void APIConnection::process_batch_() { // Handle remaining items more efficiently if (items_processed < this->deferred_batch_.size()) { - // Remove processed items from the beginning with proper cleanup + // Remove processed items from the beginning this->deferred_batch_.remove_front(items_processed); // Reschedule for remaining items this->schedule_batch_(); @@ -1835,10 +1833,10 @@ void APIConnection::process_batch_() { uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const { #ifdef USE_EVENT - // Special case: EventResponse uses string pointer + // Special case: EventResponse uses const char * pointer if (message_type == EventResponse::MESSAGE_TYPE) { auto *e = static_cast(entity); - return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); + return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single); } #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 284fa11a954..a77c93a2d56 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -177,7 +177,7 @@ class APIConnection final : public APIServerConnection { #endif #ifdef USE_EVENT - void send_event(event::Event *event, const std::string &event_type); + void send_event(event::Event *event, const char *event_type); #endif #ifdef USE_UPDATE @@ -450,7 +450,7 @@ class APIConnection final : public APIServerConnection { bool is_single); #endif #ifdef USE_EVENT - static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, + static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single); static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); #endif @@ -508,10 +508,8 @@ class APIConnection final : public APIServerConnection { // Constructor for function pointer MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; } - // Constructor for string state capture - explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); } - - // No destructor - cleanup must be called explicitly with message_type + // Constructor for const char * (Event types - no allocation needed) + explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; } // Delete copy operations - MessageCreator should only be moved MessageCreator(const MessageCreator &other) = delete; @@ -523,8 +521,6 @@ class APIConnection final : public APIServerConnection { // Move assignment MessageCreator &operator=(MessageCreator &&other) noexcept { if (this != &other) { - // IMPORTANT: Caller must ensure cleanup() was called if this contains a string! - // In our usage, this happens in add_item() deduplication and vector::erase() data_ = other.data_; other.data_.function_ptr = nullptr; } @@ -535,20 +531,10 @@ class APIConnection final : public APIServerConnection { uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const; - // Manual cleanup method - must be called before destruction for string types - void cleanup(uint8_t message_type) { -#ifdef USE_EVENT - if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) { - delete data_.string_ptr; - data_.string_ptr = nullptr; - } -#endif - } - private: union Data { MessageCreatorPtr function_ptr; - std::string *string_ptr; + const char *const_char_ptr; } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before }; @@ -568,42 +554,24 @@ class APIConnection final : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - private: - // Helper to cleanup items from the beginning - void cleanup_items_(size_t count) { - for (size_t i = 0; i < count; i++) { - items[i].creator.cleanup(items[i].message_type); - } - } - - public: DeferredBatch() { // Pre-allocate capacity for typical batch sizes to avoid reallocation items.reserve(8); } - ~DeferredBatch() { - // Ensure cleanup of any remaining items - clear(); - } - // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); // Add item to the front of the batch (for high priority messages like ping) void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); - // Clear all items with proper cleanup + // Clear all items void clear() { - cleanup_items_(items.size()); items.clear(); batch_start_time = 0; } - // Remove processed items from the front with proper cleanup - void remove_front(size_t count) { - cleanup_items_(count); - items.erase(items.begin(), items.begin() + count); - } + // Remove processed items from the front + void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } bool empty() const { return items.empty(); } size_t size() const { return items.size(); } From 799cfe1de4353022304af4e9c403c69119735c0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:26:46 -0600 Subject: [PATCH 422/461] [esp32_ble_tracker] Use initializer_list to eliminate compiler warning and reduce flash usage (#11861) --- esphome/components/esp32_ble_tracker/automation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index 054cbaa7df3..bbf7992fa45 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -10,7 +10,7 @@ namespace esphome::esp32_ble_tracker { class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { public: explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } - void set_addresses(const std::vector &addresses) { this->address_vec_ = addresses; } + void set_addresses(std::initializer_list addresses) { this->address_vec_ = addresses; } bool parse_device(const ESPBTDevice &device) override { uint64_t u64_addr = device.address_uint64(); From 5a2e6697e0ed9a11494eb52a776238b878b69e4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 17:42:50 -0600 Subject: [PATCH 423/461] [api][event] Send events immediately to prevent loss during rapid triggers (#11777) --- esphome/components/api/api_connection.cpp | 4 +- esphome/components/api/api_connection.h | 54 ++++++++++++++++++----- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ca9ddaedf4a..b4230576de8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1295,8 +1295,8 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #ifdef USE_EVENT void APIConnection::send_event(event::Event *event, const char *event_type) { - this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, - EventResponse::ESTIMATED_SIZE); + this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, + EventResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index a77c93a2d56..6cfd1089272 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -650,21 +650,30 @@ class APIConnection final : public APIServerConnection { } #endif + // Helper to check if a message type should bypass batching + // Returns true if: + // 1. It's an UpdateStateResponse (always send immediately to handle cases where + // the main loop is blocked, e.g., during OTA updates) + // 2. It's an EventResponse (events are edge-triggered - every occurrence matters) + // 3. OR: User has opted into immediate sending (should_try_send_immediately = true + // AND batch_delay = 0) + inline bool should_send_immediately_(uint8_t message_type) const { + return ( +#ifdef USE_UPDATE + message_type == UpdateStateResponse::MESSAGE_TYPE || +#endif +#ifdef USE_EVENT + message_type == EventResponse::MESSAGE_TYPE || +#endif + (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)); + } + // Helper method to send a message either immediately or via batching + // Tries immediate send if should_send_immediately_() returns true and buffer has space + // Falls back to batching if immediate send fails or isn't applicable bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, uint8_t estimated_size) { - // Try to send immediately if: - // 1. It's an UpdateStateResponse (always send immediately to handle cases where - // the main loop is blocked, e.g., during OTA updates) - // 2. OR: We should try to send immediately (should_try_send_immediately = true) - // AND Batch delay is 0 (user has opted in to immediate sending) - // 3. AND: Buffer has space available - if (( -#ifdef USE_UPDATE - message_type == UpdateStateResponse::MESSAGE_TYPE || -#endif - (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) && - this->helper_->can_write_without_blocking()) { + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { // Now actually encode and send if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { @@ -682,6 +691,27 @@ class APIConnection final : public APIServerConnection { return this->schedule_message_(entity, creator, message_type, estimated_size); } + // Overload for MessageCreator (used by events which need to capture event_type) + bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { + // Try to send immediately if message type should bypass batching and buffer has space + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { + // Now actually encode and send + if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) && + this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log the message in verbose mode + this->log_proto_message_(entity, creator, message_type); +#endif + return true; + } + + // If immediate send failed, fall through to batching + } + + // Fall back to scheduled batching + return this->schedule_message_(entity, std::move(creator), message_type, estimated_size); + } + // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size); From 72da3d0f1e3e202f7546299316bf242fbc81b439 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 17:55:06 -0600 Subject: [PATCH 424/461] [thermostat] Replace std::map with FixedVector, reduce flash usage (#11875) --- esphome/components/thermostat/climate.py | 38 ++++++++- .../thermostat/thermostat_climate.cpp | 82 +++++++++++------- .../thermostat/thermostat_climate.h | 33 +++++-- .../climate_custom_fan_modes_and_presets.yaml | 1 + .../integration/test_climate_custom_modes.py | 85 ++++++++++++++++++- 5 files changed, 195 insertions(+), 44 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index a928d208f3a..a3c155aac06 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -945,6 +945,10 @@ async def to_code(config): cg.add(var.set_humidity_hysteresis(config[CONF_HUMIDITY_HYSTERESIS])) if CONF_PRESET in config: + # Separate standard and custom presets, and build preset config variables + standard_presets: list[tuple[cg.MockObj, cg.MockObj]] = [] + custom_presets: list[tuple[str, cg.MockObj]] = [] + for preset_config in config[CONF_PRESET]: name = preset_config[CONF_NAME] standard_preset = None @@ -987,9 +991,39 @@ async def to_code(config): ) if standard_preset is not None: - cg.add(var.set_preset_config(standard_preset, preset_target_variable)) + standard_presets.append((standard_preset, preset_target_variable)) else: - cg.add(var.set_custom_preset_config(name, preset_target_variable)) + custom_presets.append((name, preset_target_variable)) + + # Build initializer list for standard presets + if standard_presets: + cg.add( + var.set_preset_config( + [ + cg.StructInitializer( + thermostat_ns.struct("ThermostatPresetEntry"), + ("preset", preset), + ("config", preset_var), + ) + for preset, preset_var in standard_presets + ] + ) + ) + + # Build initializer list for custom presets + if custom_presets: + cg.add( + var.set_custom_preset_config( + [ + cg.StructInitializer( + thermostat_ns.struct("ThermostatCustomPresetEntry"), + ("name", cg.RawExpression(f'"{name}"')), + ("config", preset_var), + ) + for name, preset_var in custom_presets + ] + ) + ) if CONF_DEFAULT_PRESET in config: default_preset_name = config[CONF_DEFAULT_PRESET] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d533ef93eca..2b51f58f4ff 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -53,8 +53,8 @@ void ThermostatClimate::setup() { if (use_default_preset) { if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) { this->change_preset_(this->default_preset_); - } else if (!this->default_custom_preset_.empty()) { - this->change_custom_preset_(this->default_custom_preset_.c_str()); + } else if (this->default_custom_preset_ != nullptr) { + this->change_custom_preset_(this->default_custom_preset_); } } @@ -319,16 +319,16 @@ climate::ClimateTraits ThermostatClimate::traits() { if (this->supports_swing_mode_vertical_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); - for (auto &it : this->preset_config_) { - traits.add_supported_preset(it.first); + for (const auto &entry : this->preset_config_) { + traits.add_supported_preset(entry.preset); } - // Extract custom preset names from the custom_preset_config_ map + // Extract custom preset names from the custom_preset_config_ vector if (!this->custom_preset_config_.empty()) { std::vector custom_preset_names; custom_preset_names.reserve(this->custom_preset_config_.size()); - for (const auto &it : this->custom_preset_config_) { - custom_preset_names.push_back(it.first.c_str()); + for (const auto &entry : this->custom_preset_config_) { + custom_preset_names.push_back(entry.name); } traits.set_supported_custom_presets(custom_preset_names); } @@ -1154,12 +1154,18 @@ void ThermostatClimate::dump_preset_config_(const char *preset_name, const Therm } void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { - auto config = this->preset_config_.find(preset); + // Linear search through preset configurations + const ThermostatClimateTargetTempConfig *config = nullptr; + for (const auto &entry : this->preset_config_) { + if (entry.preset == preset) { + config = &entry.config; + break; + } + } - if (config != this->preset_config_.end()) { + if (config != nullptr) { ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); - if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) || - this->preset.value() != preset) { + if (this->change_preset_internal_(*config) || (!this->preset.has_value()) || this->preset.value() != preset) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; this->set_preset_(preset); @@ -1178,11 +1184,18 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } void ThermostatClimate::change_custom_preset_(const char *custom_preset) { - auto config = this->custom_preset_config_.find(custom_preset); + // Linear search through custom preset configurations + const ThermostatClimateTargetTempConfig *config = nullptr; + for (const auto &entry : this->custom_preset_config_) { + if (strcmp(entry.name, custom_preset) == 0) { + config = &entry.config; + break; + } + } - if (config != this->custom_preset_config_.end()) { + if (config != nullptr) { ESP_LOGV(TAG, "Custom preset %s requested", custom_preset); - if (this->change_preset_internal_(config->second) || !this->has_custom_preset() || + if (this->change_preset_internal_(*config) || !this->has_custom_preset() || strcmp(this->get_custom_preset(), custom_preset) != 0) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; @@ -1247,14 +1260,12 @@ bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTem return something_changed; } -void ThermostatClimate::set_preset_config(climate::ClimatePreset preset, - const ThermostatClimateTargetTempConfig &config) { - this->preset_config_[preset] = config; +void ThermostatClimate::set_preset_config(std::initializer_list presets) { + this->preset_config_ = presets; } -void ThermostatClimate::set_custom_preset_config(const std::string &name, - const ThermostatClimateTargetTempConfig &config) { - this->custom_preset_config_[name] = config; +void ThermostatClimate::set_custom_preset_config(std::initializer_list presets) { + this->custom_preset_config_ = presets; } ThermostatClimate::ThermostatClimate() @@ -1293,8 +1304,16 @@ ThermostatClimate::ThermostatClimate() humidity_control_humidify_action_trigger_(new Trigger<>()), humidity_control_off_action_trigger_(new Trigger<>()) {} -void ThermostatClimate::set_default_preset(const std::string &custom_preset) { - this->default_custom_preset_ = custom_preset; +void ThermostatClimate::set_default_preset(const char *custom_preset) { + // Find the preset in custom_preset_config_ and store pointer from there + for (const auto &entry : this->custom_preset_config_) { + if (strcmp(entry.name, custom_preset) == 0) { + this->default_custom_preset_ = entry.name; + return; + } + } + // If not found, it will be caught during validation + this->default_custom_preset_ = nullptr; } void ThermostatClimate::set_default_preset(climate::ClimatePreset preset) { this->default_preset_ = preset; } @@ -1605,19 +1624,22 @@ void ThermostatClimate::dump_config() { if (!this->preset_config_.empty()) { ESP_LOGCONFIG(TAG, " Supported PRESETS:"); - for (auto &it : this->preset_config_) { - const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); - ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_preset_ ? " (default)" : ""); - this->dump_preset_config_(preset_name, it.second); + for (const auto &entry : this->preset_config_) { + const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(entry.preset)); + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, entry.preset == this->default_preset_ ? " (default)" : ""); + this->dump_preset_config_(preset_name, entry.config); } } if (!this->custom_preset_config_.empty()) { ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS:"); - for (auto &it : this->custom_preset_config_) { - const auto *preset_name = it.first.c_str(); - ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_custom_preset_ ? " (default)" : ""); - this->dump_preset_config_(preset_name, it.second); + for (const auto &entry : this->custom_preset_config_) { + const auto *preset_name = entry.name; + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, + (this->default_custom_preset_ != nullptr && strcmp(entry.name, this->default_custom_preset_) == 0) + ? " (default)" + : ""); + this->dump_preset_config_(preset_name, entry.config); } } } diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index c9795d9666c..76391f800cb 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -3,12 +3,12 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/climate/climate.h" #include "esphome/components/sensor/sensor.h" #include #include -#include namespace esphome { namespace thermostat { @@ -72,14 +72,29 @@ struct ThermostatClimateTargetTempConfig { optional mode_{}; }; +/// Entry for standard preset lookup +struct ThermostatPresetEntry { + climate::ClimatePreset preset; + ThermostatClimateTargetTempConfig config; +}; + +/// Entry for custom preset lookup +struct ThermostatCustomPresetEntry { + const char *name; + ThermostatClimateTargetTempConfig config; +}; + class ThermostatClimate : public climate::Climate, public Component { public: + using PresetEntry = ThermostatPresetEntry; + using CustomPresetEntry = ThermostatCustomPresetEntry; + ThermostatClimate(); void setup() override; void dump_config() override; void loop() override; - void set_default_preset(const std::string &custom_preset); + void set_default_preset(const char *custom_preset); void set_default_preset(climate::ClimatePreset preset); void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from); void set_set_point_minimum_differential(float differential); @@ -131,8 +146,8 @@ class ThermostatClimate : public climate::Climate, public Component { void set_supports_humidification(bool supports_humidification); void set_supports_two_points(bool supports_two_points); - void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config); - void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config); + void set_preset_config(std::initializer_list presets); + void set_custom_preset_config(std::initializer_list presets); Trigger<> *get_cool_action_trigger() const; Trigger<> *get_supplemental_cool_action_trigger() const; @@ -516,9 +531,6 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_swing_mode_trigger_{nullptr}; Trigger<> *prev_humidity_control_trigger_{nullptr}; - /// Default custom preset to use on start up - std::string default_custom_preset_{}; - /// Climate action timers std::array timer_{ ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)), @@ -534,9 +546,12 @@ class ThermostatClimate : public climate::Climate, public Component { }; /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) - std::map preset_config_{}; + FixedVector preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") - std::map custom_preset_config_{}; + FixedVector custom_preset_config_{}; + /// Default custom preset to use on start up (pointer to entry in custom_preset_config_) + private: + const char *default_custom_preset_{nullptr}; }; } // namespace thermostat diff --git a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml index bf4ef9eafd5..3996d0f169a 100644 --- a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml +++ b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml @@ -14,6 +14,7 @@ climate: id: test_thermostat name: Test Thermostat Custom Modes sensor: thermostat_sensor + default_preset: "Eco Plus" preset: - name: Away default_target_temperature_low: 16°C diff --git a/tests/integration/test_climate_custom_modes.py b/tests/integration/test_climate_custom_modes.py index ce34959d88f..67a7b0581a4 100644 --- a/tests/integration/test_climate_custom_modes.py +++ b/tests/integration/test_climate_custom_modes.py @@ -2,9 +2,13 @@ from __future__ import annotations -from aioesphomeapi import ClimateInfo, ClimatePreset +import asyncio + +import aioesphomeapi +from aioesphomeapi import ClimateInfo, ClimatePreset, EntityState import pytest +from .state_utils import InitialStateHelper from .types import APIClientConnectedFactory, RunCompiledFunction @@ -14,15 +18,50 @@ async def test_climate_custom_fan_modes_and_presets( run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, ) -> None: - """Test that custom presets are properly exposed via API.""" + """Test that custom presets are properly exposed and can be changed.""" + loop = asyncio.get_running_loop() async with run_compiled(yaml_config), api_client_connected() as client: - # Get entities and services + states: dict[int, EntityState] = {} + super_saver_future: asyncio.Future[EntityState] = loop.create_future() + vacation_future: asyncio.Future[EntityState] = loop.create_future() + + def on_state(state: EntityState) -> None: + states[state.key] = state + if isinstance(state, aioesphomeapi.ClimateState): + # Wait for Super Saver preset + if ( + state.custom_preset == "Super Saver" + and state.target_temperature_low == 20.0 + and state.target_temperature_high == 24.0 + and not super_saver_future.done() + ): + super_saver_future.set_result(state) + # Wait for Vacation Mode preset + elif ( + state.custom_preset == "Vacation Mode" + and state.target_temperature_low == 15.0 + and state.target_temperature_high == 18.0 + and not vacation_future.done() + ): + vacation_future.set_result(state) + + # Get entities and set up state synchronization entities, services = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] assert len(climate_infos) == 1, "Expected exactly 1 climate entity" test_climate = climate_infos[0] + # Subscribe with the wrapper that filters initial states + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states to be broadcast + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + # Verify enum presets are exposed (from preset: config map) assert ClimatePreset.AWAY in test_climate.supported_presets, ( "Expected AWAY in enum presets" @@ -40,3 +79,43 @@ async def test_climate_custom_fan_modes_and_presets( assert "Vacation Mode" in custom_presets, ( "Expected 'Vacation Mode' in custom presets" ) + + # Get initial state and verify default preset + initial_state = initial_state_helper.initial_states.get(test_climate.key) + assert initial_state is not None, "Climate initial state not found" + assert isinstance(initial_state, aioesphomeapi.ClimateState) + assert initial_state.custom_preset == "Eco Plus", ( + f"Expected default preset 'Eco Plus', got '{initial_state.custom_preset}'" + ) + assert initial_state.target_temperature_low == 18.0, ( + f"Expected low temp 18.0, got {initial_state.target_temperature_low}" + ) + assert initial_state.target_temperature_high == 22.0, ( + f"Expected high temp 22.0, got {initial_state.target_temperature_high}" + ) + + # Test changing to "Super Saver" custom preset + client.climate_command(test_climate.key, custom_preset="Super Saver") + + try: + super_saver_state = await asyncio.wait_for(super_saver_future, timeout=5.0) + except TimeoutError: + pytest.fail("Super Saver preset change not received within 5 seconds") + + assert isinstance(super_saver_state, aioesphomeapi.ClimateState) + assert super_saver_state.custom_preset == "Super Saver" + assert super_saver_state.target_temperature_low == 20.0 + assert super_saver_state.target_temperature_high == 24.0 + + # Test changing to "Vacation Mode" custom preset + client.climate_command(test_climate.key, custom_preset="Vacation Mode") + + try: + vacation_state = await asyncio.wait_for(vacation_future, timeout=5.0) + except TimeoutError: + pytest.fail("Vacation Mode preset change not received within 5 seconds") + + assert isinstance(vacation_state, aioesphomeapi.ClimateState) + assert vacation_state.custom_preset == "Vacation Mode" + assert vacation_state.target_temperature_low == 15.0 + assert vacation_state.target_temperature_high == 18.0 From ff107a0674091d24e51a9f2c3fd04857d857b90a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 18:40:26 -0600 Subject: [PATCH 425/461] [mqtt] Fix crash with empty broker during upload/logs (#11866) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/mqtt.py | 13 ++++- tests/unit_tests/test_main.py | 50 +++++++++++++++++++ tests/unit_tests/test_mqtt.py | 91 +++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 tests/unit_tests/test_mqtt.py diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 093ee64df4d..0d50edbc2c7 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -30,6 +30,7 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import get_int_env, get_str_env from esphome.log import AnsiFore, color +from esphome.types import ConfigType from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -154,8 +155,12 @@ def show_discover(config, username=None, password=None, client_id=None): def get_esphome_device_ip( - config, username=None, password=None, client_id=None, timeout=25 -): + config: ConfigType, + username: str | None = None, + password: str | None = None, + client_id: str | None = None, + timeout: int | float = 25, +) -> list[str]: if CONF_MQTT not in config: raise EsphomeError( "Cannot discover IP via MQTT as the config does not include the mqtt: " @@ -166,6 +171,10 @@ def get_esphome_device_ip( "Cannot discover IP via MQTT as the config does not include the device name: " "component" ) + if not config[CONF_MQTT].get(CONF_BROKER): + raise EsphomeError( + "Cannot discover IP via MQTT as the broker is not configured" + ) dev_name = config[CONF_ESPHOME][CONF_NAME] dev_ip = None diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 9e5f3993815..ccbc5a1306f 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -1166,6 +1166,56 @@ def test_upload_program_ota_with_mqtt_resolution( ) +def test_upload_program_ota_with_mqtt_empty_broker( + mock_mqtt_get_ip: Mock, + mock_is_ip_address: Mock, + mock_run_ota: Mock, + tmp_path: Path, + caplog: CaptureFixture, +) -> None: + """Test upload_program with OTA when MQTT broker is empty (issue #11653).""" + setup_core(address="192.168.1.50", platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_is_ip_address.return_value = True + mock_mqtt_get_ip.side_effect = EsphomeError( + "Cannot discover IP via MQTT as the broker is not configured" + ) + mock_run_ota.return_value = (0, "192.168.1.50") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ], + CONF_MQTT: { + CONF_BROKER: "", + }, + CONF_MDNS: { + CONF_DISABLED: True, + }, + } + args = MockArgs(username="user", password="pass", client_id="client") + devices = ["MQTTIP", "192.168.1.50"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.50" + # Verify MQTT was attempted but failed gracefully + mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client") + # Verify we fell back to the IP address + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.1.50"], 3232, None, expected_firmware + ) + # Verify warning was logged + assert "MQTT IP discovery failed" in caplog.text + + @patch("esphome.__main__.importlib.import_module") def test_upload_program_platform_specific_handler( mock_import: Mock, diff --git a/tests/unit_tests/test_mqtt.py b/tests/unit_tests/test_mqtt.py new file mode 100644 index 00000000000..4c2c34dff12 --- /dev/null +++ b/tests/unit_tests/test_mqtt.py @@ -0,0 +1,91 @@ +"""Unit tests for esphome.mqtt module.""" + +from __future__ import annotations + +import pytest + +from esphome.const import CONF_BROKER, CONF_ESPHOME, CONF_MQTT, CONF_NAME +from esphome.core import EsphomeError +from esphome.mqtt import get_esphome_device_ip + + +def test_get_esphome_device_ip_empty_broker() -> None: + """Test that get_esphome_device_ip raises EsphomeError when broker is empty.""" + config = { + CONF_MQTT: { + CONF_BROKER: "", + }, + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the broker is not configured", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_none_broker() -> None: + """Test that get_esphome_device_ip raises EsphomeError when broker is None.""" + config = { + CONF_MQTT: { + CONF_BROKER: None, + }, + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the broker is not configured", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_mqtt() -> None: + """Test that get_esphome_device_ip raises EsphomeError when mqtt config is missing.""" + config = { + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the mqtt:", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_esphome() -> None: + """Test that get_esphome_device_ip raises EsphomeError when esphome config is missing.""" + config = { + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the device name:", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_name() -> None: + """Test that get_esphome_device_ip raises EsphomeError when device name is missing.""" + config = { + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + CONF_ESPHOME: {}, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the device name:", + ): + get_esphome_device_ip(config) From afed58107967d519c8a62779fbce809cdbe79af0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 18:41:28 -0600 Subject: [PATCH 426/461] [light] Fix dangling reference in compute_color_mode causing memory corruption (#11868) --- esphome/components/api/api_connection.cpp | 3 ++- esphome/components/light/light_call.cpp | 2 +- esphome/components/light/light_traits.h | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b4230576de8..4acd2fc15c5 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -476,8 +476,9 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c auto *light = static_cast(entity); ListEntitiesLightResponse msg; auto traits = light->get_traits(); + auto supported_modes = traits.get_supported_color_modes(); // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values - msg.supported_color_modes = &traits.get_supported_color_modes(); + msg.supported_color_modes = &supported_modes; if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { msg.min_mireds = traits.get_min_mireds(); diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index df17f53adce..8365ac77cd9 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -406,7 +406,7 @@ void LightCall::transform_parameters_() { } } ColorMode LightCall::compute_color_mode_() { - const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes(); + auto supported_modes = this->parent_->get_traits().get_supported_color_modes(); int supported_count = supported_modes.size(); // Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown. diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 294b0cad1de..c3bb27a9647 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -18,7 +18,8 @@ class LightTraits { public: LightTraits() = default; - const ColorModeMask &get_supported_color_modes() const { return this->supported_color_modes_; } + // Return by value to avoid dangling reference when get_traits() returns a temporary + ColorModeMask get_supported_color_modes() const { return this->supported_color_modes_; } void set_supported_color_modes(ColorModeMask supported_color_modes) { this->supported_color_modes_ = supported_color_modes; } From 1d8b08dcce066b876d055a44b7f7ad192571985f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 18:43:51 -0600 Subject: [PATCH 427/461] [wifi][ethernet] Fix spurious warnings and unclear status after PR #9823 (#11871) --- esphome/components/ethernet/ethernet_component.cpp | 5 ++++- esphome/components/wifi/wifi_component.cpp | 13 ++++++++++++- esphome/components/wifi/wifi_component.h | 3 +++ esphome/components/wifi/wifi_component_esp8266.cpp | 2 +- esphome/components/wifi/wifi_component_esp_idf.cpp | 11 +++++++---- .../components/wifi/wifi_component_libretiny.cpp | 2 +- esphome/components/wifi/wifi_component_pico_w.cpp | 2 +- 7 files changed, 29 insertions(+), 9 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 5888ddce603..cad963b2999 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -381,7 +381,10 @@ void EthernetComponent::dump_config() { break; } - ESP_LOGCONFIG(TAG, "Ethernet:"); + ESP_LOGCONFIG(TAG, + "Ethernet:\n" + " Connected: %s", + YESNO(this->is_connected())); this->dump_connect_params_(); #ifdef USE_ETHERNET_SPI ESP_LOGCONFIG(TAG, diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 817419107f2..33aa6c81397 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -743,6 +743,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { } const LogString *get_signal_bars(int8_t rssi) { + // Check for disconnected sentinel value first + if (rssi == WIFI_RSSI_DISCONNECTED) { + // MULTIPLICATION SIGN + // Unicode: U+00D7, UTF-8: C3 97 + return LOG_STR("\033[0;31m" // red + "\xc3\x97\xc3\x97\xc3\x97\xc3\x97" + "\033[0m"); + } // LOWER ONE QUARTER BLOCK // Unicode: U+2582, UTF-8: E2 96 82 // LOWER HALF BLOCK @@ -1022,7 +1030,10 @@ void WiFiComponent::check_scanning_finished() { } void WiFiComponent::dump_config() { - ESP_LOGCONFIG(TAG, "WiFi:"); + ESP_LOGCONFIG(TAG, + "WiFi:\n" + " Connected: %s", + YESNO(this->is_connected())); this->print_connect_params_(); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 713e6f223f0..5023cf34284 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -52,6 +52,9 @@ extern "C" { namespace esphome { namespace wifi { +/// Sentinel value for RSSI when WiFi is not connected +static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; + struct SavedWifiSettings { char ssid[33]; char password[65]; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index bcb5dc4cf7f..bdaae5382ac 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -870,7 +870,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index fd7e85fb6be..8c27fe92db9 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1029,7 +1029,8 @@ bssid_t WiFiComponent::wifi_bssid() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); return bssid; } std::copy(info.bssid, info.bssid + 6, bssid.begin()); @@ -1039,7 +1040,8 @@ std::string WiFiComponent::wifi_ssid() { wifi_ap_record_t info{}; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); return ""; } auto *ssid_s = reinterpret_cast(info.ssid); @@ -1050,8 +1052,9 @@ int8_t WiFiComponent::wifi_rssi() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); - return 0; + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + return WIFI_RSSI_DISCONNECTED; } return info.rssi; } diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2946b9e8310..8c6c28ac753 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -484,7 +484,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 7025ba16bda..073b7528861 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -200,7 +200,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { From 1675408161b8e1631b3b1b53909580cfd7a1fb99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 18:44:22 -0600 Subject: [PATCH 428/461] [wifi] Fix slow reconnection after connection loss for all network types (#11873) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/wifi/wifi_component.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 33aa6c81397..880928c3e37 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -465,6 +465,8 @@ void WiFiComponent::loop() { if (!this->is_connected()) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; + // Clear error flag before reconnecting so first attempt is not seen as immediate failure + this->error_from_callback_ = false; this->retry_connect(); } else { this->status_clear_warning(); @@ -1058,6 +1060,10 @@ void WiFiComponent::check_connecting_finished() { // Reset to initial phase on successful connection (don't log transition, just reset state) this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT; this->num_retried_ = 0; + // Ensure next connection attempt does not inherit error state + // so when WiFi disconnects later we start fresh and don't see + // the first connection as a failure. + this->error_from_callback_ = false; this->print_connect_params_(); @@ -1144,6 +1150,11 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP } #endif + // Check if we should try explicit hidden networks before scanning + // This handles reconnection after connection loss where first network is hidden + if (!this->sta_.empty() && this->sta_[0].get_hidden()) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } // No more APs to try, fall back to scan return WiFiRetryPhase::SCAN_CONNECTING; From 382483b0635353b70cd1975af6eaaa4118f2091e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:56:11 -0500 Subject: [PATCH 429/461] Bump version to 2025.11.0b2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 8025c71c198..8766b8f00c6 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.11.0b1 +PROJECT_NUMBER = 2025.11.0b2 # 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 00975753c24..8c6020a8685 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0b1" +__version__ = "2025.11.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4e23a7a3e109229823f21273af30a042445d51dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 21:11:45 -0600 Subject: [PATCH 430/461] light loop --- esphome/components/light/light_state.cpp | 23 +++++++++++++++++++++++ esphome/components/light/light_state.h | 3 +++ 2 files changed, 26 insertions(+) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 4c253ec5a83..4d39ac9983f 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -24,6 +24,9 @@ void LightState::setup() { effect->init_internal(this); } + // Start with loop disabled - it will be enabled by start_transition_/start_effect_ if needed + this->disable_loop(); + // When supported color temperature range is known, initialize color temperature setting within bounds. auto traits = this->get_traits(); float min_mireds = traits.get_min_mireds(); @@ -126,6 +129,9 @@ void LightState::loop() { this->is_transformer_active_ = false; this->transformer_ = nullptr; this->target_state_reached_callback_.call(); + + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); } } @@ -228,6 +234,8 @@ void LightState::start_effect_(uint32_t effect_index) { this->active_effect_index_ = effect_index; auto *effect = this->get_active_effect_(); effect->start_internal(); + // Enable loop while effect is active + this->enable_loop(); } LightEffect *LightState::get_active_effect_() { if (this->active_effect_index_ == 0) { @@ -242,6 +250,8 @@ void LightState::stop_effect_() { effect->stop(); } this->active_effect_index_ = 0; + // Disable loop if idle (no effect and no transformer) + this->disable_loop_if_idle_(); } void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) { @@ -251,6 +261,8 @@ void LightState::start_transition_(const LightColorValues &target, uint32_t leng if (set_remote_values) { this->remote_values = target; } + // Enable loop while transition is active + this->enable_loop(); } void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) { @@ -266,6 +278,8 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b if (set_remote_values) { this->remote_values = target; }; + // Enable loop while flash is active + this->enable_loop(); } void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { @@ -277,6 +291,15 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot } this->output_->update_state(this); this->next_write_ = true; + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); +} + +void LightState::disable_loop_if_idle_() { + // Only disable loop if both transformer and effect are inactive + if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr) { + this->disable_loop(); + } } void LightState::save_remote_values_() { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index bf63c0ec270..d6c985cfb90 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -256,6 +256,9 @@ class LightState : public EntityBase, public Component { /// Internal method to save the current remote_values to the preferences void save_remote_values_(); + /// Disable loop if neither transformer nor effect is active + void disable_loop_if_idle_(); + /// Store the output to allow effects to have more access. LightOutput *output_; /// The currently active transformer for this light (transition/flash). From 9b458d25ea143636ab3c1c7c04b75adf31b0d2d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 21:20:11 -0600 Subject: [PATCH 431/461] light loop --- esphome/components/light/light_state.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 4d39ac9983f..f3b8347507e 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -139,6 +139,8 @@ void LightState::loop() { if (this->next_write_) { this->next_write_ = false; this->output_->write_state(this); + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); } } @@ -291,13 +293,12 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot } this->output_->update_state(this); this->next_write_ = true; - // Disable loop if idle (no transformer and no effect) - this->disable_loop_if_idle_(); + this->enable_loop(); } void LightState::disable_loop_if_idle_() { - // Only disable loop if both transformer and effect are inactive - if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr) { + // Only disable loop if both transformer and effect are inactive, and no pending writes + if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr && !this->next_write_) { this->disable_loop(); } } From 20649ce8cea1a1f315c4c40b6cc9fcbdc8c85ca8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 21:49:58 -0600 Subject: [PATCH 432/461] safer --- esphome/components/light/light_state.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index f3b8347507e..b9e941c9c95 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -24,8 +24,8 @@ void LightState::setup() { effect->init_internal(this); } - // Start with loop disabled - it will be enabled by start_transition_/start_effect_ if needed - this->disable_loop(); + // Start with loop disabled if idle - respects any effects/transitions set up during initialization + this->disable_loop_if_idle_(); // When supported color temperature range is known, initialize color temperature setting within bounds. auto traits = this->get_traits(); From ed7e5cd325f8d376f01704f36fba2557dbfd69a7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:00:47 +1300 Subject: [PATCH 433/461] Bump version to 2025.12.0-dev --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 8766b8f00c6..a19120b9da3 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.11.0b2 +PROJECT_NUMBER = 2025.12.0-dev # 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 8c6020a8685..a25114d80e8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0b2" +__version__ = "2025.12.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 952bdfaac238181819ef8d8f85267623dd00873b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 14 Nov 2025 00:55:48 +1000 Subject: [PATCH 434/461] [esp32] Make esp-idf default framework for P4 (#11884) --- esphome/components/esp32/__init__.py | 163 ++++++++++++--------------- 1 file changed, 74 insertions(+), 89 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 61511cba0ce..9741dc76a1b 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -381,8 +381,9 @@ PLATFORM_VERSION_LOOKUP = { } -def _check_versions(value): - value = value.copy() +def _check_versions(config): + config = config.copy() + value = config[CONF_FRAMEWORK] if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP: if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value: @@ -447,7 +448,7 @@ def _check_versions(value): "If there are connectivity or build issues please remove the manual version." ) - return value + return config def _parse_platform_version(value): @@ -598,89 +599,72 @@ def _validate_idf_component(config: ConfigType) -> ConfigType: FRAMEWORK_ESP_IDF = "esp-idf" FRAMEWORK_ARDUINO = "arduino" -FRAMEWORK_SCHEMA = cv.All( - cv.Schema( - { - cv.Optional(CONF_TYPE, default=FRAMEWORK_ARDUINO): cv.one_of( - FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO - ), - cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_RELEASE): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, - cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { - cv.string_strict: cv.string_strict - }, - cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( - *LOG_LEVELS_IDF, upper=True - ), - cv.Optional(CONF_ADVANCED, default={}): cv.Schema( - { - cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( - *ASSERTION_LEVELS, upper=True - ), - cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( - *COMPILER_OPTIMIZATIONS, upper=True - ), - cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean, - cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, - cv.Optional( - CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False - ): cv.boolean, - cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean, - # DHCP server is needed for WiFi AP mode. When WiFi component is used, - # it will handle disabling DHCP server when AP is not configured. - # Default to false (disabled) when WiFi is not used. - cv.OnlyWithout( - CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_MDNS_QUERIES, default=True - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True - ): cv.boolean, - cv.Optional( - CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True - ): cv.boolean, - cv.Optional( - CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True - ): cv.boolean, - cv.Optional( - CONF_DISABLE_VFS_SUPPORT_SELECT, default=True - ): cv.boolean, - cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, - cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, - cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( - min=8192, max=32768 - ), - } - ), - cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( - 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, - ) - ), - } - ), - _check_versions, +FRAMEWORK_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TYPE): cv.one_of(FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO), + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_RELEASE): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { + cv.string_strict: cv.string_strict + }, + cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( + *LOG_LEVELS_IDF, upper=True + ), + cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + { + cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( + *ASSERTION_LEVELS, upper=True + ), + cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( + *COMPILER_OPTIMIZATIONS, upper=True + ), + cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean, + cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean, + # DHCP server is needed for WiFi AP mode. When WiFi component is used, + # it will handle disabling DHCP server when AP is not configured. + # Default to false (disabled) when WiFi is not used. + cv.OnlyWithout( + CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False + ): cv.boolean, + cv.Optional(CONF_ENABLE_LWIP_MDNS_QUERIES, default=True): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False + ): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True + ): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True + ): cv.boolean, + cv.Optional(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, + cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, + cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( + min=8192, max=32768 + ), + } + ), + cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( + 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, + ) + ), + } ) @@ -743,11 +727,11 @@ def _show_framework_migration_message(name: str, variant: str) -> None: def _set_default_framework(config): + config = config.copy() if CONF_FRAMEWORK not in config: - config = config.copy() - - variant = config[CONF_VARIANT] config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({}) + if CONF_TYPE not in config[CONF_FRAMEWORK]: + variant = config[CONF_VARIANT] if variant in ARDUINO_ALLOWED_VARIANTS: config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO _show_framework_migration_message( @@ -787,6 +771,7 @@ CONFIG_SCHEMA = cv.All( ), _detect_variant, _set_default_framework, + _check_versions, set_core_data, cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT), ) From 0afcf67c32bcc22d60579a38fd42e3823c7072b4 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 14 Nov 2025 01:52:08 +1000 Subject: [PATCH 435/461] [esp32] Add sdkconfig flag to make OTA work for 32MB flash (#11883) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 81 +++++++++---------- tests/components/esp32/test.esp32-p4-idf.yaml | 27 +++++++ 2 files changed, 67 insertions(+), 41 deletions(-) create mode 100644 tests/components/esp32/test.esp32-p4-idf.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 9741dc76a1b..0f85e585f7e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -498,6 +498,8 @@ def final_validate(config): from esphome.components.psram import DOMAIN as PSRAM_DOMAIN errs = [] + conf_fw = config[CONF_FRAMEWORK] + advanced = conf_fw[CONF_ADVANCED] full_config = fv.full_config.get() if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS): pio_flash_size_key = "board_upload.flash_size" @@ -514,22 +516,14 @@ def final_validate(config): f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" ) ) - if ( - config[CONF_VARIANT] != VARIANT_ESP32 - and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK]) - and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED] - ): + if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_IGNORE_EFUSE_MAC_CRC]: errs.append( cv.Invalid( f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}", path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC], ) ) - if ( - config.get(CONF_FRAMEWORK, {}) - .get(CONF_ADVANCED, {}) - .get(CONF_EXECUTE_FROM_PSRAM) - ): + if advanced[CONF_EXECUTE_FROM_PSRAM]: if config[CONF_VARIANT] != VARIANT_ESP32S3: errs.append( cv.Invalid( @@ -545,6 +539,17 @@ def final_validate(config): ) ) + if ( + config[CONF_FLASH_SIZE] == "32MB" + and "ota" in full_config + and not advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES] + ): + errs.append( + cv.Invalid( + f"OTA with 32MB flash requires '{CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES}' to be set in the '{CONF_ADVANCED}' section of the esp32 configuration", + path=[CONF_FLASH_SIZE], + ) + ) if errs: raise cv.MultipleInvalid(errs) @@ -620,10 +625,12 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( *COMPILER_OPTIMIZATIONS, upper=True ), - cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean, + cv.Optional( + CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, default=False + ): cv.boolean, cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean, - cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, # DHCP server is needed for WiFi AP mode. When WiFi component is used, # it will handle disabling DHCP server when AP is not configured. # Default to false (disabled) when WiFi is not used. @@ -644,7 +651,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, - cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, + cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean, cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( min=8192, max=32768 ), @@ -790,9 +797,7 @@ def _configure_lwip_max_sockets(conf: dict) -> None: from esphome.components.socket import KEY_SOCKET_CONSUMERS # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS - user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get( - "CONFIG_LWIP_MAX_SOCKETS" - ) + user_max_sockets = conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_LWIP_MAX_SOCKETS") socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {}) total_sockets = sum(socket_consumers.values()) @@ -962,23 +967,18 @@ async def to_code(config): # WiFi component handles its own optimization when AP mode is not used # When using Arduino with Ethernet, DHCP server functions must be available # for the Network library to compile, even if not actively used - if ( - CONF_ENABLE_LWIP_DHCP_SERVER in advanced - and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] - and not ( - conf[CONF_TYPE] == FRAMEWORK_ARDUINO - and "ethernet" in CORE.loaded_integrations - ) + if advanced.get(CONF_ENABLE_LWIP_DHCP_SERVER) is False and not ( + conf[CONF_TYPE] == FRAMEWORK_ARDUINO and "ethernet" in CORE.loaded_integrations ): add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) - if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True): + if not advanced[CONF_ENABLE_LWIP_MDNS_QUERIES]: add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) - if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): + if not advanced[CONF_ENABLE_LWIP_BRIDGE_INTERFACE]: add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) _configure_lwip_max_sockets(conf) - if advanced.get(CONF_EXECUTE_FROM_PSRAM, False): + if advanced[CONF_EXECUTE_FROM_PSRAM]: add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) @@ -989,23 +989,22 @@ async def to_code(config): # - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default) # - Up to 200% slower under load when all operations queue through tcpip_thread # Enabling this makes ESP-IDF socket performance match Arduino framework. - if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True): + if advanced[CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING]: add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True) - if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True): + if advanced[CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY]: add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True) # Disable placing libc locks in IRAM to save RAM # This is safe for ESPHome since no IRAM ISRs (interrupts that run while cache is disabled) # use libc lock APIs. Saves approximately 1.3KB (1,356 bytes) of IRAM. - if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True): + if advanced[CONF_DISABLE_LIBC_LOCKS_IN_IRAM]: add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) # Disable VFS support for termios (terminal I/O functions) # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver). # Saves approximately 1.8KB of flash when disabled (default). add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_TERMIOS", - not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True), + "CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS] ) # Disable VFS support for select() with file descriptors @@ -1019,8 +1018,7 @@ async def to_code(config): else: # No component needs it - allow user to control (default: disabled) add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_SELECT", - not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True), + "CONFIG_VFS_SUPPORT_SELECT", not advanced[CONF_DISABLE_VFS_SUPPORT_SELECT] ) # Disable VFS support for directory functions (opendir, readdir, mkdir, etc.) @@ -1033,8 +1031,7 @@ async def to_code(config): else: # No component needs it - allow user to control (default: disabled) add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_DIR", - not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True), + "CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR] ) cg.add_platformio_option("board_build.partitions", "partitions.csv") @@ -1048,7 +1045,7 @@ async def to_code(config): add_idf_sdkconfig_option(flag, assertion_level == key) add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False) - compiler_optimization = advanced.get(CONF_COMPILER_OPTIMIZATION) + compiler_optimization = advanced[CONF_COMPILER_OPTIMIZATION] for key, flag in COMPILER_OPTIMIZATIONS.items(): add_idf_sdkconfig_option(flag, compiler_optimization == key) @@ -1057,18 +1054,20 @@ async def to_code(config): conf[CONF_ADVANCED][CONF_ENABLE_LWIP_ASSERT], ) - if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC): + if advanced[CONF_IGNORE_EFUSE_MAC_CRC]: add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True) add_idf_sdkconfig_option("CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False) - if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): + if advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES]: _LOGGER.warning( "Using experimental features in ESP-IDF may result in unexpected failures." ) add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True) + if config[CONF_FLASH_SIZE] == "32MB": + add_idf_sdkconfig_option( + "CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH", True + ) - cg.add_define( - "ESPHOME_LOOP_TASK_STACK_SIZE", advanced.get(CONF_LOOP_TASK_STACK_SIZE) - ) + cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE]) cg.add_define( "USE_ESP_IDF_VERSION_CODE", diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml new file mode 100644 index 00000000000..a4c930f2363 --- /dev/null +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -0,0 +1,27 @@ +esp32: + variant: esp32p4 + flash_size: 32MB + cpu_frequency: 400MHz + framework: + type: esp-idf + advanced: + enable_idf_experimental_features: yes + +ota: + platform: esphome + +wifi: + ssid: MySSID + password: password1 + +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 From 2290eb0dd2cac9d8b044aa011629dd1c134f2975 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:08:06 +0100 Subject: [PATCH 436/461] [light] Fix missing `ColorMode::BRIGHTNESS` case in logging (#11836) --- esphome/components/light/light_call.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 8365ac77cd9..b15ff84b978 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -52,8 +52,10 @@ static void log_invalid_parameter(const char *name, const LogString *message) { } static const LogString *color_mode_to_human(ColorMode color_mode) { - if (color_mode == ColorMode::UNKNOWN) - return LOG_STR("Unknown"); + if (color_mode == ColorMode::ON_OFF) + return LOG_STR("On/Off"); + if (color_mode == ColorMode::BRIGHTNESS) + return LOG_STR("Brightness"); if (color_mode == ColorMode::WHITE) return LOG_STR("White"); if (color_mode == ColorMode::COLOR_TEMPERATURE) @@ -68,7 +70,7 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { return LOG_STR("RGB + cold/warm white"); if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE) return LOG_STR("RGB + color temperature"); - return LOG_STR(""); + return LOG_STR("Unknown"); } // Helper to log percentage values From 1f408ce41c8fa3a7ba2e45c2ac58c8bf562ffdee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Nov 2025 12:35:43 -0600 Subject: [PATCH 437/461] [template.alarm_control_panel] Use FixedVector for iteration-only sensor storage --- .../template/alarm_control_panel/__init__.py | 6 +- .../template_alarm_control_panel.cpp | 35 +++-- .../template_alarm_control_panel.h | 20 ++- ...late_alarm_control_panel_many_sensors.yaml | 136 ++++++++++++++++++ ...mplate_alarm_control_panel_many_sensors.py | 118 +++++++++++++++ 5 files changed, 296 insertions(+), 19 deletions(-) create mode 100644 tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml create mode 100644 tests/integration/test_template_alarm_control_panel_many_sensors.py diff --git a/esphome/components/template/alarm_control_panel/__init__.py b/esphome/components/template/alarm_control_panel/__init__.py index 5d2421fcbc9..256c7f276a2 100644 --- a/esphome/components/template/alarm_control_panel/__init__.py +++ b/esphome/components/template/alarm_control_panel/__init__.py @@ -137,7 +137,11 @@ async def to_code(config): cg.add(var.set_arming_night_time(config[CONF_ARMING_NIGHT_TIME])) supports_arm_night = True - for sensor in config.get(CONF_BINARY_SENSORS, []): + if sensors := config.get(CONF_BINARY_SENSORS, []): + # Initialize FixedVector with the exact number of sensors + cg.add(var.init_sensors(len(sensors))) + + for sensor in sensors: bs = await cg.get_variable(sensor[CONF_INPUT]) flags = BinarySensorFlags[FLAG_NORMAL] diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index af662a05a00..f025435261e 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -20,10 +20,13 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, // Save the flags and type. Assign a store index for the per sensor data type. SensorDataStore sd; sd.last_chime_state = false; - this->sensor_map_[sensor].flags = flags; - this->sensor_map_[sensor].type = type; + AlarmSensor alarm_sensor; + alarm_sensor.sensor = sensor; + alarm_sensor.info.flags = flags; + alarm_sensor.info.type = type; + alarm_sensor.info.store_index = this->next_store_index_++; + this->sensors_.push_back(alarm_sensor); this->sensor_data_.push_back(sd); - this->sensor_map_[sensor].store_index = this->next_store_index_++; }; static const LogString *sensor_type_to_string(AlarmSensorType type) { @@ -45,7 +48,7 @@ void TemplateAlarmControlPanel::dump_config() { ESP_LOGCONFIG(TAG, "TemplateAlarmControlPanel:\n" " Current State: %s\n" - " Number of Codes: %u\n" + " Number of Codes: %zu\n" " Requires Code To Arm: %s\n" " Arming Away Time: %" PRIu32 "s\n" " Arming Home Time: %" PRIu32 "s\n" @@ -58,7 +61,8 @@ void TemplateAlarmControlPanel::dump_config() { (this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR - for (auto const &[sensor, info] : this->sensor_map_) { + for (const auto &alarm_sensor : this->sensors_) { + const uint16_t flags = alarm_sensor.info.flags; ESP_LOGCONFIG(TAG, " Binary Sensor:\n" " Name: %s\n" @@ -67,11 +71,10 @@ void TemplateAlarmControlPanel::dump_config() { " Armed night bypass: %s\n" " Auto bypass: %s\n" " Chime mode: %s", - sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME)); + alarm_sensor.sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(alarm_sensor.info.type)), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_AUTO), TRUEFALSE(flags & BINARY_SENSOR_MODE_CHIME)); } #endif } @@ -121,7 +124,9 @@ void TemplateAlarmControlPanel::loop() { #ifdef USE_BINARY_SENSOR // Test all of the sensors regardless of the alarm panel state - for (auto const &[sensor, info] : this->sensor_map_) { + for (const auto &alarm_sensor : this->sensors_) { + const auto &info = alarm_sensor.info; + auto *sensor = alarm_sensor.sensor; // Check for chime zones if (info.flags & BINARY_SENSOR_MODE_CHIME) { // Look for the transition from closed to open @@ -242,11 +247,11 @@ void TemplateAlarmControlPanel::arm_(optional code, alarm_control_p void TemplateAlarmControlPanel::bypass_before_arming() { #ifdef USE_BINARY_SENSOR - for (auto const &[sensor, info] : this->sensor_map_) { + for (const auto &alarm_sensor : this->sensors_) { // Check for faulted bypass_auto sensors and remove them from monitoring - if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) { - ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str()); - this->bypassed_sensor_indicies_.push_back(info.store_index); + if ((alarm_sensor.info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (alarm_sensor.sensor->state)) { + ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", alarm_sensor.sensor->get_name().c_str()); + this->bypassed_sensor_indicies_.push_back(alarm_sensor.info.store_index); } } #endif diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 202dc7c13fb..80ce34b8ae7 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -1,11 +1,12 @@ #pragma once #include -#include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/components/alarm_control_panel/alarm_control_panel.h" @@ -49,6 +50,13 @@ struct SensorInfo { uint8_t store_index; }; +#ifdef USE_BINARY_SENSOR +struct AlarmSensor { + binary_sensor::BinarySensor *sensor; + SensorInfo info; +}; +#endif + class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControlPanel, public Component { public: TemplateAlarmControlPanel(); @@ -63,6 +71,12 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl void bypass_before_arming(); #ifdef USE_BINARY_SENSOR + /** Initialize the sensors vector with the specified capacity. + * + * @param capacity The number of sensors to allocate space for. + */ + void init_sensors(size_t capacity) { this->sensors_.init(capacity); } + /** Add a binary_sensor to the alarm_panel. * * @param sensor The BinarySensor instance. @@ -122,8 +136,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl protected: void control(const alarm_control_panel::AlarmControlPanelCall &call) override; #ifdef USE_BINARY_SENSOR - // This maps a binary sensor to its alarm specific info - std::map sensor_map_; + // List of binary sensors with their alarm-specific info + FixedVector sensors_; // a list of automatically bypassed sensors std::vector bypassed_sensor_indicies_; #endif diff --git a/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml b/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml new file mode 100644 index 00000000000..836d3f11d5d --- /dev/null +++ b/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml @@ -0,0 +1,136 @@ +esphome: + name: template-alarm-many-sensors + friendly_name: "Template Alarm Control Panel with Many Sensors" + +logger: + +host: + +api: + +binary_sensor: + - platform: template + id: sensor1 + name: "Door 1" + - platform: template + id: sensor2 + name: "Door 2" + - platform: template + id: sensor3 + name: "Window 1" + - platform: template + id: sensor4 + name: "Window 2" + - platform: template + id: sensor5 + name: "Motion 1" + - platform: template + id: sensor6 + name: "Motion 2" + - platform: template + id: sensor7 + name: "Glass Break 1" + - platform: template + id: sensor8 + name: "Glass Break 2" + - platform: template + id: sensor9 + name: "Smoke Detector" + - platform: template + id: sensor10 + name: "CO Detector" + +alarm_control_panel: + - platform: template + id: test_alarm + name: "Test Alarm" + codes: + - "1234" + requires_code_to_arm: true + arming_away_time: 5s + arming_home_time: 3s + arming_night_time: 3s + pending_time: 10s + trigger_time: 300s + restore_mode: ALWAYS_DISARMED + binary_sensors: + - input: sensor1 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: true + chime: true + trigger_mode: DELAYED + - input: sensor2 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: true + chime: true + trigger_mode: DELAYED + - input: sensor3 + bypass_armed_home: true + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: DELAYED + - input: sensor4 + bypass_armed_home: true + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: DELAYED + - input: sensor5 + bypass_armed_home: false + bypass_armed_night: true + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor6 + bypass_armed_home: false + bypass_armed_night: true + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor7 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor8 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor9 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT_ALWAYS + - input: sensor10 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT_ALWAYS + on_disarmed: + - logger.log: "Alarm disarmed" + on_arming: + - logger.log: "Alarm arming" + on_armed_away: + - logger.log: "Alarm armed away" + on_armed_home: + - logger.log: "Alarm armed home" + on_armed_night: + - logger.log: "Alarm armed night" + on_pending: + - logger.log: "Alarm pending" + on_triggered: + - logger.log: "Alarm triggered" + on_cleared: + - logger.log: "Alarm cleared" + on_chime: + - logger.log: "Chime activated" + on_ready: + - logger.log: "Sensors ready state changed" diff --git a/tests/integration/test_template_alarm_control_panel_many_sensors.py b/tests/integration/test_template_alarm_control_panel_many_sensors.py new file mode 100644 index 00000000000..856815c731c --- /dev/null +++ b/tests/integration/test_template_alarm_control_panel_many_sensors.py @@ -0,0 +1,118 @@ +"""Integration test for template alarm control panel with many sensors.""" + +from __future__ import annotations + +import aioesphomeapi +from aioesphomeapi.model import APIIntEnum +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +class EspHomeACPFeatures(APIIntEnum): + """ESPHome AlarmControlPanel feature numbers.""" + + ARM_HOME = 1 + ARM_AWAY = 2 + ARM_NIGHT = 4 + TRIGGER = 8 + ARM_CUSTOM_BYPASS = 16 + ARM_VACATION = 32 + + +@pytest.mark.asyncio +async def test_template_alarm_control_panel_many_sensors( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test template alarm control panel with 10 binary sensors using FixedVector.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get entity info first + entities, _ = await client.list_entities_services() + + # Find the alarm control panel and binary sensors + alarm_info: aioesphomeapi.AlarmControlPanelInfo | None = None + binary_sensors: list[aioesphomeapi.BinarySensorInfo] = [] + + for entity in entities: + if isinstance(entity, aioesphomeapi.AlarmControlPanelInfo): + alarm_info = entity + elif isinstance(entity, aioesphomeapi.BinarySensorInfo): + binary_sensors.append(entity) + + assert alarm_info is not None, "Alarm control panel entity info not found" + assert alarm_info.name == "Test Alarm" + assert alarm_info.requires_code is True + assert alarm_info.requires_code_to_arm is True + + # Verify we have 10 binary sensors + assert len(binary_sensors) == 10, ( + f"Expected 10 binary sensors, got {len(binary_sensors)}" + ) + + # Verify sensor names + expected_sensor_names = { + "Door 1", + "Door 2", + "Window 1", + "Window 2", + "Motion 1", + "Motion 2", + "Glass Break 1", + "Glass Break 2", + "Smoke Detector", + "CO Detector", + } + actual_sensor_names = {sensor.name for sensor in binary_sensors} + assert actual_sensor_names == expected_sensor_names, ( + f"Sensor names mismatch. Expected: {expected_sensor_names}, " + f"Got: {actual_sensor_names}" + ) + + # Use InitialStateHelper to wait for all initial states + state_helper = InitialStateHelper(entities) + + def on_state(state: aioesphomeapi.EntityState) -> None: + # We'll receive subsequent states here after initial states + pass + + client.subscribe_states(state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states + await state_helper.wait_for_initial_states(timeout=5.0) + + # Verify the alarm state is disarmed initially + alarm_state = state_helper.initial_states.get(alarm_info.key) + assert alarm_state is not None, "Alarm control panel initial state not received" + assert isinstance(alarm_state, aioesphomeapi.AlarmControlPanelEntityState) + assert alarm_state.state == aioesphomeapi.AlarmControlPanelState.DISARMED, ( + f"Expected initial state DISARMED, got {alarm_state.state}" + ) + + # Verify all 10 binary sensors have initial states + binary_sensor_states = [ + state_helper.initial_states.get(sensor.key) for sensor in binary_sensors + ] + assert all(state is not None for state in binary_sensor_states), ( + "Not all binary sensors have initial states" + ) + + # Verify all binary sensor states are BinarySensorState type + for i, state in enumerate(binary_sensor_states): + assert isinstance(state, aioesphomeapi.BinarySensorState), ( + f"Binary sensor {i} state is not BinarySensorState: {type(state)}" + ) + + # Verify supported features + expected_features = ( + EspHomeACPFeatures.ARM_HOME + | EspHomeACPFeatures.ARM_AWAY + | EspHomeACPFeatures.ARM_NIGHT + | EspHomeACPFeatures.TRIGGER + ) + assert alarm_info.supported_features == expected_features, ( + f"Expected supported_features={expected_features} (ARM_HOME|ARM_AWAY|ARM_NIGHT|TRIGGER), " + f"got {alarm_info.supported_features}" + ) From 67524e14eec345fecf60daf7251e67be7ff9a364 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:05:02 +0000 Subject: [PATCH 438/461] Bump pylint from 4.0.2 to 4.0.3 (#11894) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 5c7cccaf25b..8d4e0ca246b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==4.0.2 +pylint==4.0.3 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.14.4 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating From e49a943cf7cd93c5123ba4b4e10fc861d8b8f4a1 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:13:48 +0100 Subject: [PATCH 439/461] [wifi] Allow `use_psram` with Arduino (#11902) --- esphome/components/wifi/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 4dbb425e4bd..11bd7798e27 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -12,7 +12,6 @@ from esphome.components.network import ( from esphome.components.psram import is_guaranteed as psram_is_guaranteed from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv -from esphome.config_validation import only_with_esp_idf from esphome.const import ( CONF_AP, CONF_BSSID, @@ -352,7 +351,7 @@ CONFIG_SCHEMA = cv.All( single=True ), cv.Optional(CONF_USE_PSRAM): cv.All( - only_with_esp_idf, cv.requires_component("psram"), cv.boolean + cv.only_on_esp32, cv.requires_component("psram"), cv.boolean ), } ), From e8f2e91db3d166326429d7a7d3c690a52596f191 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Nov 2025 08:47:15 -0600 Subject: [PATCH 440/461] [sntp] Merge multiple instances to fix crash and undefined behavior --- esphome/components/sntp/time.py | 68 +++++ tests/component_tests/sntp/__init__.py | 1 + .../sntp/config/sntp_test.yaml | 22 ++ tests/component_tests/sntp/test_init.py | 238 ++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 tests/component_tests/sntp/__init__.py create mode 100644 tests/component_tests/sntp/config/sntp_test.yaml create mode 100644 tests/component_tests/sntp/test_init.py diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index d27fc9991de..69a2436d3d2 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -1,9 +1,14 @@ +import logging + import esphome.codegen as cg from esphome.components import time as time_ +from esphome.config_helpers import merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_PLATFORM, CONF_SERVERS, + CONF_TIME, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -12,13 +17,74 @@ from esphome.const import ( PLATFORM_RTL87XX, ) from esphome.core import CORE +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["network"] + +CONF_SNTP = "sntp" + sntp_ns = cg.esphome_ns.namespace("sntp") SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock) DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"] + +def _sntp_final_validate(config: ConfigType) -> None: + """Merge multiple SNTP instances into one, similar to OTA merging behavior.""" + full_conf = fv.full_config.get() + time_confs = full_conf.get(CONF_TIME, []) + + sntp_configs: list[ConfigType] = [] + other_time_configs: list[ConfigType] = [] + + for time_conf in time_confs: + if time_conf.get(CONF_PLATFORM) == CONF_SNTP: + sntp_configs.append(time_conf) + else: + other_time_configs.append(time_conf) + + if len(sntp_configs) <= 1: + return + + # Merge all SNTP configs into the first one + merged = sntp_configs[0] + for sntp_conf in sntp_configs[1:]: + # Validate that IDs are consistent if manually specified + if merged[CONF_ID].is_manual and sntp_conf[CONF_ID].is_manual: + raise cv.Invalid( + f"Found multiple SNTP configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, sntp_conf) + + # Deduplicate servers while preserving order + servers = merged[CONF_SERVERS] + unique_servers = list(dict.fromkeys(servers)) + + # Warn if we're dropping servers due to 3-server limit + if len(unique_servers) > 3: + dropped = unique_servers[3:] + unique_servers = unique_servers[:3] + _LOGGER.warning( + "SNTP supports maximum 3 servers. Dropped excess server(s): %s", + dropped, + ) + + merged[CONF_SERVERS] = unique_servers + + _LOGGER.warning( + "Found and merged %d SNTP time configurations into one instance", + len(sntp_configs), + ) + + # Replace time configs with merged SNTP + other time platforms + other_time_configs.append(merged) + full_conf[CONF_TIME] = other_time_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = cv.All( time_.TIME_SCHEMA.extend( { @@ -40,6 +106,8 @@ CONFIG_SCHEMA = cv.All( ), ) +FINAL_VALIDATE_SCHEMA = _sntp_final_validate + async def to_code(config): servers = config[CONF_SERVERS] diff --git a/tests/component_tests/sntp/__init__.py b/tests/component_tests/sntp/__init__.py new file mode 100644 index 00000000000..7d323a49808 --- /dev/null +++ b/tests/component_tests/sntp/__init__.py @@ -0,0 +1 @@ +"""Tests for SNTP component.""" diff --git a/tests/component_tests/sntp/config/sntp_test.yaml b/tests/component_tests/sntp/config/sntp_test.yaml new file mode 100644 index 00000000000..3942c9606bb --- /dev/null +++ b/tests/component_tests/sntp/config/sntp_test.yaml @@ -0,0 +1,22 @@ +esphome: + name: sntp-test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "testssid" + password: "testpassword" + +# Test multiple SNTP instances that should be merged +time: + - platform: sntp + servers: + - 192.168.1.1 + - pool.ntp.org + - platform: sntp + servers: + - pool.ntp.org + - 192.168.1.2 diff --git a/tests/component_tests/sntp/test_init.py b/tests/component_tests/sntp/test_init.py new file mode 100644 index 00000000000..9197ff55d0a --- /dev/null +++ b/tests/component_tests/sntp/test_init.py @@ -0,0 +1,238 @@ +"""Tests for SNTP time configuration validation.""" + +from __future__ import annotations + +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.sntp.time import CONF_SNTP, _sntp_final_validate +from esphome.const import CONF_ID, CONF_PLATFORM, CONF_SERVERS, CONF_TIME +from esphome.core import ID +import esphome.final_validate as fv + + +@pytest.mark.parametrize( + ("time_configs", "expected_count", "expected_servers", "warning_messages"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + } + ], + 1, + ["192.168.1.1", "pool.ntp.org"], + [], + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_preserves_order", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2", "pool2.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_3", is_manual=False), + CONF_SERVERS: ["pool3.ntp.org"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + [ + "SNTP supports maximum 3 servers. Dropped excess server(s): ['pool2.ntp.org', 'pool3.ntp.org']", + "Found and merged 3 SNTP time configurations into one instance", + ], + id="three_instances_drops_excess_servers", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: [ + "192.168.1.1", + "pool.ntp.org", + "pool.ntp.org", + "192.168.1.1", + ], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_multiple_duplicates", + ), + ], +) +def test_sntp_instance_merging( + time_configs: list[dict[str, Any]], + expected_count: int, + expected_servers: list[str], + warning_messages: list[str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test SNTP instance merging behavior.""" + # Create a mock full config with time configs + full_conf = {CONF_TIME: time_configs.copy()} + + # Set the context var + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + # Get the updated config + updated_conf = fv.full_config.get() + + # Check if merging occurred + if len(time_configs) > 1: + # Verify only one SNTP instance remains + sntp_instances = [ + tc + for tc in updated_conf[CONF_TIME] + if tc.get(CONF_PLATFORM) == CONF_SNTP + ] + assert len(sntp_instances) == expected_count + + # Verify server list + assert sntp_instances[0][CONF_SERVERS] == expected_servers + + # Verify warnings + for expected_msg in warning_messages: + assert any( + expected_msg in record.message for record in caplog.records + ), f"Expected warning message '{expected_msg}' not found in log" + else: + # Single instance should not trigger merging or warnings + assert len(caplog.records) == 0 + # Config should be unchanged + assert updated_conf[CONF_TIME] == time_configs + finally: + fv.full_config.reset(token) + + +def test_sntp_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + # Create configs with manual IDs that are inconsistent + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=True), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=True), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple SNTP configurations but id is inconsistent", + ): + _sntp_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> None: + """Test that SNTP merging doesn't affect other time platforms.""" + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: "homeassistant", + CONF_ID: ID("homeassistant_time", is_manual=False), + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + updated_conf = fv.full_config.get() + + # Should have 2 time platforms: 1 merged SNTP + 1 homeassistant + assert len(updated_conf[CONF_TIME]) == 2 + + # Find the platforms + platforms = {tc[CONF_PLATFORM] for tc in updated_conf[CONF_TIME]} + assert platforms == {CONF_SNTP, "homeassistant"} + + # Verify SNTP was merged + sntp_instances = [ + tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == CONF_SNTP + ] + assert len(sntp_instances) == 1 + assert sntp_instances[0][CONF_SERVERS] == ["192.168.1.1", "192.168.1.2"] + finally: + fv.full_config.reset(token) From 5f10fbc4f683db47324d9283bf949b46befe9173 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Nov 2025 08:59:05 -0600 Subject: [PATCH 441/461] [web_server.ota] Merge multiple instances to prevent undefined behavior --- esphome/components/web_server/ota/__init__.py | 56 +++++++- .../ota/test_web_server_ota.py | 123 ++++++++++++++++++ 2 files changed, 178 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py index 4a98db88776..144d9e76cdb 100644 --- a/esphome/components/web_server/ota/__init__.py +++ b/esphome/components/web_server/ota/__init__.py @@ -1,17 +1,69 @@ +import logging + import esphome.codegen as cg from esphome.components.esp32 import add_idf_component from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +from esphome.config_helpers import merge_config import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network", "web_server_base"] +CONF_WEB_SERVER = "web_server" + web_server_ns = cg.esphome_ns.namespace("web_server") WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent) + +def _web_server_ota_final_validate(config: ConfigType) -> None: + """Merge multiple web_server OTA instances into one. + + Multiple web_server OTA instances register duplicate HTTP handlers for /update, + causing undefined behavior. Merge them into a single instance. + """ + full_conf = fv.full_config.get() + ota_confs = full_conf.get(CONF_OTA, []) + + web_server_ota_configs: list[ConfigType] = [] + other_ota_configs: list[ConfigType] = [] + + for ota_conf in ota_confs: + if ota_conf.get(CONF_PLATFORM) == CONF_WEB_SERVER: + web_server_ota_configs.append(ota_conf) + else: + other_ota_configs.append(ota_conf) + + if len(web_server_ota_configs) <= 1: + return + + # Merge all web_server OTA configs into the first one + merged = web_server_ota_configs[0] + for ota_conf in web_server_ota_configs[1:]: + # Validate that IDs are consistent if manually specified + if merged[CONF_ID].is_manual and ota_conf[CONF_ID].is_manual: + raise cv.Invalid( + f"Found multiple web_server OTA configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, ota_conf) + + _LOGGER.warning( + "Found and merged %d web_server OTA configurations into one instance", + len(web_server_ota_configs), + ) + + # Replace OTA configs with merged web_server + other OTA platforms + other_ota_configs.append(merged) + full_conf[CONF_OTA] = other_ota_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = ( cv.Schema( { @@ -22,6 +74,8 @@ CONFIG_SCHEMA = ( .extend(cv.COMPONENT_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = _web_server_ota_final_validate + @coroutine_with_priority(CoroPriority.WEB_SERVER_OTA) async def to_code(config): diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py index 0d8ff6f134f..d4630ff203d 100644 --- a/tests/component_tests/ota/test_web_server_ota.py +++ b/tests/component_tests/ota/test_web_server_ota.py @@ -1,6 +1,21 @@ """Tests for the web_server OTA platform.""" +from __future__ import annotations + from collections.abc import Callable +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.web_server.ota import ( + CONF_WEB_SERVER, + _web_server_ota_final_validate, +) +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM +from esphome.core import ID +import esphome.final_validate as fv def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None: @@ -100,3 +115,111 @@ def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None: # Check web server OTA component is present assert "WebServerOTAComponent" in main_cpp assert "web_server::WebServerOTAComponent" in main_cpp + + +@pytest.mark.parametrize( + ("ota_configs", "expected_count", "warning_expected"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=False), + } + ], + 1, + False, + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 1, + True, + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: "esphome", + CONF_ID: ID("ota_esphome", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 2, + True, + id="mixed_platforms_web_server_merged", + ), + ], +) +def test_web_server_ota_instance_merging( + ota_configs: list[dict[str, Any]], + expected_count: int, + warning_expected: bool, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test web_server OTA instance merging behavior.""" + full_conf = {CONF_OTA: ota_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _web_server_ota_final_validate({}) + + updated_conf = fv.full_config.get() + + # Verify total number of OTA platforms + assert len(updated_conf[CONF_OTA]) == expected_count + + # Verify warning + if warning_expected: + assert any( + "Found and merged" in record.message + and "web_server OTA" in record.message + for record in caplog.records + ), "Expected merge warning not found in log" + else: + assert len(caplog.records) == 0, "Unexpected warnings logged" + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + ota_configs = [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=True), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=True), + }, + ] + + full_conf = {CONF_OTA: ota_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple web_server OTA configurations but id is inconsistent", + ): + _web_server_ota_final_validate({}) + finally: + fv.full_config.reset(token) From 6666911ebf078bc7a0154bede97d863339747e92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Nov 2025 11:29:36 -0600 Subject: [PATCH 442/461] [analyze-memory] Show all core symbols > 100 B instead of top 15 --- esphome/analyze_memory/cli.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 718f42330d6..44ade221f8a 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -15,6 +15,11 @@ from . import ( class MemoryAnalyzerCLI(MemoryAnalyzer): """Memory analyzer with CLI-specific report generation.""" + # Symbol size threshold for detailed analysis + SYMBOL_SIZE_THRESHOLD: int = ( + 100 # Show symbols larger than this in detailed analysis + ) + # Column width constants COL_COMPONENT: int = 29 COL_FLASH_TEXT: int = 14 @@ -191,14 +196,21 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%" ) - # Top 15 largest core symbols + # All core symbols above threshold lines.append("") - lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:") sorted_core_symbols = sorted( self._esphome_core_symbols, key=lambda x: x[2], reverse=True ) + large_core_symbols = [ + (symbol, demangled, size) + for symbol, demangled, size in sorted_core_symbols + if size > self.SYMBOL_SIZE_THRESHOLD + ] - for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]): + lines.append( + f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):" + ) + for i, (symbol, demangled, size) in enumerate(large_core_symbols): lines.append(f"{i + 1}. {demangled} ({size:,} B)") lines.append("=" * self.TABLE_WIDTH) @@ -268,13 +280,15 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append("") - # Show all symbols > 100 bytes for better visibility + # Show all symbols above threshold for better visibility large_symbols = [ - (sym, dem, size) for sym, dem, size in sorted_symbols if size > 100 + (sym, dem, size) + for sym, dem, size in sorted_symbols + if size > self.SYMBOL_SIZE_THRESHOLD ] lines.append( - f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):" + f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" ) for i, (symbol, demangled, size) in enumerate(large_symbols): lines.append(f"{i + 1}. {demangled} ({size:,} B)") From 2bf6d48fcf06cce35a8e4d3211e0bc77c433d341 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:06:08 -0500 Subject: [PATCH 443/461] [uart] Improve error handling and validate buffer size (#11895) Co-authored-by: J. Nick Koston --- esphome/components/uart/__init__.py | 19 ++++++++++ .../uart/uart_component_esp_idf.cpp | 35 +++++++++++++++---- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index cbc11d0db04..7b0d9726b84 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,3 +1,4 @@ +from logging import getLogger import math import re @@ -35,6 +36,8 @@ from esphome.core import CORE, ID import esphome.final_validate as fv from esphome.yaml_util import make_data_base +_LOGGER = getLogger(__name__) + CODEOWNERS = ["@esphome/core"] uart_ns = cg.esphome_ns.namespace("uart") UARTComponent = uart_ns.class_("UARTComponent") @@ -130,6 +133,21 @@ def validate_host_config(config): return config +def validate_rx_buffer_size(config): + if CORE.is_esp32: + # ESP32 UART hardware FIFO is 128 bytes (LP UART is 16 bytes, but we use 128 as safe minimum) + # rx_buffer_size must be greater than the hardware FIFO length + min_buffer_size = 128 + if config[CONF_RX_BUFFER_SIZE] <= min_buffer_size: + _LOGGER.warning( + "UART rx_buffer_size (%d bytes) is too small and must be greater than the hardware " + "FIFO size (%d bytes). The buffer size will be automatically adjusted at runtime.", + config[CONF_RX_BUFFER_SIZE], + min_buffer_size, + ) + return config + + def _uart_declare_type(value): if CORE.is_esp8266: return cv.declare_id(ESP8266UartComponent)(value) @@ -247,6 +265,7 @@ CONFIG_SCHEMA = cv.All( ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN, CONF_PORT), validate_host_config, + validate_rx_buffer_size, ) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 73813d2d5b7..70a13c9e378 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -91,6 +91,16 @@ void IDFUARTComponent::setup() { this->uart_num_ = static_cast(next_uart_num++); this->lock_ = xSemaphoreCreateMutex(); +#if (SOC_UART_LP_NUM >= 1) + size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN); +#else + size_t fifo_len = SOC_UART_FIFO_LEN; +#endif + if (this->rx_buffer_size_ <= fifo_len) { + ESP_LOGW(TAG, "rx_buffer_size is too small, must be greater than %zu", fifo_len); + this->rx_buffer_size_ = fifo_len * 2; + } + xSemaphoreTake(this->lock_, portMAX_DELAY); this->load_settings(false); @@ -237,8 +247,12 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) { void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { xSemaphoreTake(this->lock_, portMAX_DELAY); - uart_write_bytes(this->uart_num_, data, len); + int32_t write_len = uart_write_bytes(this->uart_num_, data, len); xSemaphoreGive(this->lock_); + if (write_len != (int32_t) len) { + ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len); + this->mark_failed(); + } #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_TX, data[i]); @@ -267,6 +281,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { size_t length_to_read = len; + int32_t read_len = 0; if (!this->check_read_timeout_(len)) return false; xSemaphoreTake(this->lock_, portMAX_DELAY); @@ -277,25 +292,31 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { this->has_peek_ = false; } if (length_to_read > 0) - uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); + read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); xSemaphoreGive(this->lock_); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } #endif - return true; + return read_len == (int32_t) length_to_read; } int IDFUARTComponent::available() { - size_t available; + size_t available = 0; + esp_err_t err; xSemaphoreTake(this->lock_, portMAX_DELAY); - uart_get_buffered_data_len(this->uart_num_, &available); - if (this->has_peek_) - available++; + err = uart_get_buffered_data_len(this->uart_num_, &available); xSemaphoreGive(this->lock_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err)); + this->mark_failed(); + } + if (this->has_peek_) { + available++; + } return available; } From c32891ec025f05fd387cabd26cf18413f7881411 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:09:59 -0600 Subject: [PATCH 444/461] Bump github/codeql-action from 4.31.2 to 4.31.3 (#11911) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ab938b34369..22739753287 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 + uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 + uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 with: category: "/language:${{matrix.language}}" From 1df996601dce60fca1218f272d42b5974644afb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 19:14:07 +0000 Subject: [PATCH 445/461] Bump ruff from 0.14.4 to 0.14.5 (#11910) 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 dab660b03f0..b86d00f2aae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.4 + rev: v0.14.5 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 8d4e0ca246b..e238faa77ec 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.3 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.4 # also change in .pre-commit-config.yaml when updating +ruff==0.14.5 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating pre-commit From f8191410e375974d1eb47b53e9329c307c937ff2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Nov 2025 20:05:43 -0600 Subject: [PATCH 446/461] [core] Optimize DelayAction for no-argument case using if constexpr --- esphome/core/base_automation.h | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index a5e6139182b..4a7f550b223 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -178,7 +178,6 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) void play_complex(const Ts &...x) override { - auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; // If num_running_ > 1, we have multiple instances running in parallel @@ -187,9 +186,27 @@ template class DelayAction : public Action, public Compon // WARNING: This can accumulate delays if scripts are triggered faster than they complete! // Users should set max_runs on parallel scripts to limit concurrent executions. // Issue #10264: This is a workaround for parallel script delays interfering with each other. - App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, - /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), - /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + + // Optimization: For no-argument delays (most common case), use direct lambda + // instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution) + if constexpr (sizeof...(Ts) == 0) { + App.scheduler.set_timer_common_( + this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(), + [this]() { + if (this->num_running_ > 0) { + this->play_next_(); + } + }, + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + } else { + // For delays with arguments, use std::bind to preserve argument values + // Arguments must be copied because original references may be invalid after delay + auto f = std::bind(&DelayAction::play_next_, this, x...); + App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + } } float get_setup_priority() const override { return setup_priority::HARDWARE; } From f1bc3c68ddf7ae8e83dd301db39434af234a88c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Nov 2025 20:09:38 -0600 Subject: [PATCH 447/461] [core] Optimize DelayAction for no-argument case using if constexpr --- esphome/core/base_automation.h | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 4a7f550b223..c2519da839a 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -192,12 +192,7 @@ template class DelayAction : public Action, public Compon if constexpr (sizeof...(Ts) == 0) { App.scheduler.set_timer_common_( this, Scheduler::SchedulerItem::TIMEOUT, - /* is_static_string= */ true, "delay", this->delay_.value(), - [this]() { - if (this->num_running_ > 0) { - this->play_next_(); - } - }, + /* is_static_string= */ true, "delay", this->delay_.value(), [this]() { this->play_next_(); }, /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } else { // For delays with arguments, use std::bind to preserve argument values From cc1b547ad2e0e4c006a78994d3e976a8477a9cb2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Nov 2025 22:27:23 -0600 Subject: [PATCH 448/461] der dupe lam --- esphome/__main__.py | 5 ++ esphome/cpp_generator.py | 138 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 137 insertions(+), 6 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index b0c081a34f2..b714bd4a65b 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -497,6 +497,11 @@ def generate_cpp_contents(config: ConfigType) -> None: CORE.flush_tasks() + # Flush deferred lambda deduplication declarations after all variables are declared + from esphome import cpp_generator as cg + + cg.flush_lambda_dedup_declarations() + def write_cpp_file() -> int: code_s = indent(CORE.cpp_main_section) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 6f1af01a5be..046db6ddca4 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -24,6 +24,10 @@ from esphome.types import Expression, SafeExpType, TemplateArgsType from esphome.util import OrderedDict from esphome.yaml_util import ESPHomeDataBase +# Keys for lambda deduplication storage in CORE.data +_KEY_LAMBDA_DEDUP = "lambda_dedup" +_KEY_LAMBDA_DEDUP_DECLARATIONS = "lambda_dedup_declarations" + class RawExpression(Expression): __slots__ = ("text",) @@ -188,7 +192,7 @@ class LambdaExpression(Expression): def __init__( self, parts, parameters, capture: str = "=", return_type=None, source=None - ): + ) -> None: self.parts = parts if not isinstance(parameters, ParameterListExpression): parameters = ParameterListExpression(*parameters) @@ -197,16 +201,21 @@ class LambdaExpression(Expression): self.capture = capture self.return_type = safe_exp(return_type) if return_type is not None else None - def __str__(self): + def _format_body(self) -> str: + """Format the lambda body with source directive and content.""" + body = "" + if self.source is not None: + body += f"{self.source.as_line_directive}\n" + body += self.content + return body + + def __str__(self) -> str: # Stateless lambdas (empty capture) implicitly convert to function pointers # when assigned to function pointer types - no unary + needed cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" - cpp += " {\n" - if self.source is not None: - cpp += f"{self.source.as_line_directive}\n" - cpp += f"{self.content}\n}}" + cpp += f" {{\n{self._format_body()}\n}}" return indent_all_but_first_and_last(cpp) @property @@ -214,6 +223,37 @@ class LambdaExpression(Expression): return "".join(str(part) for part in self.parts) +class SharedFunctionLambdaExpression(LambdaExpression): + """A lambda expression that references a shared deduplicated function. + + This class wraps a function pointer but maintains the LambdaExpression + interface so calling code works unchanged. + """ + + __slots__ = ("_func_name",) + + def __init__( + self, + func_name: str, + parameters: TemplateArgsType, + return_type: SafeExpType | None = None, + ) -> None: + # Initialize parent with empty parts since we're just a function reference + super().__init__( + [], parameters, capture="", return_type=return_type, source=None + ) + self._func_name = func_name + + def __str__(self) -> str: + # Just return the function name - it's already a function pointer + return self._func_name + + @property + def content(self) -> str: + # No content, just a function reference + return "" + + # pylint: disable=abstract-method class Literal(Expression, metaclass=abc.ABCMeta): __slots__ = () @@ -583,6 +623,24 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False): CORE.add_global(expression, prepend) +def flush_lambda_dedup_declarations(): + """Flush all deferred lambda deduplication declarations to global scope. + + This must be called after all component code generation is complete + to ensure all referenced variables are declared before the shared + lambda functions that use them. + """ + if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data: + return + + declarations = CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] + for func_declaration in declarations: + add_global(RawStatement(func_declaration)) + + # Clear the list so we don't add them again + CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = [] + + def add_library(name: str, version: str | None, repository: str | None = None): """Add a library to the codegen library storage. @@ -656,6 +714,62 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: return await CORE.get_variable_with_full_id(id_) +def _try_deduplicate_lambda(lambda_expr: LambdaExpression) -> str | None: + """Try to deduplicate a lambda expression. + + If an identical lambda was already generated, returns the name of the + shared function. Otherwise, creates a new shared function and stores it. + + Args: + lambda_expr: The lambda expression to potentially deduplicate + + Returns: + The name of the shared function if this lambda should be deduplicated, + None if this is the first occurrence (caller should use original lambda) + """ + # Create a unique key from the lambda content, parameters, and return type + content = lambda_expr.content + param_str = str(lambda_expr.parameters) + return_str = ( + str(lambda_expr.return_type) if lambda_expr.return_type is not None else "void" + ) + + # Use tuple of (content, params, return_type) as key + lambda_key = (content, param_str, return_str) + + # Initialize deduplication storage in CORE.data if not exists + if _KEY_LAMBDA_DEDUP not in CORE.data: + CORE.data[_KEY_LAMBDA_DEDUP] = {} + + lambda_cache = CORE.data[_KEY_LAMBDA_DEDUP] + + # Check if we've seen this lambda before + if lambda_key in lambda_cache: + # Return name of existing shared function + return lambda_cache[lambda_key] + + # First occurrence - create a shared function + # Use the cache size as the function number + func_name = f"shared_lambda_{len(lambda_cache)}" + + # Build the function declaration using lambda's body formatting + func_declaration = ( + f"{return_str} {func_name}({param_str}) {{\n{lambda_expr._format_body()}\n}}" + ) + + # Store the declaration to be added later (after all variable declarations) + # We can't add it immediately because it might reference variables not yet declared + if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data: + CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = [] + CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS].append(func_declaration) + + # Store in cache + lambda_cache[lambda_key] = func_name + + # Return the function name (this is the first occurrence, but we still generate shared function) + return func_name + + async def process_lambda( value: Lambda, parameters: TemplateArgsType, @@ -713,6 +827,18 @@ async def process_lambda( location.line += value.content_offset else: location = None + + # Lambda deduplication: Only deduplicate stateless lambdas (empty capture). + # Stateful lambdas cannot be shared as they capture different contexts. + if capture == "": + lambda_expr = LambdaExpression( + parts, parameters, capture, return_type, location + ) + func_name = _try_deduplicate_lambda(lambda_expr) + if func_name is not None: + # Return a shared function reference instead of inline lambda + return SharedFunctionLambdaExpression(func_name, parameters, return_type) + return LambdaExpression(parts, parameters, capture, return_type, location) From 6ade327cde8f12bd4b19bc60c8ebf954f2ae2aca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:05:27 -0600 Subject: [PATCH 449/461] update tests --- tests/component_tests/text/test_text.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 99ddd78ee71..5349a5d683b 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,13 +58,21 @@ def test_text_config_value_mode_set(generate_main): def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode (optimized with stateless lambda) + Test if lambda is set for lambda mode (optimized with stateless lambda and deduplication) """ # Given + from esphome.core import CORE # When main_cpp = generate_main("tests/component_tests/text/test_text.yaml") + # Get both global and main sections to find the shared lambda definition + full_cpp = CORE.cpp_global_section + main_cpp + # Then - assert "it_4->set_template([]() -> esphome::optional {" in main_cpp - assert 'return std::string{"Hello"};' in main_cpp + # Lambda is deduplicated into a shared function (reference in main section) + assert "it_4->set_template(shared_lambda_" in main_cpp + # Lambda body should be in the code somewhere + assert 'return std::string{"Hello"};' in full_cpp + # Verify the shared lambda function is defined (in global section) + assert "esphome::optional shared_lambda_" in full_cpp From 11de9486984e57d5964be1c2249e7879a7bbc8ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:12:36 -0600 Subject: [PATCH 450/461] proper codegen --- esphome/__main__.py | 5 ----- esphome/cpp_generator.py | 9 +++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index b714bd4a65b..b0c081a34f2 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -497,11 +497,6 @@ def generate_cpp_contents(config: ConfigType) -> None: CORE.flush_tasks() - # Flush deferred lambda deduplication declarations after all variables are declared - from esphome import cpp_generator as cg - - cg.flush_lambda_dedup_declarations() - def write_cpp_file() -> int: code_s = indent(CORE.cpp_main_section) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 046db6ddca4..4f64b29f80f 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -19,6 +19,7 @@ from esphome.core import ( TimePeriodNanoseconds, TimePeriodSeconds, ) +from esphome.coroutine import CoroPriority, coroutine_with_priority from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last from esphome.types import Expression, SafeExpType, TemplateArgsType from esphome.util import OrderedDict @@ -623,10 +624,11 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False): CORE.add_global(expression, prepend) -def flush_lambda_dedup_declarations(): +@coroutine_with_priority(CoroPriority.FINAL) +async def flush_lambda_dedup_declarations() -> None: """Flush all deferred lambda deduplication declarations to global scope. - This must be called after all component code generation is complete + This is a coroutine that runs with FINAL priority (after all components) to ensure all referenced variables are declared before the shared lambda functions that use them. """ @@ -740,6 +742,9 @@ def _try_deduplicate_lambda(lambda_expr: LambdaExpression) -> str | None: # Initialize deduplication storage in CORE.data if not exists if _KEY_LAMBDA_DEDUP not in CORE.data: CORE.data[_KEY_LAMBDA_DEDUP] = {} + # Register the flush job to run after all components (FINAL priority) + # This ensures all variables are declared before shared lambda functions + CORE.add_job(flush_lambda_dedup_declarations) lambda_cache = CORE.data[_KEY_LAMBDA_DEDUP] From b7c105125e0da72288df948a8bda5306c170cd77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:13:43 -0600 Subject: [PATCH 451/461] proper codegen --- esphome/cpp_generator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 4f64b29f80f..5a8685dd0a4 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -764,9 +764,7 @@ def _try_deduplicate_lambda(lambda_expr: LambdaExpression) -> str | None: # Store the declaration to be added later (after all variable declarations) # We can't add it immediately because it might reference variables not yet declared - if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data: - CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = [] - CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS].append(func_declaration) + CORE.data.setdefault(_KEY_LAMBDA_DEDUP_DECLARATIONS, []).append(func_declaration) # Store in cache lambda_cache[lambda_key] = func_name From 62248b6bbac9b67a16768a149c1f42bcbbae24bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:20:40 -0600 Subject: [PATCH 452/461] rpeen --- esphome/cpp_generator.py | 6 +- tests/unit_tests/test_lambda_dedup.py | 183 ++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/test_lambda_dedup.py diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 5a8685dd0a4..fcc0ca2e436 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -202,7 +202,7 @@ class LambdaExpression(Expression): self.capture = capture self.return_type = safe_exp(return_type) if return_type is not None else None - def _format_body(self) -> str: + def format_body(self) -> str: """Format the lambda body with source directive and content.""" body = "" if self.source is not None: @@ -216,7 +216,7 @@ class LambdaExpression(Expression): cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" - cpp += f" {{\n{self._format_body()}\n}}" + cpp += f" {{\n{self.format_body()}\n}}" return indent_all_but_first_and_last(cpp) @property @@ -759,7 +759,7 @@ def _try_deduplicate_lambda(lambda_expr: LambdaExpression) -> str | None: # Build the function declaration using lambda's body formatting func_declaration = ( - f"{return_str} {func_name}({param_str}) {{\n{lambda_expr._format_body()}\n}}" + f"{return_str} {func_name}({param_str}) {{\n{lambda_expr.format_body()}\n}}" ) # Store the declaration to be added later (after all variable declarations) diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py new file mode 100644 index 00000000000..e25e8dff008 --- /dev/null +++ b/tests/unit_tests/test_lambda_dedup.py @@ -0,0 +1,183 @@ +"""Tests for lambda deduplication in cpp_generator.""" + +import pytest + +from esphome import cpp_generator as cg +from esphome.core import CORE + + +@pytest.fixture(autouse=True) +def reset_core(): + """Reset CORE.data before each test.""" + CORE.reset() + yield + CORE.reset() + + +def test_deduplicate_identical_lambdas(): + """Test that identical stateless lambdas are deduplicated.""" + # Create two identical lambda expressions + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + # Try to deduplicate them + func_name1 = cg._try_deduplicate_lambda(lambda1) + func_name2 = cg._try_deduplicate_lambda(lambda2) + + # Both should get the same function name (deduplication happened) + assert func_name1 == func_name2 + assert func_name1 == "shared_lambda_0" + + +def test_different_lambdas_not_deduplicated(): + """Test that different lambdas get different function names.""" + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return 24;"], # Different content + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + func_name1 = cg._try_deduplicate_lambda(lambda1) + func_name2 = cg._try_deduplicate_lambda(lambda2) + + # Different lambdas should get different function names + assert func_name1 != func_name2 + assert func_name1 == "shared_lambda_0" + assert func_name2 == "shared_lambda_1" + + +def test_different_return_types_not_deduplicated(): + """Test that lambdas with different return types are not deduplicated.""" + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return 42;"], # Same content + parameters=[], + capture="", + return_type=cg.RawExpression("float"), # Different return type + ) + + func_name1 = cg._try_deduplicate_lambda(lambda1) + func_name2 = cg._try_deduplicate_lambda(lambda2) + + # Different return types = different functions + assert func_name1 != func_name2 + + +def test_different_parameters_not_deduplicated(): + """Test that lambdas with different parameters are not deduplicated.""" + lambda1 = cg.LambdaExpression( + parts=["return x;"], + parameters=[("int", "x")], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return x;"], # Same content + parameters=[("float", "x")], # Different parameter type + capture="", + return_type=cg.RawExpression("int"), + ) + + func_name1 = cg._try_deduplicate_lambda(lambda1) + func_name2 = cg._try_deduplicate_lambda(lambda2) + + # Different parameters = different functions + assert func_name1 != func_name2 + + +def test_flush_lambda_dedup_declarations(): + """Test that deferred declarations are properly stored for later flushing.""" + # Create a lambda which will create a deferred declaration + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + cg._try_deduplicate_lambda(lambda1) + + # Check that declaration was stored + assert cg._KEY_LAMBDA_DEDUP_DECLARATIONS in CORE.data + assert len(CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS]) == 1 + + # Verify the declaration content is correct + declaration = CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS][0] + assert "shared_lambda_0" in declaration + assert "return 42;" in declaration + + # Note: The actual flushing happens via CORE.add_job with FINAL priority + # during real code generation, so we don't test that here + + +def test_shared_function_lambda_expression(): + """Test SharedFunctionLambdaExpression behaves correctly.""" + shared_lambda = cg.SharedFunctionLambdaExpression( + func_name="shared_lambda_0", + parameters=[], + return_type=cg.RawExpression("int"), + ) + + # Should output just the function name + assert str(shared_lambda) == "shared_lambda_0" + + # Should have empty capture (stateless) + assert shared_lambda.capture == "" + + # Should have empty content (just a reference) + assert shared_lambda.content == "" + + +def test_lambda_deduplication_counter(): + """Test that lambda counter increments correctly.""" + # Create 3 different lambdas + for i in range(3): + lambda_expr = cg.LambdaExpression( + parts=[f"return {i};"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + func_name = cg._try_deduplicate_lambda(lambda_expr) + assert func_name == f"shared_lambda_{i}" + + +def test_lambda_format_body(): + """Test that format_body correctly formats lambda body with source.""" + # Without source + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=None, + source=None, + ) + assert lambda1.format_body() == "return 42;" + + # With source would need a proper source object, skip for now From 1441c7fab281ea7ab4e7f348ab3b5225580f069e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:21:58 -0600 Subject: [PATCH 453/461] preen --- tests/unit_tests/test_lambda_dedup.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py index e25e8dff008..0c8925b5eac 100644 --- a/tests/unit_tests/test_lambda_dedup.py +++ b/tests/unit_tests/test_lambda_dedup.py @@ -1,19 +1,9 @@ """Tests for lambda deduplication in cpp_generator.""" -import pytest - from esphome import cpp_generator as cg from esphome.core import CORE -@pytest.fixture(autouse=True) -def reset_core(): - """Reset CORE.data before each test.""" - CORE.reset() - yield - CORE.reset() - - def test_deduplicate_identical_lambdas(): """Test that identical stateless lambdas are deduplicated.""" # Create two identical lambda expressions From 5727043cec9f6c84a24faf6d83a870ad9ae42c14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:24:38 -0600 Subject: [PATCH 454/461] preen --- tests/unit_tests/test_lambda_dedup.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py index 0c8925b5eac..ec4c0d29c9d 100644 --- a/tests/unit_tests/test_lambda_dedup.py +++ b/tests/unit_tests/test_lambda_dedup.py @@ -4,7 +4,7 @@ from esphome import cpp_generator as cg from esphome.core import CORE -def test_deduplicate_identical_lambdas(): +def test_deduplicate_identical_lambdas() -> None: """Test that identical stateless lambdas are deduplicated.""" # Create two identical lambda expressions lambda1 = cg.LambdaExpression( @@ -30,7 +30,7 @@ def test_deduplicate_identical_lambdas(): assert func_name1 == "shared_lambda_0" -def test_different_lambdas_not_deduplicated(): +def test_different_lambdas_not_deduplicated() -> None: """Test that different lambdas get different function names.""" lambda1 = cg.LambdaExpression( parts=["return 42;"], @@ -55,7 +55,7 @@ def test_different_lambdas_not_deduplicated(): assert func_name2 == "shared_lambda_1" -def test_different_return_types_not_deduplicated(): +def test_different_return_types_not_deduplicated() -> None: """Test that lambdas with different return types are not deduplicated.""" lambda1 = cg.LambdaExpression( parts=["return 42;"], @@ -78,7 +78,7 @@ def test_different_return_types_not_deduplicated(): assert func_name1 != func_name2 -def test_different_parameters_not_deduplicated(): +def test_different_parameters_not_deduplicated() -> None: """Test that lambdas with different parameters are not deduplicated.""" lambda1 = cg.LambdaExpression( parts=["return x;"], @@ -101,7 +101,7 @@ def test_different_parameters_not_deduplicated(): assert func_name1 != func_name2 -def test_flush_lambda_dedup_declarations(): +def test_flush_lambda_dedup_declarations() -> None: """Test that deferred declarations are properly stored for later flushing.""" # Create a lambda which will create a deferred declaration lambda1 = cg.LambdaExpression( @@ -126,7 +126,7 @@ def test_flush_lambda_dedup_declarations(): # during real code generation, so we don't test that here -def test_shared_function_lambda_expression(): +def test_shared_function_lambda_expression() -> None: """Test SharedFunctionLambdaExpression behaves correctly.""" shared_lambda = cg.SharedFunctionLambdaExpression( func_name="shared_lambda_0", @@ -144,7 +144,7 @@ def test_shared_function_lambda_expression(): assert shared_lambda.content == "" -def test_lambda_deduplication_counter(): +def test_lambda_deduplication_counter() -> None: """Test that lambda counter increments correctly.""" # Create 3 different lambdas for i in range(3): @@ -158,7 +158,7 @@ def test_lambda_deduplication_counter(): assert func_name == f"shared_lambda_{i}" -def test_lambda_format_body(): +def test_lambda_format_body() -> None: """Test that format_body correctly formats lambda body with source.""" # Without source lambda1 = cg.LambdaExpression( From 5989b78e93296e14be709a09c3163b1ec6431fc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:25:57 -0600 Subject: [PATCH 455/461] preen --- tests/component_tests/text/test_text.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 5349a5d683b..b2318bd4161 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -1,5 +1,7 @@ """Tests for the binary sensor component.""" +from esphome.core import CORE + def test_text_is_setup(generate_main): """ @@ -61,7 +63,6 @@ def test_text_config_lamda_is_set(generate_main): Test if lambda is set for lambda mode (optimized with stateless lambda and deduplication) """ # Given - from esphome.core import CORE # When main_cpp = generate_main("tests/component_tests/text/test_text.yaml") From 4081345013364d595ff4ca4100d8d313110a48fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:42:44 -0600 Subject: [PATCH 456/461] address bot review --- esphome/cpp_generator.py | 20 ++++++------ tests/component_tests/text/test_text.py | 4 +-- tests/unit_tests/test_lambda_dedup.py | 41 +++++++++++++++++++------ 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index fcc0ca2e436..2904cae7966 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -716,18 +716,17 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: return await CORE.get_variable_with_full_id(id_) -def _try_deduplicate_lambda(lambda_expr: LambdaExpression) -> str | None: - """Try to deduplicate a lambda expression. +def _get_shared_lambda_name(lambda_expr: LambdaExpression) -> str: + """Get the shared function name for a lambda expression. - If an identical lambda was already generated, returns the name of the - shared function. Otherwise, creates a new shared function and stores it. + If an identical lambda was already generated, returns the existing shared + function name. Otherwise, creates a new shared function and returns its name. Args: - lambda_expr: The lambda expression to potentially deduplicate + lambda_expr: The lambda expression to deduplicate Returns: - The name of the shared function if this lambda should be deduplicated, - None if this is the first occurrence (caller should use original lambda) + The name of the shared function for this lambda (either existing or newly created) """ # Create a unique key from the lambda content, parameters, and return type content = lambda_expr.content @@ -837,10 +836,9 @@ async def process_lambda( lambda_expr = LambdaExpression( parts, parameters, capture, return_type, location ) - func_name = _try_deduplicate_lambda(lambda_expr) - if func_name is not None: - # Return a shared function reference instead of inline lambda - return SharedFunctionLambdaExpression(func_name, parameters, return_type) + func_name = _get_shared_lambda_name(lambda_expr) + # Return a shared function reference instead of inline lambda + return SharedFunctionLambdaExpression(func_name, parameters, return_type) return LambdaExpression(parts, parameters, capture, return_type, location) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index b2318bd4161..ffc0fd780ae 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -1,4 +1,4 @@ -"""Tests for the binary sensor component.""" +"""Tests for the text component.""" from esphome.core import CORE @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): assert "it_3->traits.set_mode(text::TEXT_MODE_PASSWORD);" in main_cpp -def test_text_config_lamda_is_set(generate_main): +def test_text_config_lambda_is_set(generate_main) -> None: """ Test if lambda is set for lambda mode (optimized with stateless lambda and deduplication) """ diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py index ec4c0d29c9d..ebc217f308d 100644 --- a/tests/unit_tests/test_lambda_dedup.py +++ b/tests/unit_tests/test_lambda_dedup.py @@ -22,8 +22,8 @@ def test_deduplicate_identical_lambdas() -> None: ) # Try to deduplicate them - func_name1 = cg._try_deduplicate_lambda(lambda1) - func_name2 = cg._try_deduplicate_lambda(lambda2) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) # Both should get the same function name (deduplication happened) assert func_name1 == func_name2 @@ -46,8 +46,8 @@ def test_different_lambdas_not_deduplicated() -> None: return_type=cg.RawExpression("int"), ) - func_name1 = cg._try_deduplicate_lambda(lambda1) - func_name2 = cg._try_deduplicate_lambda(lambda2) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) # Different lambdas should get different function names assert func_name1 != func_name2 @@ -71,8 +71,8 @@ def test_different_return_types_not_deduplicated() -> None: return_type=cg.RawExpression("float"), # Different return type ) - func_name1 = cg._try_deduplicate_lambda(lambda1) - func_name2 = cg._try_deduplicate_lambda(lambda2) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) # Different return types = different functions assert func_name1 != func_name2 @@ -94,8 +94,8 @@ def test_different_parameters_not_deduplicated() -> None: return_type=cg.RawExpression("int"), ) - func_name1 = cg._try_deduplicate_lambda(lambda1) - func_name2 = cg._try_deduplicate_lambda(lambda2) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) # Different parameters = different functions assert func_name1 != func_name2 @@ -111,7 +111,7 @@ def test_flush_lambda_dedup_declarations() -> None: return_type=cg.RawExpression("int"), ) - cg._try_deduplicate_lambda(lambda1) + cg._get_shared_lambda_name(lambda1) # Check that declaration was stored assert cg._KEY_LAMBDA_DEDUP_DECLARATIONS in CORE.data @@ -154,7 +154,7 @@ def test_lambda_deduplication_counter() -> None: capture="", return_type=cg.RawExpression("int"), ) - func_name = cg._try_deduplicate_lambda(lambda_expr) + func_name = cg._get_shared_lambda_name(lambda_expr) assert func_name == f"shared_lambda_{i}" @@ -171,3 +171,24 @@ def test_lambda_format_body() -> None: assert lambda1.format_body() == "return 42;" # With source would need a proper source object, skip for now + + +def test_stateful_lambdas_not_deduplicated() -> None: + """Test that stateful lambdas (non-empty capture) are not deduplicated.""" + # _get_shared_lambda_name is only called for stateless lambdas (capture == "") + # Stateful lambdas bypass deduplication entirely in process_lambda + + # Verify that a stateful lambda would NOT get deduplicated + # by checking it's not in the stateless dedup cache + stateful_lambda = cg.LambdaExpression( + parts=["return x + y;"], + parameters=[], + capture="=", # Non-empty capture means stateful + return_type=cg.RawExpression("int"), + ) + + # Stateful lambdas should NOT be passed to _get_shared_lambda_name + # This is enforced by the `if capture == ""` check in process_lambda + # We verify the lambda has a non-empty capture + assert stateful_lambda.capture != "" + assert stateful_lambda.capture == "=" From 7892adb948fd2de4bf4f43b114695df83eb8a36a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:52:49 -0600 Subject: [PATCH 457/461] [ld2412] Fix stuck targets by adding timeout filter --- esphome/components/ld2412/sensor.py | 76 ++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py index abb823faad9..0bfbd9bf1d0 100644 --- a/esphome/components/ld2412/sensor.py +++ b/esphome/components/ld2412/sensor.py @@ -31,36 +31,84 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor ), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), @@ -74,7 +122,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, @@ -82,7 +136,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, From ed60d8668eab796953cdd2f228b22c19bc90463f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:56:17 -0600 Subject: [PATCH 458/461] [ld2410] Add timeout filter to prevent stuck targets --- esphome/components/ld2410/sensor.py | 76 ++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index fca2b2ceca8..3bd34963bcc 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -31,35 +31,83 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, ), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), @@ -73,7 +121,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, @@ -81,7 +135,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, From fb9e7028a03f8a00fb928ff659dbaaa6056572fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 11:58:24 -0600 Subject: [PATCH 459/461] [core] Replace seq<>/gens<> with std::index_sequence for code clarity --- esphome/components/api/user_services.h | 10 +++++--- esphome/components/script/script.h | 12 ++++----- esphome/core/automation.h | 35 ++++++++++++++++++-------- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 2a887fc52da..2f753dfc6c8 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -51,13 +51,14 @@ template class UserServiceBase : public UserServiceDescriptor { return false; if (req.args.size() != sizeof...(Ts)) return false; - this->execute_(req.args, typename gens::type()); + this->execute_(req.args, std::make_index_sequence{}); return true; } protected: virtual void execute(Ts... x) = 0; - template void execute_(const ArgsContainer &args, seq type) { + template + void execute_(const ArgsContainer &args, std::index_sequence type) { this->execute((get_execute_arg_value(args[S]))...); } @@ -95,13 +96,14 @@ template class UserServiceDynamic : public UserServiceDescriptor return false; if (req.args.size() != sizeof...(Ts)) return false; - this->execute_(req.args, typename gens::type()); + this->execute_(req.args, std::make_index_sequence{}); return true; } protected: virtual void execute(Ts... x) = 0; - template void execute_(const ArgsContainer &args, seq type) { + template + void execute_(const ArgsContainer &args, std::index_sequence type) { this->execute((get_execute_arg_value(args[S]))...); } diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 51cece01e41..ec0a8f03cec 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -46,14 +46,14 @@ template class Script : public ScriptLogger, public Trigger &tuple) { - this->execute_tuple_(tuple, typename gens::type()); + this->execute_tuple_(tuple, std::make_index_sequence{}); } // Internal function to give scripts readable names. void set_name(const LogString *name) { name_ = name; } protected: - template void execute_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void execute_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->execute(std::get(tuple)...); } @@ -157,7 +157,7 @@ template class QueueingScript : public Script, public Com const size_t queue_capacity = static_cast(this->max_runs_ - 1); auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]); this->queue_front_ = (this->queue_front_ + 1) % queue_capacity; - this->trigger_tuple_(*tuple_ptr, typename gens::type()); + this->trigger_tuple_(*tuple_ptr, std::make_index_sequence{}); } } @@ -174,7 +174,7 @@ template class QueueingScript : public Script, public Com } } - template void trigger_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void trigger_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->trigger(std::get(tuple)...); } @@ -305,7 +305,7 @@ template class ScriptWaitAction : public Action, while (!this->param_queue_.empty()) { auto ¶ms = this->param_queue_.front(); - this->play_next_tuple_(params, typename gens::type()); + this->play_next_tuple_(params, std::make_index_sequence{}); this->param_queue_.pop_front(); } // Queue is now empty - disable loop until next play_complex @@ -321,7 +321,7 @@ template class ScriptWaitAction : public Action, } protected: - template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_next_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play_next_(std::get(tuple)...); } diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 33e08c9c1c6..af57e5b7603 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -11,10 +11,23 @@ namespace esphome { -// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 -template struct seq {}; // NOLINT -template struct gens : gens {}; // NOLINT -template struct gens<0, S...> { using type = seq; }; // NOLINT +// C++20 std::index_sequence is now used for tuple unpacking +// Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility +// Remove before 2026.6.0 +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +template struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {}; +template +struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens + : gens {}; +template struct gens<0, S...> { using type = seq; }; + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif #define TEMPLATABLE_VALUE_(type, name) \ protected: \ @@ -152,11 +165,11 @@ template class Condition { /// Call check with a tuple of values as parameter. bool check_tuple(const std::tuple &tuple) { - return this->check_tuple_(tuple, typename gens::type()); + return this->check_tuple_(tuple, std::make_index_sequence{}); } protected: - template bool check_tuple_(const std::tuple &tuple, seq /*unused*/) { + template bool check_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { return this->check(std::get(tuple)...); } }; @@ -231,11 +244,11 @@ template class Action { } } } - template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_next_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play_next_(std::get(tuple)...); } void play_next_tuple_(const std::tuple &tuple) { - this->play_next_tuple_(tuple, typename gens::type()); + this->play_next_tuple_(tuple, std::make_index_sequence{}); } virtual void stop() {} @@ -277,7 +290,9 @@ template class ActionList { if (this->actions_begin_ != nullptr) this->actions_begin_->play_complex(x...); } - void play_tuple(const std::tuple &tuple) { this->play_tuple_(tuple, typename gens::type()); } + void play_tuple(const std::tuple &tuple) { + this->play_tuple_(tuple, std::make_index_sequence{}); + } void stop() { if (this->actions_begin_ != nullptr) this->actions_begin_->stop_complex(); @@ -298,7 +313,7 @@ template class ActionList { } protected: - template void play_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play(std::get(tuple)...); } From b7f60133780da3e94e8cb1c1d02bf18cf65f2134 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 12:04:02 -0600 Subject: [PATCH 460/461] [core] Replace seq<>/gens<> with std::index_sequence for code clarity --- esphome/core/automation.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index af57e5b7603..6b5d77ab961 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -19,10 +19,13 @@ namespace esphome { #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif +// NOLINTNEXTLINE(readability-identifier-naming) template struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {}; +// NOLINTNEXTLINE(readability-identifier-naming) template struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens : gens {}; +// NOLINTNEXTLINE(readability-identifier-naming) template struct gens<0, S...> { using type = seq; }; #if defined(__GNUC__) || defined(__clang__) From d7892f228920d365be433ba049ff34857fab004c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 12:04:49 -0600 Subject: [PATCH 461/461] [core] Replace seq<>/gens<> with std::index_sequence for code clarity --- esphome/core/automation.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 6b5d77ab961..97c9e9c3cc2 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -13,6 +13,7 @@ namespace esphome { // C++20 std::index_sequence is now used for tuple unpacking // Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility +// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 // Remove before 2026.6.0 #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push