ESPHome 组件测试公共总线配置与智能分组机制深度解析:以 tests/test_build_components/common 为视角

原创2026-10-01 17:02:431,981 阅读
文章标签:智能家居物联网嵌入式CLI

ESPHome 组件测试公共总线配置与智能分组机制深度解析:以 tests/test_build_components/common 为视角

本文以 ESPHome 仓库中 tests/test_build_components/common 目录及其配套测试脚本为核心,系统讲解 ESPHome 组件测试基础设施中"公共总线配置(common bus configs)+ 智能分组(intelligent grouping)"的设计思路与落地实现。读完本文,你将掌握:为什么组件测试需要标准化 I2C/SPI/UART/Modbus/BLE 总线配置、如何通过 packages 引入公共配置让多个组件共享一条总线、test_build_components.py 等脚本如何自动把兼容组件合并为一次构建以大幅压缩 CI 耗时,以及如何亲手运行与验证组件分组测试。

一、背景:为什么需要"公共总线配置"

ESPHome 仓库包含数百个传感器、执行器组件,每个组件都配有位于 tests/components 下的 YAML 测试夹具(如 test.esp32-idf.yaml、common.yaml)。在引入公共总线配置之前,每个组件测试文件都需要自行声明引脚、总线实例等样板配置,导致两大问题:

  1. 重复配置:同平台下几十上百个组件重复书写相同的 I2C/SPI/UART 引脚定义,改一个平台的引脚需要改动大量文件;
  2. CI 构建爆炸:每个组件各自编译一次固件,组件数量众多时 CI 构建次数居高不下。

公共总线配置的核心理念是:让多个组件共享同一条总线实例,把配置相同(或兼容)的组件自动合并进同一次编译,用一次构建完成原本需要多次构建的验证。

该理念落地的载体就是 tests/test_build_components/common 目录——它存放标准化的总线配置(I2C、SPI、UART、Modbus、BLE、I2S 音频等),供组件测试通过 packages 机制复用。

二、目录结构与总线变体

当前仓库中 tests/test_build_components/common 的实际目录结构如下(较 README 中的示意图更丰富,包含更多平台与变体):

common/
├── i2c/                  # 标准 I2C(默认 50kHz)
│   ├── esp32-idf.yaml / esp32-ard.yaml
│   ├── esp32-c3-idf.yaml / esp32-c3-ard.yaml
│   ├── esp32-s2-idf.yaml / esp32-s2-ard.yaml
│   ├── esp32-s3-idf.yaml / esp32-s3-ard.yaml
│   ├── esp32-p4-idf.yaml
│   ├── esp8266-ard.yaml / rp2040-ard.yaml / bk72xx-ard.yaml
│   ├── host.yaml / nrf52.yaml
├── i2c_low_freq/         # 低频 I2C(10kHz),面向需要慢速时序的传感器
├── i2c_camera/           # 摄像头专用 I2C(esp32-idf)
├── spi/                  # SPI 公共配置(含 rp2350-ard 等新平台)
├── uart/                 # 基础 UART(esp32-idf.yaml 中 baud_rate: 9600)
├── uart_115200/ uart_19200/ uart_38400/ uart_4800/ uart_2400/ uart_1200/ ...
│                         # 不同波特率变体,含奇偶校验/停止位/数据位组合
├── uart_bridge_2/ uart_bridge_4/   # 多 UART 桥接场景
├── modbus/               # Modbus 客户端(通过 packages 内嵌 uart)
├── modbus_server/        # Modbus 服务端(esp32-idf、esp8266-ard)
├── ble/                  # esp32_ble_tracker 公共基础设施
├── i2s_audio/            # I2S 音频总线(仅时钟引脚,ESP32 系列)
├── qspi/                 # QSPI 公共配置(esp32-s3-idf)
├── remote_receiver/ remote_transmitter/   # 红外接收/发射引脚约定
├── test_display/         # 显示组件测试辅助配置
└── README.md             # 本机制的设计文档(本文依据)

