Compare commits

...

421 Commits

Author SHA1 Message Date
Jesse Hills
2abe272867 Merge pull request #17017 from esphome/bump-2026.6.0b4
2026.6.0b4
2026-06-18 10:08:15 +12:00
Jesse Hills
db6b9166f4 Bump version to 2026.6.0b4 2026-06-18 08:20:15 +12:00
esphome[bot]
7ab95ddcb1 Bump bundled esphome-device-builder to 1.0.6 (#17016) 2026-06-18 08:20:02 +12:00
esphome[bot]
cdd2bfbc60 Bump bundled esphome-device-builder to 1.0.4 (#17013) 2026-06-18 08:19:05 +12:00
esphome[bot]
41f7f8cccb Bump bundled esphome-device-builder to 1.0.3 (#17005) 2026-06-18 08:19:05 +12:00
Jonathan Swoboda
045de436ba [ota] Scale ESP-IDF OTA erase watchdog to image size (#16998) 2026-06-18 08:19:05 +12:00
Jonathan Swoboda
24e276c3f9 [esp32_hosted] Bump esp_hosted to 2.12.9 (#16999) 2026-06-18 08:18:25 +12:00
Jesse Hills
9e768bb510 Merge pull request #16997 from esphome/bump-2026.6.0b3
2026.6.0b3
2026-06-16 23:52:22 +12:00
Jesse Hills
53fd99578a Bump version to 2026.6.0b3 2026-06-16 23:02:55 +12:00
Jesse Hills
310baab524 [docker] Bundle device-builder 1.0.1, make HA add-on builder-only (#16989)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-06-16 23:02:55 +12:00
Jesse Hills
0422b581cb [core] Stop parent git repos from breaking ESP-IDF/PlatformIO builds (#16994) 2026-06-16 23:02:55 +12:00
Jesse Hills
0ce89c17ab [ci] Push branch-tagged docker images to ghcr.io for local testing (#16992) 2026-06-16 23:02:55 +12:00
Jesse Hills
66be793cd8 [docker] Remove alpine base, build only on debian (#16991) 2026-06-16 23:02:54 +12:00
Jonathan Swoboda
1d38498ca7 [openthread] Fix InstanceLock releasing the lock twice on try_acquire (#16980) 2026-06-16 23:02:54 +12:00
Kevin Ahrendt
aef9b5b72f [audio] Bump microMP3 to v0.2.3 (#16977) 2026-06-16 23:02:54 +12:00
J. Nick Koston
9bf35ab8fb [core] Attribute "took a long time" blocking warning to the owning script (#16768) 2026-06-16 23:02:54 +12:00
Clyde Stubbs
33ace9d698 [mipi_dsi] Add SWRESET command to M5Stack Tab5-V2 init sequence (#16975) 2026-06-16 23:02:54 +12:00
Jesse Hills
32ab3abd7c [psram] Make schema extractable with per-variant options (#16949)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-06-16 23:02:54 +12:00
Jesse Hills
94b248527d Merge pull request #16948 from esphome/bump-2026.6.0b2
2026.6.0b2
2026-06-15 12:05:19 +12:00
Jesse Hills
a46aa594b3 Bump version to 2026.6.0b2 2026-06-15 11:04:46 +12:00
Jonathan Swoboda
99425e3a97 [esp32] Add flash_mode and flash_frequency config options (#16920)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-06-15 11:04:46 +12:00
Jonathan Swoboda
f83e3ad6a6 [core] Support platformio_options on the native ESP-IDF toolchain (#16917) 2026-06-15 11:04:46 +12:00
Jonathan Swoboda
c768e2eabc [esp32] Fix idedata generation failing on unset ESPHOME_ARDUINO (#16925) 2026-06-15 11:04:46 +12:00
Clyde Stubbs
9ffd350095 [mipi_spi] Implement automatic mapping of offsets (#16722)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-15 11:04:46 +12:00
Clyde Stubbs
26ccaf70db [lvgl] Fix schema extraction (#16895)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:04:46 +12:00
Tobiasz Jakubowski
20925b3220 [spi] Skip logging on begin_transaction() of an auto-releasing write-only SPI device (#16921)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-06-15 11:04:46 +12:00
J. Nick Koston
83504d2de2 [esp8266] Decode crash handler PC and backtrace in logs (#16911) 2026-06-15 11:04:46 +12:00
Jesse Hills
acbb662316 Merge pull request #16912 from esphome/bump-2026.6.0b1
2026.6.0b1
2026-06-11 16:06:59 +12:00
Jesse Hills
abf6212a5a [tests] Mock target branch in memory-impact exclusion test (#16913) 2026-06-11 14:05:10 +12:00
Jesse Hills
4dbc5ce920 Bump version to 2026.6.0b1 2026-06-11 12:41:19 +12:00
Keith Burzinski
92c82f3d25 [improv_serial] Report stopped state when Wi-Fi is disabled (#16904)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 10:26:50 +12:00
Kevin Ahrendt
77009cfafe [resampler] Allow resampler to passthrough bits per sample instead of converting (#16892)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-11 10:21:04 +12:00
dependabot[bot]
cd7e54dbf2 Bump cryptography from 48.0.0 to 48.0.1 (#16909)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 16:32:17 -04:00
Jonathan Swoboda
29a79b1373 [core] Make set_cpp_standard work on the native IDF toolchain (#16907) 2026-06-11 07:25:03 +12:00
Clyde Stubbs
dafc3560dd [tests] Isolate ESPHOME_LOG_STATES in main logs-states tests (#16905)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 07:44:58 -04:00
Clyde Stubbs
a25ac28ae5 [lsm6ds] Add motion platform for STMicro LSM6DS IMU (#16232)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-06-10 01:22:44 +00:00
Jonathan Swoboda
6809af3de0 [espidf] Warn when the install path is too long for Windows MAX_PATH (#16896) 2026-06-10 13:12:28 +12:00
Jonathan Swoboda
e16a877745 [platformio] De-duplicate non-ESP32 lib_deps into common:idf-component-libs (#16893) 2026-06-10 13:11:11 +12:00
Jonathan Swoboda
4963ddcb95 [espidf] Fix idedata generation on Windows (#16894) 2026-06-10 13:09:50 +12:00
Clyde Stubbs
4f62bb7171 [bmi270] Support Bosch BMI270 IMU (#16202)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-06-10 11:04:28 +10:00
Ricky Tsai
eb6d6eac7d [xdb401] XDB401 Pressure Sensor (#15108)
Co-authored-by: Ricky Tsai <ricky@rtnztech.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-06-09 19:58:02 -04:00
dependabot[bot]
7533835e04 Bump py7zr from 0.22.0 to 1.1.0 (#16901)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 16:03:31 -04:00
Oliver Kleinecke
2310b9e3fe [usb_uart] Add Prolific PL2303 USB-serial driver (#16885) 2026-06-10 04:27:37 +10:00
Tomáš Lohynský
8206df6e4e [dlms_meter] dlms_parser library (#15458)
Co-authored-by: PolarGoose <35307286+PolarGoose@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-09 08:57:13 -04:00
tomaszduda23
5faed9d5f5 [nrf52] native build - download toolchain and sdk in venv (#16388)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <swoboda1337@users.noreply.github.com>
2026-06-09 07:04:51 -04:00
PolarGoose
25d656d468 [dsmr] Update dsmr_parser library to 1.9.0 (#16881) 2026-06-09 07:04:10 -04:00
Remco van Essen
cdc63f0fed [pcm5122] Add PCM5122 audio DAC component (#15709)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: kbx81 <kbx81x@gmail.com>
2026-06-09 01:33:15 -05:00
Clyde Stubbs
ddd21ba442 [mipi_spi] add WAVESHARE-ESP32-S3-TOUCH-AMOLED-2.16 (#16887) 2026-06-09 13:06:13 +10:00
Jonathan Swoboda
a32817207c [ade7880] Fix reverse active energy reading from reserved register (#16822) 2026-06-08 20:30:03 -04:00
Jonathan Swoboda
6e01f3fccd [heatpumpir] Bump tonia/HeatpumpIR to 1.0.42 (#16880) 2026-06-08 20:29:35 -04:00
dependabot[bot]
e0072ef4c5 Bump tornado from 6.5.6 to 6.5.7 (#16883)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 19:22:48 -05:00
dependabot[bot]
b21a69f07a Bump codecov/codecov-action from 6.0.1 to 7.0.0 (#16884)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 08:08:39 +12:00
J. Nick Koston
36e043debb [tests] Fail component test merge on conflicting duplicate IDs (#16849) 2026-06-08 12:49:25 -05:00
Kevin P. Fleming
54c73bf1bc [ade7880][airthings_wave_base] Remove kpfleming from CODEOWNERS (#16858) 2026-06-08 09:04:09 -04:00
J. Nick Koston
cbc3770b11 Include model-driven display schemas in the language schema dump (#16872) 2026-06-07 17:30:43 -05:00
Jonathan Swoboda
64fc09646c [esp32] Fix clang-tidy on ESP-IDF 6 (#16850) 2026-06-06 20:00:42 -04:00
Jonathan Swoboda
8400bab926 [esp32] Make no-default-board variant test explicit about platformio toolchain (#16847) 2026-06-06 19:58:25 -04:00
Clyde Stubbs
745db9f705 [motion] Implement hub component for IMUs (#16226)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-06 18:10:08 +10:00
Jonathan Swoboda
6996b7ed1c [ci] Add ESP32 Variants clang-tidy run (S3/P4/C6) (#16825) 2026-06-05 22:03:08 -04:00
Jonathan Swoboda
8aa4157574 [fastled_base] Use FastLED IDF component on ESP32 (#16804) 2026-06-05 21:01:29 -04:00
J. Nick Koston
2a4913713a Revert "[tests] Fail component test merge on conflicting duplicate IDs" (#16848) 2026-06-05 19:37:19 -05:00
J. Nick Koston
8f8a70b2be Exit nginx bypass placeholder cleanly on SIGTERM (#16845) 2026-06-05 17:58:50 -05:00
J. Nick Koston
70d9ab25f3 [tests] Fail component test merge on conflicting duplicate IDs (#16795) 2026-06-05 17:57:42 -05:00
Jonathan Swoboda
f18cf954ba [improv_serial] Fix build on ESP32-C5/P4 and simplify variant guards (#16833) 2026-06-05 17:30:26 -05:00
Jonathan Swoboda
85fd83288d [esp32_camera] Bump esp32-camera to 2.1.7 (#16846) 2026-06-05 17:29:33 -05:00
Clyde Stubbs
93334d4e60 [scripts] Fix build_language_schema (#16816)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-05 21:44:31 +00:00
i-am-no-magic
913b9f5ca4 [tuya] Fixed hysteresis bug for Tuya climate (#16832)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-06-05 17:37:40 -04:00
Kevin Ahrendt
b63e327ae3 [audio] Deprecate unused scale_audio_samples helper (#16831) 2026-06-05 17:22:03 -04:00
Jonathan Swoboda
77f644f576 [ci] Share a cached native ESP-IDF install across clang-tidy and build jobs (#16841) 2026-06-05 16:42:51 -04:00
Ross Tyler
4cb6f2c046 [qmp6988] fix publishing bogus zero values on i2c error (#16840) 2026-06-05 14:35:33 -04:00
Ross Tyler
2ab4399ae5 [qmp6988] fix false report of software reset error (#16843) 2026-06-05 14:31:17 -04:00
Jonathan Swoboda
aa11ddb333 [zigbee][openthread][esp32_hosted] Fix clang-tidy findings (#16838) 2026-06-05 14:22:55 -04:00
Jonathan Swoboda
2b581ecd3c [esp32] Bump platform to 55.03.39, Arduino to 3.3.9 (#16803) 2026-06-05 14:22:20 -04:00
Jonathan Swoboda
42cf421f5c [usb_uart] Fix clang-tidy findings (#16835) 2026-06-05 12:03:22 -04:00
Jonathan Swoboda
351b986896 [ci] Make ESP32 IDF the comprehensive clang-tidy pass (#16823)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-06-05 12:01:53 -04:00
Jonathan Swoboda
b0e1b94c45 [mipi_dsi][mipi_rgb][st7701s][rpi_dpi_rgb] Fix clang-tidy findings (#16837) 2026-06-05 11:56:48 -04:00
Jonathan Swoboda
80c84d6665 [usb_host][usb_cdc_acm][tinyusb] Fix clang-tidy findings (#16836) 2026-06-05 11:55:39 -04:00
J. Nick Koston
61bb1805b1 [api] Fix nullptr deref when client teardown reenters state dispatch (#16834) 2026-06-05 10:47:32 -05:00
Jesse Hills
cbd3aaa1e0 [ci] Add codecov.yml to enforce 100% patch coverage on PRs (#16827) 2026-06-05 19:40:18 +12:00
Oliver Kleinecke
e209a3fa91 [usb_uart] Add FTDI FT23XX USB UART driver (#14587)
Co-authored-by: Oliver Kleinecke <kleinecke.oliver@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-06-05 13:57:05 +10:00
Jesse Hills
d72f119dd2 Merge branch 'release' into dev 2026-06-05 14:31:50 +12:00
Jesse Hills
0cd3734148 Merge pull request #16817 from esphome/bump-2026.5.3
2026.5.3
2026-06-05 14:31:04 +12:00
Jonathan Swoboda
ea3ac1ee96 [audio] Bump esp-audio-libs to v3.2.1 (#16818) 2026-06-04 20:02:22 -04:00
Jesse Hills
7f3feec3a3 Bump version to 2026.5.3 2026-06-05 11:11:36 +12:00
Jonathan Swoboda
bcf5606b31 [esp32_ble_server] Fix duplicate Device Information Service with string UUIDs (#16784) 2026-06-05 11:11:36 +12:00
Jonathan Swoboda
5662e1b7cd [rp2040] Fix lwipopts template load on Windows extended-length paths (#16783) 2026-06-05 11:11:35 +12:00
Jonathan Swoboda
375ecdfb2c [esp32][core] Restore ESP-IDF version on logs/upload fast path and clean build on framework change (#16770) 2026-06-05 11:11:25 +12:00
Jonathan Swoboda
a5b4a7cd51 [remote_base] Fix RC5 decoding at either receive polarity (#16767) 2026-06-05 11:07:33 +12:00
dependabot[bot]
772cae445f Bump github/codeql-action from 4.36.1 to 4.36.2 (#16808)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 17:30:29 -05:00
dependabot[bot]
ef64d27ed4 Bump ruff from 0.15.15 to 0.15.16 (#16807)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 17:30:18 -05:00
Jesse Hills
a8032054ea [light] Pass light reference into lambda light effect (#16815) 2026-06-05 10:28:50 +12:00
J. Nick Koston
9fbd4c38ae [i2s_audio] Move test bus into a shared package and give fixtures unique ids (#16793) 2026-06-04 17:28:32 -05:00
Jonathan Swoboda
82efa45187 [multiple] Avoid float-to-double promotion in math calls (#16812) 2026-06-04 18:16:18 -04:00
Jonathan Swoboda
d2c388f893 [ota][logger][esp32][internal_temperature] Fix clang-tidy findings surfaced by RISC-V analysis (#16811) 2026-06-04 18:01:00 -04:00
Jonathan Swoboda
5288767abf [clang-tidy] Add --exclude-grep to skip files by content (#16813) 2026-06-04 17:58:22 -04:00
Jonathan Swoboda
e2459a3923 [clang-tidy] Support RISC-V targets natively (#16809) 2026-06-04 17:57:56 -04:00
Kevin Ahrendt
419bde18b0 [audio] Bump esp-audio-libs to v3.2.0 (#16806) 2026-06-04 16:24:47 -04:00
Jonathan Swoboda
148a5ba68e [esp32] Run clang-tidy via the native ESP-IDF toolchain (#16748) 2026-06-04 11:47:42 -04:00
J. Nick Koston
c765e22622 [ci] Exclude device-builder slow e2e tests from downstream CI (#16801) 2026-06-04 10:39:54 -05:00
Clyde Stubbs
1734dc85d2 [const][animation][dfplayer] Extract CONF_LOOP to const (#16797) 2026-06-04 07:42:01 -04:00
Jonathan Swoboda
891ec33c94 [esp32] Deduplicate PlatformIO library conversion by resolving the batch together (#16756) 2026-06-04 07:39:17 -04:00
Jonathan Swoboda
ffaa31febc [clang-tidy] Hash idf_component.yml and trigger hash hook on more inputs (#16753)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-06-03 23:48:34 -04:00
Jonathan Swoboda
53d685f242 [mixer] Give mixer test its own speaker id to avoid CI grouping collision (#16792) 2026-06-03 23:37:29 -04:00
J. Nick Koston
3e562b9267 [ci] Fix memory impact build selecting unbuildable platform (#16788) 2026-06-03 22:30:36 -05:00
dependabot[bot]
d47f6b896e Bump astral-sh/setup-uv from 8.1.0 to 8.2.0 in /.github/actions/restore-python (#16790)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 22:06:59 -05:00
dependabot[bot]
a02b9c3796 Bump astral-sh/setup-uv from 8.1.0 to 8.2.0 (#16791)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 22:06:45 -05:00
Jesse Hills
93f25258ee [config] Add --no-defaults flag to config command (#16718) 2026-06-04 14:08:36 +12:00
Jonathan Swoboda
0d7d091e71 [esp32_ble_server] Fix duplicate Device Information Service with string UUIDs (#16784) 2026-06-04 09:46:17 +12:00
Jonathan Swoboda
0fcfd1e3d6 [rp2040] Fix lwipopts template load on Windows extended-length paths (#16783) 2026-06-04 09:46:08 +12:00
Jonathan Swoboda
74a1ff9fc7 [esp32][core] Restore ESP-IDF version on logs/upload fast path and clean build on framework change (#16770) 2026-06-04 09:46:01 +12:00
Jonathan Swoboda
78d8a93fff [remote_base] Fix RC5 decoding at either receive polarity (#16767) 2026-06-04 09:45:56 +12:00
Jon Little
92819d8658 [logger] Fix USB JTAG VFS symbols linked when logging is disabled (#15721)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-03 16:54:34 -04:00
Clyde Stubbs
7b8cbe2de1 [sdl] Add option to choose display screen (#16363) 2026-06-03 14:18:16 -04:00
Leonardo Rivera
3b0f669f47 [gree] Fix HEAT_COOL advertised when supports_heat is false; restrict YAN swing to vertical (#16199) 2026-06-03 13:38:00 -04:00
dependabot[bot]
87735d71a0 Bump actions/checkout from 6.0.2 to 6.0.3 (#16776)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-03 11:46:22 -04:00
dependabot[bot]
eba70dc193 Bump github/codeql-action from 4.36.0 to 4.36.1 (#16775)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-03 11:46:07 -04:00
dependabot[bot]
712ef2ec0e Bump esptool from 5.2.0 to 5.3.0 (#16774)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-03 11:45:52 -04:00
Clyde Stubbs
2009f6cc5f [lvgl] Fix indicator updates (#16780) 2026-06-03 11:43:48 -04:00
PolarGoose
89ddd34cb9 [dmsr] [breaking] Fix decryption that uses custom auth key. Add CRC to telegram sensor. Automatic hex string detection in equipment_id fields. Support EON Hungary smart meters (#16561) 2026-06-03 10:55:51 -04:00
Jonathan Swoboda
e4980713d1 [core] esphome clean wipes the whole build directory (#16772) 2026-06-03 07:35:23 -04:00
Clyde Stubbs
997ab11687 [lvgl][mipi_spi][mipi_rgb][mipi_dsi][display] Metadata (#16702) 2026-06-03 15:21:33 +10:00
Jesse Hills
792e1ff304 [i2c] Add basic host platform support (#14489)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-03 07:12:50 +12:00
Kevin Ahrendt
063770bcf4 [i2s_audio] Fix speaker DMA buffer sizing and validate bit depth at compile time (#16672) 2026-06-02 09:32:27 -04:00
Jesse Hills
6197282f1a Merge branch 'release' into dev 2026-06-02 15:40:28 +12:00
Jesse Hills
9c0ffee020 Merge pull request #16760 from esphome/bump-2026.5.2
2026.5.2
2026-06-02 15:39:40 +12:00
Bonne Eggleston
1740e54105 [ci] Fix auto label platform restructure false positive (#16734)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-01 23:20:18 -04:00
Jesse Hills
070c14b04a Bump version to 2026.5.2 2026-06-02 14:33:41 +12:00
J. Nick Koston
559cfd1555 [api] Fix crash loop on VoiceAssistantConfigurationRequest (#16757) 2026-06-02 14:33:41 +12:00
Jonathan Swoboda
571a12ffe5 [core] Clean build when the toolchain changes (#16744) 2026-06-02 14:33:41 +12:00
J. Nick Koston
a4d247fa0a [core] Persist esphome.area in StorageJSON (#16710) 2026-06-02 14:33:41 +12:00
Fyleo
8e57894af7 [sx126x] fix a typo in image calibration on 863 - 870 Mhz frequency (#16731) 2026-06-02 14:33:41 +12:00
J. Nick Koston
f9aba18f8e [libretiny] Fix RTL8710B IRAM_ATTR section being dropped from flashed image (#16616) 2026-06-02 14:33:41 +12:00
Jesse Hills
a04f6da814 [packages] Resolve git symlinks on Windows when materialized as text (#16657) 2026-06-02 14:33:41 +12:00
Jonathan Swoboda
3f57117efd [esp32] Decode crash PCs via IDF toolchain on IDF builds (#16626) 2026-06-02 14:33:41 +12:00
J. Nick Koston
d7f809181a [writer] Mark storage_should_clean as public API for device-builder (#16443) 2026-06-02 14:33:41 +12:00
Clyde Stubbs
d7d20f4f6b [cli] Allow state reporting control via env (#16746) 2026-06-02 07:04:35 +10:00
J. Nick Koston
ab46f8bd74 [api] Fix crash loop on VoiceAssistantConfigurationRequest (#16757) 2026-06-01 20:32:23 +00:00
Keith Burzinski
2454ad1645 [ethernet] Add enable_on_boot lifecycle + lazy-init to reclaim DMA-capable SRAM (#16607)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-01 15:30:07 -05:00
Keith Burzinski
4e48682468 [wifi] Defer esp_wifi_init() to lazy-init so enable_on_boot: false actually saves RAM (#16606)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 14:18:29 -05:00
Clyde Stubbs
805aa252d5 [const] Move CONF_SHA256 to common code (#16751) 2026-06-01 13:30:05 -04:00
Jonathan Swoboda
6116d10ab1 [espidf] Derive idedata from the native ESP-IDF compile_commands.json (#16742) 2026-05-31 17:44:12 -04:00
Jonathan Swoboda
48844a68ba [core] Clean build when the toolchain changes (#16744) 2026-05-31 16:29:16 -04:00
Jonathan Swoboda
7865dc33bc [ethernet] Bump espressif/dm9051 to 1.1.0 (#16735) 2026-05-31 09:50:17 -05:00
Jonathan Swoboda
bf62124032 [esp32] Refine ESP-IDF framework version suffix handling (#16726) 2026-05-30 07:43:21 -04:00
dependabot[bot]
95397948b9 Bump CodSpeedHQ/action from 4.15.1 to 4.17.0 (#16730)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-30 00:09:27 -05:00
J. Nick Koston
f0202155b3 [core] Persist esphome.area in StorageJSON (#16710) 2026-05-30 00:09:07 -05:00
Fyleo
07a57d7557 [sx126x] fix a typo in image calibration on 863 - 870 Mhz frequency (#16731) 2026-05-29 23:03:42 -04:00
Jonathan Swoboda
091a05ccde [esp32_camera] Enable PicolibC Newlib compatibility on IDF 6.0+ (#16703) 2026-05-29 05:16:55 +00:00
Jonathan Swoboda
dd961156d0 [ledc] Adapt to LEDC LL API changes in ESP-IDF 6.1 (#16697) 2026-05-29 00:54:14 -04:00
Jonathan Swoboda
10abb0647c [esp32] Add ESP32-S31, ESP32-H4 and ESP32-H21 variant scaffolding (#16700) 2026-05-29 00:30:52 -04:00
Jonathan Swoboda
a85f8ad935 [core] Use esp_rom_crc.h public API instead of legacy rom/crc.h (#16698) 2026-05-29 00:28:08 -04:00
dependabot[bot]
8945550c6c Bump ruff from 0.15.14 to 0.15.15 (#16712)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-29 03:35:37 +00:00
dependabot[bot]
4b8e06b5bc Bump tornado from 6.5.5 to 6.5.6 (#16704)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-28 09:12:35 -04:00
Mischa Siekmann
f41866a9b8 [gpio][binary_sensor] Fix pin validation for external GPIO pins (#16528) 2026-05-28 09:11:48 -04:00
tomaszduda23
5732d7135f [network] move ipv6 enforcement to validation step (#16701) 2026-05-28 08:39:11 -04:00
Jesse Hills
ec597bfc03 [docs] Update esphome-docs references to esphome.io after repo rename (#16705) 2026-05-28 14:54:42 +12:00
rwrozelle
9a6157b469 [tests] Sandbox PlatformIO paths in test_writer to fix xdist race (#16619)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-05-27 15:50:43 -04:00
GuzTech
ac29fad120 [growatt_solar] Replace hard coded register addresses with constexpr (#16581) 2026-05-27 14:21:50 -04:00
SoCuul
e87190edb4 [midea] fix casing of custom fan modes (#16419) 2026-05-27 14:20:00 -04:00
Elvin Luff
911e330c09 [core] Add Codeberg as a supported git url (#16501) 2026-05-27 14:13:03 -04:00
Jonathan Swoboda
e64b6bc398 [esp32] Stub arduino-esp32 with INTERFACE re-export to framework (#16695) 2026-05-27 11:00:51 -04:00
J. Nick Koston
21e548f1d7 [core] Sensitive redaction via yaml_util representer (#16690) 2026-05-27 09:20:50 -05:00
J. Nick Koston
3cc875c40b [core] Enable ruff BLE (flake8-blind-except) lint family (#16659) 2026-05-27 20:09:57 +12:00
Ardumine
7463a15c7e [network] Add Zephyr IPv6 networking support for nRF52 (#16336)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: tomaszduda23 <tomaszduda23@gmail.com>
2026-05-27 07:43:38 +00:00
dependabot[bot]
8d19c55be2 Bump pytest-asyncio from 1.3.0 to 1.4.0 (#16687)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 19:58:13 -05:00
dependabot[bot]
87d0e24d19 Bump aioesphomeapi from 45.2.2 to 45.3.1 (#16688)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 19:57:29 -05:00
J. Nick Koston
91ead4ff54 [core] Mark canonical sensitive fields with cv.sensitive (#16677) 2026-05-27 00:16:47 +00:00
Jonathan Swoboda
a6ef67aa65 [text_sensor] Remove deprecated public raw_state member (#16683) 2026-05-26 23:34:52 +00:00
Jonathan Swoboda
e174c44b28 [neopixelbus] Deprecate on ESP32 (#16676) 2026-05-26 19:15:25 -04:00
Jonathan Swoboda
f728cb4373 [core] Remove deprecated seq/gens templates (#16685) 2026-05-26 18:50:20 -04:00
Jonathan Swoboda
6c4a8a3245 [dsmr] Force BearSSL on ESP8266 to avoid mbedtls link failure (#16686) 2026-05-26 22:49:44 +00:00
Jonathan Swoboda
eb1196c6b2 [nfc] Remove deprecated heap-allocating format helpers (#16684) 2026-05-26 18:48:17 -04:00
Jonathan Swoboda
fb0b73980b [wifi] Default ESP8266 min_auth_mode to WPA2 (#16682) 2026-05-26 18:47:40 -04:00
Jonathan Swoboda
171ded35a5 [core] Remove cv.only_with_esp_idf and CORE.using_esp_idf (#16681) 2026-05-26 18:47:16 -04:00
Jonathan Swoboda
b71d445e79 [core] Remove deprecated const char* mark_failed/status_set_error (#16680) 2026-05-26 18:46:45 -04:00
Jonathan Swoboda
4d908798bc [core] Remove deprecated custom_components folder loading (#16679) 2026-05-26 18:45:50 -04:00
Clyde Stubbs
62b3b1cc75 [lvgl] Support rounded property for meter arcs (#16669) 2026-05-26 15:30:08 -05:00
J. Nick Koston
52ead52ef2 [core] Enable ruff PGH (pygrep-hooks) lint family (#16651) 2026-05-26 15:29:54 -05:00
J. Nick Koston
96816e2491 [core] Enable ruff DTZ (flake8-datetimez) lint family (#16660) 2026-05-26 15:29:38 -05:00
J. Nick Koston
bac62cb7de [core] Add cv.sensitive marker for schema-level sensitive fields (#16673) 2026-05-26 15:29:06 -05:00
Kevin Ahrendt
722cbfe843 [voice_assistant] Never send zero-length audio to Home Assistant (#16634) 2026-05-26 14:05:57 -04:00
J. Nick Koston
88b12a1c45 [lvgl] Build automation_schema event validators lazily (#16633) 2026-05-26 08:41:54 -05:00
J. Nick Koston
ceb9d406e1 [core] Enable ruff PIE (flake8-pie) lint family (#16658) 2026-05-26 07:46:44 -04:00
J. Nick Koston
8b62cfded7 [libretiny] Fix RTL8710B IRAM_ATTR section being dropped from flashed image (#16616) 2026-05-26 19:57:44 +12:00
Jesse Hills
423b60c90c [packages] Resolve git symlinks on Windows when materialized as text (#16657) 2026-05-26 19:56:44 +12:00
J. Nick Koston
ae74920b81 [core] Enable ruff PTH (flake8-use-pathlib) lint family (#16661) 2026-05-26 05:14:42 +00:00
J. Nick Koston
ae814cff5c [core] Enable ruff B (flake8-bugbear) lint family (#16655) 2026-05-26 02:28:14 +00:00
J. Nick Koston
489cf483d0 [core] Enable ruff PYI (flake8-pyi) lint family (#16654) 2026-05-25 20:55:35 -05:00
J. Nick Koston
dd0028c1b5 [core] Enable ruff G (flake8-logging-format) lint family (#16650) 2026-05-26 01:36:49 +00:00
J. Nick Koston
e492f8f8b6 [tests] Disable hypothesis deadline on flaky IP address test (#16652) 2026-05-25 20:14:36 -05:00
J. Nick Koston
b39b34bfe1 [core] Enable ruff C4 (flake8-comprehensions) lint family (#16653) 2026-05-25 20:14:26 -05:00
J. Nick Koston
bbc24ab546 [core] Enable ruff RSE (flake8-raise) lint family (#16649) 2026-05-25 20:06:34 -05:00
J. Nick Koston
f1839489dd [core] Enable ruff ISC (flake8-implicit-str-concat) lint family (#16646) 2026-05-25 20:06:18 -05:00
J. Nick Koston
5172227931 [core] Enable ruff SLOT (flake8-slots) lint family (#16647) 2026-05-25 20:06:01 -05:00
J. Nick Koston
97267105e1 [core] Enable ruff EXE (flake8-executable) lint family (#16648) 2026-05-25 20:05:51 -05:00
J. Nick Koston
8645f3672d [core] Enable additional zero-violation ruff lint families (#16645) 2026-05-25 18:11:40 -05:00
Boris Krivonog
a257edba62 [mitsubishi_cn105] Add basic swing support (#15653)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-25 16:46:33 -05:00
Jonathan Swoboda
61e8830a3c [espidf] Keep cmake output filter working when IDF writes raw bytes (#16642) 2026-05-25 17:40:38 -04:00
Jonathan Swoboda
fc0a4e2201 [espidf] Support github:// and https://github.com/.../.git framework sources (#16639) 2026-05-25 17:07:35 -04:00
Clyde Stubbs
0b780f1fd2 [time][homeassistant] Fix timezone handling (#16583) 2026-05-26 06:51:15 +10:00
Kevin Ahrendt
dcc30f8651 [router] Share a single I2S bus in test (#16637) 2026-05-25 15:39:54 -04:00
Kevin Ahrendt
892e116680 [router] Add a router speaker component to runtime choose output speaker (#16592) 2026-05-25 12:42:49 -04:00
Kevin Ahrendt
1c7ae96e42 [micro_wake_word] Allow task stack to be allocated in PSRAM (#16632) 2026-05-25 11:04:26 -04:00
Jonathan Swoboda
684bce8b9a [esp32] Decode crash PCs via IDF toolchain on IDF builds (#16626) 2026-05-25 14:36:41 +00:00
Kevin Ahrendt
7c494fd3ef [psram] Consolidate task stack in PSRAM handling (#16628) 2026-05-25 10:15:51 -04:00
Jonathan Swoboda
cf1fabe6d4 [esp32_hosted] Bump esp_hosted to 2.12.8 and add use_psram option (#16627) 2026-05-25 10:11:31 -04:00
J. Nick Koston
cde52ef75e [lvgl] Merge dict-extend chains to speed up schema construction (#16614) 2026-05-25 09:09:54 -05:00
Jonathan Swoboda
98e7213387 [espidf] Warn instead of skipping libraries with framework mismatch (#16630) 2026-05-25 14:08:16 +00:00
Jonathan Swoboda
e7ab78366d [core] Add esphome.build_flags option for IDF + PlatformIO (#16629) 2026-05-25 10:03:38 -04:00
J. Nick Koston
e0167e9bdf [lvgl] Memoize obj_schema by widget_type (#16615) 2026-05-24 20:17:51 -05:00
Jesse Hills
62b0a93e5e [rp2040] Add variant config option for RP2040/RP2350 (#16602) 2026-05-25 10:43:39 +12:00
Jesse Hills
1fb8c26704 Merge branch 'release' into dev 2026-05-25 10:43:04 +12:00
Jesse Hills
3d1a614e55 Merge pull request #16610 from esphome/bump-2026.5.1
2026.5.1
2026-05-25 10:42:20 +12:00
dependabot[bot]
917ffc3797 Bump aioesphomeapi from 45.0.4 to 45.2.2 (#16611)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-24 21:49:52 +00:00
J. Nick Koston
090f5a486a Lift dependabot pip open PR limit (#16609) 2026-05-24 16:32:47 -05:00
Jesse Hills
03e2eb4b4a Bump version to 2026.5.1 2026-05-25 09:28:49 +12:00
Jonathan Swoboda
ddd353d105 [esp32] Disable IDF's COMPILER_DISABLE_DEFAULT_ERRORS so -Wno-error actually undoes -Werror (#16604) 2026-05-25 09:28:49 +12:00
Jonathan Swoboda
9a34a6aabb [esp32] Replace per-class -Wno-error=X demotes with blanket -Wno-error for ESP-IDF toolchain (#16599) 2026-05-25 09:28:49 +12:00
J. Nick Koston
0babc52472 [bluetooth_proxy] Recover slot stuck in DISCONNECTING when CLOSE_EVT is dropped (#16588) 2026-05-25 09:28:49 +12:00
Jonathan Swoboda
adde7681e8 [esp32] Demote IDF #warning deprecations from error under ESP-IDF toolchain (#16584) 2026-05-25 09:28:49 +12:00
J. Nick Koston
8f6ea62628 [uart] Wake main loop on ESP8266 software serial RX (#16562) 2026-05-25 09:28:49 +12:00
J. Nick Koston
4e7bc92061 [esp8266] Use os_timer-based esp_delay() in delay() (#16563) 2026-05-25 09:28:49 +12:00
Edvard Filistovič
1f4a061572 [libretiny] Fix LN882H IRAM_ATTR injection point in patch_linker.py (#16570) 2026-05-25 09:28:49 +12:00
J. Nick Koston
59db9a4673 [dashboard] Fix flaky test_websocket_refresh_command on Windows CI (#16565) 2026-05-25 09:28:49 +12:00
Kevin Ahrendt
7ae5566472 [sendspin] Bump sendspin-cpp to v0.6.1 (#16553) 2026-05-25 09:28:49 +12:00
J. Nick Koston
f247def4ac [core] Refresh compiled config cache after upload/logs fallback (#16548) 2026-05-25 09:28:49 +12:00
Jonathan Swoboda
27d53ec117 [sx126x] Assert NSS before wait_busy so commands wake the chip from sleep (#16546) 2026-05-25 09:28:49 +12:00
J. Nick Koston
0c94a173b6 [api] Break api_connection/api_server include cycle to drop custom unique_ptr deleter (#16542) 2026-05-25 09:28:49 +12:00
Jonathan Swoboda
ae2e372762 [tuya] Restore null guard on status_pin lost in #16353 (#16539) 2026-05-25 09:28:48 +12:00
Jonathan Swoboda
e6ed275746 [esp32] Defer esp_panic_handler wrap so arduino-esp32 IDF component skips it (#16538) 2026-05-25 09:28:48 +12:00
Jonathan Swoboda
878027ff50 [espidf] Honor the dict shorthand for library.json dependencies (#16537) 2026-05-25 09:28:48 +12:00
Jonathan Swoboda
858cfd5b94 [espidf] Default to remote HEAD when cg.add_library URL has no #ref (#16535) 2026-05-25 09:28:48 +12:00
Jonathan Swoboda
5225416347 [espidf] Backport ninja linux-arm64 entry into tools.json on aarch64 hosts (#16527) 2026-05-25 09:28:48 +12:00
Jonathan Swoboda
615d5aa827 [core] Persist & restore CORE.toolchain through StorageJSON (#16531) 2026-05-25 09:28:48 +12:00
Jonathan Swoboda
e92a4c9472 [espidf] Write version.txt after extract so bootloader shows the real version (#16532) 2026-05-25 09:28:48 +12:00
Jonathan Swoboda
32fa856bf0 [espidf] Fix tarfile extract crashing on Python 3.11 with None mode (#16530) 2026-05-25 09:28:48 +12:00
Jonathan Swoboda
cc88456ce7 [espidf] Filter noisy 'git rev-parse' errors when .git is stripped (#16521) 2026-05-25 09:28:48 +12:00
dependabot[bot]
79539cb85d Bump zeroconf from 0.149.13 to 0.149.16 (#16533)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-25 09:28:48 +12:00
dependabot[bot]
16b6509a03 Bump zeroconf from 0.149.12 to 0.149.13 (#16520)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-25 09:28:48 +12:00
Kevin Ahrendt
9fcb638f33 [micro_wake_word] Use RingBufferAudioSource (#16595)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-24 15:34:51 -04:00
Kevin Ahrendt
747787ae98 [audio] Use RingBufferAudioSource for resampling (#16560) 2026-05-24 15:34:15 -04:00
Kevin Ahrendt
5cb7e62241 [audio] Use RingBufferAudioSource for decoding (#16564) 2026-05-24 15:33:32 -04:00
Kevin Ahrendt
c17c4478ac [mixer] Support any bit depth audio (#16524) 2026-05-24 15:32:43 -04:00
Kevin Ahrendt
750d52741a [voice_assistant] Use RingBufferAudioSource (#16597) 2026-05-24 08:36:53 -04:00
Rodrigo Martín
a37f27ee7f [espnow, ethernet, network, openthread, wifi] centralize network initialization for ESP32 (#14012)
Co-authored-by: kbx81 <kbx81x@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-05-24 05:27:31 +00:00
Jonathan Swoboda
5f860ff5bd [esp32] Disable IDF's COMPILER_DISABLE_DEFAULT_ERRORS so -Wno-error actually undoes -Werror (#16604) 2026-05-24 04:19:07 +00:00
Keith Burzinski
c951881eea [api] Fix uint32_t/int32_t format strings for stricter GCC toolchain (#16603) 2026-05-24 04:05:18 +00:00
Kevin Ahrendt
5cb145a8c3 [ethernet] Offload W5500 bulk SPI transfers from the busy-wait path (#16596) 2026-05-23 17:53:53 -04:00
Kevin Ahrendt
74001ccf05 [wifi] Wake main loop when requesting high performance mode (#16598) 2026-05-23 17:39:20 -04:00
Kevin Ahrendt
58931f2610 [audio] Add clear_buffered_data method to RingBufferAudioSource (#16594) 2026-05-23 17:37:59 -04:00
Jonathan Swoboda
f616103621 [esp32] Replace per-class -Wno-error=X demotes with blanket -Wno-error for ESP-IDF toolchain (#16599) 2026-05-23 15:44:25 -04:00
J. Nick Koston
188ff7ebfd [bluetooth_proxy] Recover slot stuck in DISCONNECTING when CLOSE_EVT is dropped (#16588) 2026-05-23 14:30:12 -05:00
Clyde Stubbs
be99553fd4 [ci] Fix flash memory overflow on tests (#16587) 2026-05-23 14:26:53 +10:00
Jonathan Swoboda
b0dc688c14 [esp32] Demote IDF #warning deprecations from error under ESP-IDF toolchain (#16584) 2026-05-22 20:30:25 -04:00
J. Nick Koston
2b422cbd99 [lvgl] Build widget update action schemas lazily (#16569)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-05-23 00:20:39 +00:00
dependabot[bot]
9930b3c216 Bump github/codeql-action from 4.35.5 to 4.36.0 (#16579)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 19:18:31 -05:00
dependabot[bot]
55f4e5cb75 Bump the docker-actions group across 1 directory with 2 updates (#16578)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 19:18:20 -05:00
J. Nick Koston
71550bb3be [lvgl] Memoize and lazily build container_schema (#16567) 2026-05-22 18:39:25 -05:00
J. Nick Koston
a58b4edb6a [ci] Gate unconditional CI jobs on a single determine-jobs output instead of a path filter (#16580) 2026-05-22 18:39:06 -05:00
Clyde Stubbs
f85fdb475a [homeassistant] Reduce log spam for sensors (#16555) 2026-05-23 08:07:51 +10:00
dependabot[bot]
4a78c8d45a Bump pytest-codspeed from 5.0.2 to 5.0.3 (#16575)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 15:55:49 -05:00
Kevin Ahrendt
c3bef24389 [i2s_audio] Reset dout GPIO when stopping speaker driver (#16573) 2026-05-22 14:43:50 -05:00
J. Nick Koston
7182b1a8ae [uart] Wake main loop on ESP8266 software serial RX (#16562) 2026-05-22 14:30:43 -05:00
J. Nick Koston
64e32ebe04 [esp8266] Use os_timer-based esp_delay() in delay() (#16563) 2026-05-22 14:30:28 -05:00
Edvard Filistovič
94b10981e1 [libretiny] Fix LN882H IRAM_ATTR injection point in patch_linker.py (#16570) 2026-05-22 14:09:32 -05:00
J. Nick Koston
680c9fc9c0 [dashboard] Fix flaky test_websocket_refresh_command on Windows CI (#16565) 2026-05-22 08:49:03 -05:00
dependabot[bot]
99de741f99 Bump docker/build-push-action from 7.1.0 to 7.2.0 in /.github/actions/build-image (#16545)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 08:08:29 -05:00
dependabot[bot]
ac530c33b0 Bump actions/stale from 10.2.0 to 10.3.0 (#16544)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 08:08:14 -05:00
Kevin Ahrendt
0b5e7ae8fa [sendspin] Bump sendspin-cpp to v0.6.1 (#16553) 2026-05-22 06:43:08 -04:00
Jesse Hills
0b2eb6481f [light] Add light.effect.next / light.effect.previous actions (#16491) 2026-05-22 13:42:50 +12:00
Jesse Hills
1d3eea098e [core] Support YAML frontmatter for arbitrary user metadata (#16552) 2026-05-22 13:00:22 +12:00
dependabot[bot]
4ff8eb4b15 Bump ruff from 0.15.13 to 0.15.14 (#16543)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 22:08:56 +00:00
J. Nick Koston
aea1e4d136 [core] Refresh compiled config cache after upload/logs fallback (#16548) 2026-05-21 17:05:17 -05:00
Jonathan Swoboda
38b8b41ccc [sx126x] Assert NSS before wait_busy so commands wake the chip from sleep (#16546) 2026-05-21 18:03:07 -04:00
J. Nick Koston
96eced0378 [api] Break api_connection/api_server include cycle to drop custom unique_ptr deleter (#16542) 2026-05-21 15:42:57 -05:00
Jonathan Swoboda
1ea95264bd [tuya] Restore null guard on status_pin lost in #16353 (#16539) 2026-05-21 18:08:09 +00:00
Jonathan Swoboda
d2bda0a402 [esp32] Defer esp_panic_handler wrap so arduino-esp32 IDF component skips it (#16538) 2026-05-21 14:03:55 -04:00
Jonathan Swoboda
56fd77e4c8 [espidf] Honor the dict shorthand for library.json dependencies (#16537) 2026-05-21 13:01:54 -05:00
Jonathan Swoboda
3719ea740a [espidf] Default to remote HEAD when cg.add_library URL has no #ref (#16535) 2026-05-21 13:01:19 -05:00
Jonathan Swoboda
750ae56778 [espidf] Backport ninja linux-arm64 entry into tools.json on aarch64 hosts (#16527) 2026-05-21 12:05:27 -04:00
Kevin Ahrendt
01494f7431 [audio] Bump esp-audio-libs to v3.1.0 (#16519) 2026-05-21 11:57:32 -04:00
J. Nick Koston
233a60f106 [ci] Pin uv version in setup-uv to fix Windows manifest fetch flake (#16534) 2026-05-21 10:53:34 -05:00
Jonathan Swoboda
e0076cb1a8 [core] Persist & restore CORE.toolchain through StorageJSON (#16531) 2026-05-21 10:37:46 -05:00
Jonathan Swoboda
b619e3e8c7 [espidf] Write version.txt after extract so bootloader shows the real version (#16532) 2026-05-21 10:37:10 -05:00
Jonathan Swoboda
f2bfe5cd17 [espidf] Fix tarfile extract crashing on Python 3.11 with None mode (#16530) 2026-05-21 10:36:27 -05:00
Jonathan Swoboda
90715373f2 [espidf] Filter noisy 'git rev-parse' errors when .git is stripped (#16521) 2026-05-21 10:35:51 -05:00
Jonathan Swoboda
52e7d3ccfb [esp32] Use new sdkconfig key names that replaced deprecated ones (#16522) 2026-05-21 10:35:25 -05:00
dependabot[bot]
a70e358cea Bump zeroconf from 0.149.13 to 0.149.16 (#16533)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 09:55:28 -05:00
dependabot[bot]
43a1c2067e Bump zeroconf from 0.149.12 to 0.149.13 (#16520)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 08:28:09 -05:00
Jesse Hills
11760307f7 Merge branch 'release' into dev 2026-05-21 13:49:17 +12:00
Jesse Hills
15c546b809 Merge pull request #16523 from esphome/bump-2026.5.0
2026.5.0
2026-05-21 13:48:28 +12:00
Jesse Hills
104c8bed41 Bump version to 2026.5.0 2026-05-21 11:16:58 +12:00
Jesse Hills
49bfa12eb7 Merge branch 'beta' into dev 2026-05-21 10:14:14 +12:00
Jesse Hills
ca859de212 Merge pull request #16518 from esphome/bump-2026.5.0b4
2026.5.0b4
2026-05-21 10:13:39 +12:00
Jesse Hills
de783e72d5 Bump version to 2026.5.0b4 2026-05-21 09:10:52 +12:00
Jonathan Swoboda
cd7e2d79c4 [esp32] Decouple esp-idf toolchain version check from PIO, honor framework source: override (#16516) 2026-05-21 09:10:52 +12:00
Jonathan Swoboda
ecf823b871 [espidf] Drop version field from generated idf_component.yml (#16511) 2026-05-21 09:10:52 +12:00
dependabot[bot]
9fdad68138 Bump aioesphomeapi from 45.0.3 to 45.0.4 (#16513)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 09:10:51 +12:00
dependabot[bot]
b79a306d02 Bump zeroconf from 0.149.7 to 0.149.12 (#16510)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 09:10:51 +12:00
Jonathan Swoboda
870f628637 [esp32] Decouple esp-idf toolchain version check from PIO, honor framework source: override (#16516) 2026-05-20 20:40:59 +00:00
Jonathan Swoboda
52c9a2d07b [espidf] Drop version field from generated idf_component.yml (#16511) 2026-05-20 14:31:58 -04:00
Jonathan Swoboda
60afad442c [esp32] Fix sdkconfig int values silently clamped to default (#16515) 2026-05-20 13:36:18 -04:00
dependabot[bot]
fbe212944b Bump aioesphomeapi from 45.0.3 to 45.0.4 (#16513)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-20 10:51:53 -05:00
dependabot[bot]
8927ade789 Bump zeroconf from 0.149.7 to 0.149.12 (#16510)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-20 15:40:20 +00:00
Jesse Hills
63fe977adb Merge branch 'beta' into dev 2026-05-20 14:34:33 +12:00
Jesse Hills
94badfcb19 Merge pull request #16507 from esphome/bump-2026.5.0b3
2026.5.0b3
2026-05-20 14:33:57 +12:00
Jesse Hills
19c4da2aa5 Bump version to 2026.5.0b3 2026-05-20 12:53:26 +12:00
Kevin Ahrendt
e4c8d1f430 [sendspin] Bump sendspin to v0.6.0 (#16496) 2026-05-20 12:53:26 +12:00
Kevin Ahrendt
302938f875 [i2s_audio] Optimize SPDIF encoder and suport higher bit depth audio (#16504)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-05-20 12:53:26 +12:00
Jonathan Swoboda
65e1e210de [espidf] Print RAM summary on ESP32-S3 / unified-DIRAM variants (#16494) 2026-05-20 12:53:26 +12:00
luar123
43cc9fc879 [zigbee] don't allow zigbee + thread or access point (#16499) 2026-05-20 12:53:25 +12:00
Kevin Ahrendt
41ad2ba763 [i2s_audio] Compute ring buffer size with SPDIF sample count (#16400) 2026-05-20 12:53:25 +12:00
Brandon Harvey
25739091da [sen6x] Remove incorrect AQI device class from VOC and NOx Index sensors (#16465) 2026-05-20 12:53:25 +12:00
Brandon Harvey
bbf5fe8450 [sgp4x] Remove incorrect AQI device class from VOC and NOx Index sensors (#16464) 2026-05-20 12:53:25 +12:00
Brandon Harvey
e9ef58d99d [sen5x] Remove incorrect AQI device class from VOC and NOx Index sensors (#16463) 2026-05-20 12:53:25 +12:00
dependabot[bot]
e1793a1eff Bump zeroconf from 0.149.3 to 0.149.7 (#16492)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-20 12:53:25 +12:00
Jesse Hills
9bb70d568d [ci] Move ha-addon and schema release triggers to version-notifier (#16490) 2026-05-20 12:53:25 +12:00
Kevin Ahrendt
0912122634 [sendspin] Bump sendspin to v0.6.0 (#16496) 2026-05-19 15:16:00 -04:00
Kevin Ahrendt
9924d998f1 [i2s_audio] Optimize SPDIF encoder and suport higher bit depth audio (#16504)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-05-19 13:37:41 -05:00
dependabot[bot]
e979d461f0 Bump codecov/codecov-action from 6.0.0 to 6.0.1 (#16500)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 12:15:50 -05:00
J. Nick Koston
863af482ec [esp32_ble_server] Honor client offset and MTU in long reads (#16458) 2026-05-19 12:14:15 -05:00
J. Nick Koston
80ed541032 [core] Add progmem_memcpy HAL helper (#16470) 2026-05-19 12:13:58 -05:00
Jonathan Swoboda
1d0ddfac5d [espidf] Print RAM summary on ESP32-S3 / unified-DIRAM variants (#16494) 2026-05-19 12:57:18 -04:00
luar123
c0e71fc713 [zigbee] don't allow zigbee + thread or access point (#16499) 2026-05-19 12:53:36 -04:00
Kevin Ahrendt
7ecfe4b5c9 [i2s_audio] Compute ring buffer size with SPDIF sample count (#16400) 2026-05-18 21:53:19 -05:00
Brandon Harvey
36fc36071d [sen6x] Remove incorrect AQI device class from VOC and NOx Index sensors (#16465) 2026-05-19 11:20:08 +12:00
Brandon Harvey
cb581271ed [sgp4x] Remove incorrect AQI device class from VOC and NOx Index sensors (#16464) 2026-05-19 11:19:51 +12:00
Brandon Harvey
b0af4a9f0d [sen5x] Remove incorrect AQI device class from VOC and NOx Index sensors (#16463) 2026-05-19 11:19:48 +12:00
dependabot[bot]
edb59476b1 Bump zeroconf from 0.149.3 to 0.149.7 (#16492)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-17 23:29:03 -07:00
Jesse Hills
9c696f5de1 [ci] Move ha-addon and schema release triggers to version-notifier (#16490) 2026-05-18 15:56:44 +12:00
Jesse Hills
6804965bd8 Merge branch 'beta' into dev 2026-05-18 15:31:01 +12:00
Jesse Hills
213df0412d Merge pull request #16488 from esphome/bump-2026.5.0b2
2026.5.0b2
2026-05-18 15:28:53 +12:00
Jesse Hills
cdf74c180e Bump version to 2026.5.0b2 2026-05-18 11:11:54 +12:00
Jonathan Swoboda
df31c72e4e [espidf] Switch direct framework downloader to esphome-libs/esp-idf tarballs (#16484) 2026-05-18 11:11:54 +12:00
dependabot[bot]
4f188bf9bb Bump zeroconf from 0.148.0 to 0.149.3 (#16480)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 11:11:54 +12:00
dependabot[bot]
20f92ad5e9 Bump aioesphomeapi from 45.0.2 to 45.0.3 (#16479)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 11:11:54 +12:00
J. Nick Koston
f301e90fd9 [ci] Use larger app partition for esp32-s3-idf component test grouping (#16430) 2026-05-18 11:11:54 +12:00
dependabot[bot]
2dbaaf1efd Bump aioesphomeapi from 45.0.1 to 45.0.2 (#16469)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 11:11:54 +12:00
Clyde Stubbs
da237b5070 [lvgl] Fix image define (#16468) 2026-05-18 11:11:54 +12:00
Clyde Stubbs
6a8f24b951 [ft5x06] Fix setting calibration values (#16446) 2026-05-18 11:11:54 +12:00
dependabot[bot]
26907f17f5 Bump aioesphomeapi from 45.0.0 to 45.0.1 (#16467)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 11:11:54 +12:00
Jonathan Swoboda
c6a74222f1 [esp32_hosted][fingerprint_grow] Fix two remaining ESP32 toolchain warnings (#16442) 2026-05-18 11:11:54 +12:00
J. Nick Koston
5ec0879a10 [core] Fix KeyError: 'esp32' on upload when validated-config cache is used (#16457) 2026-05-18 11:11:54 +12:00
J. Nick Koston
50495c7085 [wifi] Refuse to compile when wifi_ssid is the device-builder placeholder (#16444) 2026-05-18 11:11:54 +12:00
Kevin Ahrendt
25dbef83de [sound_level] Use RingBufferAudioSource (#16436) 2026-05-18 11:11:54 +12:00
Kevin Ahrendt
4f895425ca [audio] Bump microMP3 to v0.2.1 (#16429) 2026-05-18 11:11:54 +12:00
Jonathan Swoboda
c037058c19 [esp32_hosted] Bump esp_hosted to 2.12.7 (#16440) 2026-05-18 11:11:54 +12:00
Jonathan Swoboda
ecac6b64ec [espidf] Gate esp_idf_size --ng on IDF version (#16441) 2026-05-18 11:11:54 +12:00
Jonathan Swoboda
3831aa809f [multiple] Fix -Wformat= mismatches in component .cpp sources (#16433) 2026-05-18 11:11:54 +12:00
Jonathan Swoboda
da8286f554 [docker] Install libusb-1.0 so ESP-IDF tools can validate openocd (#16424)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-18 11:11:54 +12:00
Jonathan Swoboda
d5c6efb2fe [tests] Fix -Wformat= mismatches in test YAML lambdas/logger.log (#16435) 2026-05-18 11:11:54 +12:00
Jonathan Swoboda
dd1818661c [esp32] Sweep ESP-IDF toolchain warnings + bump deprecated mark_failed (#16432) 2026-05-18 11:11:53 +12:00
Keith Burzinski
fb659f9ac4 [tinyusb] Reject logger.hardware_uart: USB_CDC (#16417)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 11:11:53 +12:00
Keith Burzinski
ab273a1f8f [tinyusb] Reject tinyusb: configured without a USB class companion (#16413)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 11:11:53 +12:00
Jonathan Swoboda
84b5931299 [espidf] Trim has_outdated_files watch list; embed IDF version in sdkconfig (#16416) 2026-05-18 11:11:53 +12:00
Jonathan Swoboda
c863d58999 [espidf] Stop perpetual reconfigure loop on native ESP-IDF builds (#16415) 2026-05-18 11:11:53 +12:00
Jesse Hills
42ad2a6272 [espidf] Accept list input in _str_to_lst_of_str helper (#16485) 2026-05-18 10:49:03 +12:00
Jonathan Swoboda
6690725860 [espidf] Switch direct framework downloader to esphome-libs/esp-idf tarballs (#16484) 2026-05-18 07:29:38 +12:00
dependabot[bot]
155232875a Bump zeroconf from 0.148.0 to 0.149.3 (#16480)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-16 19:08:19 -07:00
dependabot[bot]
01c0d3163e Bump aioesphomeapi from 45.0.2 to 45.0.3 (#16479)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-16 18:41:09 -07:00
J. Nick Koston
7c5d5f75dc [ci] Use larger app partition for esp32-s3-idf component test grouping (#16430) 2026-05-15 22:16:52 -07:00
dependabot[bot]
fb0bfea1c8 Bump aioesphomeapi from 45.0.1 to 45.0.2 (#16469)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-15 22:06:33 -07:00
J. Nick Koston
48d17571c8 [tests] Mock determine_cpp_unit_tests in clang_tidy_mode tests (#16456) 2026-05-15 15:50:58 -07:00
Clyde Stubbs
df100681e0 [lvgl] Fix image define (#16468) 2026-05-15 22:36:53 +00:00
Clyde Stubbs
1a287bf785 [ft5x06] Fix setting calibration values (#16446) 2026-05-16 10:30:04 +12:00
dependabot[bot]
ff34e1061b Bump resvg-py from 0.3.1 to 0.3.2 (#16466)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-15 14:43:08 -07:00
dependabot[bot]
b78b78cbbb Bump aioesphomeapi from 45.0.0 to 45.0.1 (#16467)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-15 14:42:51 -07:00
dependabot[bot]
4c090c6b85 Bump github/codeql-action from 4.35.4 to 4.35.5 (#16461)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-15 12:37:56 -07:00
dependabot[bot]
ec6669fa67 Bump requests from 2.34.1 to 2.34.2 (#16460)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-15 12:37:41 -07:00
dependabot[bot]
59f8c1019f Bump pytest-codspeed from 5.0.1 to 5.0.2 (#16459)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-15 12:37:28 -07:00
david-collett
fb70095ba1 [esp32_ble_server] Fix incorrect BLECharacteristic read truncation (#16420) (#16422)
Co-authored-by: Dave <dave@morty>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-05-15 10:47:26 -07:00
Jonathan Swoboda
65d6bb18ed [esp32_hosted][fingerprint_grow] Fix two remaining ESP32 toolchain warnings (#16442) 2026-05-15 17:32:51 +00:00
J. Nick Koston
47eb2adbf2 [core] Fix KeyError: 'esp32' on upload when validated-config cache is used (#16457) 2026-05-15 10:29:15 -07:00
J. Nick Koston
35631be260 [writer] Mark storage_should_clean as public API for device-builder (#16443) 2026-05-15 10:20:31 -07:00
J. Nick Koston
96106d25bc [wifi] Refuse to compile when wifi_ssid is the device-builder placeholder (#16444) 2026-05-15 10:20:15 -07:00
J. Nick Koston
1674ed9744 [ci] Use uv for pip installs across CI workflows (#16451) 2026-05-15 10:18:27 -07:00
J. Nick Koston
46be0f4f62 [ci] Log top 30 pytest durations (#16455) 2026-05-15 10:18:07 -07:00
J. Nick Koston
ec1826a6ed [yaml_util] Promote include-discovery helper, share it with bundle (#16447) 2026-05-15 10:17:50 -07:00
Simone Chemelli
8b3bc47547 [uptime] Update device_class for Uptime sensor (#16434)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-05-15 17:16:01 +00:00
J. Nick Koston
4381a8baaa [ci] pr-title-check: skip all bot authors, not just dependabot (#16453) 2026-05-15 09:59:35 -07:00
esphome[bot]
4189979391 Synchronise Device Classes from Home Assistant (#16452)
Co-authored-by: esphomebot <esphome@openhomefoundation.org>
2026-05-15 09:57:49 -07:00
J. Nick Koston
1b1e21d470 [ci] sync-device-classes: drop branch-switch hack, skip no-commit-to-branch instead (#16450) 2026-05-15 09:54:01 -07:00
J. Nick Koston
5b6c54c961 [ci] sync-device-classes: use uv for installs and skip pylint (#16449) 2026-05-15 09:45:11 -07:00
Jonathan Swoboda
ff968a4629 [ci] Fix sync-device-classes workflow (failing daily for weeks) (#16448) 2026-05-15 09:36:01 -07:00
Edward Firmo
d832ce51cd [nextion] Replace connect_info vector with fixed-size field parser, always log device info (#16059)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-05-14 22:42:10 -05:00
Kevin Ahrendt
d663d80fde [sound_level] Use RingBufferAudioSource (#16436) 2026-05-14 20:33:36 -07:00
Kevin Ahrendt
c5c627d534 [audio] Bump microMP3 to v0.2.1 (#16429) 2026-05-14 20:31:11 -07:00
Jonathan Swoboda
d046dd7276 [esp32_hosted] Bump esp_hosted to 2.12.7 (#16440) 2026-05-14 22:51:14 -04:00
Jonathan Swoboda
56983f414f [espidf] Gate esp_idf_size --ng on IDF version (#16441) 2026-05-14 22:41:36 -04:00
Jonathan Swoboda
a92b607754 [ci] Add ci-run-all label to force full CI matrix (#16421) 2026-05-14 18:54:13 -04:00
Jonathan Swoboda
313d974983 [multiple] Fix -Wformat= mismatches in component .cpp sources (#16433) 2026-05-14 18:53:42 -04:00
Jonathan Swoboda
1d86d856d1 [docker] Install libusb-1.0 so ESP-IDF tools can validate openocd (#16424)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-14 15:51:59 -07:00
Jonathan Swoboda
1bb191aa77 [ci] Skip dashboard-deprecation bot on release/beta-bump PRs (#16427) 2026-05-14 15:51:36 -07:00
dependabot[bot]
5d9d6e83f7 Bump ruff from 0.15.12 to 0.15.13 (#16437)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-14 15:41:32 -07:00
Jonathan Swoboda
f3d7743460 [tests] Fix -Wformat= mismatches in test YAML lambdas/logger.log (#16435) 2026-05-14 18:40:40 -04:00
Jonathan Swoboda
f291dc8d2f [esp32] Sweep ESP-IDF toolchain warnings + bump deprecated mark_failed (#16432) 2026-05-14 18:39:16 -04:00
Jonathan Swoboda
a8e69a15e4 [clang-tidy] Enable readability-container-contains (#16438) 2026-05-14 18:38:09 -04:00
Keith Burzinski
7436d1c199 [tinyusb] Reject logger.hardware_uart: USB_CDC (#16417)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:29:56 +00:00
Keith Burzinski
348b92910e [tinyusb] Reject tinyusb: configured without a USB class companion (#16413)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:07:38 -05:00
Jonathan Swoboda
f89a6f4f9c [espidf] Trim has_outdated_files watch list; embed IDF version in sdkconfig (#16416) 2026-05-14 04:02:22 +00:00
Jesse Hills
c3ee962b83 Merge branch 'beta' into dev 2026-05-14 15:25:38 +12:00
Jesse Hills
59847d56e8 Merge pull request #16411 from esphome/bump-2026.5.0b1
2026.5.0b1
2026-05-14 15:24:58 +12:00
Jonathan Swoboda
e593cb6efc [espidf] Stop perpetual reconfigure loop on native ESP-IDF builds (#16415) 2026-05-13 23:19:30 -04:00
J. Nick Koston
d2107e40c8 [ci] Prohibit curly braces in PR titles for MDX safety (#16412) 2026-05-14 14:03:45 +12:00
Jesse Hills
09a926fa13 Bump version to 2026.5.0b1 2026-05-14 12:33:43 +12:00
Jesse Hills
78b60ac6fa Bump version to 2026.6.0-dev 2026-05-14 12:33:43 +12:00
Jonathan Swoboda
a3b6f92433 [espidf] Regenerate bundled CMakeLists; auto-REQUIRE via IDF build properties (#16406) 2026-05-13 23:58:48 +00:00
dependabot[bot]
06786da7dd Bump actions/create-github-app-token from 3.1.1 to 3.2.0 (#16409)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 16:28:47 -07:00
Jonathan Swoboda
910cc38dd7 [writer] Clean ESP-IDF build artifacts in clean_build (#16410) 2026-05-13 19:25:35 -04:00
dependabot[bot]
c8aba6913b Bump requests from 2.34.0 to 2.34.1 (#16408)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 15:38:13 -05:00
Boris Krivonog
ce8810bc42 [mitsubishi_cn105] Add vane and wide-vane support (#16405) 2026-05-13 13:25:32 -05:00
Jonathan Swoboda
1c6966b761 [espidf] Run PIO extraScript with SCons-env shim (#16404) 2026-05-13 13:07:59 -04:00
J. Nick Koston
03f5e4775c [tests] Add CodSpeed benchmark for compiled-config cache fast path (#16402)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-05-13 12:06:20 -05:00
Boris Krivonog
445d841229 [mitsubishi_cn105] Simplified protocol lookups (#16399) 2026-05-13 16:49:32 +00:00
Jonathan Swoboda
d7b00047bd [espidf] Emit -W warning flags at project scope so managed components also see them (#16403) 2026-05-13 12:27:06 -04:00
Jonathan Swoboda
3fee97ae5a [espidf] Partition pio_components cache by framework (#16401) 2026-05-13 12:08:51 -04:00
J. Nick Koston
cb520cda6b [core] Retry PlatformIO downloads on transport-layer errors (#16397) 2026-05-13 10:01:42 -05:00
J. Nick Koston
8bce32ec35 [tests] Cover top-level !include failure path in track_yaml_loads (#16396) 2026-05-13 10:01:26 -05:00
J. Nick Koston
b866525437 [ci] Skip native ESP-IDF compile test when no relevant files changed (#16395) 2026-05-13 10:01:11 -05:00
J. Nick Koston
0e4922a340 [core] Cache validated config to skip re-validation on upload/logs (#16381) 2026-05-13 22:14:19 +12:00
Boris Krivonog
45a4811bb4 [mitsubishi_cn105] Unified timeout handling (#16385) 2026-05-13 01:40:19 -05:00
747 changed files with 27551 additions and 6636 deletions

View File

@@ -116,7 +116,6 @@ Checks: >-
-portability-template-virtual-member-function,
-readability-ambiguous-smartptr-reset-call,
-readability-avoid-nested-conditional-operator,
-readability-container-contains,
-readability-container-data-pointer,
-readability-convert-member-functions-to-static,
-readability-else-after-return,

View File

@@ -1 +1 @@
593fd53fa09944a59af3f38521e31d87fe10b60326b8d82bb76413c5149b312c
72f02816e288b68ff4ef4b3d6fb66432c893b187a80ad3ebaa29afa443ff9ea6

View File

@@ -29,7 +29,7 @@ Required fields:
- **What does this implement/fix?**: Brief description of changes
- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.)
- **Related issue**: Use `fixes <link>` syntax if applicable
- **Pull request in esphome-docs**: Link if docs are needed
- **Pull request in esphome.io**: Link if docs are needed
- **Test Environment**: Check platforms you tested on
- **Example config.yaml**: Include working example YAML
- **Checklist**: Verify code is tested and tests added
@@ -54,9 +54,9 @@ Required fields:
- fixes https://github.com/esphome/esphome/issues/XXX
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
**Pull request in [esphome.io](https://github.com/esphome/esphome.io) with documentation (if applicable):**
- esphome/esphome-docs#XXX
- esphome/esphome.io#XXX
## Test Environment
@@ -83,7 +83,7 @@ component_name:
- [x] Tests have been added to verify that the new code works (under `tests/` folder).
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
- [ ] Documentation added/updated in [esphome.io](https://github.com/esphome/esphome.io).
```
## 5. Push and Create PR

View File

@@ -2,7 +2,7 @@
blank_issues_enabled: false
contact_links:
- name: Report an issue with the ESPHome documentation
url: https://github.com/esphome/esphome-docs/issues/new/choose
url: https://github.com/esphome/esphome.io/issues/new/choose
about: Report an issue with the ESPHome documentation.
- name: Report an issue with the ESPHome web server
url: https://github.com/esphome/esphome-webserver/issues/new/choose

View File

@@ -16,9 +16,9 @@
- fixes <link to issue>
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
**Pull request in [esphome.io](https://github.com/esphome/esphome.io) with documentation (if applicable):**
- esphome/esphome-docs#<esphome-docs PR number goes here>
- esphome/esphome.io#<esphome.io PR number goes here>
## Test Environment
@@ -43,4 +43,4 @@
- [ ] Tests have been added to verify that the new code works (under `tests/` folder).
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
- [ ] Documentation added/updated in [esphome.io](https://github.com/esphome/esphome.io).

View File

@@ -15,11 +15,6 @@ inputs:
description: "Version to build"
required: true
example: "2023.12.0"
base_os:
description: "Base OS to use"
required: false
default: "debian"
example: "debian"
runs:
using: "composite"
steps:
@@ -47,7 +42,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -60,7 +55,6 @@ runs:
build-args: |
BUILD_TYPE=${{ inputs.build_type }}
BUILD_VERSION=${{ inputs.version }}
BUILD_OS=${{ inputs.base_os }}
outputs: |
type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
@@ -73,7 +67,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -86,7 +80,6 @@ runs:
build-args: |
BUILD_TYPE=${{ inputs.build_type }}
BUILD_VERSION=${{ inputs.version }}
BUILD_OS=${{ inputs.base_os }}
outputs: |
type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true

View File

@@ -0,0 +1,46 @@
name: Cache ESP-IDF
description: >
Resolve the pinned ESP-IDF version and cache the native ESP-IDF install
(toolchains + source) at ~/.esphome-idf. Every job that installs ESP-IDF
natively (clang-tidy for IDF/Arduino and the native-IDF component build)
shares one cache, since the install is identical (ESPHOME_IDF_DEFAULT_TARGETS
defaults to "all", so all toolchains are present regardless of the chip).
Callers must set env ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf and have the
Python venv already restored.
inputs:
framework:
description: 'Which pinned IDF version to key on: "espidf" (recommended) or "arduino".'
default: espidf
runs:
using: composite
steps:
- name: Resolve ESP-IDF version for cache key
# The native-IDF version is pinned in code, not in any file that feeds the
# other cache keys, so resolve it explicitly. Keying on it means the cache
# invalidates on a version bump (actions/cache never overwrites a key).
id: version
shell: bash
run: |
. venv/bin/activate
if [ "${{ inputs.framework }}" = "arduino" ]; then
version=$(python -c 'from esphome.components.esp32 import ARDUINO_FRAMEWORK_VERSION_LOOKUP as A, ARDUINO_IDF_VERSION_LOOKUP as L; print(L[A["recommended"]])')
else
version=$(python -c 'from esphome.components.esp32 import ESP_IDF_FRAMEWORK_VERSION_LOOKUP as L; print(L["recommended"])')
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
# Mirror the adjacent PlatformIO cache: only dev-branch runs write the
# shared cache (so it lives in the default-branch scope readable by all
# PRs), and PRs are restore-only -- they never push multi-GB artifacts into
# their own scope / the repo quota (e.g. on a version-bump PR).
- name: Cache ESP-IDF install (write on dev)
if: github.ref == 'refs/heads/dev'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.esphome-idf
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
- name: Cache ESP-IDF install (restore-only off dev)
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.esphome-idf
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}

View File

@@ -27,6 +27,18 @@ runs:
path: venv
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
- name: Set up uv
# Only needed on cache miss to populate the venv. ``uv pip install``
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
# downstream jobs rely on is preserved.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
version: "0.11.15"
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows'
shell: bash
@@ -34,8 +46,8 @@ runs:
python -m venv venv
source venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_test.txt
pip install -e .
uv pip install -r requirements.txt -r requirements_test.txt
uv pip install -e .
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
shell: bash
@@ -43,5 +55,5 @@ runs:
python -m venv venv
source ./venv/Scripts/activate
python --version
pip install -r requirements.txt -r requirements_test.txt
pip install -e .
uv pip install -r requirements.txt -r requirements_test.txt
uv pip install -e .

View File

@@ -5,6 +5,7 @@ updates:
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
ignore:
# Hypotehsis is only used for testing and is updated quite often
- dependency-name: hypothesis

View File

@@ -35,6 +35,9 @@ module.exports = {
],
DOCS_PR_PATTERNS: [
/https:\/\/github\.com\/esphome\/esphome\.io\/pull\/\d+/,
/esphome\/esphome\.io#\d+/,
// Keep matching the old esphome-docs name during the transition period
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
/esphome\/esphome-docs#\d+/
]

View File

@@ -107,6 +107,8 @@ async function detectNewPlatforms(github, context, prFiles, apiData) {
/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/,
];
const removedFiles = new Set(prFiles.filter(file => file.status === 'removed').map(file => file.filename));
for (const file of addedFiles) {
for (const re of platformPathPatterns) {
const match = file.match(re);
@@ -114,6 +116,12 @@ async function detectNewPlatforms(github, context, prFiles, apiData) {
const platform = match[2];
if (!apiData.platformComponents.includes(platform)) break;
// Skip if this is a restructure between flat and subdirectory forms (either direction):
// <component>/<platform>.py <-> <component>/<platform>/__init__.py
const flatEquivalent = `esphome/components/${match[1]}/${platform}.py`;
const subdirEquivalent = `esphome/components/${match[1]}/${platform}/__init__.py`;
if (removedFiles.has(flatEquivalent) || removedFiles.has(subdirEquivalent)) break;
labels.add('new-platform');
const content = await fetchPrFileContent(github, context, file);
if (content === null) {

View File

@@ -0,0 +1,7 @@
{
"name": "auto-label-pr",
"private": true,
"scripts": {
"test": "node --test tests/*.test.js"
}
}

View File

@@ -0,0 +1,147 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { detectNewPlatforms, detectNewComponents } = require('../detectors');
// Minimal GitHub API mock — only repos.getContent is called by detectNewPlatforms/detectNewComponents
// to check for CONFIG_SCHEMA in newly added files.
function makeGithub(content = '') {
return {
rest: {
repos: {
getContent: async () => ({
data: { content: Buffer.from(content).toString('base64') }
})
}
}
};
}
const CONTEXT = {
repo: { owner: 'esphome', repo: 'esphome' },
payload: { pull_request: { head: { sha: 'abc123' }, base: { ref: 'dev' } } }
};
const API_DATA = {
targetPlatforms: ['esp32', 'esp8266', 'rp2040'],
platformComponents: ['cover', 'sensor', 'binary_sensor', 'switch', 'light', 'fan', 'climate', 'valve']
};
const WITH_SCHEMA = 'CONFIG_SCHEMA = cv.Schema({})';
const WITHOUT_SCHEMA = 'CODEOWNERS = ["@esphome/core"]';
// ---------------------------------------------------------------------------
// detectNewPlatforms
// ---------------------------------------------------------------------------
describe('detectNewPlatforms', () => {
describe('restructure detection (no false positives)', () => {
it('flat .py -> subdir __init__.py is not a new platform', async () => {
const prFiles = [
{ filename: 'esphome/components/endstop/cover.py', status: 'removed' },
{ filename: 'esphome/components/endstop/cover/__init__.py', status: 'added' },
];
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
assert.equal(result.labels.size, 0);
assert.equal(result.hasYamlLoadable, false);
});
it('subdir __init__.py -> flat .py is not a new platform', async () => {
const prFiles = [
{ filename: 'esphome/components/endstop/cover/__init__.py', status: 'removed' },
{ filename: 'esphome/components/endstop/cover.py', status: 'added' },
];
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
assert.equal(result.labels.size, 0);
assert.equal(result.hasYamlLoadable, false);
});
});
describe('genuine new platforms', () => {
it('new subdir platform with CONFIG_SCHEMA sets new-platform and hasYamlLoadable', async () => {
const prFiles = [
{ filename: 'esphome/components/my_sensor/cover/__init__.py', status: 'added' },
];
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
assert.ok(result.labels.has('new-platform'));
assert.equal(result.hasYamlLoadable, true);
});
it('new flat platform with CONFIG_SCHEMA sets new-platform and hasYamlLoadable', async () => {
const prFiles = [
{ filename: 'esphome/components/my_sensor/cover.py', status: 'added' },
];
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
assert.ok(result.labels.has('new-platform'));
assert.equal(result.hasYamlLoadable, true);
});
it('new platform without CONFIG_SCHEMA sets new-platform but not hasYamlLoadable', async () => {
const prFiles = [
{ filename: 'esphome/components/my_sensor/cover.py', status: 'added' },
];
const result = await detectNewPlatforms(makeGithub(WITHOUT_SCHEMA), CONTEXT, prFiles, API_DATA);
assert.ok(result.labels.has('new-platform'));
assert.equal(result.hasYamlLoadable, false);
});
it('non-platform file addition produces no labels', async () => {
const prFiles = [
{ filename: 'esphome/components/my_sensor/sensor.py', status: 'added' },
];
// Override platformComponents so 'sensor' is not a recognized platform -> no label expected.
const nonPlatformApiData = { ...API_DATA, platformComponents: ['cover'] };
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, nonPlatformApiData);
assert.equal(result.labels.size, 0);
assert.equal(result.hasYamlLoadable, false);
});
});
});
// ---------------------------------------------------------------------------
// detectNewComponents
// ---------------------------------------------------------------------------
describe('detectNewComponents', () => {
it('new top-level __init__.py sets new-component', async () => {
const prFiles = [
{ filename: 'esphome/components/actuator/__init__.py', status: 'added', },
];
const result = await detectNewComponents(makeGithub(WITHOUT_SCHEMA), CONTEXT, prFiles);
assert.ok(result.labels.has('new-component'));
assert.equal(result.hasYamlLoadable, false);
});
it('new top-level __init__.py with CONFIG_SCHEMA sets hasYamlLoadable', async () => {
const prFiles = [
{ filename: 'esphome/components/my_component/__init__.py', status: 'added' },
];
const result = await detectNewComponents(makeGithub(WITH_SCHEMA), CONTEXT, prFiles);
assert.ok(result.labels.has('new-component'));
assert.equal(result.hasYamlLoadable, true);
});
it('new top-level __init__.py with IS_TARGET_PLATFORM sets new-target-platform', async () => {
const prFiles = [
{ filename: 'esphome/components/my_platform/__init__.py', status: 'added' },
];
const result = await detectNewComponents(makeGithub('IS_TARGET_PLATFORM = True'), CONTEXT, prFiles);
assert.ok(result.labels.has('new-component'));
assert.ok(result.labels.has('new-target-platform'));
});
it('modified __init__.py does not set new-component', async () => {
const prFiles = [
{ filename: 'esphome/components/existing/__init__.py', status: 'modified' },
];
const result = await detectNewComponents(makeGithub(WITH_SCHEMA), CONTEXT, prFiles);
assert.equal(result.labels.size, 0);
});
it('nested __init__.py does not set new-component', async () => {
const prFiles = [
{ filename: 'esphome/components/endstop/cover/__init__.py', status: 'added' },
];
const result = await detectNewComponents(makeGithub(WITH_SCHEMA), CONTEXT, prFiles);
assert.equal(result.labels.size, 0);
});
});

View File

@@ -24,11 +24,11 @@ jobs:
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -21,11 +21,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
version: "0.11.15"
- name: Install apt dependencies
run: |
@@ -34,7 +44,7 @@ jobs:
sudo apt install -y protobuf-compiler
protoc --version
- name: Install python dependencies
run: pip install aioesphomeapi -c requirements.txt -r requirements_dev.txt
run: uv pip install --system aioesphomeapi -c requirements.txt -r requirements_dev.txt
- name: Generate files
run: script/api_protobuf/api_protobuf.py
- name: Check for changes

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0

View File

@@ -22,7 +22,7 @@ on:
- "script/platformio_install_deps.py"
permissions:
contents: read # actions/checkout only; the build does not push images
contents: read # actions/checkout only
concurrency:
# yamllint disable-line rule:line-length
@@ -33,6 +33,9 @@ jobs:
check-docker:
name: Build docker containers
runs-on: ${{ matrix.os }}
permissions:
contents: read # actions/checkout to load Dockerfile and build context
packages: write # push branch-tagged images to ghcr.io for local testing
strategy:
fail-fast: false
matrix:
@@ -41,23 +44,94 @@ jobs:
- "ha-addon"
- "docker"
# - "lint"
outputs:
tag: ${{ steps.tag.outputs.tag }}
push: ${{ steps.tag.outputs.push }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Set TAG
- name: Determine tag and whether to push
id: tag
run: |
echo "TAG=check" >> $GITHUB_ENV
# Sanitize the branch name into a valid docker tag: replace invalid
# characters, ensure the first character is valid (tags must start
# with [A-Za-z0-9_]), and cap the length at 128 characters.
branch="${{ github.head_ref || github.ref_name }}"
tag="${branch//[^a-zA-Z0-9_.-]/-}"
case "$tag" in
[a-zA-Z0-9_]*) ;;
*) tag="pr-${tag}" ;;
esac
tag="${tag:0:128}"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
# Only push branch images for same-repo pull requests. Push events
# only fire for dev/beta/release, whose images are owned by the
# release pipeline -- never overwrite those from here.
if [ "${{ github.event_name }}" = "pull_request" ] \
&& [ "${{ github.repository }}" = "esphome/esphome" ] \
&& [ "${{ github.event.pull_request.head.repo.full_name }}" = "esphome/esphome" ]; then
echo "push=true" >> "$GITHUB_OUTPUT"
else
echo "push=false" >> "$GITHUB_OUTPUT"
fi
- name: Log in to the GitHub container registry
if: steps.tag.outputs.push == 'true'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run build
run: |
docker/build.py \
--tag "${TAG}" \
--tag "${{ steps.tag.outputs.tag }}" \
--arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \
--build-type "${{ matrix.build_type }}" \
build
--registry ghcr \
build ${{ steps.tag.outputs.push == 'true' && '--push --no-cache-to' || '' }}
manifest:
name: Push ${{ matrix.build_type }} manifest to ghcr.io
needs: [check-docker]
if: needs.check-docker.outputs.push == 'true'
runs-on: ubuntu-24.04
permissions:
contents: read # actions/checkout to run docker/build.py
packages: write # buildx imagetools writes the multi-arch tag to ghcr.io
strategy:
fail-fast: false
matrix:
build_type:
- "ha-addon"
- "docker"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log in to the GitHub container registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest
run: |
docker/build.py \
--tag "${{ needs.check-docker.outputs.tag }}" \
--build-type "${{ matrix.build_type }}" \
--registry ghcr \
manifest

27
.github/workflows/ci-github-scripts.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: CI - GitHub Scripts
on:
push:
branches: [dev, beta, release]
paths:
- ".github/scripts/**"
- ".github/workflows/ci-github-scripts.yml"
pull_request:
paths:
- ".github/scripts/**"
- ".github/workflows/ci-github-scripts.yml"
permissions:
contents: read
jobs:
test-auto-label-pr:
name: Test auto-label-pr scripts
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Run tests
working-directory: .github/scripts/auto-label-pr
run: npm test

View File

@@ -49,7 +49,7 @@ jobs:
- name: Check out code from base repository
if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo

View File

@@ -6,14 +6,6 @@ on:
branches: [dev, beta, release]
pull_request:
paths:
- "**"
- "!.github/workflows/*.yml"
- "!.github/actions/build-image/*"
- ".github/workflows/ci.yml"
- "!.yamllint"
- "!.github/dependabot.yml"
- "!docker/**"
merge_group:
permissions:
@@ -36,7 +28,7 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
@@ -52,14 +44,26 @@ jobs:
path: venv
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }}
- name: Set up uv
# Only needed on cache miss to populate the venv. ``uv pip install``
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
# that ``. venv/bin/activate`` see an identical layout.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
version: "0.11.15"
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit
pip install -e .
uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit
uv pip install -e .
pylint:
name: Check pylint
@@ -70,7 +74,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -89,9 +93,11 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -107,6 +113,7 @@ jobs:
script/build_language_schema.py --check
script/generate-esp32-boards.py --check
script/generate-rp2040-boards.py --check
script/ci_check_duplicate_test_ids.py
import-time:
name: Check import esphome.__main__ time
@@ -117,7 +124,7 @@ jobs:
if: needs.determine-jobs.outputs.import-time == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -145,11 +152,11 @@ jobs:
if: needs.determine-jobs.outputs.device-builder == 'true'
steps:
- name: Check out esphome (this PR)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
path: esphome
- name: Check out esphome/device-builder
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: esphome/device-builder
ref: main
@@ -164,9 +171,13 @@ jobs:
# install step (order-of-magnitude faster on cold boots,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
version: "0.11.15"
- name: Install device-builder + esphome from PR
# Install device-builder with its esphome + test extras
# first so its pinned versions of pytest/etc. land, then
@@ -179,9 +190,12 @@ jobs:
- name: Run device-builder pytest
# ``-n auto`` runs under pytest-xdist (matches device-builder's
# own CI). No ``--cov`` here -- this is purely a downstream
# smoke check against this PR's esphome code.
# smoke check against this PR's esphome code. ``tests/e2e/slow``
# is excluded: those are real multi-minute toolchain compiles
# (LibreTiny SDK clone, native ESP-IDF install) that device-builder
# runs in its own dedicated jobs, not this smoke check.
working-directory: device-builder
run: pytest -q -n auto --maxfail=5 --durations=10 --no-cov --ignore=tests/benchmarks
run: pytest -q -n auto --maxfail=5 --durations=30 --no-cov --ignore=tests/benchmarks --ignore=tests/e2e/slow
pytest:
name: Run pytest
@@ -207,9 +221,11 @@ jobs:
runs-on: ${{ matrix.os }}
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
@@ -222,14 +238,14 @@ jobs:
if: matrix.os == 'windows-latest'
run: |
. ./venv/Scripts/activate.ps1
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
- name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
run: |
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
@@ -245,13 +261,17 @@ jobs:
needs:
- common
outputs:
core-ci: ${{ steps.determine.outputs.core-ci }}
integration-tests: ${{ steps.determine.outputs.integration-tests }}
integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
clang-tidy-full-scan: ${{ steps.determine.outputs.clang-tidy-full-scan }}
python-linters: ${{ steps.determine.outputs.python-linters }}
import-time: ${{ steps.determine.outputs.import-time }}
device-builder: ${{ steps.determine.outputs.device-builder }}
native-idf: ${{ steps.determine.outputs.native-idf }}
native-idf-components: ${{ steps.determine.outputs.native-idf-components }}
changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
@@ -265,7 +285,7 @@ jobs:
benchmarks: ${{ steps.determine.outputs.benchmarks }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Fetch enough history to find the merge base
fetch-depth: 2
@@ -285,18 +305,27 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
. venv/bin/activate
output=$(python script/determine-jobs.py)
EXTRA_ARGS=""
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-run-all') }}" == "true" ]]; then
EXTRA_ARGS="--force-all"
echo "::notice::ci-run-all label detected -- forcing every CI job to run"
fi
output=$(python script/determine-jobs.py $EXTRA_ARGS)
echo "Test determination output:"
echo "$output" | jq
# Extract individual fields
echo "core-ci=$(echo "$output" | jq -r '.core_ci')" >> $GITHUB_OUTPUT
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
echo "clang-tidy-full-scan=$(echo "$output" | jq -r '.clang_tidy_full_scan')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT
echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT
echo "native-idf=$(echo "$output" | jq -r '.native_idf')" >> $GITHUB_OUTPUT
echo "native-idf-components=$(echo "$output" | jq -r '.native_idf_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 "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
@@ -328,7 +357,7 @@ jobs:
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python 3.13
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -340,14 +369,24 @@ jobs:
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
- name: Set up uv
# Only needed on cache miss to populate the venv.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
version: "0.11.15"
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_test.txt
pip install -e .
uv pip install -r requirements.txt -r requirements_test.txt
uv pip install -e .
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run integration tests
@@ -359,7 +398,7 @@ jobs:
. venv/bin/activate
mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]')
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
pytest -vv --no-cov --tb=native --durations=30 -n auto "${test_files[@]}"
cpp-unit-tests:
name: Run C++ unit tests
@@ -370,7 +409,7 @@ jobs:
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -399,7 +438,7 @@ jobs:
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -417,9 +456,12 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1
uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0
with:
run: ${{ steps.build.outputs.binary }}
run: |
. venv/bin/activate
${{ steps.build.outputs.binary }}
pytest tests/benchmarks/python/ --codspeed --no-cov
mode: simulation
clang-tidy-single:
@@ -431,6 +473,8 @@ jobs:
if: needs.determine-jobs.outputs.clang-tidy == 'true'
env:
GH_TOKEN: ${{ github.token }}
# esp32-arduino-tidy installs ESP-IDF natively; share the native IDF cache.
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
strategy:
fail-fast: false
max-parallel: 2
@@ -441,9 +485,9 @@ jobs:
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
pio_cache_key: tidyesp8266
- id: clang-tidy
name: Run script/clang-tidy for ESP32 IDF
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
pio_cache_key: tidyesp32-idf
name: Run script/clang-tidy for ESP32 Arduino
options: --environment esp32-arduino-tidy --grep USE_ARDUINO
cache_idf: true
- id: clang-tidy
name: Run script/clang-tidy for ZEPHYR
options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
@@ -452,7 +496,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -464,36 +508,42 @@ jobs:
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
if: github.ref == 'refs/heads/dev' && matrix.pio_cache_key
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
if: github.ref != 'refs/heads/dev' && matrix.pio_cache_key
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache ESP-IDF install
# Shared with the IDF tidy + native-IDF build jobs (same install).
if: matrix.cache_idf
uses: ./.github/actions/cache-esp-idf
with:
framework: arduino
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- name: Run 'pio run --list-targets -e esp32-idf-tidy'
if: matrix.name == 'Run script/clang-tidy for ESP32 IDF'
run: |
. venv/bin/activate
mkdir -p .temp
pio run --list-targets -e esp32-idf-tidy
- name: Check if full clang-tidy scan needed
id: check_full_scan
run: |
. venv/bin/activate
if python script/clang_tidy_hash.py --check; then
# determine-jobs.clang-tidy-full-scan is true when core C++ changed
# OR the ci-run-all label forced --force-all. Independent of the
# hash check, both must produce a full scan in the job itself.
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
elif python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
@@ -505,7 +555,7 @@ jobs:
run: |
. venv/bin/activate
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (hash changed)"
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
else
echo "Running clang-tidy on changed files only"
@@ -521,7 +571,7 @@ jobs:
if: always()
clang-tidy-nosplit:
name: Run script/clang-tidy for ESP32 Arduino
name: Run script/clang-tidy for ESP32 IDF
runs-on: ubuntu-24.04
needs:
- common
@@ -529,9 +579,11 @@ jobs:
if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit'
env:
GH_TOKEN: ${{ github.token }}
# esp32-idf-tidy installs ESP-IDF natively; share the native IDF cache.
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -542,19 +594,9 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache ESP-IDF install
# Shared with the Arduino tidy + native-IDF build jobs (same install).
uses: ./.github/actions/cache-esp-idf
- name: Register problem matchers
run: |
@@ -565,7 +607,13 @@ jobs:
id: check_full_scan
run: |
. venv/bin/activate
if python script/clang_tidy_hash.py --check; then
# determine-jobs.clang-tidy-full-scan is true when core C++ changed
# OR the ci-run-all label forced --force-all. Independent of the
# hash check, both must produce a full scan in the job itself.
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
elif python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
@@ -577,11 +625,11 @@ jobs:
run: |
. venv/bin/activate
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (hash changed)"
script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
script/clang-tidy --all-headers --fix --environment esp32-idf-tidy
else
echo "Running clang-tidy on changed files only"
script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy
script/clang-tidy --all-headers --fix --changed --environment esp32-idf-tidy
fi
env:
# Also cache libdeps, store them in a ~/.platformio subfolder
@@ -600,27 +648,26 @@ jobs:
if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
env:
GH_TOKEN: ${{ github.token }}
# esp32-idf-tidy installs ESP-IDF natively; share the native IDF cache.
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
strategy:
fail-fast: false
max-parallel: 2
max-parallel: 3
matrix:
include:
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 1/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
name: Run script/clang-tidy for ESP32 IDF 1/3
options: --environment esp32-idf-tidy --split-num 3 --split-at 1
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 2/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
name: Run script/clang-tidy for ESP32 IDF 2/3
options: --environment esp32-idf-tidy --split-num 3 --split-at 2
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 3/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 4/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
name: Run script/clang-tidy for ESP32 IDF 3/3
options: --environment esp32-idf-tidy --split-num 3 --split-at 3
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -631,19 +678,9 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache ESP-IDF install
# Shared with the Arduino tidy + native-IDF build jobs (same install).
uses: ./.github/actions/cache-esp-idf
- name: Register problem matchers
run: |
@@ -654,7 +691,13 @@ jobs:
id: check_full_scan
run: |
. venv/bin/activate
if python script/clang_tidy_hash.py --check; then
# determine-jobs.clang-tidy-full-scan is true when core C++ changed
# OR the ci-run-all label forced --force-all. Independent of the
# hash check, both must produce a full scan in the job itself.
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
elif python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
@@ -666,7 +709,7 @@ jobs:
run: |
. venv/bin/activate
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (hash changed)"
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
script/clang-tidy --all-headers --fix ${{ matrix.options }}
else
echo "Running clang-tidy on changed files only"
@@ -680,6 +723,93 @@ jobs:
run: script/ci-suggest-changes
if: always()
clang-tidy-esp32-variants:
name: ${{ matrix.name }}
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.clang-tidy == 'true'
env:
GH_TOKEN: ${{ github.token }}
# The variant tidy envs install ESP-IDF natively; share the native IDF cache.
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
strategy:
fail-fast: false
max-parallel: 3
matrix:
include:
- id: clang-tidy
name: Run script/clang-tidy for ESP32 S3
options: --environment esp32s3-idf-tidy --grep USE_ESP32_VARIANT_ESP32S3
- id: clang-tidy
name: Run script/clang-tidy for ESP32 P4
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
# so their code paths differ -- lint them under the P4 build too.
# yamllint disable-line rule:line-length
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
- id: clang-tidy
name: Run script/clang-tidy for ESP32 C6
# yamllint disable-line rule:line-length
options: --environment esp32c6-idf-tidy --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache ESP-IDF install
# Shared with the IDF/Arduino clang-tidy jobs + native-IDF build (same install).
uses: ./.github/actions/cache-esp-idf
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- name: Check if full clang-tidy scan needed
id: check_full_scan
run: |
. venv/bin/activate
# determine-jobs.clang-tidy-full-scan is true when core C++ changed
# OR the ci-run-all label forced --force-all. Independent of the
# hash check, both must produce a full scan in the job itself.
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
elif python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
echo "full_scan=false" >> $GITHUB_OUTPUT
echo "reason=normal" >> $GITHUB_OUTPUT
fi
- name: Run clang-tidy
# Limited variant scan: only the files carrying that variant's code paths
# (no --all-headers; the comprehensive esp32-idf pass covers the shared tree).
run: |
. venv/bin/activate
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
script/clang-tidy --fix ${{ matrix.options }}
else
echo "Running clang-tidy on changed files only"
script/clang-tidy --fix --changed ${{ matrix.options }}
fi
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
test-build-components-split:
name: Test components batch (${{ matrix.components }})
runs-on: ubuntu-24.04
@@ -708,7 +838,7 @@ jobs:
version: 1.0
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -765,7 +895,7 @@ jobs:
fi
echo ""
# Show disk space before validation (after bind mounts setup)
# Show disk space before validation
echo "Disk space before config validation:"
df -h
echo ""
@@ -823,13 +953,17 @@ jobs:
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.native-idf == 'true'
env:
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
TEST_COMPONENTS: esp32,api,heatpumpir,bme280_i2c,bh1750,aht10,esp32_ble,esp32_ble_beacon,esp32_ble_client,esp32_ble_server,esp32_ble_tracker,ble_client,ble_presence,ble_rssi,ble_scanner
# Comma-joined subset of the native-IDF representative component list,
# computed by script/determine-jobs.py (native_idf_components_to_test).
# Single source of truth -- the full list lives in
# script/determine-jobs.py::NATIVE_IDF_TEST_COMPONENTS.
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.native-idf-components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -837,33 +971,20 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache ESPHome
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.esphome-idf
key: ${{ runner.os }}-esphome-${{ needs.common.outputs.cache-key }}
- name: Run native ESP-IDF compile test
- name: Prepare build storage on /mnt
# Bind-mount the larger /mnt disk over the IDF install + build dirs BEFORE
# restoring the cache, so the ~4.5GB restore lands on the roomier volume
# instead of being shadowed by a mount set up later in the run step.
run: |
. venv/bin/activate
# Check if /mnt has more free space than / before bind mounting
# Extract available space in KB for comparison
root_avail=$(df -k / | awk 'NR==2 {print $4}')
mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}')
echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB"
# Only use /mnt if it has more space than /
if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
echo "Using /mnt for build files (more space available)"
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
sudo mkdir -p /mnt/esphome-idf
sudo chown $USER:$USER /mnt/esphome-idf
mkdir -p ~/.esphome-idf
sudo mount --bind /mnt/esphome-idf ~/.esphome-idf
# Bind mount test build directory to /mnt
sudo mkdir -p /mnt/test_build_components_build
sudo chown $USER:$USER /mnt/test_build_components_build
mkdir -p tests/test_build_components/build
@@ -872,10 +993,19 @@ jobs:
echo "Using / for build files (more space available than /mnt or /mnt unavailable)"
fi
- name: Cache ESP-IDF install
# Shared with the IDF/Arduino clang-tidy jobs (same install); restores
# into the /mnt bind-mount prepared above when present.
uses: ./.github/actions/cache-esp-idf
- name: Run native ESP-IDF compile test
run: |
. venv/bin/activate
echo "Testing components: $TEST_COMPONENTS"
echo ""
# Show disk space before validation (after bind mounts setup)
# Show disk space before validation
echo "Disk space before config validation:"
df -h
echo ""
@@ -907,10 +1037,11 @@ jobs:
runs-on: ubuntu-latest
needs:
- common
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
- determine-jobs
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') && needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -936,7 +1067,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.base_ref }}
@@ -1118,7 +1249,7 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -1187,7 +1318,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -1230,6 +1361,7 @@ jobs:
- clang-tidy-single
- clang-tidy-nosplit
- clang-tidy-split
- clang-tidy-esp32-variants
- determine-jobs
- device-builder
- test-build-components-split

View File

@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |

View File

@@ -29,13 +29,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -52,11 +52,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:${{matrix.language}}"

View File

@@ -12,10 +12,16 @@ jobs:
dashboard-deprecation-comment:
name: Dashboard deprecation comment
runs-on: ubuntu-latest
# Release-bump PRs (bump-X.Y.Z -> beta, beta -> release) inevitably
# roll up everything merged into dev since the last cut, which can
# include dashboard changes that have already been reviewed once.
# The bot's purpose is to warn new contributors before they invest
# time -- that only applies to PRs entering dev.
if: github.event.pull_request.base.ref == 'dev'
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -16,7 +16,7 @@ jobs:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
@@ -29,10 +29,11 @@ jobs:
} = require('./.github/scripts/detect-tags.js');
const title = context.payload.pull_request.title;
const author = context.payload.pull_request.user.login;
const user = context.payload.pull_request.user;
// Skip bot PRs (e.g. dependabot) - they have their own title format
if (author === 'dependabot[bot]') {
// Skip bot PRs (e.g. dependabot, esphome[bot] device-class sync) -
// they have their own title formats.
if (user.type === 'Bot') {
return;
}
@@ -68,14 +69,15 @@ jobs:
return;
}
// Check for angle brackets not wrapped in backticks.
// Astro docs MDX treats bare < as JSX component opening tags.
// Check for MDX syntax characters not wrapped in backticks.
// Astro docs MDX treats bare `<` as JSX component opening tags and
// bare `{` as JS expressions, so both must be escaped in changelog entries.
const stripped = title.replace(/`[^`]*`/g, '');
if (/[<>]/.test(stripped)) {
if (/[<>{}]/.test(stripped)) {
core.setFailed(
'PR title contains `<` or `>` not wrapped in backticks.\n' +
'Astro docs MDX interprets bare `<` as JSX components.\n' +
'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support'
'PR title contains `<`, `>`, `{`, or `}` not wrapped in backticks.\n' +
'Astro docs MDX interprets bare `<` as JSX components and bare `{` as JS expressions.\n' +
'Please wrap these characters with backticks, e.g.: [component] Add `<feature>` support'
);
return;
}

View File

@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Get tag
id: tag
# yamllint disable rule:line-length
@@ -60,7 +60,7 @@ jobs:
contents: read # actions/checkout to build the sdist/wheel
id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
@@ -92,22 +92,22 @@ jobs:
os: "ubuntu-24.04-arm"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log in to docker hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -168,7 +168,7 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -178,17 +178,17 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -212,74 +212,6 @@ jobs:
docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
$(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
deploy-ha-addon-repo:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs:
- init
- deploy-manifest
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: home-assistant-addon
permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token)
- name: Trigger Workflow
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
let description = "ESPHome";
if (context.eventName == "release") {
description = ${{ toJSON(github.event.release.body) }};
}
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "home-assistant-addon",
workflow_id: "bump-version.yml",
ref: "main",
inputs: {
version: "${{ needs.init.outputs.tag }}",
content: description
}
})
deploy-esphome-schema:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs: [init]
environment: ${{ needs.init.outputs.deploy_env }}
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: esphome-schema
permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token)
- name: Trigger Workflow
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "esphome-schema",
workflow_id: "generate-schemas.yml",
ref: "main",
inputs: {
version: "${{ needs.init.outputs.tag }}",
}
})
version-notifier:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
@@ -289,7 +221,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -302,7 +234,7 @@ jobs:
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
github.rest.actions.createWorkflowDispatch({
await github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "version-notifier",
workflow_id: "notify.yml",

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Stale
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
remove-stale-when-updated: true

View File

@@ -19,7 +19,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -28,10 +28,10 @@ jobs:
permission-pull-requests: write # pulls.create / pulls.update to open or refresh the sync PR
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Checkout Home Assistant
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: home-assistant/core
path: lib/home-assistant
@@ -41,19 +41,56 @@ jobs:
with:
python-version: "3.14"
- name: Set up uv
# An order of magnitude faster than pip on cold boots, with its
# own wheel cache. ``--system`` (below) installs into the
# setup-python interpreter so subsequent ``pre-commit`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
version: "0.11.15"
- name: Install Home Assistant
run: |
python -m pip install --upgrade pip
pip install -e lib/home-assistant
pip install -r requirements_test.txt pre-commit
uv pip install --system -e lib/home-assistant
uv pip install --system -r requirements.txt -r requirements_test.txt pre-commit
- name: Sync
run: |
python ./script/sync-device_class.py
- name: Run pre-commit hooks
run: |
python script/run-in-env.py pre-commit run --all-files
- name: Apply pre-commit auto-fixes
# First pass: let formatters (ruff, end-of-file-fixer, etc.) modify
# files. pre-commit exits non-zero whenever a hook touches anything,
# which would otherwise abort the workflow before the auto-fixes
# can flow into the sync PR.
#
# SKIP:
# - no-commit-to-branch is a local guard against committing on
# dev/release/beta; CI runs on dev by definition, and
# peter-evans/create-pull-request creates the branch itself.
# - pylint surfaces import-error / relative-beyond-top-level
# noise here because this workflow installs only a subset of
# the runtime deps (HA + requirements*.txt); main CI already
# gates pylint on real PRs.
env:
SKIP: pylint,no-commit-to-branch
run: python script/run-in-env.py pre-commit run --all-files || true
- name: Verify pre-commit clean
# Second pass: re-run all hooks against the now-fixed tree.
# Auto-fixers exit 0 (nothing to change); any remaining failure
# from a check-only hook (flake8 / yamllint / ci-custom) is a
# real issue and fails the workflow loudly. Same SKIP list as
# above for the same reasons.
env:
SKIP: pylint,no-commit-to-branch
run: python script/run-in-env.py pre-commit run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1

1
.gitignore vendored
View File

@@ -141,6 +141,7 @@ tests/.esphome/
sdkconfig.*
!sdkconfig.defaults
!sdkconfig.defaults.*
.tests/

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.12
rev: v0.15.15
hooks:
# Run the linter.
- id: ruff
@@ -63,7 +63,7 @@ repos:
name: Update clang-tidy hash
entry: python script/clang_tidy_hash.py --update-if-changed
language: python
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt|sdkconfig\.defaults|esphome/idf_component\.yml)$
pass_filenames: false
additional_dependencies: []
- id: ci-custom

View File

@@ -462,7 +462,7 @@ This document provides essential context for AI models interacting with this pro
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
* **Documentation Contributions:**
* Documentation is hosted in the separate `esphome/esphome-docs` repository.
* Documentation is hosted in the separate `esphome/esphome.io` repository.
* The contribution workflow is the same as for the codebase.
* When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync.
@@ -681,7 +681,7 @@ This document provides essential context for AI models interacting with this pro
- [ ] 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
- [ ] Updated all internal usage and esphome.io
- [ ] Tested backward compatibility during deprecation period
* **Deprecation Pattern (C++):**

View File

@@ -19,7 +19,6 @@ esphome/components/ac_dimmer/* @glmnet
esphome/components/adc/* @esphome/core
esphome/components/adc128s102/* @DeerMaximum
esphome/components/addressable_light/* @justfalter
esphome/components/ade7880/* @kpfleming
esphome/components/ade7953/* @angelnu
esphome/components/ade7953_base/* @angelnu
esphome/components/ade7953_i2c/* @angelnu
@@ -28,7 +27,7 @@ esphome/components/ads1118/* @solomondg1
esphome/components/ags10/* @mak-42
esphome/components/aic3204/* @kbx81
esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
esphome/components/airthings_wave_base/* @jeromelaban @ncareau
esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
@@ -84,6 +83,7 @@ esphome/components/bme680_bsec/* @trvrnrth
esphome/components/bme68x_bsec2/* @kbx81 @neffs
esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs
esphome/components/bmi160/* @flaviut
esphome/components/bmi270/* @clydebarrow
esphome/components/bmp280_base/* @ademuri
esphome/components/bmp280_i2c/* @ademuri
esphome/components/bmp280_spi/* @ademuri
@@ -139,7 +139,7 @@ esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber
esphome/components/dht/* @OttoWinter
esphome/components/display_menu_base/* @numo68
esphome/components/dlms_meter/* @SimonFischer04
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its
@@ -291,6 +291,7 @@ esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/logger/select/* @clydebarrow
esphome/components/lps22/* @nagisa
esphome/components/lsm6ds/* @clydebarrow
esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita
@@ -351,6 +352,7 @@ esphome/components/modbus_server/* @exciton
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
esphome/components/mopeka_pro_check/* @spbrogan
esphome/components/mopeka_std_check/* @Fabian-Schmidt
esphome/components/motion/* @esphome/core
esphome/components/mpl3115a2/* @kbickar
esphome/components/mpu6886/* @fabaff
esphome/components/ms8607/* @e28eta
@@ -379,6 +381,7 @@ esphome/components/pca6416a/* @Mat931
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon
esphome/components/pcf8563/* @KoenBreeman
esphome/components/pcm5122/* @remcom
esphome/components/pi4ioe5v6408/* @jesserockz
esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984
@@ -417,6 +420,7 @@ esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz
esphome/components/ring_buffer/* @kahrendt
esphome/components/router/speaker/* @kahrendt
esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_ble/* @bdraco
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
@@ -594,6 +598,7 @@ esphome/components/wk2212_spi/* @DrCoolZic
esphome/components/wl_134/* @hobbypunk90
esphome/components/wts01/* @alepee
esphome/components/x9c/* @EtienneMD
esphome/components/xdb401/* @RT530
esphome/components/xgzp68xx/* @gcormier
esphome/components/xiaomi_hhccjcy10/* @fariouche
esphome/components/xiaomi_lywsd02mmc/* @juanluss31

View File

@@ -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 = 2026.5.0-dev
PROJECT_NUMBER = 2026.6.0b4
# 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

18
codecov.yml Normal file
View File

@@ -0,0 +1,18 @@
coverage:
status:
patch:
default:
target: 100%
threshold: 0%
project:
default:
informational: true
ignore:
- "esphome/components/**/*"
- "esphome/analyze_memory/**/*"
- "tests/integration/**/*"
comment:
layout: "reach, diff, flags, files"
require_changes: true

View File

@@ -1,10 +1,9 @@
ARG BUILD_VERSION=dev
ARG BUILD_OS=alpine
ARG BUILD_BASE_VERSION=2025.04.0
ARG BUILD_BASE_VERSION=2026.06.0
ARG BUILD_TYPE=docker
FROM ghcr.io/esphome/docker-base:${BUILD_OS}-${BUILD_BASE_VERSION} AS base-source-docker
FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base-source-docker
FROM ghcr.io/esphome/docker-base:debian-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
ARG BUILD_TYPE
FROM base-source-${BUILD_TYPE} AS base
@@ -13,14 +12,14 @@ RUN git config --system --add safe.directory "*" \
&& git config --system advice.detachedHead false
# Install build tools for Python packages that require compilation
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
RUN if command -v apk > /dev/null; then \
apk add --no-cache build-base; \
else \
apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*; \
fi
# (e.g., ruamel.yaml.clib used by ESP-IDF's idf-component-manager).
# Also install libusb-1.0 at runtime so the ESP-IDF tools installer can
# validate openocd-esp32 (it dynamically links libusb-1.0.so.0); without
# it idf_tools.py rejects the openocd install with exit 127 and aborts
# the whole framework setup.
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential libusb-1.0-0 \
&& rm -rf /var/lib/apt/lists/*
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
@@ -32,6 +31,9 @@ RUN \
uv pip install --no-cache-dir \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.6
RUN \
platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \

View File

@@ -20,6 +20,10 @@ TYPE_HA_ADDON = "ha-addon"
TYPE_LINT = "lint"
TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
REGISTRY_GHCR = "ghcr"
REGISTRY_DOCKERHUB = "dockerhub"
REGISTRIES = [REGISTRY_GHCR, REGISTRY_DOCKERHUB]
parser = argparse.ArgumentParser()
parser.add_argument(
@@ -34,6 +38,12 @@ parser.add_argument(
parser.add_argument(
"--build-type", choices=TYPES, required=True, help="The type of build to run"
)
parser.add_argument(
"--registry",
choices=REGISTRIES,
action="append",
help="Restrict to specific registries (default: all). May be passed multiple times.",
)
parser.add_argument(
"--dry-run", action="store_true", help="Don't run any commands, just print them"
)
@@ -45,6 +55,11 @@ build_parser.add_argument("--push", help="Also push the images", action="store_t
build_parser.add_argument(
"--load", help="Load the docker image locally", action="store_true"
)
build_parser.add_argument(
"--no-cache-to",
help="Don't write the build cache (avoids polluting the shared cache)",
action="store_true",
)
manifest_parser = subparsers.add_parser(
"manifest", help="Create a manifest from already pushed images"
)
@@ -95,11 +110,14 @@ def main():
print("Command failed")
sys.exit(1)
registries = args.registry or REGISTRIES
# detect channel from tag
match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag)
major_minor_version = None
if match is None:
channel = CHANNEL_DEV
# Custom tag (e.g. a branch name) -- push only the tag itself
channel = None
elif match.group(2) is None:
major_minor_version = match.group(1)
channel = CHANNEL_RELEASE
@@ -128,11 +146,18 @@ def main():
CHANNEL_DEV: "cache-dev",
CHANNEL_BETA: "cache-beta",
CHANNEL_RELEASE: "cache-latest",
}[channel]
cache_img = f"ghcr.io/{params.build_to}:{cache_tag}"
}.get(channel, "cache-dev")
# Cache images live alongside the pushed images; prefer GHCR when it is
# one of the selected registries, otherwise fall back to Docker Hub so a
# registry-restricted build doesn't need GHCR auth.
cache_prefix = "ghcr.io/" if REGISTRY_GHCR in registries else ""
cache_img = f"{cache_prefix}{params.build_to}:{cache_tag}"
imgs = [f"{params.build_to}:{tag}" for tag in tags_to_push]
imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push]
imgs = []
if REGISTRY_DOCKERHUB in registries:
imgs += [f"{params.build_to}:{tag}" for tag in tags_to_push]
if REGISTRY_GHCR in registries:
imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push]
# 3. build
cmd = [
@@ -155,7 +180,9 @@ def main():
for img in imgs:
cmd += ["--tag", img]
if args.push:
cmd += ["--push", "--cache-to", f"type=registry,ref={cache_img},mode=max"]
cmd += ["--push"]
if not args.no_cache_to:
cmd += ["--cache-to", f"type=registry,ref={cache_img},mode=max"]
if args.load:
cmd += ["--load"]
@@ -163,20 +190,22 @@ def main():
elif args.command == "manifest":
manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to
targets = [f"{manifest}:{tag}" for tag in tags_to_push]
targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push]
# 1. Create manifests
targets = []
if REGISTRY_DOCKERHUB in registries:
targets += [f"{manifest}:{tag}" for tag in tags_to_push]
if REGISTRY_GHCR in registries:
targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push]
# Use buildx imagetools (not `docker manifest`) so the per-arch sources,
# which buildx pushes as single-platform manifest lists, are combined
# and pushed correctly in one step.
for target in targets:
cmd = ["docker", "manifest", "create", target]
cmd = ["docker", "buildx", "imagetools", "create", "--tag", target]
for arch in ARCHS:
src = f"{DockerParams.for_type_arch(args.build_type, arch).build_to}:{args.tag}"
if target.startswith("ghcr.io"):
src = f"ghcr.io/{src}"
cmd.append(src)
run_command(*cmd)
# 2. Push manifests
for target in targets:
run_command("docker", "manifest", "push", target)
if __name__ == "__main__":

View File

@@ -27,4 +27,12 @@ if [[ -d /build ]]; then
export ESPHOME_BUILD_PATH=/build
fi
# The default CMD is "dashboard /config". Route the dashboard to the new
# Device Builder, but pass every other subcommand (compile, run, config,
# logs, ...) straight through to the esphome CLI so direct CLI use keeps working.
if [[ "$1" == "dashboard" ]]; then
shift
exec esphome-device-builder "$@"
fi
exec esphome "$@"

View File

@@ -1,22 +0,0 @@
#!/usr/bin/with-contenv bashio
# ==============================================================================
# Installs the latest prerelease of esphome-device-builder when the
# `use_new_device_builder` config option is enabled.
# This is a temporary install-on-boot step until esphome-device-builder
# becomes a direct dependency of esphome.
# ==============================================================================
if ! bashio::config.true 'use_new_device_builder'; then
exit 0
fi
bashio::log.info "Installing latest prerelease of esphome-device-builder..."
if command -v uv > /dev/null; then
uv pip install --system --no-cache-dir --prerelease=allow --upgrade \
esphome-device-builder ||
bashio::exit.nok "Failed installing esphome-device-builder."
else
pip install --no-cache-dir --pre --upgrade esphome-device-builder ||
bashio::exit.nok "Failed installing esphome-device-builder."
fi
bashio::log.info "Installed esphome-device-builder."

View File

@@ -1,96 +0,0 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View File

@@ -1,16 +0,0 @@
proxy_http_version 1.1;
proxy_ignore_client_abort off;
proxy_read_timeout 86400s;
proxy_redirect off;
proxy_send_timeout 86400s;
proxy_max_temp_file_size 0;
proxy_set_header Accept-Encoding "";
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Authorization "";

View File

@@ -1,8 +0,0 @@
root /dev/null;
server_name $hostname;
client_max_body_size 512m;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;

View File

@@ -1,8 +0,0 @@
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;

View File

@@ -1,3 +0,0 @@
upstream esphome {
server unix:/var/run/esphome.sock;
}

View File

@@ -1,30 +0,0 @@
daemon off;
user root;
pid /var/run/nginx.pid;
worker_processes 1;
error_log /proc/1/fd/1 error;
events {
worker_connections 1024;
}
http {
include /etc/nginx/includes/mime.types;
access_log off;
default_type application/octet-stream;
gzip on;
keepalive_timeout 65;
sendfile on;
server_tokens off;
tcp_nodelay on;
tcp_nopush on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
include /etc/nginx/includes/upstream.conf;
include /etc/nginx/servers/*.conf;
}

View File

@@ -1 +0,0 @@
Without requirements or design, programming is the art of adding bugs to an empty text file. (Louis Srygley)

View File

@@ -1,28 +0,0 @@
server {
{{ if not .ssl }}
listen 6052 default_server;
{{ else }}
listen 6052 default_server ssl http2;
{{ end }}
include /etc/nginx/includes/server_params.conf;
include /etc/nginx/includes/proxy_params.conf;
{{ if .ssl }}
include /etc/nginx/includes/ssl_params.conf;
ssl_certificate /ssl/{{ .certfile }};
ssl_certificate_key /ssl/{{ .keyfile }};
# Redirect http requests to https on the same port.
# https://rageagainstshell.com/2016/11/redirect-http-to-https-on-the-same-port-in-nginx/
error_page 497 https://$http_host$request_uri;
{{ end }}
# Clear Home Assistant Ingress header
proxy_set_header X-HA-Ingress "";
location / {
proxy_pass http://esphome;
}
}

View File

@@ -1,18 +0,0 @@
server {
listen 127.0.0.1:{{ .port }} default_server;
listen {{ .interface }}:{{ .port }} default_server;
include /etc/nginx/includes/server_params.conf;
include /etc/nginx/includes/proxy_params.conf;
# Set Home Assistant Ingress header
proxy_set_header X-HA-Ingress "YES";
location / {
allow 172.30.32.2;
allow 127.0.0.1;
deny all;
proxy_pass http://esphome;
}
}

View File

@@ -16,7 +16,7 @@ fi
port=$(bashio::addon.ingress_port)
# Wait for NGINX to become available
# Wait for the ESPHome Device Builder to become available
bashio::net.wait_for "${port}" "127.0.0.1" 300
config=$(\

View File

@@ -2,7 +2,7 @@
# shellcheck shell=bash
# ==============================================================================
# Home Assistant Community Add-on: ESPHome
# Take down the S6 supervision tree when ESPHome dashboard fails
# Take down the S6 supervision tree when ESPHome Device Builder fails
# ==============================================================================
declare exit_code
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
@@ -10,7 +10,7 @@ readonly exit_code_service="${1}"
readonly exit_code_signal="${2}"
bashio::log.info \
"Service ESPHome dashboard exited with code ${exit_code_service}" \
"Service ESPHome Device Builder exited with code ${exit_code_service}" \
"(by signal ${exit_code_signal})"
if [[ "${exit_code_service}" -eq 256 ]]; then

View File

@@ -2,7 +2,7 @@
# shellcheck shell=bash
# ==============================================================================
# Community Hass.io Add-ons: ESPHome
# Runs the ESPHome dashboard
# Runs the ESPHome Device Builder
# ==============================================================================
readonly pio_cache_base=/data/cache/platformio
@@ -49,12 +49,7 @@ if bashio::fs.directory_exists '/config/esphome/.esphome'; then
rm -rf /config/esphome/.esphome
fi
if bashio::config.true 'use_new_device_builder'; then
bashio::log.info "Starting ESPHome Device Builder..."
exec esphome-device-builder /config/esphome \
--ha-addon \
--ingress-port "$(bashio::addon.ingress_port)"
fi
bashio::log.info "Starting ESPHome dashboard..."
exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon
bashio::log.info "Starting ESPHome Device Builder..."
exec esphome-device-builder /config/esphome \
--ha-addon \
--ingress-port "$(bashio::addon.ingress_port)"

View File

@@ -1,35 +0,0 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
# ==============================================================================
# Community Hass.io Add-ons: ESPHome
# Configures NGINX for use with ESPHome
# ==============================================================================
# When the new device builder is enabled it serves HA ingress directly,
# so nginx is not used at all -- skip configuration.
if bashio::config.true 'use_new_device_builder'; then
bashio::log.info "Skipping NGINX setup: new device builder serves ingress directly."
bashio::exit.ok
fi
mkdir -p /var/log/nginx
# Generate Ingress configuration
bashio::var.json \
interface "$(bashio::addon.ip_address)" \
port "^$(bashio::addon.ingress_port)" \
| tempio \
-template /etc/nginx/templates/ingress.gtpl \
-out /etc/nginx/servers/ingress.conf
# Generate direct access configuration, if enabled.
if bashio::var.has_value "$(bashio::addon.port 6052)"; then
bashio::config.require.ssl
bashio::var.json \
certfile "$(bashio::config 'certfile')" \
keyfile "$(bashio::config 'keyfile')" \
ssl "^$(bashio::config 'ssl')" \
| tempio \
-template /etc/nginx/templates/direct.gtpl \
-out /etc/nginx/servers/direct.conf
fi

View File

@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-nginx/run

View File

@@ -1,25 +0,0 @@
#!/command/with-contenv bashio
# ==============================================================================
# Community Hass.io Add-ons: ESPHome
# Take down the S6 supervision tree when NGINX fails
# ==============================================================================
declare exit_code
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
readonly exit_code_service="${1}"
readonly exit_code_signal="${2}"
bashio::log.info \
"Service NGINX exited with code ${exit_code_service}" \
"(by signal ${exit_code_signal})"
if [[ "${exit_code_service}" -eq 256 ]]; then
if [[ "${exit_code_container}" -eq 0 ]]; then
echo $((128 + $exit_code_signal)) > /run/s6-linux-init-container-results/exitcode
fi
[[ "${exit_code_signal}" -eq 15 ]] && exec /run/s6/basedir/bin/halt
elif [[ "${exit_code_service}" -ne 0 ]]; then
if [[ "${exit_code_container}" -eq 0 ]]; then
echo "${exit_code_service}" > /run/s6-linux-init-container-results/exitcode
fi
exec /run/s6/basedir/bin/halt
fi

View File

@@ -1,23 +0,0 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
# ==============================================================================
# Community Hass.io Add-ons: ESPHome
# Runs the NGINX proxy
# ==============================================================================
# The new device builder handles HA ingress itself, so nginx is bypassed.
# Block the longrun forever so s6 keeps the dependency satisfied and does
# not respawn it.
if bashio::config.true 'use_new_device_builder'; then
bashio::log.info "NGINX bypassed: new device builder serves ingress directly."
exec sleep infinity
fi
bashio::log.info "Waiting for ESPHome dashboard to come up..."
while [[ ! -S /var/run/esphome.sock ]]; do
sleep 0.5
done
bashio::log.info "Starting NGINX..."
exec nginx

View File

@@ -50,6 +50,7 @@ from esphome.const import (
CONF_TOPIC,
CONF_USERNAME,
CONF_WEB_SERVER,
CONF_WIFI,
ENV_NOGITIGNORE,
KEY_CORE,
KEY_TARGET_PLATFORM,
@@ -607,7 +608,7 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
process_stacktrace = getattr(module, "process_stacktrace")
process_stacktrace = module.process_stacktrace
except (AttributeError, ImportError):
_LOGGER.info(
'Stacktrace analysis is unavailable: no compatible analyzer found for target platform "%s".',
@@ -638,7 +639,7 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
chunk = ser.read(ser.in_waiting or 1)
if not chunk:
continue
time_ = datetime.now()
time_ = datetime.now().astimezone()
milliseconds = time_.microsecond // 1000
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{milliseconds:03}]"
@@ -694,6 +695,11 @@ def _wrap_to_code(name, comp, yaml_util):
def write_cpp(config: ConfigType) -> int:
from esphome import writer
# Refresh the storage sidecar and clean an incompatible previous build
# before regenerating any sources. This may full-wipe the build dir, so it
# has to run before write_cpp_file writes src/.
writer.update_storage_json()
if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore()
@@ -733,6 +739,13 @@ def write_cpp_file() -> int:
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
# Keep this gate here, NOT in config validation: device-builder needs
# `esphome config` to keep succeeding with placeholders so onboarding can run.
if CONF_WIFI in config:
from esphome.components.wifi import check_placeholder_credentials
check_placeholder_credentials(config)
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
# If you change this format, update the regex in that script as well
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
@@ -752,6 +765,7 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
toolchain.create_factory_bin()
toolchain.create_ota_bin()
toolchain.create_elf_copy()
toolchain.get_idedata()
else:
from esphome.platformio import toolchain
@@ -786,7 +800,7 @@ def _check_and_emit_build_info() -> None:
# Read build_info from JSON
try:
with open(build_info_json_path, encoding="utf-8") as f:
with build_info_json_path.open(encoding="utf-8") as f:
build_info = json.load(f)
except (OSError, json.JSONDecodeError) as e:
_LOGGER.debug("Failed to read build_info: %s", e)
@@ -1048,7 +1062,7 @@ def _wait_for_serial_port(
def _port_found() -> bool:
if port is not None:
if os.name == "posix":
return os.path.exists(port)
return Path(port).exists()
return any(p.path == port for p in get_serial_ports())
ports = get_serial_ports()
if known_ports is not None:
@@ -1093,7 +1107,7 @@ def upload_program(
host = devices[0]
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
if getattr(module, "upload_program")(config, args, host):
if module.upload_program(config, args, host):
return 0, host
except AttributeError:
pass
@@ -1342,10 +1356,23 @@ def _validate_bootloader_binary(binary: Path) -> None:
)
def _should_subscribe_states(args: ArgsProtocol) -> bool:
"""Determine whether entity state changes should be shown in log output.
The ``--states``/``--no-states`` command line flags take precedence. When
neither is given, the ``ESPHOME_LOG_STATES`` environment variable controls
the behavior, defaulting to showing states.
"""
states = getattr(args, "states", None)
if states is not None:
return states
return get_bool_env("ESPHOME_LOG_STATES", True)
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
if getattr(module, "show_logs")(config, args, devices):
if module.show_logs(config, args, devices):
return 0
except AttributeError:
pass
@@ -1371,7 +1398,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
return run_logs(
config,
network_devices,
subscribe_states=not getattr(args, "no_states", False),
subscribe_states=_should_subscribe_states(args),
)
if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging():
@@ -1401,20 +1428,59 @@ def command_wizard(args: ArgsProtocol) -> int | None:
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import yaml_util
if not CORE.verbose:
if getattr(args, "no_defaults", False):
user_config = getattr(config, "user_config", None)
if user_config is None:
_LOGGER.warning(
"--no-defaults requested but the user-only config snapshot is "
"unavailable; falling back to the validated configuration."
)
else:
config = user_config
elif not CORE.verbose:
config = strip_default_ids(config)
output = yaml_util.dump(config, args.show_secrets)
# add the console decoration so the front-end can hide the secrets
if not args.show_secrets:
output = re.sub(
r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[8m\2\\033[28m", output
)
output = _redact_with_legacy_fallback(output)
if not CORE.quiet:
safe_print(output)
_LOGGER.info("Configuration is valid!")
return 0
# Legacy substring redaction fallback for unmigrated schemas; removed in
# 2026.12.0 once canonical sensitive fields are tagged. The lookahead skips
# values that already render themselves: ``\033[8m`` (SensitiveStr wrap),
# ``!secret`` (preserves the user-friendly tag), ``!lambda`` (multi-line
# block; first line is structural). The fragment must either start the
# field name or follow ``_`` so the warning names a real field; this avoids
# false positives like ``monkey:`` matching the ``key`` fragment.
_LEGACY_REDACTION_RE = re.compile(
r"(?P<key>\b(?:\w+_)?(?:password|key|psk|ssid))\: "
r"(?!\\033\[8m|!secret\b|!lambda\b)(?P<val>.+)"
)
_LEGACY_REDACTION_REMOVAL = "2026.12.0"
def _redact_with_legacy_fallback(output: str) -> str:
unmarked: set[str] = set()
def _replace(m: re.Match[str]) -> str:
unmarked.add(m.group("key"))
return f"{m.group('key')}: \\033[8m{m.group('val')}\\033[28m"
output = _LEGACY_REDACTION_RE.sub(_replace, output)
for key in sorted(unmarked):
_LOGGER.warning(
"Field '%s' is being redacted by a legacy substring heuristic. "
"Mark this field's schema validator with cv.sensitive(...) for "
"deterministic redaction; the heuristic will be removed in %s.",
key,
_LEGACY_REDACTION_REMOVAL,
)
return output
def command_config_hash(args: ArgsProtocol, config: ConfigType) -> int | None:
# generating code might modify config, so it must be done in order to generate
# a hash that will match what was generated when compiling and then running
@@ -1579,7 +1645,7 @@ def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import writer
try:
writer.clean_build()
writer.clean_build(full=True)
except OSError as err:
_LOGGER.error("Error deleting build files: %s", err)
return 1
@@ -1792,7 +1858,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
ram_report = ram_analyzer.generate_report()
print()
print(ram_report)
except Exception as e: # pylint: disable=broad-except
except Exception as e: # noqa: BLE001 # pylint: disable=broad-except
_LOGGER.warning("RAM strings analysis failed: %s", e)
return 0
@@ -1980,6 +2046,29 @@ SIMPLE_CONFIG_ACTIONS = [
]
def _add_states_args(parser: argparse.ArgumentParser) -> None:
"""Add mutually exclusive ``--states``/``--no-states`` flags to a parser.
When neither flag is given, the ``ESPHOME_LOG_STATES`` environment variable
controls whether entity state changes are shown (defaulting to showing them).
"""
states_group = parser.add_mutually_exclusive_group()
states_group.add_argument(
"--states",
dest="states",
action="store_true",
default=None,
help="Show entity state changes in log output (overrides ESPHOME_LOG_STATES).",
)
states_group.add_argument(
"--no-states",
dest="states",
action="store_false",
default=None,
help="Do not show entity state changes in log output.",
)
def parse_args(argv):
options_parser = argparse.ArgumentParser(add_help=False)
options_parser.add_argument(
@@ -2072,6 +2161,12 @@ def parse_args(argv):
parser_config.add_argument(
"--show-secrets", help="Show secrets in output.", action="store_true"
)
parser_config.add_argument(
"--no-defaults",
help="Only output the user-supplied configuration without "
"schema defaults applied.",
action="store_true",
)
parser_config_hash = subparsers.add_parser(
"config-hash", help="Calculate the hash of the configuration."
@@ -2156,11 +2251,7 @@ def parse_args(argv):
help="Reset the device before starting serial logs.",
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
)
parser_logs.add_argument(
"--no-states",
action="store_true",
help="Do not show entity state changes in log output.",
)
_add_states_args(parser_logs)
parser_discover = subparsers.add_parser(
"discover",
@@ -2192,11 +2283,7 @@ def parse_args(argv):
"--no-logs", help="Disable starting logs.", action="store_true"
)
parser_run.add_argument(
"--no-states",
action="store_true",
help="Do not show entity state changes in log output.",
)
_add_states_args(parser_run)
parser_run.add_argument(
"--reset",
@@ -2434,10 +2521,41 @@ def run_esphome(argv):
# Commands that don't need fresh external components: logs just connects
# to the device, and clean is about to delete the build directory.
skip_external = args.command in ("logs", "clean")
config = read_config(
dict(args.substitution) if args.substitution else {},
skip_external_update=skip_external,
command_line_substitutions = dict(args.substitution) if args.substitution else {}
# Fast path for upload/logs: reuse the validated-config cache the
# last compile wrote. Falls back to read_config when missing/stale.
# Skipped when -s overrides are passed, since the cache was written
# against the previous substitution set.
config: ConfigType | None = None
cache_eligible = (
args.command in ("upload", "logs") and not command_line_substitutions
)
if cache_eligible:
from esphome.compiled_config import load_compiled_config
config = load_compiled_config(conf_path)
if config is not None:
_LOGGER.info(
"Loaded validated config cache for %s, skipping validation.",
conf_path.name,
)
if config is None:
config = read_config(
command_line_substitutions,
skip_external_update=skip_external,
)
# Refresh the cache so the next upload/logs hits the fast path
# instead of re-running read_config. Skip when the storage
# sidecar is absent (no compile has run): the cache would
# never be loaded back, so writing secrets to disk is wasted.
if cache_eligible and config is not None:
from esphome.compiled_config import save_compiled_config
from esphome.storage_json import ext_storage_path
if ext_storage_path(conf_path.name).exists():
save_compiled_config(config)
if config is None:
return 2
CORE.config = config

View File

@@ -6,6 +6,7 @@ from collections import defaultdict
from collections.abc import Callable
import heapq
from operator import itemgetter
from pathlib import Path
import sys
from typing import TYPE_CHECKING
@@ -509,7 +510,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
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):
for i, (_symbol, demangled, size) in enumerate(large_core_symbols):
# Core symbols only track (symbol, demangled, size) without section info,
# so we don't show section labels here
lines.append(
@@ -601,7 +602,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):"
)
for i, (symbol, demangled, size, section) in enumerate(large_symbols):
for i, (_symbol, demangled, size, section) in enumerate(large_symbols):
lines.append(
f"{i + 1}. {self._format_symbol_with_section(demangled, size, section)}"
)
@@ -640,7 +641,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(
f" Symbols > {self.RAM_SYMBOL_SIZE_THRESHOLD} B ({len(large_ram_syms)}):"
)
for symbol, demangled, size, section in large_ram_syms[:10]:
for _symbol, demangled, size, section in large_ram_syms[:10]:
# Format section label consistently by stripping leading dot
section_label = section.lstrip(".") if section else ""
display_name = _format_pstorage_name(demangled)
@@ -699,7 +700,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
content = "\n".join(lines)
if output_file:
with open(output_file, "w", encoding="utf-8") as f:
with Path(output_file).open("w", encoding="utf-8") as f:
f.write(content)
else:
print(content)
@@ -737,7 +738,6 @@ def main():
# Load build directory
import json
from pathlib import Path
from esphome.platformio.toolchain import IDEData
@@ -785,7 +785,7 @@ def main():
if not idedata_path.exists():
continue
try:
with open(idedata_path, encoding="utf-8") as f:
with idedata_path.open(encoding="utf-8") as f:
raw_data = json.load(f)
idedata = IDEData(raw_data)
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)

View File

@@ -154,7 +154,7 @@ def batch_demangle(
failed_count = 0
for original, stripped, prefix, demangled in zip(
symbols, symbols_stripped, symbols_prefixes, demangled_lines
symbols, symbols_stripped, symbols_prefixes, demangled_lines, strict=True
):
# Add back any prefix that was removed
demangled = _restore_symbol_prefix(prefix, stripped, demangled)

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
import os
from pathlib import Path
import subprocess
from typing import TYPE_CHECKING
@@ -37,7 +36,7 @@ def _find_in_platformio_packages(tool_name: str) -> str | None:
Full path to the tool or None if not found
"""
# Get PlatformIO packages directory
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
platformio_home = Path("~/.platformio/packages").expanduser()
if not platformio_home.exists():
return None

View File

@@ -45,7 +45,7 @@ class AsyncThreadRunner(threading.Thread, Generic[_T]):
async def _runner(self) -> None:
try:
self.result = await self._coro_factory()
except Exception as exc: # pylint: disable=broad-except
except Exception as exc: # noqa: BLE001 # pylint: disable=broad-except
# Capture all exceptions so ``event`` is always set — otherwise a
# crash would hang the waiter forever.
self.exception = exc

View File

@@ -3,24 +3,38 @@
import json
from pathlib import Path
from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32 import get_esp32_variant, idf_version
import esphome.config_validation as cv
from esphome.core import CORE
from esphome.helpers import mkdir_p, write_file_if_changed
from esphome.writer import update_storage_json
# Replaces the IDF default C++ standard (-std=gnu++2b appended to
# CXX_COMPILE_OPTIONS by project.cmake's __build_init) with the one set via
# cg.set_cpp_standard(). Emitted between include(project.cmake) and project(),
# i.e. after IDF appends its default and before the options are consumed, and
# applies project-wide like PlatformIO build_unflags.
CPP_STANDARD_TEMPLATE = """\
idf_build_get_property(esphome_cxx_compile_options CXX_COMPILE_OPTIONS)
list(FILTER esphome_cxx_compile_options EXCLUDE REGEX "^-std=")
list(APPEND esphome_cxx_compile_options "-std={standard}")
idf_build_set_property(CXX_COMPILE_OPTIONS "${{esphome_cxx_compile_options}}")"""
def get_available_components() -> list[str] | None:
"""Get list of available ESP-IDF components from project_description.json.
"""Get list of built-in ESP-IDF components from project_description.json.
Returns only internal ESP-IDF components, excluding external/managed
components (from idf_component.yml).
Excludes ``src``, IDF-managed components (``managed_components/``), and
converted PIO libs (``pio_components/``). Returns ``None`` if the build
dir or ``project_description.json`` isn't ready yet.
"""
if CORE.build_path is None:
return None
project_desc = Path(CORE.build_path) / "build" / "project_description.json"
if not project_desc.exists():
return None
try:
with open(project_desc, encoding="utf-8") as f:
with project_desc.open(encoding="utf-8") as f:
data = json.load(f)
component_info = data.get("build_component_info", {})
@@ -31,9 +45,9 @@ def get_available_components() -> list[str] | None:
if name == "src":
continue
# Exclude managed/external components
# Exclude IDF-managed and converted-PIO components (external).
comp_dir = info.get("dir", "")
if "managed_components" in comp_dir:
if "managed_components" in comp_dir or "pio_components" in comp_dir:
continue
result.append(name)
@@ -48,17 +62,74 @@ def has_discovered_components() -> bool:
return get_available_components() is not None
def get_project_cmakelists() -> str:
"""Generate the top-level CMakeLists.txt for ESP-IDF project."""
def get_project_cmakelists(minimal: bool = False) -> str:
"""Generate the top-level CMakeLists.txt for ESP-IDF project.
When ``minimal`` is true, omit ``ESPHOME_PROJECT_BUILTIN_COMPONENTS``
since ``project_description.json`` may be stale on the first write.
"""
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
variant = get_esp32_variant()
idf_target = variant.lower().replace("-", "")
# Extract compile definitions from build flags (-DXXX -> XXX)
compile_defs = [flag for flag in sorted(CORE.build_flags) if flag.startswith("-D")]
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
# --format=raw because the legacy mode doesn't support it.
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
# Project-wide compile options: -D defines and -W warning flags (skip
# -Wl, linker flags — those go on the src component via
# target_link_options below). Emitted via idf_build_set_property so the
# flags propagate to every IDF component (including managed ones like
# esphome__micro-mp3) rather than just src/. Required so suppressions
# like ``-Wno-error=maybe-uninitialized`` actually silence warnings in
# third-party components we don't author.
project_compile_opts = [
flag
for flag in sorted(CORE.build_flags)
if flag.startswith("-D")
or (flag.startswith("-W") and not flag.startswith("-Wl,"))
]
extra_compile_options = "\n".join(
f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)'
for compile_def in compile_defs
f'idf_build_set_property(COMPILE_OPTIONS "{flag}" APPEND)'
for flag in project_compile_opts
)
cpp_standard_options = (
CPP_STANDARD_TEMPLATE.format(standard=CORE.cpp_standard)
if CORE.cpp_standard
else ""
)
# Per-project list exposed as a CMake variable so converted PIO libs
# can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking
# project-specific names into their cached CMakeLists.
#
# Emit via idf_build_set_property (not plain set()) so the value is
# serialised into build_properties.temp.cmake and visible to IDF's
# early requirements-expansion pass (component_get_requirements.cmake
# runs as a separate CMake script invocation that doesn't load the
# project's top-level CMakeLists; without this, ${ESPHOME_PROJECT_
# MANAGED_COMPONENTS} in a converted-lib REQUIRES expands to empty).
from esphome.components.esp32 import get_managed_component_require_names
managed_components_property = "\n".join(
f"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS {name} APPEND)"
for name in get_managed_component_require_names()
)
# Built-in IDF components exposed via our own property (not IDF's
# __COMPONENT_REQUIRES_COMMON, which would append them to every
# component's REQUIRES including real IDF components). Referenced by
# src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped
# on minimal writes because project_description.json may be stale.
builtin_components_property = (
""
if minimal
else "\n".join(
f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)"
for name in sorted(get_available_components() or [])
)
)
return f"""\
@@ -86,14 +157,20 @@ set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
{cpp_standard_options}
{extra_compile_options}
{managed_components_property}
{builtin_components_property}
project({CORE.name})
# Emit raw JSON size data for ESPHome to read post-build.
add_custom_command(
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
COMMAND ${{PYTHON}} -m esp_idf_size --ng --format=raw
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
${{CMAKE_PROJECT_NAME}}.map
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
@@ -102,44 +179,44 @@ add_custom_command(
"""
def get_component_cmakelists(minimal: bool = False) -> str:
"""Generate the main component CMakeLists.txt."""
idf_requires = [] if minimal else (get_available_components() or [])
requires_str = " ".join(idf_requires)
def get_component_cmakelists() -> str:
"""Generate the main component CMakeLists.txt.
# Extract compile options (-W flags, excluding linker flags)
compile_opts = [
flag
for flag in CORE.build_flags
if flag.startswith("-W") and not flag.startswith("-Wl,")
]
compile_opts_str = "\n ".join(sorted(compile_opts)) if compile_opts else ""
# Extract linker options (-Wl, flags)
REQUIRES pulls in the discovered built-in IDF components via the
project-level variables set in the top-level CMakeLists.
"""
# Extract linker options (-Wl, flags). Compile flags (-D, -W) are
# emitted project-wide via idf_build_set_property in
# get_project_cmakelists so they reach every component, not just src/.
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else ""
return f"""\
# Auto-generated by ESPHome
file(GLOB_RECURSE app_sources
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
)
# CONFIGURE_DEPENDS asks CMake to re-check the glob each build so test
# runs that reuse the build dir don't compile stale source paths. It's
# invalid in script mode (cmake -P), which is how IDF's
# component_get_requirements.cmake includes us, so skip it there.
if(CMAKE_SCRIPT_MODE_FILE)
file(GLOB_RECURSE app_sources
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
)
else()
file(GLOB_RECURSE app_sources CONFIGURE_DEPENDS
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
)
endif()
idf_component_register(
SRCS ${{app_sources}}
INCLUDE_DIRS "." "esphome"
REQUIRES {requires_str}
)
# Apply C++ standard
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
# ESPHome compile options
target_compile_options(${{COMPONENT_LIB}} PUBLIC
{compile_opts_str}
REQUIRES ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
)
# ESPHome linker options
@@ -151,22 +228,17 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
def write_project(minimal: bool = False) -> None:
"""Write ESP-IDF project files."""
# Refresh <data_dir>/storage/<name>.yaml.json so the dashboard's
# /info and /downloads endpoints can locate the build (they 404
# otherwise). This mirrors the PlatformIO build-gen path's call
# in build_gen/platformio.py:write_ini().
update_storage_json()
mkdir_p(CORE.build_path)
mkdir_p(CORE.relative_src_path())
# Write top-level CMakeLists.txt
write_file_if_changed(
CORE.relative_build_path("CMakeLists.txt"),
get_project_cmakelists(),
get_project_cmakelists(minimal=minimal),
)
# Write component CMakeLists.txt in src/
write_file_if_changed(
CORE.relative_src_path("CMakeLists.txt"),
get_component_cmakelists(minimal=minimal),
get_component_cmakelists(),
)

View File

@@ -1,7 +1,7 @@
from esphome.const import __version__
from esphome.core import CORE
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
from esphome.writer import find_begin_end, update_storage_json
from esphome.writer import find_begin_end
INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
@@ -33,12 +33,27 @@ def format_ini(data: dict[str, str | list[str]]) -> str:
return content
# All -std= variants a platform/framework may set by default, in both the GNU
# and strict dialects; unflagged so the cg.set_cpp_standard() value is the
# only standard left in the build.
CPP_STD_VARIANTS = [
f"{prefix}{year}"
for year in ("11", "14", "17", "20", "23", "26", "2a", "2b", "2c")
for prefix in ("gnu++", "c++")
]
def get_ini_content():
CORE.add_platformio_option(
"lib_deps",
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
+ ["${common.lib_deps}"],
)
if CORE.cpp_standard:
for variant in CPP_STD_VARIANTS:
if variant != CORE.cpp_standard:
CORE.add_build_unflag(f"-std={variant}")
CORE.add_build_flag(f"-std={CORE.cpp_standard}")
# Sort to avoid changing build flags order
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
@@ -58,7 +73,6 @@ def get_ini_content():
def write_ini(content):
update_storage_json()
path = CORE.relative_build_path("platformio.ini")
if path.is_file():

View File

@@ -260,42 +260,20 @@ class ConfigBundleCreator:
def _discover_yaml_includes(self) -> None:
"""Discover YAML files loaded during config parsing.
Deliberately uses a fresh re-parse and force-loads every deferred
``IncludeFile`` to include *all* potentially-reachable includes,
even branches not selected by the local substitutions. Bundles are
meant to be compiled on another system where command-line
substitution overrides may choose a different branch — e.g.
``!include network/${eth_model}/config.yaml`` must ship every
candidate so the remote build can pick any one.
Entries with unresolved substitution variables in the filename
path are skipped with a warning (they cannot be resolved without
the substitution pass).
Secrets files are tracked separately so we can filter them to
only include the keys this config actually references.
Delegates to :func:`yaml_util.discover_user_yaml_files`, which does a
fresh re-parse and force-loads every deferred ``IncludeFile`` so that
*all* potentially-reachable includes are captured (even branches not
selected by local substitutions). Bundles are meant to be compiled on
another system where command-line substitution overrides may choose a
different branch — e.g. ``!include network/${eth_model}/config.yaml``
must ship every candidate so the remote build can pick any one.
"""
# Must be a fresh parse: IncludeFile.load() caches its result in
# _content, and we discover files by listening for loader calls. On
# an already-parsed tree the cache is populated, .load() returns
# without calling the loader, the listener never fires, and the
# referenced files would be silently dropped from the bundle.
with yaml_util.track_yaml_loads() as loaded_files:
try:
data = yaml_util.load_yaml(self._config_path)
except EsphomeError:
_LOGGER.debug(
"Bundle: re-loading YAML for include discovery failed, "
"proceeding with partial file list"
)
else:
_force_load_include_files(data)
for fpath in loaded_files:
if fpath == self._config_path.resolve():
discovered = yaml_util.discover_user_yaml_files(self._config_path)
self._secrets_paths.update(discovered.secrets)
config_resolved = self._config_path.resolve()
for fpath in discovered.files:
if fpath == config_resolved:
continue # Already added as config
if fpath.name in const.SECRETS_FILES:
self._secrets_paths.add(fpath)
self._add_file(fpath)
def _discover_component_files(self) -> None:
@@ -434,7 +412,7 @@ class ConfigBundleCreator:
@staticmethod
def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None:
"""Add a BundleFile to the tar archive with deterministic metadata."""
with open(bf.source, "rb") as f:
with bf.source.open("rb") as f:
_add_bytes_to_tar(tar, bf.path, f.read())
@@ -625,57 +603,6 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
tar.addfile(info, io.BytesIO(data))
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
resolved during the substitution pass. During bundle discovery we need
the referenced files to actually load so the ``track_yaml_loads``
listener fires for them.
``IncludeFile`` instances with unresolved substitution variables in the
filename cannot be loaded — we skip and warn about those.
"""
if _seen is None:
_seen = set()
if isinstance(obj, yaml_util.IncludeFile):
if id(obj) in _seen:
return
_seen.add(id(obj))
if obj.has_unresolved_expressions():
_LOGGER.warning(
"Bundle: cannot resolve !include %s (referenced from %s) "
"with substitutions in path",
obj.file,
obj.parent_file,
)
return
try:
loaded = obj.load()
except EsphomeError as err:
_LOGGER.warning(
"Bundle: failed to load !include %s (referenced from %s): %s",
obj.file,
obj.parent_file,
err,
)
return
_force_load_include_files(loaded, _seen)
elif isinstance(obj, dict):
if id(obj) in _seen:
return
_seen.add(id(obj))
for value in obj.values():
_force_load_include_files(value, _seen)
elif isinstance(obj, (list, tuple)):
if id(obj) in _seen:
return
_seen.add(id(obj))
for item in obj:
_force_load_include_files(item, _seen)
def _resolve_include_path(include_path: Any) -> Path | None:
"""Resolve an include path to absolute, skipping system includes."""
if isinstance(include_path, str) and include_path.startswith("<"):

View File

@@ -0,0 +1,76 @@
"""Validated-config cache for the upload/logs fast path.
compile dumps the validated config to <data_dir>/storage/<file>.validated.yaml;
the next upload/logs for that YAML reuses it instead of running the full
read_config pipeline. YAML round-trip (yaml_util.dump/load_yaml) keeps
!lambda/!include/IDs/paths intact; mtime gates staleness.
"""
from __future__ import annotations
import logging
from pathlib import Path
from esphome.core import CORE
from esphome.helpers import write_file
from esphome.storage_json import StorageJSON, ext_storage_path
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
def compiled_config_path(config_filename: str) -> Path:
"""Path to the cached validated config alongside the storage sidecar."""
return CORE.data_dir / "storage" / f"{config_filename}.validated.yaml"
def _cache_is_fresh(cache_path: Path, source_path: Path) -> bool:
"""True iff the cache file exists and isn't older than the source."""
try:
return cache_path.stat().st_mtime >= source_path.stat().st_mtime
except OSError:
return False
def save_compiled_config(config: ConfigType) -> None:
"""Write the validated-config cache. Always-write so mtime stays fresh.
Mode 0600 because show_secrets=True resolves !secret inline.
Failures are non-fatal: the fast path falls back to read_config.
"""
from esphome import yaml_util
try:
rendered = yaml_util.dump(config, show_secrets=True)
write_file(compiled_config_path(CORE.config_filename), rendered, private=True)
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
_LOGGER.debug("Skipping compiled config cache write: %s", err)
def load_compiled_config(conf_path: Path) -> ConfigType | None:
"""Load the cached validated config and apply storage metadata to CORE.
Returns None (caller falls back to read_config) when the cache is
missing, older than the source YAML, unparseable, or the sidecar
is incomplete.
"""
cache_path = compiled_config_path(conf_path.name)
if not _cache_is_fresh(cache_path, conf_path):
return None
from esphome import yaml_util
try:
config = yaml_util.load_yaml(cache_path, clear_secrets=False)
except Exception: # noqa: BLE001 # pylint: disable=broad-except
return None
storage = StorageJSON.load(ext_storage_path(conf_path.name))
if storage is None:
return None
# apply_to_core assumes a real compile wrote the sidecar; wizard-only
# sidecars leave both of these unset and can't drive upload/logs.
if not storage.core_platform and not storage.target_platform:
return None
storage.apply_to_core()
return config

View File

@@ -1 +0,0 @@
CODEOWNERS = ["@kpfleming"]

View File

@@ -87,14 +87,24 @@ void ADE7880::update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_
sensor->publish_state(f(val));
}
template<typename F>
void ADE7880::update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) {
if (sensor == nullptr) {
void ADE7880::update_active_energy_(PowerChannel *channel, uint16_t a_register) {
if (channel->forward_active_energy == nullptr && channel->reverse_active_energy == nullptr) {
return;
}
float val = this->read_s32_register16_(a_register);
sensor->publish_state(f(val));
// The ADE7880 has no separate forward/reverse active energy accumulators. The xWATTHR registers
// accumulate signed energy since the last read (positive = imported/forward, negative = exported/
// reverse), so split the value by sign into the forward and reverse running totals.
float val = this->read_s32_register16_(a_register) / 14400.0f;
if (val >= 0.0f) {
if (channel->forward_active_energy != nullptr) {
channel->forward_active_energy->publish_state(channel->forward_active_energy_total += val);
}
} else {
if (channel->reverse_active_energy != nullptr) {
channel->reverse_active_energy->publish_state(channel->reverse_active_energy_total -= val);
}
}
}
void ADE7880::update() {
@@ -117,12 +127,7 @@ void ADE7880::update() {
this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; });
this->update_sensor_from_s16_register16_(chan->power_factor, APF,
[](float val) { return std::abs(val / -327.68f); });
this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
this->update_active_energy_(chan, AWATTHR);
}
if (this->channel_b_ != nullptr) {
@@ -133,12 +138,7 @@ void ADE7880::update() {
this->update_sensor_from_s24zp_register16_(chan->apparent_power, BVA, [](float val) { return val / 100.0f; });
this->update_sensor_from_s16_register16_(chan->power_factor, BPF,
[](float val) { return std::abs(val / -327.68f); });
this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
this->update_active_energy_(chan, BWATTHR);
}
if (this->channel_c_ != nullptr) {
@@ -149,12 +149,7 @@ void ADE7880::update() {
this->update_sensor_from_s24zp_register16_(chan->apparent_power, CVA, [](float val) { return val / 100.0f; });
this->update_sensor_from_s16_register16_(chan->power_factor, CPF,
[](float val) { return std::abs(val / -327.68f); });
this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
this->update_active_energy_(chan, CWATTHR);
}
ESP_LOGD(TAG, "update took %" PRIu32 " ms", millis() - start);

View File

@@ -105,7 +105,8 @@ class ADE7880 : public i2c::I2CDevice, public PollingComponent {
// the callable will be passed a 'float' value and is expected to return a 'float'
template<typename F> void update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
template<typename F> void update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
template<typename F> void update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
void update_active_energy_(PowerChannel *channel, uint16_t a_register);
void reset_device_();

View File

@@ -84,9 +84,7 @@ constexpr uint16_t CWATTHR = 0xE402;
constexpr uint16_t AFWATTHR = 0xE403;
constexpr uint16_t BFWATTHR = 0xE404;
constexpr uint16_t CFWATTHR = 0xE405;
constexpr uint16_t ARWATTHR = 0xE406;
constexpr uint16_t BRWATTHR = 0xE407;
constexpr uint16_t CRWATTHR = 0xE408;
// 0xE406-0xE408 are reserved on the ADE7880 (it does not implement total reactive energy accumulation)
constexpr uint16_t AFVARHR = 0xE409;
constexpr uint16_t BFVARHR = 0xE40A;
constexpr uint16_t CFVARHR = 0xE40B;

View File

@@ -21,7 +21,7 @@ from esphome.const import (
UNIT_VOLT,
)
CODEOWNERS = ["@ncareau", "@jeromelaban", "@kpfleming"]
CODEOWNERS = ["@ncareau", "@jeromelaban"]
DEPENDENCIES = ["ble_client"]

View File

@@ -2,6 +2,7 @@ import logging
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_LOOP
import esphome.components.image as espImage
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_REPEAT
@@ -14,7 +15,6 @@ DEPENDENCIES = ["display"]
MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
CONF_LOOP = "loop"
CONF_START_FRAME = "start_frame"
CONF_END_FRAME = "end_frame"
CONF_FRAME = "frame"

View File

@@ -234,7 +234,7 @@ ACTIONS_SCHEMA = automation.validate_automation(
ENCRYPTION_SCHEMA = cv.Schema(
{
cv.Optional(CONF_KEY): validate_encryption_key,
cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key),
}
)

View File

@@ -1,5 +1,6 @@
#include "api_connection.h"
#ifdef USE_API
#include "api_connection_buffer.h" // for encode_to_buffer / get_batch_delay_ms_ inlines
#ifdef USE_API_NOISE
#include "api_frame_helper_noise.h"
#endif
@@ -1168,7 +1169,7 @@ void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
void APIConnection::on_get_time_response(const GetTimeResponse &value) {
if (homeassistant::global_homeassistant_time != nullptr) {
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
#ifdef USE_TIME_TIMEZONE
#if defined(USE_HOMEASSISTANT_TIMEZONE) && defined(USE_TIME_TIMEZONE)
if (!value.timezone.empty()) {
// Check if the sender provided pre-parsed timezone data.
// If std_offset is non-zero or DST rules are present, the parsed data was populated.
@@ -1305,6 +1306,9 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) {
VoiceAssistantConfigurationResponse resp;
if (!this->check_voice_assistant_api_connection_()) {
// send_message encodes synchronously, so this stack local outlives the encode
const std::vector<std::string> empty_wake_words;
resp.active_wake_words = &empty_wake_words;
return this->send_message(resp);
}

View File

@@ -11,7 +11,8 @@
#endif
#include "api_pb2.h"
#include "api_pb2_service.h"
#include "api_server.h"
#include "list_entities.h"
#include "subscribe_state.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#ifdef USE_ESP32_CRASH_HANDLER
@@ -36,6 +37,9 @@ class ComponentIterator;
namespace esphome::api {
// Forward-declared to break the api_server.h cycle; full-type inlines are in api_connection_buffer.h.
class APIServer;
// Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending
@@ -411,44 +415,10 @@ class APIConnection final : public APIServerConnectionBase {
// Non-template buffer management for send_message
bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
// Core batch encoding logic. Computes header size, checks fit, resizes buffer, encodes.
// ALWAYS_INLINE so the compiler can devirtualize encode_fn at hot call sites.
static inline uint16_t ESPHOME_ALWAYS_INLINE encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn,
const void *msg, APIConnection *conn,
uint32_t remaining_size) {
#ifdef HAS_PROTO_MESSAGE_DUMP
if (conn->flags_.log_only_mode) {
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
DumpBuffer dump_buf;
conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
return 1;
}
#endif
const uint8_t footer_size = conn->helper_->frame_footer_size();
// First message uses max padding (already in buffer), subsequent use exact header size
size_t to_add;
if (conn->flags_.batch_first_message) {
conn->flags_.batch_first_message = false;
conn->batch_header_size_ = conn->helper_->frame_header_padding();
to_add = calculated_size;
} else {
conn->batch_header_size_ = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_);
to_add = calculated_size + conn->batch_header_size_ + footer_size;
}
// Check if it fits (using actual header size, not max padding)
uint16_t total_calculated_size = calculated_size + conn->batch_header_size_ + footer_size;
if (total_calculated_size > remaining_size)
return 0;
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
shared_buf.resize(shared_buf.size() + to_add);
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
return total_calculated_size;
}
// Core batch encoding logic. ALWAYS_INLINE so encode_fn devirtualizes at hot call sites.
// Defined in api_connection_buffer.h (needs APIServer complete).
static uint16_t ESPHOME_ALWAYS_INLINE encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn,
const void *msg, APIConnection *conn, uint32_t remaining_size);
// Noinline version of encode_to_buffer for cold paths (entity info, zero-payload messages).
// All cold callers share this single copy instead of each getting an ALWAYS_INLINE expansion.
@@ -792,7 +762,8 @@ class APIConnection final : public APIServerConnectionBase {
// Read by process_batch_multi_ to pass into MessageInfo.
uint8_t batch_header_size_{0};
uint32_t get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); }
// Defined in api_connection_buffer.h (needs APIServer complete).
uint32_t get_batch_delay_ms_() const;
// Message will use 8 more bytes than the minimum size, and typical
// MTU is 1500. Sometimes users will see as low as 1460 MTU.
// If its IPv6 the header is 40 bytes, and if its IPv4

View File

@@ -0,0 +1,54 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_API
// Inline APIConnection methods that need APIServer complete. Include this
// instead of api_connection.h when calling encode_to_buffer or get_batch_delay_ms_.
#include "api_connection.h"
#include "api_server.h"
namespace esphome::api {
inline uint16_t ESPHOME_ALWAYS_INLINE APIConnection::encode_to_buffer(uint32_t calculated_size,
MessageEncodeFn encode_fn, const void *msg,
APIConnection *conn, uint32_t remaining_size) {
#ifdef HAS_PROTO_MESSAGE_DUMP
if (conn->flags_.log_only_mode) {
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
DumpBuffer dump_buf;
conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
return 1;
}
#endif
const uint8_t footer_size = conn->helper_->frame_footer_size();
// First message uses max padding (already in buffer), subsequent use exact header size
size_t to_add;
if (conn->flags_.batch_first_message) {
conn->flags_.batch_first_message = false;
conn->batch_header_size_ = conn->helper_->frame_header_padding();
to_add = calculated_size;
} else {
conn->batch_header_size_ = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_);
to_add = calculated_size + conn->batch_header_size_ + footer_size;
}
// Check if it fits (using actual header size, not max padding)
uint16_t total_calculated_size = calculated_size + conn->batch_header_size_ + footer_size;
if (total_calculated_size > remaining_size)
return 0;
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
shared_buf.resize(shared_buf.size() + to_add);
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
return total_calculated_size;
}
inline uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); }
} // namespace esphome::api
#endif

View File

@@ -1,6 +1,7 @@
#include "api_server.h"
#ifdef USE_API
#include <cerrno>
#include <cinttypes>
#include "api_connection.h"
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
@@ -30,11 +31,6 @@ APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-c
APIServer::APIServer() { global_api_server = this; }
// Custom deleter defined here so `delete` sees the complete APIConnection type.
// This prevents libc++ from emitting an "incomplete type" error when other
// translation units only have the forward declaration of APIConnection.
void APIServer::APIConnectionDeleter::operator()(APIConnection *p) const { delete p; }
void APIServer::socket_failed_(const LogString *msg) {
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
this->destroy_socket_();
@@ -190,8 +186,12 @@ void APIServer::remove_client_(uint8_t client_index) {
if (client_index < last_index) {
std::swap(this->clients_[client_index], this->clients_[last_index]);
}
this->clients_[last_index].reset();
// Drop the count before resetting the slot. reset() runs ~APIConnection(), which can reenter the
// server (e.g. voice_assistant unsubscribes in its disconnect trigger, publishing entity state ->
// on_*_update iterating active_clients()). Excluding the dying slot from the active range first
// keeps that reentrant iteration from dereferencing the now-null slot.
this->api_connection_count_--;
this->clients_[last_index].reset();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) {
@@ -682,7 +682,7 @@ uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConn
// Schedule automatic cleanup after timeout (client will have given up by then)
// Uses numeric ID overload to avoid heap allocation from str_sprintf
this->set_timeout(action_call_id, USE_API_ACTION_CALL_TIMEOUT_MS, [this, action_call_id]() {
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
ESP_LOGD(TAG, "Action call %" PRIu32 " timed out", action_call_id);
this->unregister_active_action_call(action_call_id);
});
@@ -726,7 +726,7 @@ void APIServer::send_action_response(uint32_t action_call_id, bool success, Stri
return;
}
}
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %" PRIu32, action_call_id);
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message,
@@ -738,7 +738,7 @@ void APIServer::send_action_response(uint32_t action_call_id, bool success, Stri
return;
}
}
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %" PRIu32, action_call_id);
}
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES

View File

@@ -3,6 +3,8 @@
#include "esphome/core/defines.h"
#ifdef USE_API
#include "api_buffer.h"
// Must precede clients_ so APIConnection is complete for default_delete (libc++).
#include "api_connection.h"
#include "api_noise_context.h"
#include "api_pb2.h"
#include "api_pb2_service.h"
@@ -12,8 +14,6 @@
#include "esphome/core/controller.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
#include "list_entities.h"
#include "subscribe_state.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
@@ -191,15 +191,9 @@ class APIServer final : public Component,
bool is_connected_with_state_subscription() const;
// Range-for view over the populated slice [0, api_connection_count_). Read-only with respect
// to ownership callers get `const unique_ptr&` so they can invoke non-const methods on the
// to ownership; callers get `const unique_ptr&` so they can invoke non-const methods on the
// APIConnection but cannot reset/move the slot and break the count invariant.
// Custom deleter is defined out-of-line in api_server.cpp so libc++ does not
// eagerly instantiate `delete static_cast<APIConnection *>(p)` here, where
// only the forward declaration of APIConnection is visible (incomplete type).
struct APIConnectionDeleter {
void operator()(APIConnection *p) const;
};
using APIConnectionPtr = std::unique_ptr<APIConnection, APIConnectionDeleter>;
using APIConnectionPtr = std::unique_ptr<APIConnection>;
class ActiveClientsView {
const APIConnectionPtr *begin_;
const APIConnectionPtr *end_;

View File

@@ -101,13 +101,14 @@ async def async_run_logs(
client_info=f"ESPHome Logs {__version__}",
noise_psk=noise_psk,
addresses=addresses, # Pass all addresses for automatic retry
provide_time=False,
)
# Try platform-specific stacktrace handler first, fall back to generic
platform_process_stacktrace = None
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
platform_process_stacktrace = getattr(module, "process_stacktrace")
platform_process_stacktrace = module.process_stacktrace
except (AttributeError, ImportError):
_LOGGER.info(
'Stacktrace analysis is unavailable: no compatible analyzer found for target platform "%s".',
@@ -118,7 +119,7 @@ async def async_run_logs(
def on_log(msg: SubscribeLogsResponse) -> None:
"""Handle a new log message."""
time_ = datetime.now()
time_ = datetime.now().astimezone()
message: bytes = msg.message
text = message.decode("utf8", "backslashreplace")
nanoseconds = time_.microsecond // 1000

View File

@@ -100,7 +100,7 @@ def position(min=-MAX_POSITION, max=MAX_POSITION):
if isinstance(value, str) and value.endswith("%"):
value = percent_to_position(value)
if isinstance(value, str) and (value.endswith("°") or value.endswith("deg")):
if isinstance(value, str) and value.endswith(("°", "deg")):
return angle_to_position(
value,
min=round(min * POSITION_TO_ANGLE),

View File

@@ -335,7 +335,7 @@ async def to_code(config):
add_idf_component(
name="esphome/esp-audio-libs",
ref="3.0.0",
ref="3.2.1",
)
data = _get_data()
@@ -395,7 +395,7 @@ async def to_code(config):
)
if data.mp3_support:
cg.add_define("USE_AUDIO_MP3_SUPPORT")
add_idf_component(name="esphome/micro-mp3", ref="0.2.0")
add_idf_component(name="esphome/micro-mp3", ref="0.2.3")
_emit_memory_pair(
data.mp3.buffer_memory,
"CONFIG_MP3_DECODER_PREFER_PSRAM",

View File

@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h" // for ESPDEPRECATED
#include <cstddef>
#include <cstdint>
@@ -143,6 +144,8 @@ AudioFileType detect_audio_file_type(const char *content_type, const char *url);
/// @param output_buffer Buffer to store the scaled samples
/// @param scale_factor Q15 fixed point scaling factor
/// @param samples_to_scale Number of samples to scale
// Remove before 2026.12.0
ESPDEPRECATED("Use esp_audio_libs::gain::apply() (from <gain.h>) instead. Removed in 2026.12.0.", "2026.6.0")
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
size_t samples_to_scale);

View File

@@ -9,9 +9,12 @@ namespace esphome::audio {
static const char *const TAG = "audio.decoder";
static const uint32_t DECODING_TIMEOUT_MS = 50; // The decode function will yield after this duration
static const uint32_t READ_WRITE_TIMEOUT_MS = 20; // Timeout for transferring audio data
// Max consecutive decode iterations that consume input but produce no output; e.g., skipping a large metadata block,
// before yielding and returning.
static const uint8_t MAX_NO_OUTPUT_ITERATIONS = 32;
static const uint32_t MAX_POTENTIALLY_FAILED_COUNT = 10;
AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size)
@@ -20,11 +23,13 @@ AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size)
}
esp_err_t AudioDecoder::add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer) {
auto source = AudioSourceTransferBuffer::create(this->input_buffer_size_);
// Zero-copy source reading directly from the ring buffer's internal storage. Raw file data is byte
// aligned, so no frame alignment is required.
auto source = RingBufferAudioSource::create(input_ring_buffer.lock(), this->input_buffer_size_);
if (source == nullptr) {
return ESP_ERR_NO_MEM;
// create() only returns nullptr for invalid arguments (expired ring buffer or zero buffer size)
return ESP_ERR_INVALID_ARG;
}
source->set_source(input_ring_buffer);
this->input_buffer_ = std::move(source);
return ESP_OK;
}
@@ -141,13 +146,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
}
FileDecoderState state = FileDecoderState::MORE_TO_PROCESS;
uint32_t decoding_start = millis();
bool first_loop_iteration = true;
size_t bytes_processed = 0;
size_t bytes_available_before_processing = 0;
uint8_t no_output_iterations = 0;
while (state == FileDecoderState::MORE_TO_PROCESS) {
// Transfer decoded out
@@ -161,45 +160,39 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
this->playback_ms_ +=
this->audio_stream_info_.value().frames_to_milliseconds_with_remainder(&this->accumulated_frames_written_);
}
if ((bytes_written > 0) && (this->output_transfer_buffer_->available() == 0)) {
// All decoded audio has been flushed to the sink; return so the caller can react to stop/pause before
// decoding the next batch
return AudioDecoderState::DECODING;
}
} else {
// If paused, block to avoid wasting CPU resources
delay(READ_WRITE_TIMEOUT_MS);
}
// Verify there is enough space to store more decoded audio and that the function hasn't been running too long
if ((this->output_transfer_buffer_->free() < this->free_buffer_required_) ||
(millis() - decoding_start > DECODING_TIMEOUT_MS)) {
if (this->output_transfer_buffer_->available() > 0) {
// Output transfer buffer indicates backpressure, return so caller can handle other events;
// e.g., stop/pause, before trying again
return AudioDecoderState::DECODING;
}
// Decode more audio
// Never shift the input buffer; every decoder buffers internally and consumes only what it processed.
size_t bytes_read = this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
if (!first_loop_iteration && (this->input_buffer_->available() < bytes_processed)) {
// Less data is available than what was processed in last iteration, so don't attempt to decode.
// This attempts to avoid the decoder from consistently trying to decode an incomplete frame. The transfer buffer
// will shift the remaining data to the start and copy more from the source the next time the decode function is
// called
break;
// Reaching here means no decoded output is pending (any would have returned above). Bounds long no-output
// stretches; e.g., skipping a large metadata block, so a source that keeps the ring buffer full can't spin this
// loop without yielding and trip the watchdog. The delay yields allowing other tasks to feed the watchdog and
// the return keeps stop/pause responsive.
if (++no_output_iterations >= MAX_NO_OUTPUT_ITERATIONS) {
delay(1);
return AudioDecoderState::DECODING;
}
bytes_available_before_processing = this->input_buffer_->available();
// Expose the next chunk of file data. Every decoder buffers internally and consumes only what it
// processed, so the source does not need to accumulate or stitch chunks across fill() calls.
this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) {
// Failed to decode in last attempt and there is no new data
const size_t available_before_decode = this->input_buffer_->available();
if ((this->input_buffer_->free() == 0) && first_loop_iteration) {
// The input buffer is full (or read-only, e.g. const flash source). Since it previously failed on the exact
// same data, we can never recover. For const sources this is correct: the entire file is already available, so
// a decode failure is genuine, not a transient out-of-data condition.
state = FileDecoderState::FAILED;
} else {
// Attempt to get more data next time
state = FileDecoderState::IDLE;
}
} else if (this->input_buffer_->available() == 0) {
if (available_before_decode == 0) {
// No data to decode, attempt to get more data next time
state = FileDecoderState::IDLE;
} else {
@@ -231,9 +224,6 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
}
}
first_loop_iteration = false;
bytes_processed = bytes_available_before_processing - this->input_buffer_->available();
if (state == FileDecoderState::POTENTIALLY_FAILED) {
++this->potentially_failed_count_;
} else if (state == FileDecoderState::END_OF_FILE) {
@@ -241,7 +231,16 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
} else if (state == FileDecoderState::FAILED) {
return AudioDecoderState::FAILED;
} else if (state == FileDecoderState::MORE_TO_PROCESS) {
this->potentially_failed_count_ = 0;
// Reset the failsafe only when the iteration made forward progress: input was consumed or output was
// produced (output_transfer_buffer_ is drained empty above, so any available bytes are new). A
// MORE_TO_PROCESS that neither consumes input nor produces output means the decoder is stalled; count it
// toward the failsafe so a stuck stream eventually surfaces as FAILED instead of looping forever.
if ((this->input_buffer_->available() < available_before_decode) ||
(this->output_transfer_buffer_->available() > 0)) {
this->potentially_failed_count_ = 0;
} else {
++this->potentially_failed_count_;
}
}
}
return AudioDecoderState::DECODING;

View File

@@ -61,15 +61,16 @@ class AudioDecoder {
*/
public:
/// @brief Allocates the output transfer buffer and stores the input buffer size for later use by add_source()
/// @param input_buffer_size Size of the input transfer buffer in bytes.
/// @param input_buffer_size Soft cap on the bytes a ring buffer source exposes per fill, in bytes.
/// @param output_buffer_size Size of the output transfer buffer in bytes.
AudioDecoder(size_t input_buffer_size, size_t output_buffer_size);
~AudioDecoder() = default;
/// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr.
/// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership
/// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
/// @brief Adds a source ring buffer for raw file data. Shares ownership of the ring buffer via a shared_ptr.
/// The decoder reads directly from the ring buffer's internal storage with a zero-copy RingBufferAudioSource.
/// @param input_ring_buffer weak_ptr of the source ring buffer to read from
/// @return ESP_OK if successful, ESP_ERR_INVALID_ARG if the ring buffer is expired or the buffer size is zero
esp_err_t add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer);
/// @brief Adds a sink ring buffer for decoded audio. Takes ownership of the ring buffer in a shared_ptr.

View File

@@ -12,16 +12,17 @@ static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
AudioResampler::AudioResampler(size_t input_buffer_size, size_t output_buffer_size)
: input_buffer_size_(input_buffer_size), output_buffer_size_(output_buffer_size) {
this->input_transfer_buffer_ = AudioSourceTransferBuffer::create(input_buffer_size);
this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size);
}
esp_err_t AudioResampler::add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer) {
if (this->input_transfer_buffer_ != nullptr) {
this->input_transfer_buffer_->set_source(input_ring_buffer);
return ESP_OK;
// The zero-copy RingBufferAudioSource is created lazily on the first resample() call, once both the ring
// buffer (stored here) and the input stream info (set by start()) are available, in either order.
this->source_ring_buffer_ = input_ring_buffer.lock();
if (this->source_ring_buffer_ == nullptr) {
return ESP_ERR_INVALID_STATE;
}
return ESP_ERR_NO_MEM;
return ESP_OK;
}
esp_err_t AudioResampler::add_sink(std::weak_ptr<ring_buffer::RingBuffer> &output_ring_buffer) {
@@ -47,7 +48,7 @@ esp_err_t AudioResampler::start(AudioStreamInfo &input_stream_info, AudioStreamI
this->input_stream_info_ = input_stream_info;
this->output_stream_info_ = output_stream_info;
if ((this->input_transfer_buffer_ == nullptr) || (this->output_transfer_buffer_ == nullptr)) {
if (this->output_transfer_buffer_ == nullptr) {
return ESP_ERR_NO_MEM;
}
@@ -56,6 +57,13 @@ esp_err_t AudioResampler::start(AudioStreamInfo &input_stream_info, AudioStreamI
return ESP_ERR_NOT_SUPPORTED;
}
// Reject frame sizes that can't be used as the zero-copy source's alignment up front, where the caller checks
// the return code. The lazy create() in resample() keeps its own guard since it runs before the uint8_t cast.
const size_t bytes_per_frame = this->input_stream_info_.frames_to_bytes(1);
if ((bytes_per_frame == 0) || (bytes_per_frame > RingBufferAudioSource::MAX_ALIGNMENT_BYTES)) {
return ESP_ERR_NOT_SUPPORTED;
}
if ((input_stream_info.get_sample_rate() != output_stream_info.get_sample_rate()) ||
(input_stream_info.get_bits_per_sample() != output_stream_info.get_bits_per_sample())) {
this->resampler_ = make_unique<esp_audio_libs::resampler::Resampler>(
@@ -87,8 +95,27 @@ esp_err_t AudioResampler::start(AudioStreamInfo &input_stream_info, AudioStreamI
}
AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_differential) {
if (this->audio_source_ == nullptr) {
// Lazily create the zero-copy source on first use. Frame-aligned reads ensure multi-channel frames are
// never split across the ring buffer's wrap boundary.
const size_t bytes_per_frame = this->input_stream_info_.frames_to_bytes(1);
if ((bytes_per_frame == 0) || (bytes_per_frame > RingBufferAudioSource::MAX_ALIGNMENT_BYTES)) {
// Stream info is unset or the frame is too large to use as an alignment; the uint8_t cast below would
// truncate it and could yield a source that tears frames.
return AudioResamplerState::FAILED;
}
// Pass the shared_ptr by copy so a failed create() leaves source_ring_buffer_ intact; release our
// reference only after the source has taken ownership.
this->audio_source_ = RingBufferAudioSource::create(this->source_ring_buffer_, this->input_buffer_size_,
static_cast<uint8_t>(bytes_per_frame));
if (this->audio_source_ == nullptr) {
return AudioResamplerState::FAILED;
}
this->source_ring_buffer_.reset();
}
if (stop_gracefully) {
if (!this->input_transfer_buffer_->has_buffered_data() && (this->output_transfer_buffer_->available() == 0)) {
if (!this->audio_source_->has_buffered_data() && (this->output_transfer_buffer_->available() == 0)) {
return AudioResamplerState::FINISHED;
}
}
@@ -102,9 +129,11 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
delay(READ_WRITE_TIMEOUT_MS);
}
this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
// Expose a chunk of the ring buffer's internal storage. pre_shift is ignored by RingBufferAudioSource
// (there is no intermediate transfer buffer to compact).
this->audio_source_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
if (this->input_transfer_buffer_->available() == 0) {
if (this->audio_source_->available() == 0) {
// No samples available to process
return AudioResamplerState::RESAMPLING;
}
@@ -112,17 +141,17 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
const size_t bytes_free = this->output_transfer_buffer_->free();
const uint32_t frames_free = this->output_stream_info_.bytes_to_frames(bytes_free);
const size_t bytes_available = this->input_transfer_buffer_->available();
const size_t bytes_available = this->audio_source_->available();
const uint32_t frames_available = this->input_stream_info_.bytes_to_frames(bytes_available);
if ((this->input_stream_info_.get_sample_rate() != this->output_stream_info_.get_sample_rate()) ||
(this->input_stream_info_.get_bits_per_sample() != this->output_stream_info_.get_bits_per_sample())) {
// Adjust gain by -3 dB to avoid clipping due to the resampling process
esp_audio_libs::resampler::ResamplerResults results =
this->resampler_->resample(this->input_transfer_buffer_->get_buffer_start(),
this->output_transfer_buffer_->get_buffer_end(), frames_available, frames_free, -3);
this->resampler_->resample(this->audio_source_->data(), this->output_transfer_buffer_->get_buffer_end(),
frames_available, frames_free, -3);
this->input_transfer_buffer_->decrease_buffer_length(this->input_stream_info_.frames_to_bytes(results.frames_used));
this->audio_source_->consume(this->input_stream_info_.frames_to_bytes(results.frames_used));
this->output_transfer_buffer_->increase_buffer_length(
this->output_stream_info_.frames_to_bytes(results.frames_generated));
@@ -146,10 +175,10 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
const size_t bytes_to_transfer = std::min(this->output_stream_info_.frames_to_bytes(frames_free),
this->input_stream_info_.frames_to_bytes(frames_available));
std::memcpy((void *) this->output_transfer_buffer_->get_buffer_end(),
(void *) this->input_transfer_buffer_->get_buffer_start(), bytes_to_transfer);
std::memcpy((void *) this->output_transfer_buffer_->get_buffer_end(), (const void *) this->audio_source_->data(),
bytes_to_transfer);
this->input_transfer_buffer_->decrease_buffer_length(bytes_to_transfer);
this->audio_source_->consume(bytes_to_transfer);
this->output_transfer_buffer_->increase_buffer_length(bytes_to_transfer);
}

View File

@@ -22,7 +22,7 @@ namespace esphome::audio {
enum class AudioResamplerState : uint8_t {
RESAMPLING, // More data is available to resample
FINISHED, // All file data has been resampled and transferred
FAILED, // Unused state included for consistency among Audio classes
FAILED, // Failed to allocate the audio source
};
class AudioResampler {
@@ -32,14 +32,16 @@ class AudioResampler {
* component). Also supports converting bits per sample.
*/
public:
/// @brief Allocates the input and output transfer buffers
/// @param input_buffer_size Size of the input transfer buffer in bytes.
/// @brief Allocates the output transfer buffer. The input source is created later in resample().
/// @param input_buffer_size Max bytes exposed per fill() call on the zero-copy input source.
/// @param output_buffer_size Size of the output transfer buffer in bytes.
AudioResampler(size_t input_buffer_size, size_t output_buffer_size);
/// @brief Adds a source ring buffer for audio data. Takes ownership of the ring buffer in a shared_ptr.
/// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership
/// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
/// @brief Sets the ring buffer the audio is read from and takes shared ownership of it. The zero-copy
/// RingBufferAudioSource that reads directly from its internal storage is created lazily on the first
/// resample() call, so add_source() and start() may be called in any order.
/// @param input_ring_buffer weak_ptr of a shared_ptr of the source ring buffer to transfer ownership
/// @return ESP_OK if successful, ESP_ERR_INVALID_STATE if the ring buffer is no longer alive
esp_err_t add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer);
/// @brief Adds a sink ring buffer for resampled audio. Takes ownership of the ring buffer in a shared_ptr.
@@ -78,7 +80,8 @@ class AudioResampler {
void set_pause_output_state(bool pause_state) { this->pause_output_ = pause_state; }
protected:
std::unique_ptr<AudioSourceTransferBuffer> input_transfer_buffer_;
std::shared_ptr<ring_buffer::RingBuffer> source_ring_buffer_;
std::unique_ptr<RingBufferAudioSource> audio_source_;
std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_;
size_t input_buffer_size_;

View File

@@ -252,6 +252,22 @@ void RingBufferAudioSource::consume(size_t bytes) {
}
}
void RingBufferAudioSource::clear_buffered_data() {
// Release the held item before reset() so the source no longer references memory the reset will reclaim.
if (this->acquired_item_ != nullptr) {
this->ring_buffer_->receive_release(this->acquired_item_);
this->acquired_item_ = nullptr;
}
this->current_data_ = nullptr;
this->current_available_ = 0;
this->queued_data_ = nullptr;
this->queued_length_ = 0;
this->item_trailing_ptr_ = nullptr;
this->item_trailing_length_ = 0;
this->splice_length_ = 0;
this->ring_buffer_->reset();
}
bool RingBufferAudioSource::has_buffered_data() const {
// splice_length_ is deliberately not considered here. It holds an incomplete frame whose completion
// bytes must still arrive through the ring buffer, which ring_buffer_->available() already reports.

View File

@@ -250,6 +250,10 @@ class RingBufferAudioSource : public AudioReadableBuffer {
/// exposure stays in place and fill() returns 0 until it is fully consumed.
size_t fill(TickType_t ticks_to_wait, bool pre_shift) override;
/// @brief Discards all buffered audio: releases any held ring buffer item, clears the source's in-flight
/// state, and resets the underlying ring buffer. Must be invoked from the ring buffer's consumer thread.
void clear_buffered_data();
/// @brief Returns a mutable pointer to the currently exposed audio data.
/// The pointer may reference the ring buffer's internal storage or, when exposing a stitched frame
/// across a wrap boundary, an internal splice buffer. In either case mutations are safe but data

View File

@@ -72,7 +72,7 @@ def _file_schema(value: ConfigType | str) -> ConfigType:
def _validate_file_shorthand(value: str) -> ConfigType:
value = cv.string_strict(value)
if value.startswith("http://") or value.startswith("https://"):
if value.startswith(("http://", "https://")):
return _file_schema(
{
CONF_TYPE: TYPE_WEB,
@@ -98,7 +98,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
else:
raise cv.Invalid("Unsupported file source")
with open(path, "rb") as f:
with path.open("rb") as f:
data = f.read()
try:

View File

@@ -1,7 +1,5 @@
from typing import Any
import esphome.codegen as cg
from esphome.components import audio, esp32, media_source, psram
from esphome.components import audio, media_source, psram
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM
from esphome.types import ConfigType
@@ -21,19 +19,13 @@ def _request_micro_decoder(config: ConfigType) -> ConfigType:
return config
def _validate_task_stack_in_psram(value: Any) -> bool:
if value := cv.boolean(value):
return cv.requires_component(psram.DOMAIN)(value)
return value
CONFIG_SCHEMA = cv.All(
media_source.media_source_schema(
AudioFileMediaSource,
)
.extend(
{
cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram,
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
}
)
.extend(cv.COMPONENT_SCHEMA),
@@ -49,6 +41,4 @@ async def to_code(config: ConfigType) -> None:
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
)
psram.request_external_task_stack()

View File

@@ -1,7 +1,5 @@
from typing import Any
import esphome.codegen as cg
from esphome.components import audio, esp32, media_source, psram
from esphome.components import audio, media_source, psram
import esphome.config_validation as cv
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM
from esphome.types import ConfigType
@@ -20,14 +18,6 @@ def _request_micro_decoder(config: ConfigType) -> ConfigType:
return config
def _validate_task_stack_in_psram(value: Any) -> bool:
# Only require the psram component when actually enabling PSRAM stacks; validating
# the boolean first means `false` doesn't trigger the requires_component check.
if value := cv.boolean(value):
return cv.requires_component(psram.DOMAIN)(value)
return value
CONFIG_SCHEMA = cv.All(
media_source.media_source_schema(
AudioHTTPMediaSource,
@@ -37,7 +27,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_BUFFER_SIZE, default=50000): cv.int_range(
min=5000, max=1000000
),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram,
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
}
)
.extend(cv.COMPONENT_SCHEMA),
@@ -53,7 +43,5 @@ async def to_code(config: ConfigType) -> None:
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
)
psram.request_external_task_stack()
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))

View File

@@ -135,12 +135,26 @@ void BluetoothConnection::loop() {
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
if (this->state() != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->send_service_ == DONE_SENDING_SERVICES)) {
// Never disable while DISCONNECTING — BLEClientBase::loop() needs to keep running so the
// 10s safety timeout can force IDLE if CLOSE_EVT is never delivered.
if (this->state() != espbt::ClientState::INIT && this->state() != espbt::ClientState::DISCONNECTING &&
(this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->send_service_ == DONE_SENDING_SERVICES)) {
this->disable_loop();
}
}
void BluetoothConnection::on_disconnect_complete(esp_err_t reason) {
// Called from both the CLOSE_EVT handler and the DISCONNECTING safety timeout in the
// base class. Free the proxy slot, notify the API client, and reset send_service_.
// address_ may already be 0 if reset_connection_ ran earlier on this teardown.
if (this->address_ == 0) {
return;
}
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_, reason);
this->reset_connection_(reason);
}
void BluetoothConnection::reset_connection_(esp_err_t reason) {
// Send disconnection notification
this->proxy_->send_device_connection(this->address_, false, 0, reason);
@@ -372,14 +386,6 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
break;
}
case ESP_GATTC_CLOSE_EVT: {
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_,
param->close.reason);
// Now the GATT connection is fully closed and controller resources are freed
// Safe to mark the connection slot as available
this->reset_connection_(param->close.reason);
break;
}
case ESP_GATTC_OPEN_EVT: {
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
this->reset_connection_(param->open.status);

View File

@@ -33,6 +33,8 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
protected:
friend class BluetoothProxy;
void on_disconnect_complete(esp_err_t reason) override;
bool supports_efficient_uuids_() const;
void send_service_for_discovery_();
void reset_connection_(esp_err_t reason);

View File

@@ -1,5 +1,6 @@
#include "bluetooth_proxy.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"
#include "esphome/core/macros.h"
#include "esphome/core/application.h"

View File

@@ -161,7 +161,7 @@ void BME680BSECComponent::dump_config() {
" IAQ Mode: %s\n"
" Supply Voltage: %sV\n"
" Sample Rate: %s\n"
" State Save Interval: %ims",
" State Save Interval: %" PRIu32 "ms",
this->temperature_offset_, this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile",
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8",
BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_);
@@ -461,7 +461,7 @@ int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t devid, uint8_t a_registe
}
void BME680BSECComponent::delay_ms(uint32_t period) {
ESP_LOGV(TAG, "Delaying for %ums", period);
ESP_LOGV(TAG, "Delaying for %" PRIu32 "ms", period);
delay(period);
}

View File

@@ -169,7 +169,7 @@ async def to_code_base(config):
path = _compute_local_file_path(_compute_url(config))
try:
with open(path, encoding="utf-8") as f:
with path.open(encoding="utf-8") as f:
bsec2_iaq_config = f.read()
except Exception as e:
raise core.EsphomeError(

View File

@@ -0,0 +1,10 @@
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.motion import MotionComponent
CODEOWNERS = ["@clydebarrow"]
CONF_BMI270_ID = "bmi270_id"
# C++ namespace / class
bmi270_ns = cg.esphome_ns.namespace("bmi270")
BMI270Component = bmi270_ns.class_("BMI270Component", MotionComponent, i2c.I2CDevice)

View File

@@ -0,0 +1,209 @@
#include "bmi270.h"
#include "bmi270_config.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome::bmi270 {
static const char *const TAG = "bmi270";
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
static const size_t MAX_I2C_BUFFER_SIZE = 32;
#else
static const size_t MAX_I2C_BUFFER_SIZE = 256;
#endif
// Configuration blob upload
// The BMI270 requires a firmware config blob to be written to its internal
// memory after every power-on before sensors can be used.
bool BMI270Component::load_config_file_() {
// 1. Disable advanced power-save so the config port is accessible
if (!this->write_byte(BMI270_REG_PWR_CONF, 0x00))
return false;
delay(1);
// 2. Prepare config load: write 0x00 to INIT_CTRL to start
if (!this->write_byte(BMI270_REG_INIT_CTRL, 0x00))
return false;
// 3. Burst-write the config in pages
const uint8_t *cfg = BMI270_CONFIG_FILE;
constexpr size_t cfg_len = sizeof(BMI270_CONFIG_FILE);
size_t index = 0;
while (index != cfg_len) {
// Set the page address in INIT_ADDR registers
uint8_t addr_lsb = (uint8_t) ((index / 2) & 0x0F);
uint8_t addr_msb = (uint8_t) ((index / 2) >> 4);
if (!this->write_byte(BMI270_REG_INIT_ADDR_0, addr_lsb))
return false;
if (!this->write_byte(BMI270_REG_INIT_ADDR_0 + 1, addr_msb))
return false;
// Write a burst of up to the maximum allowed size
size_t burst = clamp_at_most(cfg_len - index, MAX_I2C_BUFFER_SIZE);
if (this->write_register(BMI270_REG_INIT_DATA, cfg + index, burst) != i2c::ERROR_OK)
return false;
index += burst;
}
// 4. Signal end of config load
if (!this->write_byte(BMI270_REG_INIT_CTRL, 0x01))
return false;
delay(20); // spec: wait ≥20 ms for init to complete
// 5. Check INTERNAL_STATUS: bit[0:3] should be 0x01 ("initialisation OK")
uint8_t status = 0;
if (!this->read_byte(BMI270_REG_INTERNAL_STATUS, &status))
return false;
if ((status & 0x0F) != 0x01) {
ESP_LOGE(TAG, "Config load failed: INTERNAL_STATUS=0x%02X (expected 0x01)", status);
return false;
}
return true;
}
// setup() ─
void BMI270Component::setup() {
MotionComponent::setup();
// 1. Verify chip ID
uint8_t chip_id = 0;
if (!this->read_byte(BMI270_REG_CHIP_ID, &chip_id)) {
ESP_LOGE(TAG, "Failed to read chip ID check wiring / address");
this->mark_failed();
return;
}
if (chip_id != BMI270_CHIP_ID_VALUE) {
ESP_LOGE(TAG, "Wrong chip ID: 0x%02X (expected 0x%02X)", chip_id, BMI270_CHIP_ID_VALUE);
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Chip ID: 0x%02X", chip_id);
// 2. Soft-reset via CMD register (0x7E = 0xB6)
if (!this->write_byte(0x7E, 0xB6)) {
this->mark_failed();
return;
}
delay(20);
// 4. Upload the configuration blob
if (!load_config_file_()) {
ESP_LOGE(TAG, "Config file upload failed");
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Config blob uploaded ✓");
// 5. Configure accelerometer
// ACC_CONF: ODR | BWP(0x2 = normal avg4) | perf_mode(1)
uint8_t acc_conf = (uint8_t) (accel_odr_) | (0x2 << 4) | (1 << 7);
if (!this->write_byte(BMI270_REG_ACC_CONF, acc_conf)) {
this->mark_failed();
return;
}
if (!this->write_byte(BMI270_REG_ACC_RANGE, (uint8_t) accel_range_)) {
this->mark_failed();
return;
}
// 6. Configure gyroscope
// GYR_CONF: ODR | BWP(0x2 = normal) | noise_perf(1) | filter_perf(1)
uint8_t gyr_conf = (uint8_t) (gyro_odr_) | (0x2 << 4) | (1 << 6) | (1 << 7);
if (!this->write_byte(BMI270_REG_GYR_CONF, gyr_conf)) {
this->mark_failed();
return;
}
if (!this->write_byte(BMI270_REG_GYR_RANGE, (uint8_t) gyro_range_)) {
this->mark_failed();
return;
}
// 7. Enable accelerometer, gyroscope, and temperature sensor
// PWR_CTRL bits: temp_en[3] | gyr_en[2] | acc_en[1]
if (!this->write_byte(BMI270_REG_PWR_CTRL, 0x0E)) {
this->mark_failed();
return;
}
delay(5);
// 8. Re-enable advanced power save (optional; keeps current low between reads)
// Disabled here for simplicity leave in performance mode
if (!this->write_byte(BMI270_REG_PWR_CONF, 0x02)) { // bit1 = fifo_self_wakeup
this->mark_failed();
return;
}
ESP_LOGCONFIG(TAG, "BMI270 initialised successfully");
}
void BMI270Component::dump_config() {
ESP_LOGCONFIG(TAG, "BMI270 IMU:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, " Communication failed!");
return;
}
static constexpr const char *const ACCEL_RANGE_STRS[] = {"±2g", "±4g", "±8g", "±16g"};
static constexpr const char *const GYRO_RANGE_STRS[] = {"±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s"};
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[accel_range_]);
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[gyro_range_]);
MotionComponent::dump_config();
}
// update() ─
// Reads all 6 axes + temperature in one block
bool BMI270Component::update_data(motion::MotionData &data) {
if (this->is_failed())
return false;
// Accelerometer: registers 0x0C0x11 (6 bytes: x_lsb, x_msb, y_lsb, y_msb, z_lsb, z_msb)
uint8_t raw_data[REG_READ_LEN];
if (!this->read_bytes(BMI270_REG_DATA_8, raw_data, REG_READ_LEN)) {
ESP_LOGW(TAG, "Failed to read IMU data");
return false;
}
// Scale factor: LSB/g depends on range
// raw is a signed 16-bit value; full-scale = range_g * 2^15 lsb
static constexpr float ACCEL_SCALE[] = {
2.0f / 32768.0f,
4.0f / 32768.0f,
8.0f / 32768.0f,
16.0f / 32768.0f,
};
float scale = ACCEL_SCALE[this->accel_range_];
data.acceleration[motion::X_AXIS] = (int16_t) ((raw_data[1] << 8) | raw_data[0]) * scale;
data.acceleration[motion::Y_AXIS] = (int16_t) ((raw_data[3] << 8) | raw_data[2]) * scale;
data.acceleration[motion::Z_AXIS] = (int16_t) ((raw_data[5] << 8) | raw_data[4]) * scale;
// Gyroscope: registers 0x120x17 (6 bytes)
// Scale: full-scale range / 2^15
static constexpr float GYRO_SCALE[] = {
2000.0f / 32768.0f, 1000.0f / 32768.0f, 500.0f / 32768.0f, 250.0f / 32768.0f, 125.0f / 32768.0f,
};
static constexpr uint8_t GYR_OFFS = BMI270_REG_DATA_14 - BMI270_REG_DATA_8;
scale = GYRO_SCALE[this->gyro_range_];
data.angular_rate[motion::X_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 1] << 8) | raw_data[GYR_OFFS + 0]) * scale;
data.angular_rate[motion::Y_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 3] << 8) | raw_data[GYR_OFFS + 2]) * scale;
data.angular_rate[motion::Z_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 5] << 8) | raw_data[GYR_OFFS + 4]) * scale;
if (this->temperature_callback_.empty())
return true;
// Temperature: registers 0x220x23
// Formula from datasheet: T[°C] = raw / 512 + 23
static constexpr uint8_t TEMP_OFFS = BMI270_REG_TEMP_0 - BMI270_REG_DATA_8;
int16_t raw_t = (int16_t) ((raw_data[TEMP_OFFS + 1] << 8) | raw_data[TEMP_OFFS + 0]);
float temperature = (raw_t / 512.0f) + 23.0f;
this->temperature_callback_.call(temperature);
return true;
}
} // namespace esphome::bmi270

Some files were not shown because too many files have changed in this diff Show More