Compare commits

...

278 Commits

Author SHA1 Message Date
J. Nick Koston 5b1618331f DO NOT MERGE: prove memory-impact no-common-platform fix in CI
Chained on top of the memory-impact fix to exercise it in real CI.

Changed components and their base-test platforms:
- const: variant-only test (test-display.esp32-s3-idf.yaml, no base test).
  --base-only cannot build it, so the fix must exclude it; it only injects
  an esp32-s3-idf platform hint.
- shelly_dimmer: base test esp8266-ard (real C++).
- daikin: base test esp8266-ard (real C++), the working third component.

Without the fix, the esp32-s3-idf hint (and const's variant counted as a
supported platform) selects esp32-s3-idf, which none of the three can build
under --base-only, yielding 0 builds and a failed memory extraction. With the
fix, const is excluded, the unbuildable hint is ignored in favor of the common
esp8266-ard platform, and shelly_dimmer + daikin generate memory impact.
2026-06-03 21:47:52 -05:00
J. Nick Koston 3ad851993f [ci] Fix memory impact build selecting unbuildable platform
Memory impact analysis gathered candidate platforms from all test files
(including variant test-*.yaml) but the CI build runs
test_build_components.py with --base-only, which only compiles base
test.<platform>.yaml files. It could therefore select a platform that no
changed component has a base test for, leaving the merged build with
nothing to compile and failing memory extraction.