每个平台目录下按 平台-框架 命名,例如 esp32-idf(ESP-IDF 框架)、esp32-ard(Arduino 框架)、esp8266-ard、rp2040-ard、bk72xx-ard 等,保证同一总线在不同芯片/框架组合下拥有各自的引脚分配。

公共配置的"标准样板"

以 tests/test_build_components/common/i2c/esp32-idf.yaml 为例,一份公共 I2C 配置只做三件事:定义引脚 substitutions、声明共享总线实例、开启扫描:

# Common I2C configuration for ESP32 IDF tests
substitutions:
  scl_pin: GPIO16
  sda_pin: GPIO17

i2c:
  - id: i2c_bus
    scl: ${scl_pin}
    sda: ${sda_pin}
    scan: true

注意这里总线 ID 统一约定为 i2c_bus,这正是后续组件能引用共享总线的前提。UART 配置同理,tests/test_build_components/common/uart/esp32-idf.yaml 提供 uart_bus(TX=GPIO17、RX=GPIO16、baud_rate: 9600)。README 中记录的 UART 波特率为 19200,而当前仓库在此基础上进一步拆分出了 uart_19200、uart_115200、uart_2400_even 等一系列波特率/校验位变体目录,组件可根据自身时序需求引用对应变体。

三、工作机制:组件测试如何接入共享总线

3.1 通过字典式 packages 引入

每个组件测试夹具(位于 tests/components)需要同时引入公共总线配置和组件自身的 common.yaml。关键约束:必须使用字典风格的 packages: 写法,因为分组脚本只识别字典式 packages;列表式 packages 或顶层 <<: 合并键会导致无法正确批量分组。

以 BH1750 光照传感器为例,tests/components/bh1750/test.esp32-idf.yaml:

packages:
  i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml

<<: !include common.yaml

packages 键的命名约定是:总线包以总线名命名(i2c、spi、uart、modbus),组件自身的公共配置以组件名命名(如 bh1750),这样分组脚本可以通过解析 packages 键快速识别每个组件依赖的总线。

3.2 组件 common.yaml 引用共享总线实例

组件的 tests/components/bh1750/common.yaml 不再自己声明引脚,而是直接引用公共配置提供的总线 ID:

sensor:
  - platform: bh1750
    i2c_id: i2c_bus
    name: Living Room Brightness
    address: 0x23

公共配置向所有组件提供两类资产:

  • 标准化的引脚分配:每个平台一套统一引脚,杜绝组件间引脚冲突;
  • 共享总线实例:i2c_bus、spi_bus、uart_bus、modbus_bus、i2s_audio_bus 等。未显式指定 i2c_id/spi_id/uart_id 的组件会自动使用该总线(配置文件注释中明确写了 "Components will auto-use this bus if they don't specify i2c_id")。

3.3 包依赖:Modbus 自动继承 UART

有些包会内嵌其他包以避免重复。Modbus 依赖 UART 传输,因此 tests/test_build_components/common/modbus/esp32-idf.yaml 在自身 packages 中引入 uart 包:

packages:
  uart: !include ../uart/esp32-idf.yaml  # Modbus requires UART

substitutions:
  flow_control_pin: GPIO4

modbus:
  - id: modbus_bus
    uart_id: uart_bus
    flow_control_pin: ${flow_control_pin}

效果是:引用 modbus 包的组件自动获得 uart 包,无需各自重复引入。

3.4 I2S 音频总线:共享时钟、独享数据线

tests/test_build_components/common/i2s_audio/esp32-idf.yaml 提供了共享的 i2s_audio_bus(仅时钟引脚):

substitutions:
  i2s_bclk_pin: GPIO5
  i2s_lrclk_pin: GPIO4
  i2s_mclk_pin: GPIO15

i2s_audio:
  - id: i2s_audio_bus
    i2s_bclk_pin: ${i2s_bclk_pin}
    i2s_lrclk_pin: ${i2s_lrclk_pin}
    i2s_mclk_pin: ${i2s_mclk_pin}

