1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-23 20:23:50 +01:00

Merge branch 'dev' into ListEntitiesServicesArgument_FixedVector

This commit is contained in:
J. Nick Koston
2025-10-15 10:00:15 -10:00
committed by GitHub
33 changed files with 390 additions and 110 deletions

View File

@@ -1 +1 @@
049d60eed541730efaa4c0dc5d337b4287bf29b6daa350b5dfc1f23915f1c52f d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248

View File

@@ -114,8 +114,7 @@ jobs:
matrix: matrix:
python-version: python-version:
- "3.11" - "3.11"
- "3.12" - "3.14"
- "3.13"
os: os:
- ubuntu-latest - ubuntu-latest
- macOS-latest - macOS-latest
@@ -124,13 +123,9 @@ jobs:
# Minimize CI resource usage # Minimize CI resource usage
# by only running the Python version # by only running the Python version
# version used for docker images on Windows and macOS # version used for docker images on Windows and macOS
- python-version: "3.13" - python-version: "3.14"
os: windows-latest os: windows-latest
- python-version: "3.12" - python-version: "3.14"
os: windows-latest
- python-version: "3.13"
os: macOS-latest
- python-version: "3.12"
os: macOS-latest os: macOS-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: needs:
@@ -178,6 +173,7 @@ jobs:
python-linters: ${{ steps.determine.outputs.python-linters }} python-linters: ${{ steps.determine.outputs.python-linters }}
changed-components: ${{ steps.determine.outputs.changed-components }} changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }} changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
component-test-count: ${{ steps.determine.outputs.component-test-count }} component-test-count: ${{ steps.determine.outputs.component-test-count }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
@@ -206,6 +202,7 @@ jobs:
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
integration-tests: integration-tests:
@@ -358,48 +355,13 @@ jobs:
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
if: always() if: always()
test-build-components:
name: Component test ${{ matrix.file }}
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100
strategy:
fail-fast: false
max-parallel: 2
matrix:
file: ${{ fromJson(needs.determine-jobs.outputs.changed-components-with-tests) }}
steps:
- name: Cache apt packages
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
with:
packages: libsdl2-dev
version: 1.0
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Validate config for ${{ matrix.file }}
run: |
. venv/bin/activate
python3 script/test_build_components.py -e config -c ${{ matrix.file }}
- name: Compile config for ${{ matrix.file }}
run: |
. venv/bin/activate
python3 script/test_build_components.py -e compile -c ${{ matrix.file }}
test-build-components-splitter: test-build-components-splitter:
name: Split components for intelligent grouping (40 weighted per batch) name: Split components for intelligent grouping (40 weighted per batch)
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- determine-jobs - determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100 if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
outputs: outputs:
matrix: ${{ steps.split.outputs.components }} matrix: ${{ steps.split.outputs.components }}
steps: steps:
@@ -417,9 +379,10 @@ jobs:
# Use intelligent splitter that groups components with same bus configs # Use intelligent splitter that groups components with same bus configs
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}' components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
echo "Splitting components intelligently..." echo "Splitting components intelligently..."
output=$(python3 script/split_components_for_ci.py --components "$components" --batch-size 40 --output github) output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
echo "$output" >> $GITHUB_OUTPUT echo "$output" >> $GITHUB_OUTPUT
@@ -430,7 +393,7 @@ jobs:
- common - common
- determine-jobs - determine-jobs
- test-build-components-splitter - test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100 if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: ${{ (github.base_ref == 'beta' || github.base_ref == 'release') && 8 || 4 }} max-parallel: ${{ (github.base_ref == 'beta' || github.base_ref == 'release') && 8 || 4 }}
@@ -477,18 +440,34 @@ jobs:
# Convert space-separated components to comma-separated for Python script # Convert space-separated components to comma-separated for Python script
components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',') components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
echo "Testing components: $components_csv" # Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
#
# WHY ISOLATE DIRECTLY CHANGED COMPONENTS?
# - Isolated tests run WITHOUT --testing-mode, enabling full validation
# - This catches pin conflicts and other issues in directly changed code
# - Grouped tests use --testing-mode to allow config merging (disables some checks)
# - Dependencies are safe to group since they weren't modified in this PR
if [ "${{ github.base_ref }}" = "beta" ] || [ "${{ github.base_ref }}" = "release" ]; then
directly_changed_csv=""
echo "Testing components: $components_csv"
echo "Target branch: ${{ github.base_ref }} - grouping all components"
else
directly_changed_csv=$(echo '${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}' | jq -r 'join(",")')
echo "Testing components: $components_csv"
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components: $directly_changed_csv"
fi
echo "" echo ""
# Run config validation with grouping # Run config validation with grouping and isolation
python3 script/test_build_components.py -e config -c "$components_csv" -f python3 script/test_build_components.py -e config -c "$components_csv" -f --isolate "$directly_changed_csv"
echo "" echo ""
echo "Config validation passed! Starting compilation..." echo "Config validation passed! Starting compilation..."
echo "" echo ""
# Run compilation with grouping # Run compilation with grouping and isolation
python3 script/test_build_components.py -e compile -c "$components_csv" -f python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv"
pre-commit-ci-lite: pre-commit-ci-lite:
name: pre-commit.ci lite name: pre-commit.ci lite
@@ -521,7 +500,6 @@ jobs:
- integration-tests - integration-tests
- clang-tidy - clang-tidy
- determine-jobs - determine-jobs
- test-build-components
- test-build-components-splitter - test-build-components-splitter
- test-build-components-split - test-build-components-split
- pre-commit-ci-lite - pre-commit-ci-lite

View File