Detect platforms using the same base-test discovery as the runner via a
shared get_component_test_platforms helper, and keep only components that
have a base test on the selected platform, falling back to the platform
supported by the most components when a hint picks an unbuildable one.
2026-06-03 21:18:09 -05: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
377 changed files with 11071 additions and 2767 deletions
+1 -1
View File
@@ -1 +1 @@
27aaab4e0ebfc10491720345aa746fc2dffa6a3985f73ec111b12dd99078d46f
a30d2e50f2cac76e9c504eb7e5b250070dc92df23469c44a7eb8e52e26fd375d
+4 -4
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
+1 -1
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
+3 -3
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).
+2 -2
View File
@@ -47,7 +47,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
@@ -73,7 +73,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
+16 -4
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@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.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 .
+1
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
@@ -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+/
]
@@ -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) {
@@ -0,0 +1,7 @@
{
"name": "auto-label-pr",
"private": true,
"scripts": {
"test": "node --test tests/*.test.js"
}
}
@@ -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);
});
});
+1 -1
View File
@@ -24,7 +24,7 @@ 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
+12 -2
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@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.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
+1 -1
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
+2 -2
View File
@@ -42,13 +42,13 @@ jobs:
- "docker"
# - "lint"
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
run: |
+27
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
@@ -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
+64 -39
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@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.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:
@@ -117,7 +123,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 +151,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
@@ -167,6 +173,10 @@ jobs:
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.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
@@ -181,7 +191,7 @@ jobs:
# own CI). No ``--cov`` here -- this is purely a downstream
# smoke check against this PR's esphome code.
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
pytest:
name: Run pytest
@@ -207,9 +217,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 +234,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@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
@@ -245,6 +257,7 @@ 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 }}
@@ -268,7 +281,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
@@ -298,6 +311,7 @@ jobs:
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
@@ -339,7 +353,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
@@ -351,14 +365,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@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.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
@@ -370,7 +394,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
@@ -381,7 +405,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
@@ -410,7 +434,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
@@ -428,7 +452,7 @@ 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: |
. venv/bin/activate
@@ -466,7 +490,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
@@ -551,7 +575,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
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
@@ -646,7 +670,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
@@ -740,7 +764,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:
@@ -865,7 +889,7 @@ jobs:
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
@@ -943,10 +967,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:
@@ -972,7 +997,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 }}
@@ -1154,7 +1179,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:
@@ -1223,7 +1248,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:
@@ -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: |
@@ -29,7 +29,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 }}
+3 -3
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@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
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@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
with:
category: "/language:${{matrix.language}}"
+5 -4
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;
}
+11 -79
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@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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
@@ -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",
+1 -1
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
+34 -14
View File
@@ -28,15 +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
- name: Create sync branch
# Switch off dev before pre-commit runs so the
# no-commit-to-branch hook passes (it blocks dev/release/beta).
run: git checkout -B sync/device-classes
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
@@ -46,13 +41,24 @@ 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@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.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
# Project requirements are needed so pylint can resolve runtime
# imports (e.g. smpclient in esphome/components/nrf52/ota.py).
pip install -r requirements.txt -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: |
@@ -63,13 +69,27 @@ jobs:
# 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 (pylint / flake8 / yamllint / ci-custom)
# is a real issue and fails the workflow loudly.
# 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
+1 -1
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
+2 -2
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++):**
+1
View File
@@ -417,6 +417,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
+109 -24
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():
@@ -1404,17 +1431,47 @@ def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
if 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 +1636,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 +1849,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 +2037,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(
@@ -2156,11 +2236,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 +2268,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",
@@ -2441,7 +2513,10 @@ def run_esphome(argv):
# Skipped when -s overrides are passed, since the cache was written
# against the previous substitution set.
config: ConfigType | None = None
if args.command in ("upload", "logs") and not command_line_substitutions:
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)
@@ -2456,6 +2531,16 @@ def run_esphome(argv):
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
+6 -6
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)
+1 -1
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)
+1 -2
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
+1 -1
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
+1 -7
View File
@@ -7,7 +7,6 @@ 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
def get_available_components() -> list[str] | None:
@@ -24,7 +23,7 @@ def get_available_components() -> list[str] | None:
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", {})
@@ -213,11 +212,6 @@ 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())
+1 -2
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 ============"
@@ -58,7 +58,6 @@ def get_ini_content():
def write_ini(content):
update_storage_json()
path = CORE.relative_build_path("platformio.ini")
if path.is_file():
+13 -86
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("<"):
+2 -2
View File
@@ -43,7 +43,7 @@ def save_compiled_config(config: ConfigType) -> None:
try:
rendered = yaml_util.dump(config, show_secrets=True)
write_file(compiled_config_path(CORE.config_filename), rendered, private=True)
except Exception as err: # pylint: disable=broad-except
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
_LOGGER.debug("Skipping compiled config cache write: %s", err)
@@ -62,7 +62,7 @@ def load_compiled_config(conf_path: Path) -> ConfigType | None:
try:
config = yaml_util.load_yaml(cache_path, clear_secrets=False)
except Exception: # pylint: disable=broad-except
except Exception: # noqa: BLE001 # pylint: disable=broad-except
return None
storage = StorageJSON.load(ext_storage_path(conf_path.name))
+1 -1
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),
}
)
+5 -1
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);
}
+11 -40
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
@@ -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
+4 -8
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_();
@@ -682,7 +678,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 +722,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 +734,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
+4 -10
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_;
+3 -2
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
+1 -1
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),
+1 -1
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.1.0",
)
data = _get_data()
+41 -42
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;
+5 -4
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.
+45 -16
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);
}
+10 -7
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_;
@@ -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.
@@ -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
+2 -2
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:
@@ -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()
+3 -15
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]))
@@ -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);
@@ -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);
@@ -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"
+1 -1
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(
@@ -1,5 +1,8 @@
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_component
from esphome.components.esp32 import (
add_idf_component,
require_libc_picolibc_newlib_compat,
)
import esphome.config_validation as cv
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TYPE
from esphome.types import ConfigType
@@ -51,6 +54,8 @@ async def to_code(config: ConfigType) -> None:
cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE]))
if config[CONF_TYPE] == ESP32_CAMERA_ENCODER:
add_idf_component(name="espressif/esp32-camera", ref="2.1.5")
# esp32-camera 2.1.5 needs the Newlib shim on IDF 6.0+; remove when fixed upstream
require_libc_picolibc_newlib_compat()
cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER")
var = cg.new_Pvariable(
config[CONF_ID],
+1
View File
@@ -22,6 +22,7 @@ CONF_PARITY = "parity"
CONF_RECEIVER_FREQUENCY = "receiver_frequency"
CONF_REQUEST_HEADERS = "request_headers"
CONF_ROWS = "rows"
CONF_SHA256 = "sha256"
CONF_STOP_BITS = "stop_bits"
CONF_USE_PSRAM = "use_psram"
CONF_VOLUME_INCREMENT = "volume_increment"
+2
View File
@@ -1,3 +1,5 @@
// DNM: do not merge. Trivial touch to mark daikin changed; this is the working
// esp8266-ard component that should generate memory impact (see PR #16788).
#include "daikin.h"
#include "esphome/components/remote_base/remote_base.h"
+89 -13
View File
@@ -3,11 +3,18 @@ from dataclasses import dataclass
from esphome import automation, core
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components.const import KEY_METADATA
from esphome.components.const import (
BYTE_ORDER_BIG,
CONF_BYTE_ORDER,
CONF_DRAW_ROUNDING,
KEY_METADATA,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_DIMENSIONS,
CONF_FROM,
CONF_HEIGHT,
CONF_ID,
CONF_LAMBDA,
CONF_PAGE_ID,
@@ -16,10 +23,11 @@ from esphome.const import (
CONF_TO,
CONF_TRIGGER_ID,
CONF_UPDATE_INTERVAL,
CONF_WIDTH,
SCHEDULER_DONT_RUN,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.final_validate import full_config
DOMAIN = "display"
IS_PLATFORM_COMPONENT = True
@@ -159,29 +167,97 @@ async def setup_display_core_(var, config):
class DisplayMetaData:
width: int = 0
height: int = 0
has_writer: bool = False
has_hardware_rotation: bool = False
byte_order: str = BYTE_ORDER_BIG
has_writer: bool = False
rotation: int = 0
draw_rounding: int = 0
def _get_metadata_list() -> list[tuple]:
"""Get the raw metadata list. Each entry is (id, DisplayMetaData)."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, [])
def get_all_display_metadata() -> dict[str, DisplayMetaData]:
"""Get all display metadata."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
"""Get all display metadata as a dict keyed by resolved ID strings.
Must not be called before IDs have been finalised.
"""
entries = _get_metadata_list()
assert all(id_.id is not None for id_, _ in entries), (
"get_all_display_metadata called before display IDs have been resolved"
)
return {id_.id: meta for id_, meta in entries}
def get_display_metadata(display_id: str) -> DisplayMetaData | None:
"""Get display metadata by ID for use by other components."""
return get_all_display_metadata().get(display_id, DisplayMetaData())
def get_display_metadata(display_id: ID) -> DisplayMetaData:
"""Get display metadata by ID object
Must not be called before IDs have been finalised.
"""
for id_, meta in _get_metadata_list():
if id_ is display_id:
return meta
assert id_.id is not None, (
"get_display_metadata called before display IDs have been resolved"
)
if id_.id == display_id.id:
return meta
# No metadata found, display driver may not yet support it.
# Read the raw config to populate the returned data
global_config = full_config.get()
path = global_config.get_path_for_id(display_id)[:-1]
disp_config = global_config.get_config_for_path(path)
dimensions = disp_config.get(CONF_DIMENSIONS, (0, 0))
if isinstance(dimensions, dict):
dimensions = (dimensions.get(CONF_WIDTH, 0), dimensions.get(CONF_HEIGHT, 0))
elif not isinstance(dimensions, tuple) or len(dimensions) != 2:
dimensions = (0, 0)
meta = DisplayMetaData(
width=dimensions[0],
height=dimensions[1],
has_hardware_rotation=False,
byte_order=disp_config.get(CONF_BYTE_ORDER, cv.UNDEFINED),
has_writer=disp_config.get(CONF_AUTO_CLEAR_ENABLED) is True
or disp_config.get(CONF_PAGES) is not None
or disp_config.get(CONF_LAMBDA) is not None
or disp_config.get(CONF_SHOW_TEST_CARD) is True,
rotation=disp_config.get(CONF_ROTATION, 0),
draw_rounding=disp_config.get(CONF_DRAW_ROUNDING, 0),
)
_get_metadata_list().append((display_id, meta))
return meta
def add_metadata(
id: str | MockObj,
id: ID,
width: int,
height: int,
has_writer: bool,
has_hardware_rotation: bool = False,
byte_order: str = BYTE_ORDER_BIG,
has_writer: bool = False,
rotation: int = 0,
draw_rounding: int = 0,
):
get_all_display_metadata()[str(id)] = DisplayMetaData(
width, height, has_writer, has_hardware_rotation
entries = _get_metadata_list()
assert not any(existing_id is id for existing_id, _ in entries), (
f"Duplicate display metadata for ID {id}"
)
entries.append(
(
id,
DisplayMetaData(
width=width,
height=height,
has_hardware_rotation=has_hardware_rotation,
byte_order=byte_order,
has_writer=has_writer,
rotation=rotation,
draw_rounding=draw_rounding,
),
)
)
+1 -1
View File
@@ -87,7 +87,7 @@ async def to_code(config):
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID]))
cg.add_library("esphome/dsmr_parser", "1.4.0")
cg.add_library("esphome/dsmr_parser", "1.8.0")
def final_validate(config: ConfigType) -> ConfigType:
+4 -3
View File
@@ -153,8 +153,9 @@ void Dsmr::receive_encrypted_telegram_() {
bool Dsmr::parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram) {
this->stop_requesting_data_();
ESP_LOGV(TAG, "Trying to parse telegram (%zu bytes)", telegram.content().size());
ESP_LOGVV(TAG, "Telegram content:\n %.*s", static_cast<int>(telegram.content().size()), telegram.content().data());
ESP_LOGV(TAG, "Trying to parse telegram (%zu bytes)", telegram.full_content().size());
ESP_LOGVV(TAG, "Telegram content:\n %.*s", static_cast<int>(telegram.full_content().size()),
telegram.full_content().data());
MyData data;
if (const bool res = dsmr_parser::DsmrParser::parse(data, telegram); !res) {
@@ -167,7 +168,7 @@ bool Dsmr::parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram)
// Publish the telegram, after publishing the sensors so it can also trigger action based on latest values
if (this->s_telegram_ != nullptr) {
this->s_telegram_->publish_state(telegram.content().data(), telegram.content().size());
this->s_telegram_->publish_state(telegram.full_content().data(), telegram.full_content().size());
}
return true;
}
+15 -5
View File
@@ -16,9 +16,14 @@
#include <span>
#include <vector>
// On ESP8266 Arduino, BearSSL is the native crypto. The mbedtls headers can
// still be in scope when a sibling component (e.g. wireguard) pulls in
// esp_mbedtls_esp8266, but that build leaves MBEDTLS_GCM_C disabled so the
// gcm.h symbols are unresolved at link time. Force BearSSL on ESP8266 to
// avoid that linker error.
#if __has_include(<psa/crypto.h>)
#include <dsmr_parser/decryption/aes128gcm_tfpsa.h>
#elif __has_include(<mbedtls/gcm.h>)
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
#if __has_include(<mbedtls/esp_config.h>)
#include <mbedtls/esp_config.h>
#endif
@@ -33,7 +38,7 @@ namespace esphome::dsmr {
#if __has_include(<psa/crypto.h>)
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmTfPsa;
#elif __has_include(<mbedtls/gcm.h>)
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmMbedTls;
#else
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmBearSsl;
@@ -69,7 +74,8 @@ class Dsmr : public Component, public uart::UARTDevice {
receive_timeout_(receive_timeout),
request_pin_(request_pin),
buffer_(max_telegram_length),
packet_accumulator_(buffer_, crc_check) {
packet_accumulator_(buffer_, crc_check),
dlms_decryptor_(gcm_decryptor_, crc_check) {
this->set_decryption_key_(decryption_key);
}
@@ -92,7 +98,11 @@ class Dsmr : public Component, public uart::UARTDevice {
// Remove before 2026.8.0
ESPDEPRECATED("Use 'decryption_key' configuration parameter. This method will be removed in 2026.8.0", "2026.2.0")
void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key_(decryption_key.c_str()); }
void set_decryption_key(const std::string &decryption_key) {
// Some YAML configs pass a string longer than 32 symbols. We only need the first 32 symbols,
// otherwise `Aes128GcmDecryptionKey::from_hex` will fail.
this->set_decryption_key_(std::string(decryption_key, 0, 32).c_str());
}
// Sensor setters
#define DSMR_SET_SENSOR(s) \
@@ -138,7 +148,7 @@ class Dsmr : public Component, public uart::UARTDevice {
std::vector<uint8_t> buffer_;
dsmr_parser::PacketAccumulator packet_accumulator_;
Aes128GcmDecryptorImpl gcm_decryptor_;
dsmr_parser::DlmsPacketDecryptor dlms_decryptor_{gcm_decryptor_};
dsmr_parser::DlmsPacketDecryptor dlms_decryptor_;
std::array<uint8_t, 256> uart_chunk_reading_buf_;
};
} // namespace esphome::dsmr
+4 -4
View File
@@ -248,10 +248,6 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("electricity_switch_position"): sensor.sensor_schema(
accuracy_decimals=3,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("electricity_failures"): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
@@ -808,6 +804,10 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("electricity_switch_position"): cv.invalid(
"'electricity_switch_position' has moved to the 'text_sensor' platform."
"Move it under 'text_sensor' to fix."
),
}
).extend(cv.COMPONENT_SCHEMA)
+1
View File
@@ -14,6 +14,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional("p1_version"): text_sensor.text_sensor_schema(),
cv.Optional("p1_version_be"): text_sensor.text_sensor_schema(),
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_switch_position"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_tariff"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_tariff_il"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_failure_log"): text_sensor.text_sensor_schema(),
+2
View File
@@ -52,6 +52,8 @@ class E131Component : public esphome::Component {
if (!this->udp_.parsePacket())
return -1;
return this->udp_.read(buf, len);
#else
return -1;
#endif
}
bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet);
+224 -77
View File
@@ -46,7 +46,7 @@ from esphome.const import (
Toolchain,
__version__,
)
from esphome.core import CORE, HexInt, Library
from esphome.core import CORE, EsphomeError, HexInt, Library
from esphome.core.config import BOARD_MAX_LENGTH
from esphome.coroutine import CoroPriority, coroutine_with_priority
from esphome.espidf.component import generate_idf_component
@@ -56,7 +56,7 @@ from esphome.types import ConfigType
from esphome.writer import clean_build, clean_cmake_cache
from .boards import BOARDS, STANDARD_BOARDS
from .const import ( # noqa
from .const import (
KEY_ARDUINO_LIBRARIES,
KEY_BOARD,
KEY_COMPONENTS,
@@ -78,15 +78,18 @@ from .const import ( # noqa
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32H4,
VARIANT_ESP32H21,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
VARIANT_FRIENDLY,
VARIANTS,
)
# force import gpio to register pin schema
from .gpio import esp32_pin_to_code # noqa
from .gpio import esp32_pin_to_code # noqa: F401
_LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["preferences"]
@@ -113,6 +116,7 @@ ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32"
ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}"
ARDUINO_LIBS_NAME = f"{ARDUINO_FRAMEWORK_NAME}-libs"
ARDUINO_LIBS_PKG = f"pioarduino/{ARDUINO_LIBS_NAME}"
ARDUINO_ESP32_COMPONENT_NAME = "espressif/arduino-esp32"
LOG_LEVELS_IDF = [
"NONE",
@@ -402,9 +406,12 @@ CPU_FREQUENCIES = {
VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160),
VARIANT_ESP32C61: get_cpu_frequencies(80, 120, 160),
VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96),
VARIANT_ESP32H4: get_cpu_frequencies(48, 64, 96),
VARIANT_ESP32H21: get_cpu_frequencies(48, 64, 96),
VARIANT_ESP32P4: get_cpu_frequencies(40, 360, 400),
VARIANT_ESP32S2: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32S3: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32S31: get_cpu_frequencies(240, 320),
}
# Make sure not missed here if a new variant added.
@@ -463,21 +470,20 @@ def set_core_data(config):
framework_ver = cv.Version.parse(config[CONF_FRAMEWORK][CONF_VERSION])
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = framework_ver
# Store the underlying IDF version for framework-agnostic checks
# Store the underlying IDF version for framework-agnostic checks.
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = framework_ver
elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
if CORE.using_toolchain_esp_idf:
# Official ESP-IDF frameworks don't use extra
idf_ver = cv.Version(idf_ver.major, idf_ver.minor, idf_ver.patch)
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = idf_ver
else:
idf_ver = framework_ver
elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is None:
raise cv.Invalid(
f"Arduino version {framework_ver} has no known ESP-IDF version mapping. "
"Please update ARDUINO_IDF_VERSION_LOOKUP.",
path=[CONF_FRAMEWORK, CONF_VERSION],
)
# The esp-idf toolchain doesn't use pioarduino's packaging revision; PIO does.
if CORE.using_toolchain_esp_idf:
idf_ver = _strip_pioarduino_revision(idf_ver)
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = idf_ver
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
@@ -714,6 +720,9 @@ ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"dev": cv.Version(3, 3, 8),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(
4, 0, 0, "alpha1"
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"),
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
@@ -734,6 +743,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# These versions correspond to pioarduino/esp-idf releases
# See: https://github.com/pioarduino/esp-idf/releases
ARDUINO_IDF_VERSION_LOOKUP = {
cv.Version(4, 0, 0, "alpha1"): cv.Version(6, 0, 1),
cv.Version(3, 3, 8): cv.Version(5, 5, 4),
cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"),
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
@@ -792,19 +802,15 @@ PLATFORM_VERSION_LOOKUP = {
}
def _check_pio_versions(config):
config = config.copy()
value = config[CONF_FRAMEWORK]
def _resolve_framework_version(value: ConfigType) -> cv.Version:
"""Resolve a named or raw framework version and validate the minimum.
Normalises value[CONF_VERSION] to its string form and returns the parsed
cv.Version. Shared between the PIO and esp-idf toolchain paths; toolchain-
specific concerns (source defaults, platform_version) live in the per-
toolchain functions.
"""
if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP:
if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value:
raise cv.Invalid(
"Version needs to be explicitly set when a custom source or platform_version is used."
)
platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]]
value[CONF_PLATFORM_VERSION] = _parse_pio_platform_version(str(platform_lookup))
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
else:
@@ -817,7 +823,48 @@ def _check_pio_versions(config):
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
if version < cv.Version(3, 0, 0):
raise cv.Invalid("Only Arduino 3.0+ is supported.")
recommended_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"]
recommended = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"]
else:
if version < cv.Version(5, 0, 0):
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
recommended = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"]
if version != recommended:
_LOGGER.warning(
"The selected framework version is not the recommended one. "
"If there are connectivity or build issues please remove the manual version."
)
return version
def _strip_pioarduino_revision(ver: cv.Version) -> cv.Version:
"""Drop a numeric 'extra' (pioarduino packaging revision, e.g. "5.5.3-1").
Alphanumeric prerelease extras (e.g. "6.0.0-rc1") are kept.
"""
if ver.extra.isdigit():
return cv.Version(ver.major, ver.minor, ver.patch)
return ver
def _check_pio_versions(config: ConfigType) -> ConfigType:
config = config.copy()
value = config[CONF_FRAMEWORK]
is_named_version = value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP
if is_named_version and (CONF_SOURCE in value or CONF_PLATFORM_VERSION in value):
raise cv.Invalid(
"Version needs to be explicitly set when a custom source or platform_version is used."
)
if is_named_version:
value[CONF_PLATFORM_VERSION] = _parse_pio_platform_version(
str(PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]])
)
version = _resolve_framework_version(value)
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
platform_lookup = ARDUINO_PLATFORM_VERSION_LOOKUP.get(version)
value[CONF_SOURCE] = value.get(
CONF_SOURCE, _format_framework_arduino_version(version)
@@ -825,9 +872,6 @@ def _check_pio_versions(config):
if _is_framework_url(value[CONF_SOURCE]):
value[CONF_SOURCE] = f"{ARDUINO_FRAMEWORK_PKG}@{value[CONF_SOURCE]}"
else:
if version < cv.Version(5, 0, 0):
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
recommended_version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"]
platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
value[CONF_SOURCE] = value.get(
CONF_SOURCE,
@@ -843,12 +887,6 @@ def _check_pio_versions(config):
)
value[CONF_PLATFORM_VERSION] = _parse_pio_platform_version(str(platform_lookup))
if version != recommended_version:
_LOGGER.warning(
"The selected framework version is not the recommended one. "
"If there are connectivity or build issues please remove the manual version."
)
if value[CONF_PLATFORM_VERSION] != _parse_pio_platform_version(
str(PLATFORM_VERSION_LOOKUP["recommended"])
):
@@ -860,19 +898,28 @@ def _check_pio_versions(config):
return config
def _check_esp_idf_versions(config):
config = _check_pio_versions(config)
def _check_esp_idf_versions(config: ConfigType) -> ConfigType:
config = config.copy()
value = config[CONF_FRAMEWORK]
# Remove unwanted keys if present
for key in (CONF_SOURCE, CONF_PLATFORM_VERSION):
value.pop(key, None)
# platform_version is a PlatformIO concept; drop it if a user carried it
# over from a PIO-style config. CONF_SOURCE, on the other hand, is kept:
# it lets a user override the framework tarball URL under the esp-idf
# toolchain (the espidf framework downloader consults it).
value.pop(CONF_PLATFORM_VERSION, None)
# Official ESP-IDF frameworks don't use extra
version = cv.Version.parse(value[CONF_VERSION])
version = cv.Version(version.major, version.minor, version.patch)
version = _resolve_framework_version(value)
value[CONF_VERSION] = str(version)
if CONF_SOURCE in value:
_LOGGER.warning(
"A custom framework source is set. "
"If there are connectivity or build issues please remove the manual source."
)
# esp-idf framework only: drop pioarduino's packaging revision (config + download).
# Arduino keeps its extra (it's the arduino-esp32 release tag / lookup key).
if value[CONF_TYPE] == FRAMEWORK_ESP_IDF:
value[CONF_VERSION] = str(_strip_pioarduino_revision(version))
return config
@@ -881,11 +928,16 @@ def _validate_toolchain(value) -> Toolchain:
return Toolchain(cv.one_of(*(t.value for t in Toolchain), lower=True)(value))
def _check_versions(config):
def _resolve_toolchain(value: ConfigType) -> ConfigType:
# Resolve toolchain: CLI (already on CORE.toolchain) > YAML > default.
# Runs before _detect_variant so downstream validators can rely on
# CORE.toolchain instead of re-resolving it from the config dict.
if CORE.toolchain is None:
CORE.toolchain = config.get(CONF_TOOLCHAIN, Toolchain.PLATFORMIO)
CORE.toolchain = value.get(CONF_TOOLCHAIN, Toolchain.PLATFORMIO)
return value
def _check_versions(config: ConfigType) -> ConfigType:
if CORE.using_toolchain_esp_idf:
return _check_esp_idf_versions(config)
return _check_pio_versions(config)
@@ -907,7 +959,21 @@ def _detect_variant(value):
variant = value.get(CONF_VARIANT)
if variant and board is None:
# If variant is set, we can derive the board from it
# variant has already been validated against the known set
# variant has already been validated against the known set.
# PlatformIO needs a real board name to find its board file; the
# ESP-IDF toolchain only uses CONF_BOARD as the informational
# ESPHOME_BOARD string, so synthesize one from the friendly variant
# name rather than carrying a PIO board name through the IDF build.
if CORE.using_toolchain_esp_idf:
value = value.copy()
value[CONF_BOARD] = VARIANT_FRIENDLY[variant].lower()
return value
if variant not in STANDARD_BOARDS:
raise cv.Invalid(
f"No default board is known for {variant}. "
f"Please specify the `board:` option explicitly.",
path=[CONF_VARIANT],
)
value = value.copy()
value[CONF_BOARD] = STANDARD_BOARDS[variant]
if variant == VARIANT_ESP32P4:
@@ -1194,6 +1260,7 @@ KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
KEY_FATFS_REQUIRED = "fatfs_required"
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
KEY_LIBC_PICOLIBC_NEWLIB_COMPAT_REQUIRED = "libc_picolibc_newlib_compat_required"
def require_vfs_select() -> None:
@@ -1302,6 +1369,15 @@ def require_adc_oneshot_iram() -> None:
CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_REQUIRED] = True
def require_libc_picolibc_newlib_compat() -> None:
"""Keep CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY enabled on IDF 6.0+.
Call this from components that link against precompiled Newlib binaries
referencing types/symbols the shim provides (e.g. esp32-camera).
"""
CORE.data[KEY_ESP32][KEY_LIBC_PICOLIBC_NEWLIB_COMPAT_REQUIRED] = True
def _parse_idf_component(value: str) -> ConfigType:
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
# Match operator followed by version-like string (digit or *)
@@ -1580,6 +1656,7 @@ CONFIG_SCHEMA = cv.All(
),
}
),
_resolve_toolchain,
_detect_variant,
_set_default_framework,
_check_versions,
@@ -1706,6 +1783,26 @@ async def _write_arduino_libraries_sdkconfig() -> None:
add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs)
@coroutine_with_priority(CoroPriority.FINAL)
async def _set_libc_picolibc_newlib_compat() -> None:
"""Apply the PicolibC Newlib compatibility shim option on IDF 6.0+.
IDF 6.0 switched from Newlib to PicolibC; the shim is disabled by default.
Runs at FINAL priority so every require_libc_picolibc_newlib_compat() call
(default priority) is seen before the option is written. A user-supplied
sdkconfig_options value takes precedence.
"""
if idf_version() < cv.Version(6, 0, 0):
return
option = "CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY"
if option in CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]:
return
add_idf_sdkconfig_option(
option,
CORE.data[KEY_ESP32].get(KEY_LIBC_PICOLIBC_NEWLIB_COMPAT_REQUIRED, False),
)
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_yaml_idf_components(components: list[ConfigType]):
"""Add IDF components from YAML config with final priority to override code-added components."""
@@ -1718,6 +1815,31 @@ async def _add_yaml_idf_components(components: list[ConfigType]):
)
@coroutine_with_priority(CoroPriority.FINAL - 1)
async def _finalize_arduino_aware_flags():
"""Build flags that depend on whether arduino-esp32 is linked in.
Scheduler runs lower priority values later, so ``FINAL - 1`` fires
after every ``FINAL`` job (incl. ``_add_yaml_idf_components``) --
by then ``KEY_COMPONENTS`` is fully populated.
- Skip our esp_panic_handler wrap when Arduino is linked; Arduino
wraps the same symbol and the linker errors on the duplicate.
- Define USE_ARDUINO in the hybrid esp-idf+arduino-esp32-component
case so ESPHome's ``#ifdef USE_ARDUINO`` paths light up. The
framework=arduino branch already adds it inline in to_code.
"""
arduino_linked = (
CORE.using_arduino
or ARDUINO_ESP32_COMPONENT_NAME in CORE.data[KEY_ESP32][KEY_COMPONENTS]
)
if not arduino_linked:
cg.add_build_flag("-Wl,--wrap=esp_panic_handler")
cg.add_define("USE_ESP32_CRASH_HANDLER")
elif not CORE.using_arduino:
cg.add_build_flag("-DUSE_ARDUINO")
async def to_code(config):
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
conf = config[CONF_FRAMEWORK]
@@ -1765,11 +1887,12 @@ async def to_code(config):
Path(__file__).parent / "iram_fix.py.script",
)
else:
cg.add_build_flag("-Wno-error=format")
cg.add_build_flag("-Wno-error=maybe-uninitialized")
cg.add_build_flag("-Wno-error=overloaded-virtual")
cg.add_build_flag("-Wno-error=reorder")
cg.add_build_flag("-Wno-error=volatile")
# Demote IDF's blanket -Werror to warnings so third-party libs
# and user lambdas don't need a -Wno-error=<class> per warning.
# The sdkconfig knob disables IDF's rewrite to -Werror=all (which
# can't be globally undone); -Wno-error then handles the demotion.
add_idf_sdkconfig_option("CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS", False)
cg.add_build_flag("-Wno-error")
# -Wno- (not -Wno-error=): suppress entirely, too noisy on C++ aggregates
cg.add_build_flag("-Wno-missing-field-initializers")
@@ -1777,11 +1900,8 @@ async def to_code(config):
cg.add_build_flag("-DUSE_ESP32")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.add_build_flag("-Wl,-z,noexecstack")
# Arduino already wraps esp_panic_handler for its own backtrace handler,
# so only add our wrap when using ESP-IDF framework to avoid linker conflicts.
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-Wl,--wrap=esp_panic_handler")
cg.add_define("USE_ESP32_CRASH_HANDLER")
# Deferred so KEY_COMPONENTS is fully populated -- see the coroutine.
CORE.add_job(_finalize_arduino_aware_flags)
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
variant = config[CONF_VARIANT]
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}")
@@ -1962,7 +2082,7 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH", True)
# Setup watchdog
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_INIT", True)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
@@ -2004,7 +2124,8 @@ async def to_code(config):
if not advanced[CONF_ENABLE_LWIP_MDNS_QUERIES]:
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
if not advanced[CONF_ENABLE_LWIP_BRIDGE_INTERFACE]:
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
# Kconfig range is [1,63]; 0 gets clamped to the default.
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 1)
_configure_lwip_max_sockets(conf)
@@ -2096,7 +2217,6 @@ async def to_code(config):
for key, flag in ASSERTION_LEVELS.items():
add_idf_sdkconfig_option(flag, assertion_level == key)
add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False)
compiler_optimization = advanced[CONF_COMPILER_OPTIMIZATION]
for key, flag in COMPILER_OPTIMIZATIONS.items():
add_idf_sdkconfig_option(flag, compiler_optimization == key)
@@ -2216,17 +2336,8 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False)
# Disable PicolibC Newlib compatibility shim on IDF 6.0+
# IDF 6.0 switched from Newlib to PicolibC. The shim provides thread-local
# stdin/stdout/stderr and getreent() for code compiled against Newlib.
# ESPHome doesn't link against Newlib-built libraries that use stdio.
# If a component needs it (e.g. precompiled Newlib binaries), re-enable via:
# esp32:
# framework:
# sdkconfig_options:
# CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY: "y"
if idf_version() >= cv.Version(6, 0, 0):
add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", False)
# FINAL priority: runs after every require_libc_picolibc_newlib_compat() call
CORE.add_job(_set_libc_picolibc_newlib_compat)
# Disable regi2c control functions in IRAM
# Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled
@@ -2251,7 +2362,8 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 2)
elif advanced[CONF_DISABLE_FATFS]:
add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", True)
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 0)
# Kconfig range is [1,10]; 0 gets clamped to the default.
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 1)
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
@@ -2488,9 +2600,8 @@ def _write_sdkconfig():
def _platformio_library_to_dependency(library: Library) -> tuple[str, dict[str, str]]:
dependency: dict[str, str] = {}
name, version, path = generate_idf_component(library)
name, _version, path = generate_idf_component(library)
dependency["override_path"] = str(path)
dependency["version"] = version
return name, dependency
@@ -2534,6 +2645,26 @@ def _write_idf_component_yml():
"override_path": str(stub_path),
}
# On the PlatformIO toolchain, framework-arduinoespressif32 already
# ships arduino-esp32. Stub the managed component so anything that
# `REQUIRES arduino-esp32` (e.g. third-party FastLED) resolves to a
# CMake target that re-exports the framework's INTERFACE properties
# (INCLUDE_DIRS, public compile options like -DESP32, transitive
# REQUIRES) instead of triggering a duplicate download/rebuild.
if CORE.using_toolchain_platformio:
arduino_stub = stubs_dir / "arduino-esp32"
arduino_stub.mkdir(exist_ok=True)
write_file_if_changed(
arduino_stub / "CMakeLists.txt",
"idf_component_register()\n"
"target_link_libraries(${COMPONENT_LIB} "
f"INTERFACE idf::{ARDUINO_FRAMEWORK_NAME})\n",
)
dependencies[ARDUINO_ESP32_COMPONENT_NAME] = {
"version": "*",
"override_path": str(arduino_stub),
}
# Remove stubs for components that are now required by enabled libraries
for component_name in required_idf_components:
stub_path = stubs_dir / _idf_component_stub_name(component_name)
@@ -2542,7 +2673,7 @@ def _write_idf_component_yml():
if CORE.using_toolchain_esp_idf:
add_idf_component(
name="espressif/arduino-esp32",
name=ARDUINO_ESP32_COMPONENT_NAME,
ref=str(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]),
)
@@ -2609,16 +2740,32 @@ def copy_files():
def _decode_pc(config, addr):
from esphome.platformio import toolchain
# _decode_pc runs from the api log processor's asyncio callback, which
# only catches EsphomeError. Any other exception escaping here tears down
# the protocol and triggers an infinite reconnect/replay loop. Convert
# toolchain-resolution errors (e.g. missing build dir / cmake cache) into
# EsphomeError so the caller can disable decoding cleanly.
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain as idf_toolchain
idedata = toolchain.get_idedata(config)
if not idedata.addr2line_path or not idedata.firmware_elf_path:
try:
addr2line_path = idf_toolchain.get_addr2line_path()
firmware_elf_path = idf_toolchain.get_elf_path()
except RuntimeError as err:
raise EsphomeError(f"ESP-IDF toolchain not available: {err}") from err
else:
from esphome.platformio import toolchain
idedata = toolchain.get_idedata(config)
addr2line_path = idedata.addr2line_path
firmware_elf_path = idedata.firmware_elf_path
if not addr2line_path or not firmware_elf_path:
_LOGGER.debug("decode_pc no addr2line")
return
command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr]
command = [str(addr2line_path), "-pfiaC", "-e", str(firmware_elf_path), addr]
try:
translation = subprocess.check_output(command, close_fds=False).decode().strip()
except Exception: # pylint: disable=broad-except
except Exception: # noqa: BLE001 # pylint: disable=broad-except
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
return
-4
View File
@@ -9,7 +9,6 @@ from .const import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANTS,
)
STANDARD_BOARDS = {
@@ -25,9 +24,6 @@ STANDARD_BOARDS = {
VARIANT_ESP32S3: "esp32-s3-devkitc-1",
}
# Make sure not missed here if a new variant added.
assert all(v in STANDARD_BOARDS for v in VARIANTS)
ESP32_BASE_PINS = {
"TX": 1,
"RX": 3,
+9
View File
@@ -24,9 +24,12 @@ VARIANT_ESP32C5 = "ESP32C5"
VARIANT_ESP32C6 = "ESP32C6"
VARIANT_ESP32C61 = "ESP32C61"
VARIANT_ESP32H2 = "ESP32H2"
VARIANT_ESP32H4 = "ESP32H4"
VARIANT_ESP32H21 = "ESP32H21"
VARIANT_ESP32P4 = "ESP32P4"
VARIANT_ESP32S2 = "ESP32S2"
VARIANT_ESP32S3 = "ESP32S3"
VARIANT_ESP32S31 = "ESP32S31"
VARIANTS = [
VARIANT_ESP32,
VARIANT_ESP32C2,
@@ -35,9 +38,12 @@ VARIANTS = [
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32H4,
VARIANT_ESP32H21,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
]
VARIANT_FRIENDLY = {
@@ -48,9 +54,12 @@ VARIANT_FRIENDLY = {
VARIANT_ESP32C6: "ESP32-C6",
VARIANT_ESP32C61: "ESP32-C61",
VARIANT_ESP32H2: "ESP32-H2",
VARIANT_ESP32H4: "ESP32-H4",
VARIANT_ESP32H21: "ESP32-H21",
VARIANT_ESP32P4: "ESP32-P4",
VARIANT_ESP32S2: "ESP32-S2",
VARIANT_ESP32S3: "ESP32-S3",
VARIANT_ESP32S31: "ESP32-S31",
}
esp32_ns = cg.esphome_ns.namespace("esp32")
+18
View File
@@ -31,9 +31,12 @@ from .const import (
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32H4,
VARIANT_ESP32H21,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
esp32_ns,
)
from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports
@@ -43,9 +46,12 @@ from .gpio_esp32_c5 import esp32_c5_validate_gpio_pin, esp32_c5_validate_support
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
from .gpio_esp32_c61 import esp32_c61_validate_gpio_pin, esp32_c61_validate_supports
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
from .gpio_esp32_h4 import esp32_h4_validate_gpio_pin, esp32_h4_validate_supports
from .gpio_esp32_h21 import esp32_h21_validate_gpio_pin, esp32_h21_validate_supports
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports
from .gpio_esp32_s31 import esp32_s31_validate_gpio_pin, esp32_s31_validate_supports
ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin)
@@ -120,6 +126,14 @@ _esp32_validations = {
pin_validation=esp32_h2_validate_gpio_pin,
usage_validation=esp32_h2_validate_supports,
),
VARIANT_ESP32H4: ESP32ValidationFunctions(
pin_validation=esp32_h4_validate_gpio_pin,
usage_validation=esp32_h4_validate_supports,
),
VARIANT_ESP32H21: ESP32ValidationFunctions(
pin_validation=esp32_h21_validate_gpio_pin,
usage_validation=esp32_h21_validate_supports,
),
VARIANT_ESP32P4: ESP32ValidationFunctions(
pin_validation=esp32_p4_validate_gpio_pin,
usage_validation=esp32_p4_validate_supports,
@@ -132,6 +146,10 @@ _esp32_validations = {
pin_validation=esp32_s3_validate_gpio_pin,
usage_validation=esp32_s3_validate_supports,
),
VARIANT_ESP32S31: ESP32ValidationFunctions(
pin_validation=esp32_s31_validate_gpio_pin,
usage_validation=esp32_s31_validate_supports,
),
}
@@ -0,0 +1,34 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
# Partial set from the ESP-IDF / esptool boot-mode docs:
# https://docs.espressif.com/projects/esptool/en/latest/esp32h21/advanced-topics/boot-mode-selection.html
# The full list awaits the ESP32-H21 datasheet's "Strapping Pins" section.
_ESP32H21_STRAPPING_PINS: set[int] = {13, 14}
_LOGGER = logging.getLogger(__name__)
def esp32_h21_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 25:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-25)")
return value
def esp32_h21_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 25:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-25)")
if is_input:
# All ESP32 pins support input mode
pass
check_strapping_pin(value, _ESP32H21_STRAPPING_PINS, _LOGGER)
return value
+34
View File
@@ -0,0 +1,34 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
# Partial set from the ESP-IDF / esptool boot-mode docs:
# https://docs.espressif.com/projects/esptool/en/latest/esp32h4/advanced-topics/boot-mode-selection.html
# The full list awaits the ESP32-H4 datasheet's "Strapping Pins" section.
_ESP32H4_STRAPPING_PINS: set[int] = {13, 14}
_LOGGER = logging.getLogger(__name__)
def esp32_h4_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 39:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)")
return value
def esp32_h4_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 39:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-39)")
if is_input:
# All ESP32 pins support input mode
pass
check_strapping_pin(value, _ESP32H4_STRAPPING_PINS, _LOGGER)
return value
@@ -0,0 +1,38 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
# Per the ESP32-S31 datasheet (page 96):
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
_ESP32S31_SPI_FLASH_PINS: set[int] = {27, 28, 29, 31, 32, 33}
_ESP32S31_STRAPPING_PINS: set[int] = {60, 61}
_LOGGER = logging.getLogger(__name__)
def esp32_s31_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 61:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)")
if value in _ESP32S31_SPI_FLASH_PINS:
raise cv.Invalid(
f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
)
return value
def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 61:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-61)")
if is_input:
# All ESP32 pins support input mode
pass
check_strapping_pin(value, _ESP32S31_STRAPPING_PINS, _LOGGER)
return value
@@ -72,6 +72,7 @@ void BLEClientBase::loop() {
// never delivered CLOSE_EVT/DISCONNECT_EVT, services would leak without this call.
this->release_services();
this->set_idle_();
this->on_disconnect_complete(ESP_GATT_CONN_TIMEOUT);
}
}
@@ -418,6 +419,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->log_gattc_lifecycle_event_("CLOSE");
this->release_services();
this->set_idle_();
this->on_disconnect_complete(param->close.reason);
break;
}
case ESP_GATTC_SEARCH_RES_EVT: {
@@ -140,6 +140,12 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void log_gattc_warning_(const char *operation, esp_err_t err);
void log_connection_params_(const char *param_type);
void handle_connection_result_(esp_err_t ret);
/// Hook called once a connection has been fully torn down (after release_services() and
/// set_idle_()), from both the CLOSE_EVT handler and the DISCONNECTING safety timeout.
/// Subclasses with extra per-connection accounting (e.g. bluetooth_proxy slot state)
/// override this to release that state. `reason` is the controller reason code, or
/// ESP_GATT_CONN_TIMEOUT for the safety-timeout path.
virtual void on_disconnect_complete(esp_err_t reason) {}
/// Transition to IDLE and reset conn_id — call when the connection is fully dead.
void set_idle_() {
this->set_state(espbt::ClientState::IDLE);
@@ -149,6 +155,10 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void set_disconnecting_() {
this->disconnecting_started_ = millis();
this->set_state(espbt::ClientState::DISCONNECTING);
// BluetoothConnection::loop() disables the component loop after service discovery
// completes, so the DISCONNECTING timeout check in loop() would never run if CLOSE_EVT
// gets lost. Re-enable the loop so the 10s safety timeout can force IDLE.
this->enable_loop();
}
// Compact error logging helpers to reduce flash usage
void log_error_(const char *message);
@@ -62,6 +62,26 @@ MANUFACTURER_NAME_CHARACTERISTIC_UUID = 0x2A29
MODEL_CHARACTERISTIC_UUID = 0x2A24
FIRMWARE_VERSION_CHARACTERISTIC_UUID = 0x2A26
# Suffix of the Bluetooth Base UUID used to expand 16/32 bit UUIDs to 128 bit.
_BASE_UUID_SUFFIX = "-0000-1000-8000-00805F9B34FB"
def uuid_is(uuid: int | str, uuid16: int) -> bool:
"""Return True if a validated UUID refers to the given 16-bit short UUID.
A service/characteristic UUID may be an ``int`` (from ``cv.hex_uint32_t``) or an
uppercase string in 16, 32 or 128 bit form (from ``bt_uuid``), so every
representation of the same UUID must be considered equivalent.
"""
if isinstance(uuid, int):
return uuid == uuid16
return uuid.upper() in (
f"{uuid16:04X}",
f"{uuid16:08X}",
f"{uuid16:08X}{_BASE_UUID_SUFFIX}",
)
# Core key to store the global configuration
KEY_NOTIFY_REQUIRED = "notify_required"
KEY_SET_VALUE = "set_value"
@@ -195,7 +215,7 @@ def create_description_cud(char_config):
return char_config
# If the config displays a description, there cannot be a descriptor with the CUD UUID
for desc in char_config[CONF_DESCRIPTORS]:
if desc[CONF_UUID] == CUD_DESCRIPTOR_UUID:
if uuid_is(desc[CONF_UUID], CUD_DESCRIPTOR_UUID):
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has a description, but a CUD descriptor is already present"
)
@@ -218,7 +238,7 @@ def create_notify_cccd(char_config):
return char_config
# If the CCCD descriptor is already present, return the config
for desc in char_config[CONF_DESCRIPTORS]:
if desc[CONF_UUID] == CCCD_DESCRIPTOR_UUID:
if uuid_is(desc[CONF_UUID], CCCD_DESCRIPTOR_UUID):
# Check if the WRITE property is set
if not desc[CONF_WRITE]:
raise cv.Invalid(
@@ -244,7 +264,7 @@ def create_device_information_service(config):
# If there is already a device information service,
# there cannot be CONF_MODEL, CONF_MANUFACTURER or CONF_FIRMWARE_VERSION properties
for service in config[CONF_SERVICES]:
if service[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID:
if uuid_is(service[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID):
if (
CONF_MODEL in config
or CONF_MANUFACTURER in config
@@ -592,7 +612,7 @@ async def to_code(config):
)
for char_conf in service_config[CONF_CHARACTERISTICS]:
await to_code_characteristic(service_var, char_conf)
if service_config[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID:
if uuid_is(service_config[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID):
cg.add(var.set_device_information_service(service_var))
else:
cg.add(var.enqueue_start_service(service_var))
@@ -196,42 +196,35 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
(*this->on_read_callback_)(param->read.conn_id);
}
uint16_t max_offset = 22;
// Use the client-supplied offset for long reads; short reads always start at 0.
// The Bluedroid stack truncates ATT_READ_RSP / ATT_READ_BLOB_RSP to MTU-1, so we
// just provide as much data as we have from the requested offset and let the stack
// handle framing. The client issues subsequent blob reads with increasing offsets
// until it has received the whole value.
const uint16_t offset = param->read.is_long ? param->read.offset : 0;
esp_gatt_status_t status = ESP_GATT_OK;
esp_gatt_rsp_t response;
if (param->read.is_long) {
if (this->value_read_offset_ >= this->value_.size()) {
response.attr_value.len = 0;
response.attr_value.offset = this->value_read_offset_;
this->value_read_offset_ = 0;
} else if (this->value_.size() - this->value_read_offset_ < max_offset) {
// Last message in the chain
response.attr_value.len = this->value_.size() - this->value_read_offset_;
response.attr_value.offset = this->value_read_offset_;
memcpy(response.attr_value.value, this->value_.data() + response.attr_value.offset, response.attr_value.len);
this->value_read_offset_ = 0;
} else {
response.attr_value.len = max_offset;
response.attr_value.offset = this->value_read_offset_;
memcpy(response.attr_value.value, this->value_.data() + response.attr_value.offset, response.attr_value.len);
this->value_read_offset_ += max_offset;
}
response.attr_value.offset = offset;
if (offset > this->value_.size()) {
status = ESP_GATT_INVALID_OFFSET;
response.attr_value.len = 0;
} else {
response.attr_value.offset = 0;
if (this->value_.size() + 1 > max_offset) {
response.attr_value.len = max_offset;
this->value_read_offset_ = max_offset;
} else {
response.attr_value.len = this->value_.size();
size_t remaining = this->value_.size() - offset;
if (remaining > ESP_GATT_MAX_ATTR_LEN) {
ESP_LOGW(TAG, "Characteristic length %u exceeds buffer size of %u, truncating",
static_cast<unsigned>(remaining), ESP_GATT_MAX_ATTR_LEN);
remaining = ESP_GATT_MAX_ATTR_LEN;
}
memcpy(response.attr_value.value, this->value_.data(), response.attr_value.len);
response.attr_value.len = remaining;
memcpy(response.attr_value.value, this->value_.data() + offset, remaining);
}
response.attr_value.handle = this->handle_;
response.attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE;
esp_err_t err =
esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &response);
esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, status, &response);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err);
}
@@ -79,7 +79,6 @@ class BLECharacteristic {
esp_gatt_char_prop_t properties_;
uint16_t handle_{0xFFFF};
uint16_t value_read_offset_{0};
std::vector<uint8_t> value_;
std::vector<BLEDescriptor *> descriptors_;
+7 -1
View File
@@ -3,7 +3,11 @@ import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
from esphome.components.esp32 import (
add_idf_component,
add_idf_sdkconfig_option,
require_libc_picolibc_newlib_compat,
)
from esphome.components.psram import DOMAIN as psram_domain
import esphome.config_validation as cv
from esphome.const import (
@@ -402,6 +406,8 @@ async def to_code(config):
add_idf_component(name="espressif/esp32-camera", ref="2.1.5")
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True)
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False)
# esp32-camera 2.1.5 needs the Newlib shim on IDF 6.0+; remove when fixed upstream
require_libc_picolibc_newlib_compat()
for conf in config.get(CONF_ON_STREAM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+9 -1
View File
@@ -3,6 +3,7 @@ from pathlib import Path
from esphome import pins
from esphome.components import esp32
from esphome.components.const import CONF_USE_PSRAM
import esphome.config_validation as cv
from esphome.const import (
CONF_CLK_PIN,
@@ -39,6 +40,7 @@ BASE_SCHEMA = cv.Schema(
cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True),
cv.Required(CONF_ACTIVE_HIGH): cv.boolean,
cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_USE_PSRAM, default=False): cv.boolean,
}
)
@@ -242,6 +244,12 @@ async def to_code(config):
else:
_configure_spi(config)
# Place the transport mempool in PSRAM. Required on memory-tight host
# configurations (e.g. P4 with a large LVGL UI) where the internal-RAM
# mempool allocation fails at boot with `sdio_mempool_create` assert.
if config[CONF_USE_PSRAM]:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_MEMPOOL_PREFER_SPIRAM", True)
# Library versions
idf_ver = esp32.idf_version()
os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}"
@@ -249,7 +257,7 @@ async def to_code(config):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.5.1")
esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.2")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.7")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.8")
else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
@@ -3,6 +3,7 @@ from typing import Any
import esphome.codegen as cg
from esphome.components import esp32, update
from esphome.components.const import CONF_SHA256
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PATH, CONF_SOURCE, CONF_TYPE
from esphome.core import CORE, ID, HexInt
@@ -11,7 +12,6 @@ CODEOWNERS = ["@swoboda1337"]
AUTO_LOAD = ["sha256", "watchdog", "json"]
DEPENDENCIES = ["esp32_hosted"]
CONF_SHA256 = "sha256"
CONF_HTTP_REQUEST_ID = "http_request_id"
TYPE_EMBEDDED = "embedded"
@@ -75,7 +75,7 @@ def _validate_firmware(config: dict[str, Any]) -> None:
return
path = CORE.relative_config_path(config[CONF_PATH])
with open(path, "rb") as f:
with path.open("rb") as f:
firmware_data = f.read()
calculated = hashlib.sha256(firmware_data).hexdigest()
expected = config[CONF_SHA256].lower()
@@ -93,7 +93,7 @@ async def to_code(config: dict[str, Any]) -> None:
if config[CONF_TYPE] == TYPE_EMBEDDED:
path = config[CONF_PATH]
with open(CORE.relative_config_path(path), "rb") as f:
with CORE.relative_config_path(path).open("rb") as f:
firmware_data = f.read()
rhs = [HexInt(x) for x in firmware_data]
arr_id = ID(f"{config[CONF_ID]}_data", is_declaration=True, type=cg.uint8)
@@ -121,7 +121,7 @@ void Esp32HostedUpdate::setup() {
}
} else {
ESP_LOGW(TAG, "Invalid app description magic word: 0x%08" PRIx32 " (expected 0x%08" PRIx32 ")",
app_desc->magic_word, ESP_APP_DESC_MAGIC_WORD);
app_desc->magic_word, static_cast<uint32_t>(ESP_APP_DESC_MAGIC_WORD));
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
} else {
+1 -1
View File
@@ -472,7 +472,7 @@ def _decode_pc(config, addr):
command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr]
try:
translation = subprocess.check_output(command, close_fds=False).decode().strip()
except Exception: # pylint: disable=broad-except
except Exception: # noqa: BLE001 # pylint: disable=broad-except
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
return
+12 -12
View File
@@ -5,6 +5,7 @@
#include <Arduino.h>
#include <core_esp8266_features.h>
#include <coredecls.h>
extern "C" {
#include <user_interface.h>
@@ -71,23 +72,22 @@ uint32_t IRAM_ATTR HOT millis() {
return result;
}
// Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object
// call to the original millis() that --wrap can't intercept, so calling ::delay()
// would keep the slow Arduino millis body alive in IRAM. optimistic_yield still
// enters esp_schedule()/esp_suspend_within_cont() via yield(), so SDK tasks and
// WiFi run correctly. Theoretically less power-efficient than Arduino's
// os_timer-based delay() for long waits, but nearly all ESPHome delays are short
// (sensor/I²C/SPI settling in the 1100 ms range) where the difference is
// negligible.
// Delegate to Arduino's 1-arg esp_delay(), which uses os_timer + esp_suspend to
// suspend the cont task for `ms` milliseconds without polling millis(). This
// matches pre-2026.5.0 behavior (when esphome::delay() forwarded to ::delay())
// and lets the SDK run freely while we wait, which timing-sensitive
// interrupt-driven code (e.g. ESP8266 software-serial RX in components like
// fingerprint_grow) depends on. The poll-based busy-wait that this replaced
// rarely yielded inside short waits like delay(1), starving WiFi/SDK tasks and
// extending interrupt latency. Unlike ::delay(), esp_delay()'s 1-arg form does
// not call millis(), so the slow Arduino millis() body is not pulled into IRAM
// by this path (the --wrap=millis goal of #15662 is preserved).
void HOT delay(uint32_t ms) {
if (ms == 0) {
optimistic_yield(1000);
return;
}
uint32_t start = millis();
while (millis() - start < ms) {
optimistic_yield(1000);
}
esp_delay(ms);
}
void arch_restart() {
+6
View File
@@ -58,6 +58,12 @@ __attribute__((always_inline)) inline const char *progmem_read_ptr(const char *c
__attribute__((always_inline)) inline uint16_t progmem_read_uint16(const uint16_t *addr) {
return pgm_read_word(addr); // NOLINT
}
// Bulk PROGMEM copy: routes to the SDK's aligned-flash `memcpy_P` so callers
// don't have to drop to a byte-by-byte `progmem_read_byte` loop, which on
// ESP8266 is ~4x as many flash accesses as the bulk path.
__attribute__((always_inline)) inline void progmem_memcpy(void *dst, const void *src, size_t len) {
memcpy_P(dst, src, len); // NOLINT
}
// NOLINTNEXTLINE(readability-identifier-naming)
__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
+1 -1
View File
@@ -133,7 +133,7 @@ CONFIG_SCHEMA = cv.All(
host=8082,
): cv.port,
cv.Optional(CONF_ALLOW_PARTITION_ACCESS, default=False): cv.boolean,
cv.Optional(CONF_PASSWORD): cv.string,
cv.Optional(CONF_PASSWORD): cv.sensitive(),
cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid(
f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode"
),
+1 -1
View File
@@ -17,7 +17,7 @@ from esphome.core import HexInt
from esphome.types import ConfigType
CODEOWNERS = ["@jesserockz"]
AUTO_LOAD = ["network"]
byte_vector = cg.std_vector.template(cg.uint8)
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
@@ -149,12 +149,6 @@ bool ESPNowComponent::is_wifi_enabled() {
}
void ESPNowComponent::setup() {
#ifndef USE_WIFI
// Initialize LwIP stack for wake_loop_threadsafe() socket support
// When WiFi component is present, it handles esp_netif_init()
ESP_ERROR_CHECK(esp_netif_init());
#endif
if (this->enable_on_boot_) {
this->enable_();
} else {
@@ -174,8 +168,6 @@ void ESPNowComponent::enable() {
void ESPNowComponent::enable_() {
if (!this->is_wifi_enabled()) {
esp_event_loop_create_default();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
+29 -1
View File
@@ -2,6 +2,7 @@ from dataclasses import dataclass
import logging
from esphome import automation, pins
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.network import ip_address_literal
from esphome.config_helpers import filter_source_files_from_platform
@@ -13,6 +14,7 @@ from esphome.const import (
CONF_DNS1,
CONF_DNS2,
CONF_DOMAIN,
CONF_ENABLE_ON_BOOT,
CONF_GATEWAY,
CONF_ID,
CONF_INTERRUPT_PIN,
@@ -163,7 +165,7 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = {
"KSZ8081": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"),
"KSZ8081RNA": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"),
"W5500": IDFRegistryComponent("espressif/w5500", "1.0.1"),
"DM9051": IDFRegistryComponent("espressif/dm9051", "1.0.0"),
"DM9051": IDFRegistryComponent("espressif/dm9051", "1.1.0"),
"ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"),
"LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"),
}
@@ -217,6 +219,10 @@ MANUAL_IP_SCHEMA = cv.Schema(
EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component)
ManualIP = ethernet_ns.struct("ManualIP")
EthernetConnectedCondition = ethernet_ns.class_("EthernetConnectedCondition", Condition)
EthernetEnabledCondition = ethernet_ns.class_("EthernetEnabledCondition", Condition)
EthernetEnableAction = ethernet_ns.class_("EthernetEnableAction", automation.Action)
EthernetDisableAction = ethernet_ns.class_("EthernetDisableAction", automation.Action)
def _is_framework_spi_polling_mode_supported() -> bool:
@@ -348,6 +354,7 @@ BASE_SCHEMA = cv.Schema(
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
}
@@ -494,6 +501,9 @@ async def to_code(config):
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
# enable_on_boot defaults to true in C++ - only set if false
if not config[CONF_ENABLE_ON_BOOT]:
cg.add(var.set_enable_on_boot(False))
CORE.data.setdefault(KEY_ETHERNET, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE]
if CONF_MANUAL_IP in config:
@@ -715,3 +725,21 @@ def _filter_source_files() -> list[str]:
FILTER_SOURCE_FILES = _filter_source_files
async def _new_pvariable_to_code(config, id_, template_arg, args):
return cg.new_Pvariable(id_, template_arg)
for _name, _cls in (
("ethernet.connected", EthernetConnectedCondition),
("ethernet.enabled", EthernetEnabledCondition),
):
automation.register_condition(_name, _cls, cv.Schema({}))(_new_pvariable_to_code)
for _name, _cls in (
("ethernet.enable", EthernetEnableAction),
("ethernet.disable", EthernetDisableAction),
):
automation.register_action(_name, _cls, cv.Schema({}), synchronous=True)(
_new_pvariable_to_code
)
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ETHERNET
#include "ethernet_component.h"
namespace esphome::ethernet {
template<typename... Ts> class EthernetConnectedCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return global_eth_component->is_connected(); }
};
template<typename... Ts> class EthernetEnabledCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return global_eth_component->is_enabled(); }
};
template<typename... Ts> class EthernetEnableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_eth_component->enable(); }
};
template<typename... Ts> class EthernetDisableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_eth_component->disable(); }
};
} // namespace esphome::ethernet
#endif
@@ -124,6 +124,17 @@ class EthernetComponent final : public Component {
void on_powerdown() override { powerdown(); }
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
// Per-interface lifecycle (parallels WiFiComponent::enable/disable/is_disabled).
// enable_on_boot defaults to true; when false, setup() runs all the driver/netif
// installation but skips esp_eth_start(), keeping the link cold until enable() is
// called. This is the primary lever for memory reclamation in multi-interface
// configurations where only one interface should carry traffic at a time.
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
void enable();
void disable();
bool is_disabled() { return this->disabled_; }
bool is_enabled() { return !this->disabled_; }
void set_type(EthernetType type);
#ifdef USE_ETHERNET_MANUAL_IP
void set_manual_ip(const ManualIP &manual_ip);
@@ -194,6 +205,16 @@ class EthernetComponent final : public Component {
void finish_connect_();
void dump_connect_params_();
#ifdef USE_ESP32
// ESP-IDF only: defers the SPI bus init, netif creation, MAC/PHY install, driver
// install, netif attach, and event handler registration (which together allocate
// ~3-8KB of DMA-capable internal SRAM via SPI driver state + eth driver RX queue)
// until ethernet actually needs to come up. Idempotent — guarded by the
// ethernet_initialized_ flag. Called from setup() when enable_on_boot_=true, or
// from enable() on first runtime enable. Mirrors wifi_lazy_init_() in WiFi.
void ethernet_lazy_init_();
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void notify_ip_state_listeners_();
#endif
@@ -287,6 +308,17 @@ class EthernetComponent final : public Component {
bool started_{false};
bool connected_{false};
bool got_ipv4_address_{false};
// Codegen-time YAML option. When false, setup() defers esp_eth_start().
bool enable_on_boot_{true};
// Mirror of "is the link intentionally stopped" — set when setup() honors
// enable_on_boot=false, cleared by enable(), set again by disable().
bool disabled_{false};
#ifdef USE_ESP32
// Tracks whether ethernet_lazy_init_() has completed successfully. Allows enable()
// to be called at runtime after enable_on_boot:false without re-allocating, and
// ensures setup() skips the heavy init when enable_on_boot_ is false.
bool ethernet_initialized_{false};
#endif
#if LWIP_IPV6
uint8_t ipv6_count_{0};
bool ipv6_setup_done_{false};
@@ -5,6 +5,7 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "w5500_custom_spi.h"
#include <lwip/dns.h>
#include <cinttypes>
@@ -137,6 +138,24 @@ void EthernetComponent::setup() {
delay(300); // NOLINT
}
if (this->enable_on_boot_) {
this->ethernet_lazy_init_();
if (!this->ethernet_initialized_) {
// lazy_init bailed early via ESPHL_ERROR_CHECK or mark_failed; nothing more to do.
return;
}
esp_err_t err = esp_eth_start(this->eth_handle_);
ESPHL_ERROR_CHECK(err, "ETH start error");
} else {
ESP_LOGCONFIG(TAG, "Skipping init (enable_on_boot: false)");
this->disabled_ = true;
}
}
void EthernetComponent::ethernet_lazy_init_() {
if (this->ethernet_initialized_)
return;
esp_err_t err;
#ifdef USE_ETHERNET_SPI
@@ -163,11 +182,7 @@ void EthernetComponent::setup() {
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
#endif
err = esp_netif_init();
ESPHL_ERROR_CHECK(err, "ETH netif init error");
err = esp_event_loop_create_default();
ESPHL_ERROR_CHECK(err, "ETH event loop error");
// Network interface setup handled by network component
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
this->eth_netif_ = esp_netif_new(&cfg);
@@ -207,6 +222,10 @@ void EthernetComponent::setup() {
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
w5500_config.poll_period_ms = this->polling_interval_;
#endif
// Install the custom SPI driver that offloads the bulk RX/TX frame transfers off the busy-wait
// path. w5500_config (and the devcfg it references) outlives esp_eth_mac_new_w5500() below, which
// runs the driver's init().
install_w5500_async_spi(w5500_config);
#elif defined(USE_ETHERNET_DM9051)
dm9051_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
@@ -370,9 +389,41 @@ void EthernetComponent::setup() {
ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error");
#endif /* USE_NETWORK_IPV6 */
/* start Ethernet driver state machine */
err = esp_eth_start(this->eth_handle_);
ESPHL_ERROR_CHECK(err, "ETH start error");
this->ethernet_initialized_ = true;
}
void EthernetComponent::enable() {
if (!this->disabled_)
return;
ESP_LOGD(TAG, "Enabling");
this->ethernet_lazy_init_();
if (!this->ethernet_initialized_) {
ESP_LOGE(TAG, "Cannot enable - init failed");
return;
}
esp_err_t err = esp_eth_start(this->eth_handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_eth_start failed: %s", esp_err_to_name(err));
return;
}
this->disabled_ = false;
// The ETH_EVENT_START handler will set started_=true; the loop state machine
// will then drive the STOPPED -> CONNECTING -> CONNECTED transitions.
this->enable_loop();
}
void EthernetComponent::disable() {
if (this->disabled_)
return;
ESP_LOGD(TAG, "Disabling");
esp_err_t err = esp_eth_stop(this->eth_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_eth_stop failed: %s — disabling anyway", esp_err_to_name(err));
}
this->disabled_ = true;
// ETH_EVENT_STOP will clear started_; loop() will transition to STOPPED.
}
void EthernetComponent::dump_config() {
@@ -486,6 +537,8 @@ void EthernetComponent::dump_config() {
network::IPAddresses EthernetComponent::get_ip_addresses() {
network::IPAddresses addresses;
if (!this->ethernet_initialized_)
return addresses; // all-zero IPs
esp_netif_ip_info_t ip;
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
if (err != ESP_OK) {
@@ -708,6 +761,10 @@ void EthernetComponent::start_connect_() {
}
void EthernetComponent::dump_connect_params_() {
if (!this->ethernet_initialized_) {
ESP_LOGCONFIG(TAG, " uninitialized/disabled");
return;
}
esp_netif_ip_info_t ip;
esp_netif_get_ip_info(this->eth_netif_, &ip);
const ip_addr_t *dns_ip1;
@@ -775,6 +832,16 @@ void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy
#endif
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
if (!this->ethernet_initialized_) {
// External callers (mdns, ethernet_info, etc.) may ask for the MAC before/regardless
// of whether ethernet is enabled. Use the configured MAC if set, else the system ETH MAC.
if (this->fixed_mac_.has_value()) {
memcpy(mac, this->fixed_mac_->data(), 6);
} else {
esp_read_mac(mac, ESP_MAC_ETH);
}
return;
}
esp_err_t err;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac);
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
@@ -794,6 +861,8 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
}
eth_duplex_t EthernetComponent::get_duplex_mode() {
if (!this->ethernet_initialized_)
return ETH_DUPLEX_HALF;
esp_err_t err;
eth_duplex_t duplex_mode;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode);
@@ -802,6 +871,8 @@ eth_duplex_t EthernetComponent::get_duplex_mode() {
}
eth_speed_t EthernetComponent::get_link_speed() {
if (!this->ethernet_initialized_)
return ETH_SPEED_10M;
esp_err_t err;
eth_speed_t speed;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed);
@@ -361,6 +361,23 @@ void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
void EthernetComponent::enable() {
// RP2040 uses arduino-pico's LwipIntfDev which manages link state internally;
// there is no clean enable/disable hook today. The YAML option is accepted on
// RP2040 for schema parity but has no effect.
if (!this->disabled_)
return;
ESP_LOGW(TAG, "enable_on_boot/disable not supported");
this->disabled_ = false;
}
void EthernetComponent::disable() {
if (this->disabled_)
return;
ESP_LOGW(TAG, "enable_on_boot/disable not supported");
this->disabled_ = true;
}
} // namespace esphome::ethernet
#endif // USE_ETHERNET && USE_RP2040
@@ -0,0 +1,118 @@
#include "w5500_custom_spi.h"
#if defined(USE_ESP32) && defined(USE_ETHERNET_W5500)
#include <driver/spi_master.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <cstring>
#include <new>
namespace esphome::ethernet {
namespace {
// Per-device context returned by init() and handed back to read/write/deinit.
struct W5500CustomSpiContext {
spi_device_handle_t handle;
SemaphoreHandle_t lock;
};
// Transfers up to the ESP32 SPI hardware FIFO size (64 bytes) stay on the polling path; larger
// transfers (the frame payloads) use the blocking, DMA-backed transmit.
constexpr uint32_t W5500_SPI_BULK_THRESHOLD = 64;
constexpr uint32_t W5500_SPI_LOCK_TIMEOUT_MS = 50;
void *w5500_custom_spi_init(const void *spi_config) {
const auto *config = static_cast<const eth_w5500_config_t *>(spi_config);
auto *ctx = new (std::nothrow) W5500CustomSpiContext{};
if (ctx == nullptr) {
return nullptr;
}
// The W5500 SPI frame carries the 16-bit address in the command phase and the 8-bit control
// byte in the address phase; mirror what the stock driver configures.
spi_device_interface_config_t devcfg = *config->spi_devcfg;
devcfg.command_bits = 16;
devcfg.address_bits = 8;
if (spi_bus_add_device(config->spi_host_id, &devcfg, &ctx->handle) != ESP_OK) {
delete ctx;
return nullptr;
}
ctx->lock = xSemaphoreCreateMutex();
if (ctx->lock == nullptr) {
spi_bus_remove_device(ctx->handle);
delete ctx;
return nullptr;
}
return ctx;
}
esp_err_t w5500_custom_spi_deinit(void *spi_ctx) {
auto *ctx = static_cast<W5500CustomSpiContext *>(spi_ctx);
spi_bus_remove_device(ctx->handle);
vSemaphoreDelete(ctx->lock);
delete ctx;
return ESP_OK;
}
// Runs one transaction under the device lock, choosing the polling vs blocking transmit by size.
// Bulk payloads (> FIFO size) block so the calling task sleeps while DMA runs; small register
// accesses stay on the cheaper polling path. Used by both read and write.
esp_err_t w5500_custom_spi_transfer(W5500CustomSpiContext *ctx, spi_transaction_t *trans, uint32_t len) {
if (xSemaphoreTake(ctx->lock, pdMS_TO_TICKS(W5500_SPI_LOCK_TIMEOUT_MS)) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
esp_err_t ret;
if (len > W5500_SPI_BULK_THRESHOLD) {
ret = spi_device_transmit(ctx->handle, trans);
} else {
ret = spi_device_polling_transmit(ctx->handle, trans);
}
xSemaphoreGive(ctx->lock);
return ret;
}
esp_err_t w5500_custom_spi_write(void *spi_ctx, uint32_t cmd, uint32_t addr, const void *data, uint32_t len) {
auto *ctx = static_cast<W5500CustomSpiContext *>(spi_ctx);
spi_transaction_t trans = {};
trans.cmd = static_cast<uint16_t>(cmd);
trans.addr = addr;
trans.length = 8 * len;
trans.tx_buffer = data;
return w5500_custom_spi_transfer(ctx, &trans, len);
}
esp_err_t w5500_custom_spi_read(void *spi_ctx, uint32_t cmd, uint32_t addr, void *data, uint32_t len) {
auto *ctx = static_cast<W5500CustomSpiContext *>(spi_ctx);
spi_transaction_t trans = {};
// Reads of <= 4 bytes use the transaction's inline RX buffer to avoid 4-byte boundary
// overwrites of adjacent registers (same guard the stock driver uses).
const bool use_rxdata = len <= 4;
trans.flags = use_rxdata ? SPI_TRANS_USE_RXDATA : 0;
trans.cmd = static_cast<uint16_t>(cmd);
trans.addr = addr;
trans.length = 8 * len;
trans.rx_buffer = data;
esp_err_t ret = w5500_custom_spi_transfer(ctx, &trans, len);
if (use_rxdata && (ret == ESP_OK)) {
memcpy(data, trans.rx_data, len);
}
return ret;
}
} // namespace
void install_w5500_async_spi(eth_w5500_config_t &config) {
// Point the custom driver's config at the W5500 config itself; init() reads spi_host_id and
// spi_devcfg back out of it. The self-reference is valid because both the config and the
// spi_devcfg it points at outlive the esp_eth_mac_new_w5500() call that runs init().
config.custom_spi_driver.config = &config;
config.custom_spi_driver.init = w5500_custom_spi_init;
config.custom_spi_driver.deinit = w5500_custom_spi_deinit;
config.custom_spi_driver.read = w5500_custom_spi_read;
config.custom_spi_driver.write = w5500_custom_spi_write;
}
} // namespace esphome::ethernet
#endif // USE_ESP32 && USE_ETHERNET_W5500
@@ -0,0 +1,35 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_ETHERNET_W5500)
#include <esp_idf_version.h>
// IDF 6.0 moved the per-chip SPI MAC drivers to the Espressif Component Registry; eth_w5500_config_t
// is no longer reachable through esp_eth.h and needs the explicit header.
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
#include <esp_eth_mac_w5500.h>
#else
#include <esp_eth.h>
#endif
namespace esphome::ethernet {
// Installs a custom W5500 SPI driver that offloads the bulk frame transfers off the busy-wait path.
//
// The stock W5500 driver runs every SPI transfer through spi_device_polling_transmit(), which
// busy-waits the CPU for the whole transfer. The frame payload (one large read per received frame,
// one large write per transmitted frame) is by far the biggest transfer, so the RX task and the TX
// caller each spin for hundreds of microseconds per frame. This driver sends payload transfers
// through the blocking, interrupt-driven spi_device_transmit() instead, so the calling task sleeps
// while DMA moves the bytes. Small register accesses stay on the polling path, where the busy-wait
// is cheaper than an interrupt round-trip.
//
// Must be called before esp_eth_mac_new_w5500(). The driver reads spi_host_id and spi_devcfg back
// out of `config` in its init() callback, so `config` (and the spi_devcfg it points at) must stay
// alive until esp_eth_mac_new_w5500() returns.
void install_w5500_async_spi(eth_w5500_config_t &config);
} // namespace esphome::ethernet
#endif // USE_ESP32 && USE_ETHERNET_W5500
@@ -81,7 +81,7 @@ def _process_single_config(config: dict[str, Any]) -> None:
elif conf[CONF_TYPE] == TYPE_LOCAL:
components_dir = Path(CORE.relative_config_path(conf[CONF_PATH]))
else:
raise NotImplementedError()
raise NotImplementedError
if config[CONF_COMPONENTS] == "all":
num_components = len(list(components_dir.glob("*/__init__.py")))
@@ -206,6 +206,7 @@ uint8_t FingerprintGrowComponent::save_fingerprint_() {
break;
case ENROLL_MISMATCH:
ESP_LOGE(TAG, "Scans do not match");
[[fallthrough]];
default:
return this->data_[0];
}

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