这里的设计要点是:每个消费者(扬声器/麦克风组件)保留自己的 i2s_dout_pin/i2s_din_pin 替换变量,且必须使用唯一的数据引脚,因为分组后多个扬声器/麦克风会共享同一条 I2S 时钟总线。i2s_audio 组件本身以及孤立的 PDM microphone 则继续内联定义总线、不参与分组。

3.5 BLE:共享 Tracker 基础设施

tests/test_build_components/common/ble/esp32-idf.yaml 提供共享的 esp32_ble_tracker 扫描参数,各 BLE 客户端组件各自定义携带不同 MAC 地址的 ble_client:

esp32_ble_tracker:
  scan_parameters:
    interval: 1100ms
    window: 1100ms
    active: true

四、智能分组:一次构建验证多个组件

4.1 分组签名与合并配置

分组脚本以"总线签名(bus signature)"为分组依据。总线签名由组件测试夹具中的 packages 引用(如 i2c/esp32-idf.yaml)推导而来;引用同一份总线配置的组件即具备相同签名,可自动分到同一组。

分组时,script/merge_component_configs.py 会把多个组件的配置合并为一份"自动生成的合并配置":

# Auto-generated merged config (created by test_build_components.py)
packages:
  i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml

sensor:
  - platform: bme280_i2c
    i2c_id: i2c_bus
    temperature:
      name: BME280 Temperature
  - platform: bh1750
    i2c_id: i2c_bus
    name: BH1750 Illuminance
  - platform: sht3xd
    i2c_id: i2c_bus
    temperature:
      name: SHT3xD Temperature

结果:3 个组件合并为 1 次编译测试,而不是 3 次独立测试。README 中的典型收益是构建次数减少 80%–90%,例如 3 个 I2C 组件合并后节省 2 次构建。

4.2 主测试运行器 test_build_components.py

script/test_build_components.py 是承载智能分组的主测试运行器,其关键流程(对应 test_components → run_grouped_component_tests 调用链)如下:

  1. 扫描测试夹具(find_component_tests,L123):按通配模式遍历 tests/components,收集各组件测试文件;
  2. 分析组件总线(analyze_all_components,来自 script/analyze_component_buses.py):解析每个测试文件使用的公共总线包、是否引用本地文件、是否为平台抽象组件、是否使用 !extend/!remove,据此划分"可分组"与"不可分组";
  3. 构建分组计划:按 (platform, bus_signature) 聚合组件;无总线组件(NO_BUSES_SIGNATURE)被轮询分配到现有组中,前提是与组内既有组件无冲突(components_conflict);
  4. 跨总线合并优化(merge_compatible_bus_groups):总线配置兼容(如 BLE + UART 无冲突)的组可以进一步合并;
  5. 冲突拆分(split_conflicting_groups,来自 script/helpers.py):即使总线签名相同,声明了 CONFLICTS_WITH 的组件(如 bme680_bsec 与会自动加载 bme68x_bsec2 的 bme68x_bsec2_i2c)也会被拆到不同构建中;
  6. 执行分组构建(run_grouped_test,L462):合并配置后以 python -m esphome --testing-mode <command> 方式运行,并对组件列表+平台做 MD5 哈希生成唯一设备名(如 comptestesp32idf<8位哈希>),避免不同分组的构建产物互相冲突;
  7. 兜底单测(run_individual_component_test,L971):未进入任何组的组件逐一编译;配置校验专用文件(validate.*.yaml)永远单独运行,不参与编译分组。

关于 --testing-mode,源码注释(L649-L654)说明了它的语义:分组测试使用 --testing-mode 会关闭引脚冲突检查等校验(以允许多组件合并);而直接改动过的组件(通过 --isolate 指定)则以完整校验模式单独测试,确保 PR 中直接改动的组件能暴露引脚冲突等问题。

4.3 必须单独测试的组件类型