@@ -16,7 +16,9 @@
#include "bluetooth_connection.h" #include "bluetooth_connection.h"
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h> #include <esp_bt.h>
#endif
#include <esp_bt_device.h> #include <esp_bt_device.h>
namespace esphome::bluetooth_proxy { namespace esphome::bluetooth_proxy {

View File

@@ -324,7 +324,7 @@ def _is_framework_url(source: str) -> str:
# The default/recommended arduino framework version # The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases # - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = { ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 2, 1), "recommended": cv.Version(3, 3, 2),
"latest": cv.Version(3, 3, 2), "latest": cv.Version(3, 3, 2),
"dev": cv.Version(3, 3, 2), "dev": cv.Version(3, 3, 2),
} }
@@ -343,7 +343,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# The default/recommended esp-idf framework version # The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases # - https://github.com/espressif/esp-idf/releases
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(5, 4, 2), "recommended": cv.Version(5, 5, 1),
"latest": cv.Version(5, 5, 1), "latest": cv.Version(5, 5, 1),
"dev": cv.Version(5, 5, 1), "dev": cv.Version(5, 5, 1),
} }
@@ -363,7 +363,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version # The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases # - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = { PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(54, 3, 21, "2"), "recommended": cv.Version(55, 3, 31, "1"),
"latest": cv.Version(55, 3, 31, "1"), "latest": cv.Version(55, 3, 31, "1"),
"dev": cv.Version(55, 3, 31, "1"), "dev": cv.Version(55, 3, 31, "1"),
} }
@@ -544,6 +544,7 @@ CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking" CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety" CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
def _validate_idf_component(config: ConfigType) -> ConfigType: def _validate_idf_component(config: ConfigType) -> ConfigType:
@@ -606,6 +607,9 @@ FRAMEWORK_SCHEMA = cv.All(
cv.Optional( cv.Optional(
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
): cv.boolean, ): cv.boolean,
cv.Optional(
CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True
): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
} }
), ),
@@ -864,6 +868,12 @@ async def to_code(config):
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True): if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True) 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):
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
cg.add_platformio_option("board_build.partitions", "partitions.csv") cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config: if CONF_PARTITIONS in config:
add_extra_build_file( add_extra_build_file(

View File

@@ -1564,6 +1564,10 @@ BOARDS = {
"name": "DFRobot Beetle ESP32-C3", "name": "DFRobot Beetle ESP32-C3",
"variant": VARIANT_ESP32C3, "variant": VARIANT_ESP32C3,
}, },
"dfrobot_firebeetle2_esp32c6": {
"name": "DFRobot FireBeetle 2 ESP32-C6",
"variant": VARIANT_ESP32C6,
},
"dfrobot_firebeetle2_esp32e": { "dfrobot_firebeetle2_esp32e": {
"name": "DFRobot Firebeetle 2 ESP32-E", "name": "DFRobot Firebeetle 2 ESP32-E",
"variant": VARIANT_ESP32, "variant": VARIANT_ESP32,
@@ -1604,6 +1608,22 @@ BOARDS = {
"name": "Ai-Thinker ESP-C3-M1-I-Kit", "name": "Ai-Thinker ESP-C3-M1-I-Kit",
"variant": VARIANT_ESP32C3, "variant": VARIANT_ESP32C3,
}, },
"esp32-c5-devkitc-1": {
"name": "Espressif ESP32-C5-DevKitC-1 4MB no PSRAM",
"variant": VARIANT_ESP32C5,
},
"esp32-c5-devkitc1-n16r4": {
"name": "Espressif ESP32-C5-DevKitC-1 N16R4 (16 MB Flash Quad, 4 MB PSRAM Quad)",
"variant": VARIANT_ESP32C5,
},
"esp32-c5-devkitc1-n4": {
"name": "Espressif ESP32-C5-DevKitC-1 N4 (4MB no PSRAM)",
"variant": VARIANT_ESP32C5,
},
"esp32-c5-devkitc1-n8r4": {
"name": "Espressif ESP32-C5-DevKitC-1 N8R4 (8 MB Flash Quad, 4 MB PSRAM Quad)",
"variant": VARIANT_ESP32C5,
},
"esp32-c6-devkitc-1": { "esp32-c6-devkitc-1": {
"name": "Espressif ESP32-C6-DevKitC-1", "name": "Espressif ESP32-C6-DevKitC-1",
"variant": VARIANT_ESP32C6, "variant": VARIANT_ESP32C6,
@@ -2048,6 +2068,10 @@ BOARDS = {
"name": "M5Stack Station", "name": "M5Stack Station",
"variant": VARIANT_ESP32, "variant": VARIANT_ESP32,
}, },
"m5stack-tab5-p4": {
"name": "M5STACK Tab5 esp32-p4 Board",
"variant": VARIANT_ESP32P4,
},
"m5stack-timer-cam": { "m5stack-timer-cam": {
"name": "M5Stack Timer CAM", "name": "M5Stack Timer CAM",
"variant": VARIANT_ESP32, "variant": VARIANT_ESP32,
@@ -2476,6 +2500,10 @@ BOARDS = {
"name": "YelloByte YB-ESP32-S3-AMP (Rev.3)", "name": "YelloByte YB-ESP32-S3-AMP (Rev.3)",
"variant": VARIANT_ESP32S3, "variant": VARIANT_ESP32S3,
}, },
"yb_esp32s3_drv": {
"name": "YelloByte YB-ESP32-S3-DRV",
"variant": VARIANT_ESP32S3,
},
"yb_esp32s3_eth": { "yb_esp32s3_eth": {
"name": "YelloByte YB-ESP32-S3-ETH", "name": "YelloByte YB-ESP32-S3-ETH",
"variant": VARIANT_ESP32S3, "variant": VARIANT_ESP32S3,

View File

@@ -387,6 +387,15 @@ def final_validation(config):
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
validate_connection_slots(max_connections) validate_connection_slots(max_connections)
# Check if hosted bluetooth is being used
if "esp32_hosted" in full_config:
add_idf_sdkconfig_option("CONFIG_BT_CLASSIC_ENABLED", False)
add_idf_sdkconfig_option("CONFIG_BT_BLE_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLUEDROID_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_CONTROLLER_DISABLED", True)
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True)
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True)
# Check if BLE Server is needed # Check if BLE Server is needed
has_ble_server = "esp32_ble_server" in full_config has_ble_server = "esp32_ble_server" in full_config

View File

@@ -6,7 +6,15 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h> #include <esp_bt.h>
#else
extern "C" {
#include <esp_hosted.h>
#include <esp_hosted_misc.h>
#include <esp_hosted_bluedroid.h>
}
#endif
#include <esp_bt_device.h> #include <esp_bt_device.h>
#include <esp_bt_main.h> #include <esp_bt_main.h>
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
@@ -136,6 +144,7 @@ void ESP32BLE::advertising_init_() {
bool ESP32BLE::ble_setup_() { bool ESP32BLE::ble_setup_() {
esp_err_t err; esp_err_t err;
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
if (!btStart()) { if (!btStart()) {
ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status());
@@ -169,6 +178,28 @@ bool ESP32BLE::ble_setup_() {
#endif #endif
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
#else
esp_hosted_connect_to_slave(); // NOLINT
if (esp_hosted_bt_controller_init() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed");
return false;
}
if (esp_hosted_bt_controller_enable() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed");
return false;
}
hosted_hci_bluedroid_open();
esp_bluedroid_hci_driver_operations_t operations = {
.send = hosted_hci_bluedroid_send,
.check_send_available = hosted_hci_bluedroid_check_send_available,
.register_host_callback = hosted_hci_bluedroid_register_host_callback,
};
esp_bluedroid_attach_hci_driver(&operations);
#endif
err = esp_bluedroid_init(); err = esp_bluedroid_init();
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -257,6 +288,7 @@ bool ESP32BLE::ble_dismantle_() {
return false; return false;
} }
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
if (!btStop()) { if (!btStop()) {
ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status()); ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status());
@@ -286,6 +318,19 @@ bool ESP32BLE::ble_dismantle_() {
return false; return false;
} }
} }
#endif
#else
if (esp_hosted_bt_controller_disable() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");
return false;
}
if (esp_hosted_bt_controller_deinit(false) != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed");
return false;
}
hosted_hci_bluedroid_close();
#endif #endif
return true; return true;
} }

View File

@@ -10,7 +10,9 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP32_BLE_ADVERTISING #ifdef USE_ESP32_BLE_ADVERTISING
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h> #include <esp_bt.h>
#endif
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>

View File

@@ -3,7 +3,9 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h> #include <esp_bt.h>
#endif
#include <esp_bt_main.h> #include <esp_bt_main.h>
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>

View File

@@ -5,7 +5,9 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h> #include <esp_bt.h>
#endif
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
namespace esphome { namespace esphome {

View File

@@ -10,7 +10,9 @@
#include <nvs_flash.h> #include <nvs_flash.h>
#include <freertos/FreeRTOSConfig.h> #include <freertos/FreeRTOSConfig.h>
#include <esp_bt_main.h> #include <esp_bt_main.h>
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h> #include <esp_bt.h>
#endif
#include <freertos/task.h> #include <freertos/task.h>
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>

View File

@@ -7,7 +7,9 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h> #include <esp_bt.h>
#endif
#include <esp_bt_defs.h> #include <esp_bt_defs.h>
#include <esp_bt_main.h> #include <esp_bt_main.h>
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
@@ -845,6 +847,7 @@ void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
void ESP32BLETracker::update_coex_preference_(bool force_ble) { void ESP32BLETracker::update_coex_preference_(bool force_ble) {
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
if (force_ble && !this->coex_prefer_ble_) { if (force_ble && !this->coex_prefer_ble_) {
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection."); ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
this->coex_prefer_ble_ = true; this->coex_prefer_ble_ = true;
@@ -854,6 +857,7 @@ void ESP32BLETracker::update_coex_preference_(bool force_ble) {
this->coex_prefer_ble_ = false; this->coex_prefer_ble_ = false;
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
} }
#endif // CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
} }
#endif #endif

View File

@@ -92,9 +92,14 @@ async def to_code(config):
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.10.2") if framework_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.5.11")
else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11")
esp32.add_extra_script( esp32.add_extra_script(
"post", "post",
"esp32_hosted.py", "esp32_hosted.py",

View File

@@ -42,6 +42,11 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
symbols[i] = params->bit0; symbols[i] = params->bit0;
} }
} }
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) {
*done = true;
}
#endif
return RMT_SYMBOLS_PER_BYTE; return RMT_SYMBOLS_PER_BYTE;
} }

View File

@@ -486,7 +486,6 @@ CONF_RESUME_ON_INPUT = "resume_on_input"
CONF_RIGHT_BUTTON = "right_button" CONF_RIGHT_BUTTON = "right_button"
CONF_ROLLOVER = "rollover" CONF_ROLLOVER = "rollover"
CONF_ROOT_BACK_BTN = "root_back_btn" CONF_ROOT_BACK_BTN = "root_back_btn"
CONF_ROWS = "rows"
CONF_SCALE_LINES = "scale_lines" CONF_SCALE_LINES = "scale_lines"
CONF_SCROLLBAR_MODE = "scrollbar_mode" CONF_SCROLLBAR_MODE = "scrollbar_mode"
CONF_SELECTED_INDEX = "selected_index" CONF_SELECTED_INDEX = "selected_index"

View File

@@ -2,7 +2,7 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.key_provider import KeyProvider from esphome.components.key_provider import KeyProvider
import esphome.config_validation as cv 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 esphome.cpp_generator import MockObj
from ..automation import action_to_code from ..automation import action_to_code
@@ -15,7 +15,6 @@ from ..defines import (
CONF_ONE_CHECKED, CONF_ONE_CHECKED,
CONF_PAD_COLUMN, CONF_PAD_COLUMN,
CONF_PAD_ROW, CONF_PAD_ROW,
CONF_ROWS,
CONF_SELECTED, CONF_SELECTED,
) )
from ..helpers import lvgl_components_required from ..helpers import lvgl_components_required