从 script/test_build_components.py 的分组逻辑可以归纳出五类不参与分组、需单独构建的组件:

  • 总线平台组件:i2c、spi、uart、modbus、canbus 等基础总线组件(BASE_BUS_COMPONENTS),它们本身就在测试总线平台的实现;
  • ISOLATED_COMPONENTS 列表中的组件:存在已知构建冲突,README 说明其必须单独测试;
  • 引用本地文件的组件:使用 $component_dir 引用组件目录内文件;
  • 平台抽象组件:IS_PLATFORM_COMPONENT = True 的基类组件;
  • 未迁移到 packages 的组件:直接内联定义总线(源码中标记为 "NEEDS MIGRATION")。

此外,分组脚本会把 validate.*.yaml(仅配置校验、不编译)单独处理,-e compile 模式下不会把它们送进工具链构建。

五、引脚分配总览

README 给出了完整的引脚分配表,这是所有组件测试共用的硬件约定(同一平台内组件测试统一使用下表引脚,从根本上避免引脚冲突):

I2C(标准,50kHz)

平台 SCL SDA
ESP32 IDF GPIO16 GPIO17
ESP32 Arduino GPIO22 GPIO21
ESP32-C3 GPIO5 GPIO4
ESP32-S2/S3 GPIO9 GPIO8
ESP8266 GPIO5 GPIO4
RP2040 GPIO5 GPIO4
BK72xx P20 P21

i2c_low_freq(10kHz)变体引脚分配相同,仅频率降低,面向需要慢速 I2C 时序的传感器。

SPI

平台 CLK MOSI MISO
ESP32 GPIO18 GPIO23 GPIO19
ESP32-C3 GPIO6 GPIO7 GPIO5
ESP32-S2 GPIO36 GPIO35 GPIO37
ESP32-S3 GPIO40 GPIO6 GPIO41
ESP8266 GPIO14 GPIO13 GPIO12
RP2040 GPIO18 GPIO19 GPIO16

CS(片选)引脚为组件特定,每个 SPI 设备需要独立的 CS。

UART

平台 TX RX
ESP32 IDF GPIO17 GPIO16
ESP32-C3 IDF GPIO7 GPIO6
ESP8266 GPIO1 GPIO3
RP2040 GPIO0 GPIO1

Modbus(含 UART)

在对应平台 UART 引脚基础上,统一增加 flow_control_pin: GPIO4(所有平台一致)。

I2S 音频(仅 ESP32 系列)

平台 BCLK LRCLK MCLK
ESP32 IDF / ESP32-S3 IDF GPIO5 GPIO4 GPIO15

消费者组件各自保留独立的 i2s_dout_pin/i2s_din_pin 且数据引脚必须唯一。

BLE

  • ESP32 系列共享 esp32_ble_tracker 基础设施;
  • 每个组件定义使用不同 MAC 地址的独立 ble_client。

六、组件兼容性判定与"可分组化"改造

6.1 可分组组件的判定条件

组件被自动分组需要同时满足:

  • 引用了相同的总线包(如都用 i2c/esp32-idf.yaml);
  • 无本地文件引用($component_dir);
  • 无 !extend / !remove 指令;
  • 正确引用总线 ID(i2c_id: i2c_bus、spi_id: spi_bus 等)。

6.2 不可分组的场景

组件需要单独测试的情况包括:使用了不同总线频率(i2c vs i2c_low_freq)、引用本地文件、属于平台抽象组件、直接定义总线(未迁移到 packages)、或在 ISOLATED_COMPONENTS 列表中。

6.3 让组件"可分组化"的注意事项

警告:在组件测试文件中使用 !extend 或 !remove 指令会阻止 CI 中的自动分组,使构建变慢。 当需要平台特定参数时,应内联完整配置而非使用 !extend/!remove,这样组件才能与其他兼容组件合并分组。

不过 README 同时指出,部分组件合理使用 !extend/!remove 是必要的,例如 adc 组件需要在 ESP8266 上移除仅 ESP32 支持的 attenuation 参数——这类组件会被正确识别为不可分组。

七、测试命令与日常使用

7.1 测试单个组件

# 仅校验配置
./script/test_build_components -c bme280_i2c -t esp32-idf -e config

# 完整编译
./script/test_build_components -c bme280_i2c,bh1750,sht3xd -t esp32-idf -e compile

其中 -c/--components 指定组件(支持逗号分隔),-t/--target 指定平台(如 esp32-idf),-e/--esphome-command 可选 config/compile/clean(默认 compile)。更完整的功能参数可参考 main() 中的参数定义(script/test_build_components.py):-f 失败后继续、--no-grouping 关闭分组、--isolate 指定隔离组件、--base-only 仅测基础夹具、--toolchain 选择工具链、--fail-on-no-tests 无匹配时以非零退出。

7.2 全组件合并测试

./script/test_component_grouping.py -e config --all

该命令在单次构建中测试全部组件,用于发现单独测试时不会暴露的问题:

  • 组件间的 ID 冲突;
  • 组件能否在同一配置中共存;
  • i2c_id、spi_id、uart_id 是否正确指定。

-e config 适合快速校验配置,-e compile 做完整编译测试。

7.3 按总线签名测试组件组

# 所有 I2C 组件一起测试
./script/test_component_grouping.py -s i2c -e config

# 自定义组大小
./script/test_component_grouping.py --min-size 5 --max-size 20 --max-groups 3

八、实现细节:脚本家族与 CI 集成

8.1 相关脚本

脚本 职责
script/analyze_component_buses.py 分析组件测试,检测总线使用情况与分组兼容性(analyze_all_components、create_grouping_signature、merge_compatible_bus_groups 等)
script/merge_component_configs.py 将多个组件配置合并为单个测试文件(merge_component_configs、deduplicate_by_id 等)
script/test_build_components.py 主测试运行器,承载智能分组逻辑
script/test_component_grouping.py 测试指定组件组或全部组件
script/split_components_for_ci.py 将组件拆分为批次供 CI 并行执行(create_intelligent_batches)

辅助支撑:script/helpers.py 提供 parse_test_filename、components_conflict、split_conflicting_groups 等通用工具;ISOLATED_COMPONENTS、BASE_BUS_COMPONENTS、NO_BUSES_SIGNATURE 等常量定义于 script/analyze_component_buses.py。

8.2 CI 集成与日志可读性

按 README 的说明,组件测试在 CI 中以 max-parallel: 5 并行执行批次。同时 test_build_components.py 针对 GitHub Actions 做了日志优化(L91-L120):每次 esphome config/compile 的完整输出被折叠进 ::group:: 可折叠日志块,避免上万行编译日志淹没通过/失败结论;失败时脚本会打印可直接复制粘贴的复现命令,并在 GITHUB_STEP_SUMMARY 中生成 Markdown 汇总(分组测试、独立测试及耗时统计)。

8.3 维护收益

  • 共享总线 = 减少重复:README 统计显示 200+ I2C 组件、60+ SPI 组件、80+ UART 组件、6 个 Modbus 组件(含 UART)复用公共配置;
  • 智能分组降低 CI 耗时:配置相同的组件自动合并,典型情况下构建数减少 80%–90%,且 CI 并行执行进一步摊薄总时长;
  • 集中维护:修改某个平台的引脚只需改一处公共配置文件,所有组件测试同步生效,同时保证全组件引脚分配一致。

九、总结

tests/test_build_components/common 是 ESPHome 组件测试体系的"总线标准化底座":它以字典式 packages 为唯一入口约定,以 i2c_bus/spi_bus/uart_bus/modbus_bus/i2s_audio_bus 等共享总线实例为纽带,配合 script/test_build_components.py 的分组签名分析、配置合并、冲突拆分与 --testing-mode 构建,实现了"多组件共享一次编译"的高效 CI 模式。对组件开发者而言,遵循"用字典式 packages 引入公共总线、内联平台差异、正确引用总线 ID"这三条规则,就能让自己的组件自动获得分组收益;对 CI 基建维护者而言,公共总线目录 + 脚本家族构成了一个可审计、可扩展的测试编排体系,其设计思路同样适用于其他多组件固件项目的测试提速改造。

登录后查看全文
esphome