View File

@@ -2,7 +2,7 @@ from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import key_provider from esphome.components import key_provider
import esphome.config_validation as cv 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"] CODEOWNERS = ["@ssieb"]
@@ -19,7 +19,6 @@ MatrixKeyTrigger = matrix_keypad_ns.class_(
) )
CONF_KEYPAD_ID = "keypad_id" CONF_KEYPAD_ID = "keypad_id"
CONF_ROWS = "rows"
CONF_COLUMNS = "columns" CONF_COLUMNS = "columns"
CONF_KEYS = "keys" CONF_KEYS = "keys"
CONF_DEBOUNCE_TIME = "debounce_time" CONF_DEBOUNCE_TIME = "debounce_time"

View File

@@ -145,7 +145,7 @@ class BSDSocketImpl : public Socket {
} }
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override {
return ::sendto(fd_, buf, len, flags, to, tolen); return ::sendto(fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument)
} }
int setblocking(bool blocking) override { int setblocking(bool blocking) override {

View File

@@ -836,6 +836,7 @@ CONF_RMT_CHANNEL = "rmt_channel"
CONF_RMT_SYMBOLS = "rmt_symbols" CONF_RMT_SYMBOLS = "rmt_symbols"
CONF_ROTATION = "rotation" CONF_ROTATION = "rotation"
CONF_ROW = "row" CONF_ROW = "row"
CONF_ROWS = "rows"
CONF_RS_PIN = "rs_pin" CONF_RS_PIN = "rs_pin"
CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance" CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance"
CONF_RTD_WIRES = "rtd_wires" CONF_RTD_WIRES = "rtd_wires"

View File

@@ -202,7 +202,7 @@
#define USB_HOST_MAX_REQUESTS 16 #define USB_HOST_MAX_REQUESTS 16
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1) #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
#define USE_ETHERNET #define USE_ETHERNET
#define USE_ETHERNET_KSZ8081 #define USE_ETHERNET_KSZ8081
#endif #endif

View File

@@ -125,9 +125,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script
; This are common settings for the ESP32 (all variants) using Arduino. ; This are common settings for the ESP32 (all variants) using Arduino.
[common:esp32-arduino] [common:esp32-arduino]
extends = common:arduino extends = common:arduino
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-2/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-1/platform-espressif32.zip
platform_packages = platform_packages =
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.2/esp32-3.3.2.zip
framework = arduino, espidf ; Arduino as an ESP-IDF component framework = arduino, espidf ; Arduino as an ESP-IDF component
lib_deps = lib_deps =
@@ -161,9 +161,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
; This are common settings for the ESP32 (all variants) using IDF. ; This are common settings for the ESP32 (all variants) using IDF.
[common:esp32-idf] [common:esp32-idf]
extends = common:idf extends = common:idf
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-2/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-1/platform-espressif32.zip
platform_packages = platform_packages =
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.4.2/esp-idf-v5.4.2.zip pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.1/esp-idf-v5.5.1.zip
framework = espidf framework = espidf
lib_deps = lib_deps =

View File

@@ -23,10 +23,6 @@ cairosvg==2.8.2
freetype-py==2.5.1 freetype-py==2.5.1
jinja2==3.1.6 jinja2==3.1.6
# esp-idf requires this, but doesn't bundle it by default
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
kconfiglib==13.7.1
# esp-idf >= 5.0 requires this # esp-idf >= 5.0 requires this
pyparsing >= 3.0 pyparsing >= 3.0

View File

@@ -1,4 +1,4 @@
pylint==4.0.0 pylint==4.0.1
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.14.0 # also change in .pre-commit-config.yaml when updating ruff==0.14.0 # 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.0 # also change in .pre-commit-config.yaml when updating

View File

@@ -75,6 +75,8 @@ ISOLATED_COMPONENTS = {
"ethernet": "Defines ethernet: which conflicts with wifi: used by most components", "ethernet": "Defines ethernet: which conflicts with wifi: used by most components",
"ethernet_info": "Related to ethernet component which conflicts with wifi", "ethernet_info": "Related to ethernet component which conflicts with wifi",
"lvgl": "Defines multiple SDL displays on host platform that conflict when merged with other display configs", "lvgl": "Defines multiple SDL displays on host platform that conflict when merged with other display configs",
"openthread": "Conflicts with wifi: used by most components",
"openthread_info": "Conflicts with wifi: used by most components",
"matrix_keypad": "Needs isolation due to keypad", "matrix_keypad": "Needs isolation due to keypad",
"mcp4725": "no YAML config to specify i2c bus id", "mcp4725": "no YAML config to specify i2c bus id",
"mcp47a1": "no YAML config to specify i2c bus id", "mcp47a1": "no YAML config to specify i2c bus id",

View File

@@ -501,7 +501,7 @@ def lint_constants_usage():
continue continue
errs.append( errs.append(
f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the " f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the "
f"constant to const.py (Uses: {', '.join(uses)}) in a separate PR. " f"constant to const.py (Uses: {', '.join(str(u) for u in uses)}) in a separate PR. "
"See https://developers.esphome.io/contributing/code/#python" "See https://developers.esphome.io/contributing/code/#python"
) )
return errs return errs

View File

@@ -31,6 +31,7 @@ Options:
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from functools import cache
import json import json
import os import os
from pathlib import Path from pathlib import Path
@@ -45,7 +46,6 @@ from helpers import (
changed_files, changed_files,
get_all_dependencies, get_all_dependencies,
get_components_from_integration_fixtures, get_components_from_integration_fixtures,
parse_list_components_output,
root_path, root_path,
) )
@@ -212,6 +212,24 @@ def _any_changed_file_endswith(branch: str | None, extensions: tuple[str, ...])
return any(file.endswith(extensions) for file in changed_files(branch)) return any(file.endswith(extensions) for file in changed_files(branch))
@cache
def _component_has_tests(component: str) -> bool:
"""Check if a component has test files.
Cached to avoid repeated filesystem operations for the same component.
Args:
component: Component name to check
Returns:
True if the component has test YAML files
"""
tests_dir = Path(root_path) / "tests" / "components" / component
if not tests_dir.exists():
return False
return any(tests_dir.glob("test.*.yaml"))
def main() -> None: def main() -> None:
"""Main function that determines which CI jobs to run.""" """Main function that determines which CI jobs to run."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
@@ -228,23 +246,37 @@ def main() -> None:
run_clang_format = should_run_clang_format(args.branch) run_clang_format = should_run_clang_format(args.branch)
run_python_linters = should_run_python_linters(args.branch) run_python_linters = should_run_python_linters(args.branch)
# Get changed components using list-components.py for exact compatibility # Get both directly changed and all changed components (with dependencies) in one call
script_path = Path(__file__).parent / "list-components.py" script_path = Path(__file__).parent / "list-components.py"
cmd = [sys.executable, str(script_path), "--changed"] cmd = [sys.executable, str(script_path), "--changed-with-deps"]
if args.branch: if args.branch:
cmd.extend(["-b", args.branch]) cmd.extend(["-b", args.branch])
result = subprocess.run(cmd, capture_output=True, text=True, check=True) result = subprocess.run(cmd, capture_output=True, text=True, check=True)
changed_components = parse_list_components_output(result.stdout) component_data = json.loads(result.stdout)
directly_changed_components = component_data["directly_changed"]
changed_components = component_data["all_changed"]
# Filter to only components that have test files # Filter to only components that have test files
# Components without tests shouldn't generate CI test jobs # Components without tests shouldn't generate CI test jobs
tests_dir = Path(root_path) / "tests" / "components"
changed_components_with_tests = [ changed_components_with_tests = [
component for component in changed_components if _component_has_tests(component)
]
# Get directly changed components with tests (for isolated testing)
# These will be tested WITHOUT --testing-mode in CI to enable full validation
# (pin conflicts, etc.) since they contain the actual changes being reviewed
directly_changed_with_tests = [
component component
for component in changed_components for component in directly_changed_components
if (component_test_dir := tests_dir / component).exists() if _component_has_tests(component)
and any(component_test_dir.glob("test.*.yaml")) ]
# Get dependency-only components (for grouped testing)
dependency_only_components = [
component
for component in changed_components_with_tests
if component not in directly_changed_components
] ]
# Build output # Build output
@@ -255,7 +287,11 @@ def main() -> None:
"python_linters": run_python_linters, "python_linters": run_python_linters,
"changed_components": changed_components, "changed_components": changed_components,
"changed_components_with_tests": changed_components_with_tests, "changed_components_with_tests": changed_components_with_tests,
"directly_changed_components_with_tests": directly_changed_with_tests,
"dependency_only_components_with_tests": dependency_only_components,
"component_test_count": len(changed_components_with_tests), "component_test_count": len(changed_components_with_tests),
"directly_changed_count": len(directly_changed_with_tests),
"dependency_only_count": len(dependency_only_components),
} }
# Output as JSON # Output as JSON

View File

@@ -185,18 +185,32 @@ def main():
"-c", "-c",
"--changed", "--changed",
action="store_true", action="store_true",
help="List all components required for testing based on changes", help="List all components required for testing based on changes (includes dependencies)",
)
parser.add_argument(
"--changed-direct",
action="store_true",
help="List only directly changed components (without dependencies)",
)
parser.add_argument(
"--changed-with-deps",
action="store_true",
help="Output JSON with both directly changed and all changed components",
) )
parser.add_argument( parser.add_argument(
"-b", "--branch", help="Branch to compare changed files against" "-b", "--branch", help="Branch to compare changed files against"
) )
args = parser.parse_args() args = parser.parse_args()
if args.branch and not args.changed: if args.branch and not (
parser.error("--branch requires --changed") args.changed or args.changed_direct or args.changed_with_deps
):
parser.error(
"--branch requires --changed, --changed-direct, or --changed-with-deps"
)
if args.changed: if args.changed or args.changed_direct or args.changed_with_deps:
# When --changed is passed, only get the changed files # When --changed* is passed, only get the changed files
changed = changed_files(args.branch) changed = changed_files(args.branch)
# If any base test file(s) changed, there's no need to filter out components # If any base test file(s) changed, there's no need to filter out components
@@ -210,8 +224,25 @@ def main():
# Get all component files # Get all component files
files = get_all_component_files() files = get_all_component_files()
for c in get_components(files, args.changed): if args.changed_with_deps:
print(c) # Return JSON with both directly changed and all changed components
import json
directly_changed = get_components(files, False)
all_changed = get_components(files, True)
output = {
"directly_changed": directly_changed,
"all_changed": all_changed,
}
print(json.dumps(output))
elif args.changed_direct:
# Return only directly changed components (without dependencies)
for c in get_components(files, False):
print(c)
else:
# Return all changed components (with dependencies) - default behavior
for c in get_components(files, args.changed):
print(c)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -56,6 +56,7 @@ def create_intelligent_batches(
components: list[str], components: list[str],
tests_dir: Path, tests_dir: Path,
batch_size: int = 40, batch_size: int = 40,
directly_changed: set[str] | None = None,
) -> list[list[str]]: ) -> list[list[str]]:
"""Create batches optimized for component grouping. """Create batches optimized for component grouping.
@@ -63,6 +64,7 @@ def create_intelligent_batches(
components: List of component names to batch components: List of component names to batch
tests_dir: Path to tests/components directory tests_dir: Path to tests/components directory
batch_size: Target size for each batch batch_size: Target size for each batch
directly_changed: Set of directly changed components (for logging only)
Returns: Returns:
List of component batches (lists of component names) List of component batches (lists of component names)
@@ -94,10 +96,17 @@ def create_intelligent_batches(
for component in components_with_tests: for component in components_with_tests:
# Components that can't be grouped get unique signatures # Components that can't be grouped get unique signatures
# This includes both manually curated ISOLATED_COMPONENTS and # This includes:
# automatically detected non_groupable components # - Manually curated ISOLATED_COMPONENTS
# - Automatically detected non_groupable components
# - Directly changed components (passed via --isolate in CI)
# These can share a batch/runner but won't be grouped/merged # These can share a batch/runner but won't be grouped/merged
if component in ISOLATED_COMPONENTS or component in non_groupable: is_isolated = (
component in ISOLATED_COMPONENTS
or component in non_groupable
or (directly_changed and component in directly_changed)
)
if is_isolated:
signature_groups[f"isolated_{component}"].append(component) signature_groups[f"isolated_{component}"].append(component)
continue continue
@@ -187,6 +196,10 @@ def main() -> int:
default=Path("tests/components"), default=Path("tests/components"),
help="Path to tests/components directory", help="Path to tests/components directory",
) )
parser.add_argument(
"--directly-changed",
help="JSON array of directly changed component names (for logging only)",
)
parser.add_argument( parser.add_argument(
"--output", "--output",
"-o", "-o",
@@ -208,11 +221,21 @@ def main() -> int:
print("Components must be a JSON array", file=sys.stderr) print("Components must be a JSON array", file=sys.stderr)
return 1 return 1
# Parse directly changed components list from JSON (if provided)
directly_changed = None
if args.directly_changed:
try:
directly_changed = set(json.loads(args.directly_changed))
except json.JSONDecodeError as e:
print(f"Error parsing directly-changed JSON: {e}", file=sys.stderr)
return 1
# Create intelligent batches # Create intelligent batches
batches = create_intelligent_batches( batches = create_intelligent_batches(
components=components, components=components,
tests_dir=args.tests_dir, tests_dir=args.tests_dir,
batch_size=args.batch_size, batch_size=args.batch_size,
directly_changed=directly_changed,
) )
# Convert batches to space-separated strings for CI # Convert batches to space-separated strings for CI
@@ -238,13 +261,37 @@ def main() -> int:
isolated_count = sum( isolated_count = sum(
1 1
for comp in all_batched_components for comp in all_batched_components
if comp in ISOLATED_COMPONENTS or comp in non_groupable if comp in ISOLATED_COMPONENTS
or comp in non_groupable
or (directly_changed and comp in directly_changed)
) )
groupable_count = actual_components - isolated_count groupable_count = actual_components - isolated_count
print("\n=== Intelligent Batch Summary ===", file=sys.stderr) print("\n=== Intelligent Batch Summary ===", file=sys.stderr)
print(f"Total components requested: {len(components)}", file=sys.stderr) print(f"Total components requested: {len(components)}", file=sys.stderr)
print(f"Components with test files: {actual_components}", file=sys.stderr) print(f"Components with test files: {actual_components}", file=sys.stderr)
# Show breakdown of directly changed vs dependencies
if directly_changed:
direct_count = sum(
1 for comp in all_batched_components if comp in directly_changed
)
dep_count = actual_components - direct_count
direct_comps = [
comp for comp in all_batched_components if comp in directly_changed
]
dep_comps = [
comp for comp in all_batched_components if comp not in directly_changed
]
print(
f" - Direct changes: {direct_count} ({', '.join(sorted(direct_comps))})",
file=sys.stderr,
)
print(
f" - Dependencies: {dep_count} ({', '.join(sorted(dep_comps))})",
file=sys.stderr,
)
print(f" - Groupable (weight=1): {groupable_count}", file=sys.stderr) print(f" - Groupable (weight=1): {groupable_count}", file=sys.stderr)
print(f" - Isolated (weight=10): {isolated_count}", file=sys.stderr) print(f" - Isolated (weight=10): {isolated_count}", file=sys.stderr)
if actual_components < len(components): if actual_components < len(components):

View File

@@ -365,6 +365,7 @@ def run_grouped_component_tests(
build_dir: Path, build_dir: Path,
esphome_command: str, esphome_command: str,
continue_on_fail: bool, continue_on_fail: bool,
additional_isolated: set[str] | None = None,
) -> tuple[set[tuple[str, str]], list[str], list[str], dict[str, str]]: ) -> tuple[set[tuple[str, str]], list[str], list[str], dict[str, str]]:
"""Run grouped component tests. """Run grouped component tests.
@@ -376,6 +377,7 @@ def run_grouped_component_tests(
build_dir: Path to build directory build_dir: Path to build directory
esphome_command: ESPHome command (config/compile) esphome_command: ESPHome command (config/compile)
continue_on_fail: Whether to continue on failure continue_on_fail: Whether to continue on failure
additional_isolated: Additional components to treat as isolated (not grouped)
Returns: Returns:
Tuple of (tested_components, passed_tests, failed_tests, failed_commands) Tuple of (tested_components, passed_tests, failed_tests, failed_commands)
@@ -397,6 +399,17 @@ def run_grouped_component_tests(
# Track why components can't be grouped (for detailed output) # Track why components can't be grouped (for detailed output)
non_groupable_reasons = {} non_groupable_reasons = {}
# Merge additional isolated components with predefined ones
# ISOLATED COMPONENTS are tested individually WITHOUT --testing-mode
# This is critical because:
# - Grouped tests use --testing-mode which disables pin conflict checks and other validation
# - These checks are disabled to allow config merging (multiple components in one build)
# - For directly changed components (via --isolate), we need full validation to catch issues
# - Dependencies are safe to group since they weren't modified in the PR
all_isolated = set(ISOLATED_COMPONENTS.keys())
if additional_isolated:
all_isolated.update(additional_isolated)
# Group by (platform, bus_signature) # Group by (platform, bus_signature)
for component, platforms in component_buses.items(): for component, platforms in component_buses.items():
if component not in all_tests: if component not in all_tests:
@@ -404,7 +417,7 @@ def run_grouped_component_tests(
# Skip components that must be tested in isolation # Skip components that must be tested in isolation
# These are shown separately and should not be in non_groupable_reasons # These are shown separately and should not be in non_groupable_reasons
if component in ISOLATED_COMPONENTS: if component in all_isolated:
continue continue
# Skip base bus components (these test the bus platforms themselves) # Skip base bus components (these test the bus platforms themselves)
@@ -453,15 +466,28 @@ def run_grouped_component_tests(
print("\nGrouping Plan:") print("\nGrouping Plan:")
print("-" * 80) print("-" * 80)
# Show isolated components (must test individually due to known issues) # Show isolated components (must test individually due to known issues or direct changes)
isolated_in_tests = [c for c in ISOLATED_COMPONENTS if c in all_tests] isolated_in_tests = [c for c in all_isolated if c in all_tests]
if isolated_in_tests: if isolated_in_tests:
print( predefined_isolated = [c for c in isolated_in_tests if c in ISOLATED_COMPONENTS]
f"\n{len(isolated_in_tests)} components must be tested in isolation (known build issues):" additional_in_tests = [
) c for c in isolated_in_tests if c in (additional_isolated or set())
for comp in sorted(isolated_in_tests): ]
reason = ISOLATED_COMPONENTS[comp]
print(f" - {comp}: {reason}") if predefined_isolated:
print(
f"\n{len(predefined_isolated)} components must be tested in isolation (known build issues):"
)
for comp in sorted(predefined_isolated):
reason = ISOLATED_COMPONENTS[comp]
print(f" - {comp}: {reason}")
if additional_in_tests:
print(
f"\n{len(additional_in_tests)} components tested in isolation (directly changed in PR):"
)
for comp in sorted(additional_in_tests):
print(f" - {comp}")
# Show base bus components (test the bus platform implementations) # Show base bus components (test the bus platform implementations)
base_bus_in_tests = [c for c in BASE_BUS_COMPONENTS if c in all_tests] base_bus_in_tests = [c for c in BASE_BUS_COMPONENTS if c in all_tests]
@@ -733,6 +759,7 @@ def test_components(
esphome_command: str, esphome_command: str,
continue_on_fail: bool, continue_on_fail: bool,
enable_grouping: bool = True, enable_grouping: bool = True,
isolated_components: set[str] | None = None,
) -> int: ) -> int:
"""Test components with optional intelligent grouping. """Test components with optional intelligent grouping.
@@ -742,6 +769,10 @@ def test_components(
esphome_command: ESPHome command (config/compile) esphome_command: ESPHome command (config/compile)
continue_on_fail: Whether to continue on failure continue_on_fail: Whether to continue on failure
enable_grouping: Whether to enable component grouping enable_grouping: Whether to enable component grouping
isolated_components: Set of component names to test in isolation (not grouped).
These are tested WITHOUT --testing-mode to enable full validation
(pin conflicts, etc). This is used in CI for directly changed components
to catch issues that would be missed with --testing-mode.
Returns: Returns:
Exit code (0 for success, 1 for failure) Exit code (0 for success, 1 for failure)
@@ -788,6 +819,7 @@ def test_components(
build_dir=build_dir, build_dir=build_dir,
esphome_command=esphome_command, esphome_command=esphome_command,
continue_on_fail=continue_on_fail, continue_on_fail=continue_on_fail,
additional_isolated=isolated_components,
) )
# Then run individual tests for components not in groups # Then run individual tests for components not in groups
@@ -912,18 +944,30 @@ def main() -> int:
action="store_true", action="store_true",
help="Disable component grouping (test each component individually)", help="Disable component grouping (test each component individually)",
) )
parser.add_argument(
"--isolate",
help="Comma-separated list of components to test in isolation (not grouped with others). "
"These are tested WITHOUT --testing-mode to enable full validation. "
"Used in CI for directly changed components to catch pin conflicts and other issues.",
)
args = parser.parse_args() args = parser.parse_args()
# Parse component patterns # Parse component patterns
component_patterns = [p.strip() for p in args.components.split(",")] component_patterns = [p.strip() for p in args.components.split(",")]
# Parse isolated components
isolated_components = None
if args.isolate:
isolated_components = {c.strip() for c in args.isolate.split(",") if c.strip()}
return test_components( return test_components(
component_patterns=component_patterns, component_patterns=component_patterns,
platform_filter=args.target, platform_filter=args.target,
esphome_command=args.esphome_command, esphome_command=args.esphome_command,
continue_on_fail=args.continue_on_fail, continue_on_fail=args.continue_on_fail,
enable_grouping=not args.no_grouping, enable_grouping=not args.no_grouping,
isolated_components=isolated_components,
) )

View File

@@ -0,0 +1,19 @@
<<: !include common.yaml
esp32_ble_tracker:
max_connections: 9
bluetooth_proxy:
active: true
connection_slots: 9
esp32_hosted:
active_high: true
variant: ESP32C6
reset_pin: GPIO54
cmd_pin: GPIO19
clk_pin: GPIO18
d0_pin: GPIO14
d1_pin: GPIO15
d2_pin: GPIO16
d3_pin: GPIO17

View File

@@ -5,6 +5,7 @@ esp32:
advanced: advanced:
enable_lwip_mdns_queries: true enable_lwip_mdns_queries: true
enable_lwip_bridge_interface: true enable_lwip_bridge_interface: true
disable_libc_locks_in_iram: false # Test explicit opt-out of RAM optimization
wifi: wifi:
ssid: MySSID ssid: MySSID

View File

@@ -4,6 +4,7 @@ esp32:
type: esp-idf type: esp-idf
advanced: advanced:
execute_from_psram: true execute_from_psram: true
disable_libc_locks_in_iram: true # Test default RAM optimization enabled
psram: psram:
mode: octal mode: octal

View File

@@ -73,9 +73,11 @@ def test_main_all_tests_should_run(
mock_should_run_clang_format.return_value = True mock_should_run_clang_format.return_value = True
mock_should_run_python_linters.return_value = True mock_should_run_python_linters.return_value = True
# Mock list-components.py output # Mock list-components.py output (now returns JSON with --changed-with-deps)
mock_result = Mock() mock_result = Mock()
mock_result.stdout = "wifi\napi\nsensor\n" mock_result.stdout = json.dumps(
{"directly_changed": ["wifi", "api"], "all_changed": ["wifi", "api", "sensor"]}
)
mock_subprocess_run.return_value = mock_result mock_subprocess_run.return_value = mock_result
# Run main function with mocked argv # Run main function with mocked argv
@@ -116,7 +118,7 @@ def test_main_no_tests_should_run(
# Mock empty list-components.py output # Mock empty list-components.py output
mock_result = Mock() mock_result = Mock()
mock_result.stdout = "" mock_result.stdout = json.dumps({"directly_changed": [], "all_changed": []})
mock_subprocess_run.return_value = mock_result mock_subprocess_run.return_value = mock_result
# Run main function with mocked argv # Run main function with mocked argv
@@ -177,7 +179,9 @@ def test_main_with_branch_argument(
# Mock list-components.py output # Mock list-components.py output
mock_result = Mock() mock_result = Mock()
mock_result.stdout = "mqtt\n" mock_result.stdout = json.dumps(
{"directly_changed": ["mqtt"], "all_changed": ["mqtt"]}
)
mock_subprocess_run.return_value = mock_result mock_subprocess_run.return_value = mock_result
with patch("sys.argv", ["script.py", "-b", "main"]): with patch("sys.argv", ["script.py", "-b", "main"]):
@@ -192,7 +196,7 @@ def test_main_with_branch_argument(
# Check that list-components.py was called with branch # Check that list-components.py was called with branch
mock_subprocess_run.assert_called_once() mock_subprocess_run.assert_called_once()
call_args = mock_subprocess_run.call_args[0][0] call_args = mock_subprocess_run.call_args[0][0]
assert "--changed" in call_args assert "--changed-with-deps" in call_args
assert "-b" in call_args assert "-b" in call_args
assert "main" in call_args assert "main" in call_args
@@ -411,7 +415,12 @@ def test_main_filters_components_without_tests(
# Mock list-components.py output with 3 components # Mock list-components.py output with 3 components
# wifi: has tests, sensor: has tests, airthings_ble: no tests # wifi: has tests, sensor: has tests, airthings_ble: no tests
mock_result = Mock() mock_result = Mock()
mock_result.stdout = "wifi\nsensor\nairthings_ble\n" mock_result.stdout = json.dumps(
{
"directly_changed": ["wifi", "sensor"],
"all_changed": ["wifi", "sensor", "airthings_ble"],
}
)
mock_subprocess_run.return_value = mock_result mock_subprocess_run.return_value = mock_result
# Create test directory structure # Create test directory structure
@@ -436,6 +445,8 @@ def test_main_filters_components_without_tests(
patch.object(determine_jobs, "root_path", str(tmp_path)), patch.object(determine_jobs, "root_path", str(tmp_path)),
patch("sys.argv", ["determine-jobs.py"]), patch("sys.argv", ["determine-jobs.py"]),
): ):
# Clear the cache since we're mocking root_path
determine_jobs._component_has_tests.cache_clear()
determine_jobs.main() determine_jobs.main()
# Check output # Check output