Compare commits

..
Author SHA1 Message Date
J. Nick KostonandGitHub 28384673f6 Merge branch 'dev' into espidf_parallel_tool_prefetch 2026-08-19 16:32:48 -05:00
J. Nick KostonandGitHub 0b1065feee [ci] Stop jobs hanging on apt by restoring the cached apt action and bounding raw apt calls (#18518) 2026-08-20 09:30:34 +12:00
Clyde StubbsandGitHub 47da743d11 [ai] Add instructions for concise comments (#18522) 2026-08-19 16:09:12 -05:00
J. Nick Koston b361342bde Do not end a progress bar that never drew a frame 2026-08-19 15:13:04 -05:00
J. Nick KostonandGitHub d34eed6d2d Merge branch 'dev' into espidf_parallel_tool_prefetch 2026-08-19 14:41:44 -05:00
J. Nick Koston 12238eec3b Log prefetch failures after the bar, assert on the shared progress instance 2026-08-19 13:57:15 -05:00
J. Nick Koston fe53ff6b6b Finish the batch bar, cancel queued archives on Ctrl-C, report known sizes 2026-08-19 13:28:42 -05:00
470226ca03 [image] Restore defaults:/files: support for platform entries (#18032)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-08-19 12:30:12 -05:00
esphome[bot]andGitHub 7b7107556f Bump bundled esphome-device-builder to 1.12.0 (#18514) 2026-08-19 12:21:58 -05:00
J. Nick Koston d0f38d7644 [espidf] Download tool archives in parallel during the install prefetch 2026-08-19 12:03:39 -05:00
J. Nick KostonandGitHub f90b776071 [ci] Key PlatformIO cache on the Python version so a runner image bump does not serve a broken LibreTiny venv (#18512) 2026-08-19 16:39:01 +00:00
esphome[bot]andGitHub e26237e57d Bump bundled esphome-device-builder to 1.11.5 (#18507) 2026-08-19 00:42:05 -05:00
esphome[bot]andGitHub d2c3f749ab Bump bundled esphome-device-builder to 1.11.4 (#18506) 2026-08-18 23:24:23 -05:00
esphome[bot]andGitHub b7cc271219 Bump bundled esphome-device-builder to 1.11.3 (#18505) 2026-08-18 21:57:51 -05:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
17eed7055b Bump resvg-py from 0.3.4 to 0.4.0 (#18460)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 22:22:51 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b7121940c8 Update wheel requirement from <0.48,>=0.43 to >=0.43,<0.49 (#18459)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 22:08:23 -04:00
Jonathan SwobodaandGitHub 07fa16e2e7 [ci] Stop persisting the integration test ccache (#18504) 2026-08-18 21:38:18 -04:00
Jonathan SwobodaandGitHub 8aa7db15e5 [esp32] Fix ESP32-P4 bootloop on rev3 (v3.x) chips when only variant is set (#18500) 2026-08-18 21:25:09 -04:00
J. Nick KostonandGitHub 8b637b339b [vscode] Report the origin of an unexpected exception during validation (#18494) 2026-08-18 19:37:10 -05:00
J. Nick KostonandGitHub 4f866c563b [platformio] Give the ccache wrapper a cmd.exe safe path (#18495) 2026-08-18 19:36:57 -05:00
J. Nick KostonandGitHub 7a999f9a48 [ci] Install requirements_dev.txt when the venv cache misses (#18502) 2026-08-18 19:06:55 -05:00
esphome[bot]andGitHub 0a88c81d95 Bump aioesphomeapi from 45.11.0 to 45.12.0 (#18501) 2026-08-18 18:38:23 -05:00
J. Nick KostonandGitHub 804e8fb856 [socket] Remove constant duplicated by the beta merge (#18496) 2026-08-18 15:44:38 -05:00
Jesse Hills 43939a9199 Merge branch 'beta' into dev 2026-08-19 08:01:33 +12:00
Jesse HillsandGitHub f414a07bcd Merge pull request #18481 from esphome/bump-2026.8.0b5
2026.8.0b5
2026-08-19 08:01:14 +12:00
esphome[bot]andGitHub 5c9d050ebe Bump aioesphomeapi from 45.10.3 to 45.11.0 (#18493) 2026-08-18 17:02:54 +00:00
285a508e09 [modbus] CRC scan all unknown function codes (#18483)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 11:09:12 -05:00
J. Nick KostonandGitHub 92f55f721f [api] Bump noise-c to 0.1.21 (#18484) 2026-08-18 14:32:37 +00:00
Jonathan SwobodaandGitHub 476d540065 [ci] Fall back to files API when PR diff exceeds GitHub line limit (#18486) 2026-08-18 10:22:28 -04:00
J. Nick KostonandGitHub ae730d6357 [ci] Fail the benchmark job when the C++ benchmark build fails (#18480) 2026-08-18 09:03:33 -05:00
J. Nick KostonandGitHub 9823205ef3 [api] Bump noise-c to 0.1.20 (#18482) 2026-08-18 09:03:14 -05:00
Jesse Hills d1391c2b10 Bump version to 2026.8.0b5 2026-08-18 16:04:49 +12:00
J. Nick KostonandJesse Hills 8b888f31e0 [gpio_expander][pcf8574][pca9554][tca9555][pca6416a][pi4ioe5v6408][mcp23016][mcp23xxx_base] Reject unsupported interrupt_pin options (inverted, allow_other_uses) (#18472) 2026-08-18 16:04:47 +12:00
Jesse Hills 6a247dfe91 [light] Replace rgb_order/is_rgbw/is_wrgb with channel_colors (#18474) 2026-08-18 16:01:54 +12:00
J. Nick KostonandJesse Hills 482869fbbe [socket] Fix multi-second TCP stalls on ESP8266 by yielding to the SYS context (#18455) 2026-08-18 14:27:54 +12:00
esphome[bot]andJesse Hills 4dea147386 Bump bundled esphome-device-builder to 1.11.2 (#18477) 2026-08-18 14:27:54 +12:00
J. Nick KostonandJesse Hills 1fd6337254 [api] Bump noise-c to 0.1.19 (#18473) 2026-08-18 14:27:54 +12:00
esphome[bot]andJesse Hills 4ce6d59484 Bump bundled esphome-device-builder to 1.11.1 (#18475) 2026-08-18 14:27:54 +12:00
J. Nick KostonandJesse Hills 014cc19902 [api] Bump noise-c to 0.1.18 (#18451) 2026-08-18 14:27:54 +12:00
esphome[bot]Jesse Hillsesphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
b9041566ea Bump aioesphomeapi from 45.10.2 to 45.10.3 (#18433)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-08-18 14:27:54 +12:00
J. Nick KostonandJesse Hills 3a403c40d5 [ld2420] Drop the setup priority override so setup runs after the UART bus (#18428) 2026-08-18 14:27:54 +12:00
esphome[bot]andJesse Hills 096e71bd67 Bump aioesphomeapi from 45.10.1 to 45.10.2 (#18357) 2026-08-18 14:27:54 +12:00
J. Nick KostonandGitHub fffa902a1a [gpio_expander][pcf8574][pca9554][tca9555][pca6416a][pi4ioe5v6408][mcp23016][mcp23xxx_base] Reject unsupported interrupt_pin options (inverted, allow_other_uses) (#18472) 2026-08-18 13:32:03 +12:00
Jesse HillsandGitHub 463e3833da [light] Replace rgb_order/is_rgbw/is_wrgb with channel_colors (#18474) 2026-08-18 13:19:48 +12:00
J. Nick KostonandGitHub 4416aacebb [socket] Fix multi-second TCP stalls on ESP8266 by yielding to the SYS context (#18455) 2026-08-18 13:09:29 +12:00
esphome[bot]andGitHub 15a626bcf3 Bump bundled esphome-device-builder to 1.11.2 (#18477) 2026-08-17 19:16:38 -05:00
J. Nick KostonandGitHub a347a2e879 [api] Bump noise-c to 0.1.19 (#18473) 2026-08-17 18:58:11 -05:00
dependabot[bot]andGitHub 7be4566b41 Bump platformdirs from 4.11.2 to 4.11.3 (#18468)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 18:33:13 -05:00
esphome[bot]andGitHub 96e26c6a5f Bump bundled esphome-device-builder to 1.11.1 (#18475) 2026-08-17 17:54:23 -05:00
J. Nick KostonandGitHub 7362c01c67 [core] Replace base64 lookup tables with arithmetic mapping (#18454) 2026-08-17 16:14:06 -05:00
Jesse HillsandGitHub e45b4e4938 [core] Make FINAL_VALIDATE_SCHEMA functions return None (#18457) 2026-08-18 08:26:24 +12:00
J. Nick KostonandGitHub e32329cc11 [core] Sync pre-commit ruff hook with requirements (0.16.3) (#18469) 2026-08-17 15:00:37 -05:00
dependabot[bot]andGitHub 199e368fe2 Bump astral-sh/setup-uv from 9.0.0 to 10.0.1 (#18463)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 14:57:50 -05:00
dependabot[bot]andGitHub 55726120db Bump esphome/workflows/.github/workflows/lock.yml from 2026.7.0 to 2026.8.1 (#18465)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 14:47:28 -05:00
dependabot[bot]andGitHub 346ba7e831 Bump github/codeql-action/analyze from 4.37.6 to 4.37.7 (#18467)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 14:47:18 -05:00
dependabot[bot]andGitHub 9518006724 Bump ruff from 0.16.2 to 0.16.3 (#18458)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 14:46:04 -05:00
dependabot[bot]andGitHub 3d5f6f692f Bump filelock from 3.32.2 to 3.32.3 (#18461)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 14:45:41 -05:00
dependabot[bot]andGitHub 4712c15c75 Bump github/codeql-action/init from 4.37.6 to 4.37.7 (#18466)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 14:42:24 -05:00
dependabot[bot]andGitHub 9bc72529a6 Bump astral-sh/setup-uv from 9.0.0 to 10.0.1 in /.github/actions/restore-python (#18462)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 14:42:08 -05:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6d20943a9c Bump esphome/workflows/.github/workflows/stale.yml from 61fd37a044cad4e9aa4303027b2a61b6a34da855 to a1c1485ab46ef41a84a6a9d8abd7fa4b7628fd70 (#18464)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 15:38:06 -04:00
J. Nick KostonandGitHub a3af82867b [api] Bump noise-c to 0.1.18 (#18451) 2026-08-17 14:00:39 -05:00
J. Nick KostonandGitHub 27483a4101 [core] Retry gh CLI calls on transient network errors in CI scripts (#18292) 2026-08-17 13:15:31 -05:00
J. Nick KostonandGitHub 6d20ebc66b [socket] Shorten lwip TAG string (#18450) 2026-08-17 17:18:43 +00:00
J. Nick KostonandGitHub 031a038b49 [deep_sleep] Shorten bk72xx TAG string (#18445) 2026-08-17 11:56:37 -05:00
J. Nick KostonandGitHub f6c7434b2a [i2c] Shorten platform backend TAG strings (#18440) 2026-08-17 11:54:55 -05:00
J. Nick KostonandGitHub 3d9fecb562 [remote_receiver] Shorten esp32 TAG string (#18447) 2026-08-17 11:54:52 -05:00
J. Nick KostonandGitHub ebe93e2c68 [bluetooth_connection] Shorten platform TAG strings (#18449) 2026-08-17 11:54:35 -05:00
J. Nick KostonandGitHub 1f4fcead38 [nextion] Shorten upload TAG strings (#18448) 2026-08-17 11:54:29 -05:00
J. Nick KostonandGitHub 1f000ba668 [mqtt] Shorten esp32 backend TAG string (#18446) 2026-08-17 11:54:17 -05:00
J. Nick KostonandGitHub e0d28d7f5c [http_request] Shorten platform backend TAG strings (#18444) 2026-08-17 11:54:03 -05:00
J. Nick KostonandGitHub 47a58dd799 [internal_temperature] Shorten platform TAG strings (#18443) 2026-08-17 11:53:56 -05:00
J. Nick KostonandGitHub 37bea1c153 [spi] Shorten platform backend TAG strings (#18442) 2026-08-17 11:53:48 -05:00
J. Nick KostonandGitHub d1a7b8df8b [adc] Shorten platform TAG strings (#18441) 2026-08-17 11:53:42 -05:00
J. Nick KostonandGitHub c01f24553c [uart] Shorten platform backend TAG strings (#18439) 2026-08-17 11:53:29 -05:00
J. Nick KostonandGitHub 07e8b303b9 [ota] Shorten platform backend TAG strings (#18438) 2026-08-17 11:52:56 -05:00
esphome[bot]GitHubesphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
ebb0923362 Bump aioesphomeapi from 45.10.2 to 45.10.3 (#18433)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-08-16 22:34:00 -05:00
J. Nick KostonandGitHub e1c279718f [ld2420] Drop the setup priority override so setup runs after the UART bus (#18428) 2026-08-16 21:57:12 -05:00
J. Nick KostonandGitHub cf764740cf [api] Create the camera image reader lazily (#18421) 2026-08-16 21:56:58 -05:00
J. Nick KostonandGitHub 58d549ed4c [api] Move NoiseProtocolId off the connection object (#18420) 2026-08-16 21:56:37 -05:00
Jesse Hills 6b22d8068d Merge branch 'beta' into dev 2026-08-17 11:36:10 +12:00
Jesse HillsandGitHub 443d8f1f28 Merge pull request #18424 from esphome/bump-2026.8.0b4
2026.8.0b4
2026-08-17 11:33:27 +12:00
Jesse Hills 1ec21a2245 Bump version to 2026.8.0b4 2026-08-17 10:22:41 +12:00
J. Nick KostonandJesse Hills bb7d4c3630 [esp32] Split crash handler addr2line hint per core (#18418) 2026-08-17 10:22:39 +12:00
J. Nick KostonandJesse Hills f42fe9af29 [core] Skip redundant ESP8266 main loop wake posts from ISR context (#18416) 2026-08-17 10:22:39 +12:00
J. Nick KostonandJesse Hills 0bc2d71370 [bk72xx_ble] Fail early with a clear error on non BLE 5.x SoCs (#18406) 2026-08-17 10:22:39 +12:00
luar123andJesse Hills 594c12b3d9 [zigbee] bump esp-zigbee-sdk to 2.0.4 (#18415) 2026-08-17 10:22:39 +12:00
Jonathan SwobodaandJesse Hills bca72e9b6d [sensor] Pass NaN through the delta filter again (#18400) 2026-08-17 10:22:39 +12:00
J. Nick KostonandJesse Hills ce09504c92 [platformio] Skip ccache when the binary on PATH fails to run (#18407) 2026-08-17 10:22:39 +12:00
esphome[bot]andJesse Hills dda4566b9e Bump bundled esphome-device-builder to 1.11.0 (#18403) 2026-08-17 10:22:39 +12:00
Samuel SiebandJesse Hills 46a5665a66 [rotary_encoder] account for min and max value when resetting (#18197) 2026-08-17 10:22:39 +12:00
esphome[bot]andJesse Hills 9161f74bb1 Bump bundled esphome-device-builder to 1.10.0 (#18389) 2026-08-17 10:22:39 +12:00
J. Nick KostonandGitHub 3f01f9895f [esp32_hosted] Require ESP-IDF 5.3 or newer (#18417) 2026-08-16 12:44:49 -07:00
J. Nick KostonandGitHub 801a1817b5 [esp32] Split crash handler addr2line hint per core (#18418) 2026-08-16 12:44:00 -07:00
J. Nick KostonandGitHub 32c76ae828 [core] Skip redundant ESP8266 main loop wake posts from ISR context (#18416) 2026-08-16 12:43:27 -07:00
J. Nick KostonandGitHub c664f5fc95 [bk72xx_ble] Fail early with a clear error on non BLE 5.x SoCs (#18406) 2026-08-16 11:06:22 -07:00
luar123andGitHub 2bc4681fd6 [zigbee] bump esp-zigbee-sdk to 2.0.4 (#18415) 2026-08-16 13:33:25 -04:00
Jonathan SwobodaandGitHub 646501b0ef [sensor] Pass NaN through the delta filter again (#18400) 2026-08-16 12:36:51 -04:00
J. Nick KostonandGitHub de3e657d8b [platformio] Skip ccache when the binary on PATH fails to run (#18407) 2026-08-16 08:42:47 -07:00
esphome[bot]andGitHub 1add726892 Bump bundled esphome-device-builder to 1.11.0 (#18403) 2026-08-15 13:36:52 -07:00
Samuel SiebandGitHub 5a000cf5e4 [rotary_encoder] account for min and max value when resetting (#18197) 2026-08-15 11:16:04 -07:00
Joppy FurrandGitHub 6ed676fe32 [lvgl] Restore long_press_repeat_time functionality (#18393) 2026-08-14 23:14:53 -07:00
esphome[bot]andGitHub 7cceddb8a3 Bump bundled esphome-device-builder to 1.10.0 (#18389) 2026-08-15 01:48:27 +00:00
J. Nick KostonandGitHub 039b897e7b [ethernet] Defer clk_mode removal to 2026.11.0 (#18380) 2026-08-14 18:45:36 -07:00
J. Nick KostonandGitHub b178f74e5d [core] Save the validated config cache on the first upload or logs run (#18367) 2026-08-14 18:45:23 -07:00
Clyde StubbsandGitHub e5224e22ae [ci] Compare merge-branch base ref against the default branch (#18385) 2026-08-15 06:18:22 +10:00
Clyde StubbsandGitHub be66e8b99c [ci] Disable CodSpeed benchmarks job outside esphome/esphome (#18372) 2026-08-15 04:33:47 +10:00
Jesse Hills 4b91c8aff4 Merge branch 'beta' into dev 2026-08-14 23:15:57 +12:00
Jesse HillsandGitHub 9d7997f55a Merge pull request #18368 from esphome/bump-2026.8.0b3
2026.8.0b3
2026-08-14 22:49:40 +12:00
Jesse Hills 4db47de556 Bump version to 2026.8.0b3 2026-08-14 17:36:56 +12:00
Karl BeeckenandJesse Hills c8de632764 [core] fix PYTHONPATH leak (#18360) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills b794b7b1d1 [core] Restore cv.parse_esphome_version as a deprecated helper (#18366) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills add18d4e35 [core] Partially revert "Hash entity keys from the raw name to fix collisions" (#18361) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills 02c1810c3a [core] Load component aliases from a generated registry (#18335) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills 4f3153375a [ota] Retry uploads that fail from network errors (#18332) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills 1c3a67b5e8 [wifi] Fix ESP8266 crash in cnx_node_search when lwIP transmits after disconnect (#18333) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills 236ff33a09 [esp32_ble] Silence spurious warnings for local key GAP events (#18359) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills 7c07fb48c5 [esp32_ble_tracker] Fix missed BLE advertisements with WiFi on ESP-IDF 5.5.5 (#18356) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills d72bab79d7 [web_server_base] Stop deleting the web server on captive portal teardown (#18324) 2026-08-14 17:36:54 +12:00
Karl BeeckenandGitHub 617e2ec1e0 [core] fix PYTHONPATH leak (#18360) 2026-08-14 17:23:34 +12:00
J. Nick KostonandGitHub 1afac0312d [core] Restore cv.parse_esphome_version as a deprecated helper (#18366) 2026-08-14 17:22:22 +12:00
J. Nick KostonandGitHub b05465145f [core] Add preference key stability integration test (#18364) 2026-08-13 21:14:30 -05:00
dependabot[bot]andGitHub 990fc402fd Bump bleak from 2.1.1 to 3.0.2 (#16246)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 20:24:02 -05:00
J. Nick KostonandGitHub dd51624fbb [core] Partially revert "Hash entity keys from the raw name to fix collisions" (#18361) 2026-08-13 20:06:48 -05:00
dependabot[bot]andGitHub 45a056e337 Bump platformdirs from 4.11.1 to 4.11.2 (#18363)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 18:54:51 -05:00
dependabot[bot]andGitHub 37782f7206 Bump prek from 0.4.12 to 0.4.13 (#18362)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 18:54:37 -05:00
J. Nick KostonandGitHub 945c2458b3 [core] Load component aliases from a generated registry (#18335) 2026-08-13 13:31:19 -05:00
J. Nick KostonandGitHub 7420d23867 [ota] Retry uploads that fail from network errors (#18332) 2026-08-13 13:31:06 -05:00
J. Nick KostonandGitHub 191686c5b3 [wifi] Fix ESP8266 crash in cnx_node_search when lwIP transmits after disconnect (#18333) 2026-08-13 13:30:53 -05:00
J. Nick KostonandGitHub 137351fa8d [esp32_ble] Silence spurious warnings for local key GAP events (#18359) 2026-08-13 13:27:01 -05:00
J. Nick KostonandGitHub db5173697a [esp32_ble_tracker] Fix missed BLE advertisements with WiFi on ESP-IDF 5.5.5 (#18356) 2026-08-13 13:22:48 -05:00
Oliver KleineckeGitHubOliver Kleineckepre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
c7940382a9 [const] move CONF_LABEL to components/const (#18354)
Co-authored-by: Oliver Kleinecke <kleinecke.oliver@googlemail.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-08-13 14:18:22 -04:00
esphome[bot]andGitHub 87045ab9c0 Bump aioesphomeapi from 45.10.1 to 45.10.2 (#18357) 2026-08-13 11:14:14 -05:00
f1c4086778 [const] Move CONF_SLOT to components/const (#18350)
Co-authored-by: Oliver Kleinecke <p1ngb4ck@users.noreply.github.com>
2026-08-13 23:36:11 +12:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Jonathan Swoboda
f337d0acff Bump pylint from 4.0.6 to 4.0.7 (#18320)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-08-13 02:13:29 +00:00
J. Nick KostonandGitHub 9cc05b30d4 [web_server_base] Stop deleting the web server on captive portal teardown (#18324) 2026-08-12 21:09:01 -05:00
Jesse Hills e0b68c4d6d Merge branch 'beta' into dev 2026-08-13 14:06:46 +12:00
J. Nick KostonandGitHub 8e624b4117 [core] Retry framework downloads on transient network errors (#18330) 2026-08-13 12:14:50 +12:00
J. Nick KostonandGitHub 787a909aa4 [core] Don't block logs startup on MQTT IP discovery when addresses are known (#18313) 2026-08-13 12:11:32 +12:00
J. Nick KostonandGitHub 905485b673 [ld2420] Fix out-of-bounds read when device reports unknown command error (#18322) 2026-08-13 11:49:34 +12:00
esphome[bot]andGitHub 99677390e0 Bump bundled esphome-device-builder to 1.9.6 (#18328) 2026-08-12 16:27:14 -05:00
Kevin AhrendtandGitHub f2121130f9 [sendspin] Bump sendspin-cpp to v0.7.2 (#18316) 2026-08-12 12:49:54 -04:00
esphome[bot]andGitHub 1a01c34ec4 Bump aioesphomeapi from 45.10.0 to 45.10.1 (#18318) 2026-08-12 16:33:45 +00:00
J. Nick KostonandGitHub 3c46cc9c35 [usb_uart] Fix uint32_t format specifier warning in pl2303 (#18310) 2026-08-12 10:59:24 -04:00
Jesse Hills e192ec8fee Merge branch 'beta' into dev 2026-08-13 00:29:08 +12:00
Jesse Hills 8a1aa5753d Bump version to 2026.9.0-dev 2026-08-12 22:53:42 +12:00
269 changed files with 5531 additions and 2079 deletions
+3 -3
View File
@@ -32,7 +32,7 @@ runs:
# 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@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -49,7 +49,7 @@ runs:
python -m venv venv
source venv/bin/activate
python --version
uv pip install -r requirements.txt -r requirements_test.txt
uv pip install -r requirements.txt -r requirements_dev.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'
@@ -58,5 +58,5 @@ runs:
python -m venv venv
source ./venv/Scripts/activate
python --version
uv pip install -r requirements.txt -r requirements_test.txt
uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt
uv pip install -e .
+2 -1
View File
@@ -70,6 +70,7 @@ async function isStackedPr(github, context) {
async function detectMergeBranch(github, context) {
const labels = new Set();
const baseRef = context.payload.pull_request.base.ref;
const defaultBranch = context.payload.repository.default_branch;
if (baseRef === 'release') {
labels.add('merging-to-release');
@@ -78,7 +79,7 @@ async function detectMergeBranch(github, context) {
} else if (await isStackedPr(github, context)) {
// GitHub manages the merge order for a stack, so these are not blocked.
labels.add('stacked-pr');
} else if (baseRef !== 'dev') {
} else if (baseRef !== defaultBranch) {
// A chain built by hand: it must not merge until its base branch does.
labels.add('chained-pr');
}
@@ -43,14 +43,14 @@ const WITHOUT_SCHEMA = 'CODEOWNERS = ["@esphome/core"]';
// Builds a fresh context for detectMergeBranch tests instead of mutating the
// shared CONTEXT fixture above (which other describe blocks rely on).
function makeMergeContext(baseRef, { stack } = {}) {
function makeMergeContext(baseRef, { stack, defaultBranch = 'dev' } = {}) {
const pull_request = { number: 1, base: { ref: baseRef } };
if (stack !== undefined) {
pull_request.stack = stack;
}
return {
repo: { owner: 'esphome', repo: 'esphome' },
payload: { pull_request }
payload: { pull_request, repository: { default_branch: defaultBranch } }
};
}
@@ -136,6 +136,21 @@ describe('detectMergeBranch', () => {
assert.deepEqual(Array.from(labels).sort(), ['chained-pr']);
assert.equal(state.calls, 1);
});
it('base ref matches default branch adds no labels', async () => {
const { github } = makeStackGithub({ stack: null });
const context = makeMergeContext('other', { defaultBranch: 'other' });
const labels = await detectMergeBranch(github, context);
assert.deepEqual(Array.from(labels).sort(), []);
});
it('base ref dev when the default branch is main adds chained-pr', async () => {
const { github } = makeStackGithub({ stack: null });
const context = makeMergeContext('dev', { defaultBranch: 'main' });
const labels = await detectMergeBranch(github, context);
assert.deepEqual(Array.from(labels).sort(), ['chained-pr']);
});
});
// ---------------------------------------------------------------------------
+26 -4
View File
@@ -29,7 +29,7 @@ jobs:
- 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@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
@@ -41,10 +41,32 @@ jobs:
version: "0.11.15"
- name: Install apt dependencies
# PR-only workflow, so nothing on dev could seed a shared apt cache
# entry; the cached apt action would save one copy per PR. Plain apt
# with every call bounded: the apt.conf.d timeouts make a dead
# mirror fail over in seconds, and timeout runs under sudo so it can
# kill apt-get itself. Install without update first: image lists are
# fresh, and the index refresh is what a congested mirror makes slow.
timeout-minutes: 15
run: |
sudo apt update
sudo apt-cache show protobuf-compiler
sudo apt install -y protobuf-compiler
sudo tee /etc/apt/apt.conf.d/99ci-acquire-timeouts >/dev/null <<'EOF'
Acquire::Retries "1";
Acquire::http::Timeout "15";
Acquire::https::Timeout "15";
EOF
# Common path: the image's package lists are fresh enough.
if sudo DEBIAN_FRONTEND=noninteractive timeout -k 15 90 \
apt-get install -y protobuf-compiler; then
protoc --version
exit 0
fi
# Rescue path: refresh the lists once with a generous bound; the
# apt config already fails a stalled mirror over quickly.
sudo DEBIAN_FRONTEND=noninteractive timeout -k 10 30 \
dpkg --configure -a || true
sudo timeout -k 15 300 apt-get update
sudo DEBIAN_FRONTEND=noninteractive timeout -k 15 300 \
apt-get install -y protobuf-compiler
protoc --version
- name: Install python dependencies
run: uv pip install --system aioesphomeapi -c requirements.txt -r requirements_dev.txt
+106 -42
View File
@@ -49,7 +49,7 @@ jobs:
# 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@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -68,6 +68,22 @@ jobs:
uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt
uv pip install -e .
seed-apt-cache:
name: Seed apt package cache
runs-on: ubuntu-24.04
# PR-branch cache saves are invisible to other PRs, so dev/beta/release
# pushes seed the one shared entry PR jobs restore. The key is derived
# only from the package list and version; keep both identical in every
# step that restores it. In ci-status needs so a broken seed fails dev.
if: github.event_name == 'push'
timeout-minutes: 10
steps:
- name: Install apt packages (cached)
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
determine-jobs:
name: Determine which jobs to run
runs-on: ubuntu-24.04
@@ -179,6 +195,7 @@ jobs:
. venv/bin/activate
script/ci-custom.py
script/build_codeowners.py --check
script/build_alias_registry.py --check
script/build_language_schema.py --check
script/generate-esp32-boards.py --check
script/generate-rp2-boards.py --check
@@ -322,7 +339,8 @@ jobs:
integration-tests:
name: Run integration tests (${{ matrix.bucket.name }})
runs-on: ubuntu-latest
# Must match seed-apt-cache's image: the apt cache key has no OS in it.
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
@@ -334,24 +352,16 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install ccache
# Speeds up the host compiles: tests in a bucket compile overlapping
# component sets, so later tests reuse earlier tests' objects.
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends ccache
- name: Restore ccache (restore-only)
# esphome stores the PlatformIO ccache under the machine-global cache
# dir (see _ccache_env() in esphome/platformio/toolchain.py). The
# bucket-name prefix prefers a same-bucket seed; the bare prefix falls
# back to any seed when the bucket layout differs from dev.
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- name: Install apt packages (cached)
# ccache speeds up the host compiles. A cache hit never touches apt
# (mirror outages cannot hang the job); the timeout bounds the cold
# path. Packages and version must match seed-apt-cache exactly;
# libsdl2-dev is unused here and carried only for cache-key parity.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
path: ~/.cache/esphome/platformio-ccache
key: integration-ccache-${{ matrix.bucket.name }}-${{ github.sha }}
restore-keys: |
integration-ccache-${{ matrix.bucket.name }}-
integration-ccache-
packages: libsdl2-dev ccache
version: 1.1
- name: Set up Python 3.13
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
@@ -366,7 +376,7 @@ jobs:
- 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@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -400,14 +410,6 @@ jobs:
# esphome stores the PlatformIO ccache under the machine-global cache
# dir (see _ccache_env() in esphome/platformio/toolchain.py).
run: CCACHE_DIR="$HOME/.cache/esphome/platformio-ccache" ccache -s
- name: Save ccache
# Pull request saves land in per-PR scopes nothing else can reuse;
# dev pushes seed the shared copy instead.
if: github.event_name != 'pull_request'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/esphome/platformio-ccache
key: integration-ccache-${{ matrix.bucket.name }}-${{ github.sha }}
import-time:
name: Check import esphome.__main__ time
@@ -440,12 +442,17 @@ jobs:
benchmarks:
name: Run CodSpeed benchmarks
runs-on: ubuntu-24.04
timeout-minutes: 30
needs:
- common
- determine-jobs
if: >-
(github.event_name == 'push' && github.ref_name == 'dev') ||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
github.repository == 'esphome/esphome' && (
(github.event_name == 'push' && github.ref_name == 'dev') ||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
)
# CodSpeed benchmarks require a CodSpeed account linked to the repository to run
# (https://codspeed.io) -- disabled on forks that aren't esphome/esphome itself.
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -459,12 +466,58 @@ jobs:
- name: Build benchmarks
id: build
run: |
# pipefail: without it a failed build is masked by the grep/cut
# pipeline below, leaving BINARY empty and silently dropping every
# C++ benchmark from the run while the job still reports success.
set -o pipefail
. venv/bin/activate
export BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py)
# --build-only prints BUILD_BINARY=<path> to stdout
BINARY=$(script/cpp_benchmark.py --all --build-only | grep '^BUILD_BINARY=' | tail -1 | cut -d= -f2-)
BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py)
export BENCHMARK_LIB_CONFIG
# --build-only prints BUILD_BINARY=<path> to stdout; the grep is
# non-fatal so a missing marker reaches the check below instead of
# tripping errexit at this assignment
BINARY=$(script/cpp_benchmark.py --all --build-only | { grep '^BUILD_BINARY=' || true; } | tail -1 | cut -d= -f2-)
if [ -z "$BINARY" ]; then
echo "::error::Benchmark build did not report a binary path"
exit 1
fi
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Bound apt fetches and pre-install libc6-dbg
# The CodSpeed runner installs valgrind + libc6-dbg via its own
# unbounded apt-get update; per-invocation apt options cannot reach
# it. The apt.conf.d timeouts below bound every later apt call in
# this job, the runner's included. Pre-installing libc6-dbg lets the
# runner skip apt once its valgrind cache is restored (it checks
# ``dpkg -s libc6-dbg``, so the cache action's unregistered restores
# would not count). Install without update first: image lists are
# fresh, and the index refresh is what a congested mirror makes
# slow. Best effort; the job timeout is the last backstop.
timeout-minutes: 15
continue-on-error: true
run: |
sudo tee /etc/apt/apt.conf.d/99ci-acquire-timeouts >/dev/null <<'EOF'
Acquire::Retries "1";
Acquire::http::Timeout "15";
Acquire::https::Timeout "15";
EOF
if dpkg -s libc6-dbg >/dev/null 2>&1; then
echo "libc6-dbg already installed"
exit 0
fi
# Common path: the image's package lists are fresh enough.
if sudo DEBIAN_FRONTEND=noninteractive timeout -k 15 90 \
apt-get install -y libc6-dbg; then
exit 0
fi
# Rescue path: refresh the lists once with a generous bound; the
# apt config already fails a stalled mirror over quickly.
sudo DEBIAN_FRONTEND=noninteractive timeout -k 10 30 \
dpkg --configure -a || true
sudo timeout -k 15 300 apt-get update
sudo DEBIAN_FRONTEND=noninteractive timeout -k 15 300 \
apt-get install -y libc6-dbg
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@4296e51e7041e24dadb86d1d6e8b9320d223dbe8 # v5.0.3
with:
@@ -549,24 +602,29 @@ jobs:
fetch-depth: 2
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
# Key on the exact Python version as well: LibreTiny creates a venv under
# ~/.platformio/penv whose interpreter is a symlink into the runner's
# hosted toolcache, so a cache saved on an older runner image breaks once
# a new image ships a newer patch release and drops the old interpreter.
- name: Cache platformio
if: github.ref == 'refs/heads/dev' && matrix.pio_cache_key
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
key: platformio-${{ matrix.pio_cache_key }}-${{ steps.restore-python.outputs.python-version }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev' && matrix.pio_cache_key
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
key: platformio-${{ matrix.pio_cache_key }}-${{ steps.restore-python.outputs.python-version }}-${{ hashFiles('platformio.ini') }}
- name: Cache ESP-IDF install
if: matrix.cache_idf
@@ -883,12 +941,17 @@ jobs:
- name: List components
run: echo ${{ matrix.batch.components }}
- name: Install apt packages
# Not cached: this job is pull-request-only, so a cache save could
# never be shared and would only consume quota.
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends libsdl2-dev ccache
- name: Install apt packages (cached)
# A cache hit (seeded on dev by seed-apt-cache) never touches apt,
# so mirror outages cannot hang this PR-only job; the timeout bounds
# the cold path. Packages and version must match seed-apt-cache
# exactly. The action has no --no-install-recommends; same package
# set this job used before #17463.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -1090,7 +1153,7 @@ jobs:
# install step (order-of-magnitude faster on cold boots,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -1423,6 +1486,7 @@ jobs:
# this check.
needs:
- common
- seed-apt-cache
- determine-jobs
- ci-custom
- pylint
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
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@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
permissions:
issues: write # issues.lock on closed issues
pull-requests: write # issues.lock on closed pull requests
uses: esphome/workflows/.github/workflows/lock.yml@9f6577fd37b5cf773ab1b9be929714a0dcd15661 # 2026.7.0
uses: esphome/workflows/.github/workflows/lock.yml@0fdd5e311b7e744069166696072a1a9cbc5fbeb6 # 2026.8.1
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
# GitHub App token so the labels, comments and closures come from
# esphome[bot] instead of github-actions[bot].
uses: esphome/workflows/.github/workflows/stale.yml@61fd37a044cad4e9aa4303027b2a61b6a34da855 # main
uses: esphome/workflows/.github/workflows/stale.yml@a1c1485ab46ef41a84a6a9d8abd7fa4b7628fd70 # main
secrets:
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
with:
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``prek`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
+1 -1
View File
@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.16.0
rev: v0.16.3
hooks:
# Run the linter.
- id: ruff
+10
View File
@@ -763,3 +763,13 @@ The project uses English for non-code content. When drafting documentation, code
PR descriptions, and similar text, avoid technical jargon. Instead, express concepts in plain English,
using standard technical terms only when required. Ensure the text is readily comprehensible to a wide
audience, including non-native English speakers.
## 10. Code Comments
Code comments on individual lines should be used only where necessary to flag issues that may not be obvious
on a simple reading of the code. Keep them short (e.g. 1 or 2 lines).
Function and method comment blocks may include more detail as required to make
calling contracts clear and document parameter usage, but should still be kept concise.
Avoid redundancy and repetition; comments should never simply restate what the code already says.
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.8.0b2
PROJECT_NUMBER = 2026.9.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.9.6
RUN uv pip install --no-cache-dir esphome-device-builder==1.12.0
RUN \
platformio settings set enable_telemetry No \
+14 -14
View File
@@ -2732,7 +2732,8 @@ def run_esphome(argv):
conf_path.name,
)
if config is None:
cache_missed = config is None
if cache_missed:
from esphome.config import read_config
config = read_config(
@@ -2741,26 +2742,25 @@ def run_esphome(argv):
# Snapshot only needed by `esphome config --no-defaults`.
snapshot_user_config=getattr(args, "no_defaults", False),
)
# 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
if config is None:
return 2
CORE.config = config
# Fallback for platforms whose validators didn't set the toolchain
# (only the esp32 component reads esp32.framework.toolchain). All
# other platforms only support PlatformIO today.
# other platforms only support PlatformIO today. Must run before the
# cache refresh below so its sidecar records the same toolchain a
# compile would.
if CORE.toolchain is None:
CORE.toolchain = Toolchain.PLATFORMIO
# Refresh the cache so the next upload/logs hits the fast path
# instead of re-running read_config.
if cache_eligible and cache_missed:
from esphome.compiled_config import save_compiled_config_and_sidecar
save_compiled_config_and_sidecar(config)
if args.command not in POST_CONFIG_ACTIONS:
safe_print(f"Unknown command {args.command}")
return 1
+69 -8
View File
@@ -18,9 +18,9 @@ from pathlib import Path
from typing import Any
from esphome.const import __version__ as ESPHOME_VERSION
from esphome.core import CORE, Lambda
from esphome.core import CORE, EsphomeError, Lambda
from esphome.helpers import write_file
from esphome.storage_json import StorageJSON, ext_storage_path
from esphome.storage_json import StorageJSON, ext_storage_path, storage_path
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -65,7 +65,71 @@ def save_compiled_config(config: ConfigType) -> None:
# non-basic dict key), so every upload/logs pays the slow path.
_LOGGER.warning("Cannot cache the validated config: %s", err)
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
_LOGGER.debug("Skipping compiled config cache write: %s", err)
# Likely persistent (permissions, full disk): every upload/logs
# pays the slow path until it clears, so surface it.
_LOGGER.warning("Skipping compiled config cache write: %s", err)
def save_compiled_config_and_sidecar(config: ConfigType) -> None:
"""Refresh the cache from the upload/logs fallback (CORE.config must be set).
The cache is only written when a complete sidecar is on disk:
load_compiled_config can't use it otherwise, and it holds resolved
secrets.
"""
if _refresh_sidecar():
save_compiled_config(config)
def _refresh_sidecar() -> bool:
"""Ensure a complete sidecar is on disk; True when one is.
Writes one (without claiming a build) when missing or wizard-only.
Failures are non-fatal; the next upload/logs pays the slow path again.
"""
try:
path = storage_path()
try:
old = StorageJSON.load_strict(path)
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
# Present but unreadable: it may hold a real build's metadata,
# and a fresh rewrite would also stop the next compile from
# cleaning a possibly incoherent build tree.
_LOGGER.warning(
"Not caching: storage sidecar %s is unreadable (%s)", path, err
)
return False
if old is not None and old.can_apply_to_core():
# Compile-written; nothing to refresh.
return True
if CORE.build_path is not None and CORE.build_path.exists():
# An unvalidated build tree: its absent or mismatched sidecar
# is what makes the next compile wipe it, so don't vouch for
# a build this run never saw.
_LOGGER.warning(
"Not caching: build tree %s has no matching sidecar; "
"'esphome compile' will settle it",
CORE.build_path,
)
return False
new = StorageJSON.from_esphome_core(CORE, old, claim_build=False)
if not new.can_apply_to_core():
_LOGGER.warning("Not caching: rebuilt storage sidecar is still incomplete")
return False
new.save(path)
return True
except (OSError, EsphomeError) as err:
# write_file wraps OSError into EsphomeError. Persistent
# (unwritable storage dir), so surface that every upload/logs
# pays the slow path.
_LOGGER.warning("Could not refresh the storage sidecar: %s", err)
except Exception: # noqa: BLE001 # pylint: disable=broad-except
# A structural bug; keep the traceback so it isn't mistaken
# for the I/O failure above.
_LOGGER.warning(
"Unexpected error refreshing the storage sidecar", exc_info=True
)
return False
def load_compiled_config(conf_path: Path) -> ConfigType | None:
@@ -98,11 +162,8 @@ def load_compiled_config(conf_path: Path) -> ConfigType | None:
return None
storage = StorageJSON.load(ext_storage_path(conf_path.name))
if storage is None:
return None
# apply_to_core assumes a real compile wrote the sidecar; wizard-only
# sidecars leave both of these unset and can't drive upload/logs.
if not storage.core_platform and not storage.target_platform:
if storage is None or not storage.can_apply_to_core():
_LOGGER.debug("Ignoring compiled config cache: sidecar missing or incomplete")
return None
storage.apply_to_core()
return config
+10
View File
@@ -0,0 +1,10 @@
"""Component alias registry.
Generated by script/build_alias_registry.py - do not edit manually.
See the component-alias section of esphome/loader.py.
"""
# alias -> (canonical component, removal version or None)
COMPONENT_ALIASES: dict[str, tuple[str, str | None]] = {
"rp2040": ("rp2", "2027.7.0"),
}
+1 -1
View File
@@ -3,7 +3,7 @@
namespace esphome::adc {
static const char *const TAG = "adc.common";
static const char *const TAG = "adc";
const LogString *sampling_mode_to_str(SamplingMode mode) {
switch (mode) {
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::adc {
static const char *const TAG = "adc.esp32";
static const char *const TAG = "adc";
adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr};
@@ -13,7 +13,7 @@ ADC_MODE(ADC_VCC)
namespace esphome::adc {
static const char *const TAG = "adc.esp8266";
static const char *const TAG = "adc";
void ADCSensor::setup() {
#ifndef USE_ADC_SENSOR_VCC
@@ -5,7 +5,7 @@
namespace esphome::adc {
static const char *const TAG = "adc.libretiny";
static const char *const TAG = "adc";
void ADCSensor::setup() {
#ifndef USE_ADC_SENSOR_VCC
+1 -1
View File
@@ -17,7 +17,7 @@
namespace esphome::adc {
static const char *const TAG = "adc.rp2";
static const char *const TAG = "adc";
// The on-die temperature sensor sits on the last ADC channel: input 4 on RP2040
// and RP2350A, but input 8 on RP2350B, which has eight external channels rather
+1 -1
View File
@@ -7,7 +7,7 @@
namespace esphome::adc {
static const char *const TAG = "adc.zephyr";
static const char *const TAG = "adc";
void ADCSensor::setup() {
if (!adc_is_ready_dt(this->channel_)) {
+1 -1
View File
@@ -497,7 +497,7 @@ async def to_code(config: ConfigType) -> None:
# and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.11")
cg.add_library("esphome/noise-c", "0.1.21")
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
+15 -15
View File
@@ -160,11 +160,6 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
#else
#error "No frame helper defined"
#endif
#ifdef USE_CAMERA
if (camera::Camera::instance() != nullptr) {
this->image_reader_ = std::unique_ptr<camera::CameraImageReader>{camera::Camera::instance()->create_image_reader()};
}
#endif
}
void APIConnection::start() {
@@ -448,7 +443,7 @@ void APIConnection::on_disconnect_response() {
uint16_t APIConnection::fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg,
CalculateSizeFn size_fn, MessageEncodeFn encode_fn,
APIConnection *conn, uint32_t remaining_size) {
msg.key = entity->get_entity_key();
msg.key = entity->get_object_id_hash();
#ifdef USE_DEVICES
msg.device_id = entity->get_device_id();
#endif
@@ -459,7 +454,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp
CalculateSizeFn size_fn, MessageEncodeFn encode_fn,
APIConnection *conn, uint32_t remaining_size) {
// Set common fields that are shared by all entity types
msg.key = entity->get_entity_key();
msg.key = entity->get_object_id_hash();
if (entity->has_own_name()) {
msg.name = entity->get_name();
@@ -1140,6 +1135,7 @@ void APIConnection::try_send_camera_image_() {
if (!this->image_reader_)
return;
const auto *cam = camera::Camera::instance();
// Send as many chunks as possible without blocking
while (this->image_reader_->available()) {
if (!this->helper_->can_write_without_blocking())
@@ -1149,11 +1145,11 @@ void APIConnection::try_send_camera_image_() {
bool done = this->image_reader_->available() == to_send;
CameraImageResponse msg;
msg.key = camera::Camera::instance()->get_entity_key();
msg.key = cam->get_object_id_hash();
msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
msg.done = done;
#ifdef USE_DEVICES
msg.device_id = camera::Camera::instance()->get_device_id();
msg.device_id = cam->get_device_id();
#endif
if (!this->send_message(msg)) {
@@ -1169,15 +1165,19 @@ void APIConnection::try_send_camera_image_() {
void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) {
if (!this->flags_.state_subscription)
return;
if (!this->image_reader_)
if (this->image_reader_ && this->image_reader_->available())
return;
if (this->image_reader_->available())
if (!image->was_requested_by(esphome::camera::API_REQUESTER) && !image->was_requested_by(esphome::camera::IDLE))
return;
if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) {
this->image_reader_->set_image(std::move(image));
// Try to send immediately to reduce latency
this->try_send_camera_image_();
if (!this->image_reader_) {
// Created on the first image this connection will send, so connections
// that never receive one never pay for a reader. Only a registered
// camera's listener can reach this, so instance() is non-null here.
this->image_reader_ = std::unique_ptr<camera::CameraImageReader>{camera::Camera::instance()->create_image_reader()};
}
this->image_reader_->set_image(std::move(image));
// Try to send immediately to reduce latency
this->try_send_camera_image_();
}
uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *camera = static_cast<camera::Camera *>(entity);
@@ -591,18 +591,21 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
*/
APIError APINoiseFrameHelper::init_handshake_() {
int err;
memset(&nid_, 0, sizeof(nid_));
// const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
// err = noise_protocol_name_to_id(&nid_, proto, strlen(proto));
nid_.pattern_id = NOISE_PATTERN_NN;
nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY;
nid_.dh_id = NOISE_DH_CURVE25519;
nid_.prefix_id = NOISE_PREFIX_STANDARD;
nid_.hybrid_id = NOISE_DH_NONE;
nid_.hash_id = NOISE_HASH_SHA256;
nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
// Noise_NNpsk0_25519_ChaChaPoly_SHA256, built on the stack:
// noise_handshakestate_new_by_id copies it, so a member would waste
// 104 bytes per connection, and a static const would sit in RAM on
// ESP8266 (.rodata is DRAM there).
const NoiseProtocolId nid = {
.prefix_id = NOISE_PREFIX_STANDARD,
.pattern_id = NOISE_PATTERN_NN,
.modifier_ids = {NOISE_MODIFIER_PSK0},
.dh_id = NOISE_DH_CURVE25519,
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
.hash_id = NOISE_HASH_SHA256,
.hybrid_id = NOISE_DH_NONE,
};
err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
err = noise_handshakestate_new_by_id(&handshake_, &nid, NOISE_ROLE_RESPONDER);
APIError aerr =
handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
@@ -63,9 +63,6 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Buffer for noise handshake prologue (released after handshake)
APIBuffer prologue_;
// NoiseProtocolId (size depends on implementation)
NoiseProtocolId nid_;
// Group small types together
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
@@ -300,46 +300,12 @@ void BekenSPILEDStripLightOutput::write_state(light::LightState *state) {
}
light::ESPColorView BekenSPILEDStripLightOutput::get_view_internal(int32_t index) const {
int32_t r = 0, g = 0, b = 0;
switch (this->rgb_order_) {
case ORDER_RGB:
r = 0;
g = 1;
b = 2;
break;
case ORDER_RBG:
r = 0;
g = 2;
b = 1;
break;
case ORDER_GRB:
r = 1;
g = 0;
b = 2;
break;
case ORDER_GBR:
r = 2;
g = 0;
b = 1;
break;
case ORDER_BGR:
r = 2;
g = 1;
b = 0;
break;
case ORDER_BRG:
r = 1;
g = 2;
b = 0;
break;
}
uint8_t multiplier = this->is_rgbw_ || this->is_wrgb_ ? 4 : 3;
uint8_t white = this->is_wrgb_ ? 0 : 3;
return {this->buf_ + (index * multiplier) + r + this->is_wrgb_,
this->buf_ + (index * multiplier) + g + this->is_wrgb_,
this->buf_ + (index * multiplier) + b + this->is_wrgb_,
this->is_rgbw_ || this->is_wrgb_ ? this->buf_ + (index * multiplier) + white : nullptr,
const light::ChannelColors &colors = this->channel_colors_;
uint8_t *led = this->buf_ + (index * colors.bytes_per_led());
return {led + colors.r,
led + colors.g,
led + colors.b,
colors.has_white() ? led + colors.w : nullptr,
&this->effect_data_[index],
&this->correction_};
}
@@ -349,35 +315,12 @@ void BekenSPILEDStripLightOutput::dump_config() {
"Beken SPI LED Strip:\n"
" Pin: %u",
this->pin_);
const char *rgb_order;
switch (this->rgb_order_) {
case ORDER_RGB:
rgb_order = "RGB";
break;
case ORDER_RBG:
rgb_order = "RBG";
break;
case ORDER_GRB:
rgb_order = "GRB";
break;
case ORDER_GBR:
rgb_order = "GBR";
break;
case ORDER_BGR:
rgb_order = "BGR";
break;
case ORDER_BRG:
rgb_order = "BRG";
break;
default:
rgb_order = "UNKNOWN";
break;
}
char channel_colors[5];
ESP_LOGCONFIG(TAG,
" RGB Order: %s\n"
" Channel colors: %s\n"
" Max refresh rate: %" PRIu32 "\n"
" Number of LEDs: %u",
rgb_order, this->max_refresh_rate_.value_or(0), this->num_leds_);
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_.value_or(0), this->num_leds_);
}
float BekenSPILEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -3,6 +3,7 @@
#ifdef USE_BK72XX
#include "esphome/components/light/addressable_light.h"
#include "esphome/components/light/channel_colors.h"
#include "esphome/components/light/light_output.h"
#include "esphome/core/color.h"
#include "esphome/core/component.h"
@@ -10,15 +11,6 @@
namespace esphome::beken_spi_led_strip {
enum RGBOrder : uint8_t {
ORDER_RGB,
ORDER_RBG,
ORDER_GRB,
ORDER_GBR,
ORDER_BGR,
ORDER_BRG,
};
class BekenSPILEDStripLightOutput final : public light::AddressableLight {
public:
void setup() override;
@@ -28,7 +20,7 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
if (this->is_rgbw_ || this->is_wrgb_) {
if (this->channel_colors_.has_white()) {
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
} else {
traits.set_supported_color_modes({light::ColorMode::RGB});
@@ -38,16 +30,13 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; }
void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; }
void set_channel_colors(light::ChannelColors channel_colors) { this->channel_colors_ = channel_colors; }
/// Set a maximum refresh rate in µs as some lights do not like being updated too often.
void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
void set_led_params(uint8_t bit0, uint8_t bit1, uint32_t spi_frequency);
void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; }
void clear_effect_data() override {
for (int i = 0; i < this->size(); i++)
this->effect_data_[i] = 0;
@@ -58,7 +47,7 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
protected:
light::ESPColorView get_view_internal(int32_t index) const override;
size_t get_buffer_size_() const { return this->num_leds_ * (this->is_rgbw_ || this->is_wrgb_ ? 4 : 3); }
size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); }
uint8_t *buf_{nullptr};
uint8_t *effect_data_{nullptr};
@@ -66,13 +55,11 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
uint8_t pin_;
uint16_t num_leds_;
bool is_rgbw_;
bool is_wrgb_;
uint32_t spi_frequency_{6666666};
uint8_t bit0_{0xE0};
uint8_t bit1_{0xFC};
RGBOrder rgb_order_;
light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE};
uint32_t last_refresh_{0};
optional<uint32_t> max_refresh_rate_{};
+17 -24
View File
@@ -3,6 +3,7 @@ from dataclasses import dataclass
from esphome import pins
import esphome.codegen as cg
from esphome.components import libretiny, light
from esphome.components.const import CONF_CHANNEL_COLORS, CONF_IS_WRGB
import esphome.config_validation as cv
from esphome.const import (
CONF_CHIPSET,
@@ -13,6 +14,7 @@ from esphome.const import (
CONF_PIN,
CONF_RGB_ORDER,
)
from esphome.types import ConfigType
CODEOWNERS = ["@Mat931"]
DEPENDENCIES = ["libretiny"]
@@ -22,17 +24,6 @@ BekenSPILEDStripLightOutput = beken_spi_led_strip_ns.class_(
"BekenSPILEDStripLightOutput", light.AddressableLight
)
RGBOrder = beken_spi_led_strip_ns.enum("RGBOrder")
RGB_ORDERS = {
"RGB": RGBOrder.ORDER_RGB,
"RBG": RGBOrder.ORDER_RBG,
"GRB": RGBOrder.ORDER_GRB,
"GBR": RGBOrder.ORDER_GBR,
"BGR": RGBOrder.ORDER_BGR,
"BRG": RGBOrder.ORDER_BRG,
}
@dataclass
class LEDStripTimings:
@@ -57,8 +48,6 @@ CHIPSETS = {
}
CONF_IS_WRGB = "is_wrgb"
SUPPORTED_PINS = {
libretiny.const.FAMILY_BK7231N: [16],
libretiny.const.FAMILY_BK7231T: [16],
@@ -79,10 +68,9 @@ def _validate_pin(value):
return value
def _validate_num_leds(value):
max_num_leds = 165 # 170
if value[CONF_IS_RGBW] or value[CONF_IS_WRGB]:
max_num_leds = 123 # 127
def _validate_num_leds(value: ConfigType) -> ConfigType:
# A white channel makes each LED one byte wider, so fewer of them fit in the DMA buffer.
max_num_leds = 123 if "W" in value[CONF_CHANNEL_COLORS] else 165 # 127 / 170
if value[CONF_NUM_LEDS] > max_num_leds:
raise cv.Invalid(
f"The maximum number of LEDs for this configuration is {max_num_leds}.",
@@ -99,18 +87,23 @@ CONFIG_SCHEMA = cv.All(
pins.internal_gpio_output_pin_number, _validate_pin
),
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
cv.Optional(CONF_CHANNEL_COLORS): light.validate_channel_colors,
# Deprecated in favour of CONF_CHANNEL_COLORS, remove in 2027.3.0
cv.Optional(CONF_RGB_ORDER): cv.one_of(*light.RGB_ORDERS, upper=True),
cv.Optional(CONF_IS_RGBW): cv.boolean,
cv.Optional(CONF_IS_WRGB): cv.boolean,
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
cv.Required(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
cv.Optional(CONF_IS_WRGB, default=False): cv.boolean,
}
),
light.migrate_channel_colors(
removed_in="2027.3.0", component="beken_spi_led_strip"
),
_validate_num_leds,
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await light.register_light(var, config)
await cg.register_component(var, config)
@@ -130,6 +123,6 @@ async def to_code(config):
)
)
cg.add(var.set_rgb_order(config[CONF_RGB_ORDER]))
cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
cg.add(
var.set_channel_colors(light.channel_colors_struct(config[CONF_CHANNEL_COLORS]))
)
+37 -6
View File
@@ -5,11 +5,11 @@ bring-up and the controller BLE address. Consumers (bk72xx_ble_tracker) build
on this component and contain no SDK calls of their own.
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7238/BK7252N/BK7253
(BLE 5.2), and any future BLE-5.x SoC. Capability is detected at compile time,
not by a chip list: the C++ guards on `__has_include("ble_api.h")` the Beken
BLE 5.x public API header, which the LibreTiny beken-72xx builder ships only
for BLE-5.x SoCs. BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) fail
with a clear #error.
(BLE 5.2), and any future BLE-5.x SoC. Known non-5.x families are rejected in
to_code; unknown families are capability-checked at compile time via
`__has_include("app_ble.h")`, a header only on the BLE 5.x include path
(ble_api.h ships for every SoC, so it cannot be the probe). A non-5.x build
fails with a clear #error.
No framework patch is needed: the LibreTiny beken-72xx builder already compiles
and links the BLE 5.x stack (CFG_SUPPORT_BLE=1 + CFG_BLE_VERSION=BLE_VERSION_5_x;
@@ -21,9 +21,16 @@ import logging
import esphome.codegen as cg
from esphome.components import libretiny
from esphome.components.libretiny.const import FAMILY_BK7231N, FAMILY_BK7238
from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7231Q,
FAMILY_BK7231T,
FAMILY_BK7238,
FAMILY_BK7251,
)
import esphome.config_validation as cv
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID
from esphome.core import EsphomeError
from esphome.types import ConfigType
DEPENDENCIES = ["bk72xx"]
@@ -50,7 +57,31 @@ CONFIG_SCHEMA = cv.Schema(
request_scan_listener_slot = cg.slot_counter("BK72XX_BLE_SCAN_LISTENER_COUNT")
def _unsupported_family_message(family: str) -> str | None:
if family in (FAMILY_BK7231T, FAMILY_BK7251):
return (
f"bk72xx_ble does not support {family}: this SoC has the Beken BLE 4.2 "
"stack; a BLE 5.x SoC such as BK7231N or BK7238 is required"
)
if family == FAMILY_BK7231Q:
return "bk72xx_ble does not support BK7231Q: this SoC has no BLE"
return None
def _final_validate(config: ConfigType) -> None:
# Warn only: a hard error here would break the validate-only CI fixtures,
# which run on a BLE 4.2 board. The hard error is raised at codegen.
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
_LOGGER.warning("%s (this configuration cannot compile)", msg)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
raise EsphomeError(msg)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
+2 -2
View File
@@ -10,7 +10,7 @@
#ifdef USE_BK72XX_BLE
// Same SDK gate as bk72xx_ble.cpp (which carries the explanatory #error).
#if !defined(CLANG_TIDY) && __has_include("ble_api.h")
#if !defined(CLANG_TIDY) && __has_include("ble_api.h") && __has_include("app_ble.h")
extern "C" {
#include "app_ble.h" // app_ble_env, app_ble_run, app_ble_reset, actv_state_t,
@@ -115,5 +115,5 @@ BdkOpResult bdk_scan_release(uint8_t activity_idx, bool created, int *err_out) {
} // namespace esphome::bk72xx_ble
#endif // !CLANG_TIDY && ble_api.h
#endif // !CLANG_TIDY && ble_api.h && app_ble.h
#endif // USE_BK72XX_BLE
+13 -9
View File
@@ -34,22 +34,26 @@
// ---------------------------------------------------------------------------
// SDK-capability gate (not a chip allowlist).
// This component drives the Beken BLE *5.x* controller via its public API,
// `ble_api.h`, which the LibreTiny beken-72xx builder ships only for the
// BLE-5.x SoCs (it selects the `ble_pub` 5.x stack from CFG_BLE_VERSION; the
// 4.2 SoCs build a different, older API with no ble_api.h). Gate on the header
// itself so any BLE-5.x Beken chip — present or future — is supported without a
// hard-coded list, and a non-5.x build fails here with a clear message instead
// of a cryptic "ble_api.h: No such file or directory".
// This component drives the Beken BLE *5.x* controller. `ble_api.h` cannot be
// the probe: it ships for every SoC (driver/include) and merely switches on
// CFG_BLE_VERSION internally. `app_ble.h` is on the include path only when the
// LibreTiny beken-72xx builder selects a 5.x stack, so gating on it supports
// any BLE-5.x chip — present or future — without a hard-coded list, and a
// non-5.x build fails here with a clear message instead of a cryptic
// "app_ble.h: No such file or directory".
// ---------------------------------------------------------------------------
#if defined(CLANG_TIDY)
// The clang-tidy environment does not carry the full Beken BDK BLE 5.x API
// (its ble_api.h variant lacks parts of the 5.x surface), so there is nothing
// accurate to analyze the SDK calls against — skip the file under analysis.
#define BK72XX_BLE_NO_SDK
#elif !__has_include("ble_api.h")
#elif !__has_include("ble_api.h") || !__has_include("app_ble.h")
// Also skip the SDK body: #error does not stop the preprocessor, and on a 4.2
// SoC ble_api.h exists, so without the guard the 5.x symbols would fail one by
// one and bury this message.
#define BK72XX_BLE_NO_SDK
#error \
"bk72xx_ble requires a BLE 5.x Beken SDK (ble_api.h). Supported SoCs: BK7231N/BK7236 (BLE 5.1) and BK7238/BK7252N/BK7253 (BLE 5.2). BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) are not supported."
"bk72xx_ble requires a BLE 5.x Beken SDK (app_ble.h). Supported SoCs: BK7231N/BK7236 (BLE 5.1) and BK7238/BK7252N/BK7253 (BLE 5.2). BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) are not supported."
#endif
#ifndef BK72XX_BLE_NO_SDK
+14 -6
View File
@@ -37,7 +37,7 @@ from esphome.const import (
CONF_INTERVAL,
KEY_TARGET_PLATFORM,
)
from esphome.core import CORE, ID, KEY_CORE
from esphome.core import CORE, ID, KEY_CORE, TimePeriod
from esphome.types import ConfigType
CODEOWNERS = ["@Bl00d-B0b"]
@@ -243,19 +243,27 @@ def validate_scan_parameters(config: ConfigType) -> ConfigType:
return config
# The historical scan window default shared by the trackers that do not pin
# their own; also the fallback for esp32's conditional default.
DEFAULT_SCAN_WINDOW = "30ms"
def scan_parameters_schema(
interval_default: str,
*,
window_default: str = "30ms",
window_default: str | Callable[[], TimePeriod] = DEFAULT_SCAN_WINDOW,
) -> cv.All:
"""Build the scan_parameters value schema shared by all BLE trackers.
interval_default and window_default are per chip (e.g. esp32 320/30 ms,
bk72xx/rp2 100/30 ms the reference scan rates of the respective stacks;
LN882H's SDK recommends 100/50 ms). The `active` option (default on) is
unconditional: active scanning is part of the tracker contract every
current proxy client assumes it, so a passive-only tracker must not share
this schema.
LN882H's SDK recommends 100/50 ms). window_default may also be a zero-arg
callable evaluated per validation when the user omits the key (esp32 uses
this to record that the window was defaulted, so a later validation step
can adjust it once sibling keys are resolved). The `active` option
(default on) is unconditional: active scanning is part of the tracker
contract every current proxy client assumes it, so a passive-only
tracker must not share this schema.
"""
schema = {
cv.Optional(CONF_DURATION, default="5min"): cv.positive_time_period_seconds,
@@ -20,7 +20,7 @@
namespace esphome::bluetooth_connection {
static const char *const TAG = "bluetooth_connection.bluedroid";
static const char *const TAG = "bluetooth_connection";
using ble_device_base::FAST_CONN_TIMEOUT;
using ble_device_base::FAST_MAX_CONN_INTERVAL;
@@ -15,7 +15,7 @@
namespace esphome::bluetooth_connection {
static const char *const TAG = "bluetooth_connection.rp2";
static const char *const TAG = "bluetooth_connection";
using ble_device_base::ESPBTUUID;
using ble_device_base::GATT_ERR_NOT_CONNECTED;
+2 -1
View File
@@ -103,7 +103,8 @@ struct CameraImageSpec {
/** Abstract camera base class. Collaborates with API.
* 1) API server starts and registers as a listener (add_listener)
* to receive new images from the camera.
* 2) New API client connects and creates a new image reader (create_image_reader).
* 2) API connection creates an image reader (create_image_reader) when it receives
* the first image it will send.
* 3) API connection receives protobuf CameraImageRequest and calls request_image.
* 3.a) API connection receives protobuf CameraImageRequest and calls start_stream.
* 4) Camera implementation provides JPEG data in the CameraImage and notifies listeners.
@@ -61,7 +61,7 @@ CONFIG_SCHEMA = cv.All(
)
def _final_validate(config: ConfigType) -> ConfigType:
def _final_validate(config: ConfigType) -> None:
full_config = fv.full_config.get()
wifi_conf = full_config.get("wifi")
@@ -88,8 +88,6 @@ def _final_validate(config: ConfigType) -> ConfigType:
socket.consume_sockets(3, "captive_portal")(config)
socket.consume_sockets(1, "captive_portal", socket.SocketType.UDP)(config)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
+4
View File
@@ -10,6 +10,7 @@ CONF_ACCELEROMETER_RANGE = "accelerometer_range"
CONF_B_CONSTANT = "b_constant"
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
CONF_BYTE_ORDER = "byte_order"
CONF_CHANNEL_COLORS = "channel_colors"
CONF_CLIMATE_ID = "climate_id"
CONF_CO2_EQUIVALENT = "co2_equivalent"
CONF_COLOR_DEPTH = "color_depth"
@@ -22,6 +23,8 @@ CONF_GYROSCOPE_ODR = "gyroscope_odr"
CONF_GYROSCOPE_RANGE = "gyroscope_range"
CONF_IAQ = "iaq"
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_IS_WRGB = "is_wrgb"
CONF_LABEL = "label"
CONF_LIBRETINY = "libretiny"
CONF_LOOP = "loop"
CONF_NOX_INDEX = "nox_index"
@@ -35,6 +38,7 @@ CONF_REQUEST_HEADERS = "request_headers"
CONF_ROWS = "rows"
CONF_SCAN_PARAMETERS = "scan_parameters"
CONF_SHA256 = "sha256"
CONF_SLOT = "slot"
CONF_STATE_SAVE_INTERVAL = "state_save_interval"
CONF_STOP_BITS = "stop_bits"
CONF_TARGET_COUNT = "target_count"
@@ -5,7 +5,7 @@
namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep.bk72xx";
static const char *const TAG = "deep_sleep";
#ifdef USE_DEEP_SLEEP_ON_WAKE
WakeupCause get_wakeup_cause() {
@@ -3,6 +3,7 @@ import re
from esphome import automation, core
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components.const import CONF_LABEL
from esphome.components.number import Number
from esphome.components.select import Select
from esphome.components.switch import Switch
@@ -30,7 +31,6 @@ display_menu_base_ns = cg.esphome_ns.namespace("display_menu_base")
CONF_ROTARY = "rotary"
CONF_JOYSTICK = "joystick"
CONF_LABEL = "label"
CONF_MENU = "menu"
CONF_BACK = "back"
CONF_SELECT = "select"
+1 -3
View File
@@ -88,7 +88,7 @@ async def to_code(config):
cg.add_library("esphome/dsmr_parser", "1.9.0")
def final_validate(config: ConfigType) -> ConfigType:
def final_validate(config: ConfigType) -> None:
full_config = fv.full_config.get()
for uart_conf in full_config["uart"]:
@@ -102,7 +102,5 @@ def final_validate(config: ConfigType) -> ConfigType:
)
break
return config
FINAL_VALIDATE_SCHEMA = final_validate
+2 -2
View File
@@ -59,7 +59,7 @@ CONFIG_SCHEMA = (
)
def final_validate(config: ConfigType) -> ConfigType:
def final_validate(config: ConfigType) -> None:
full_config = fv.full_config.get()
# Count sensors registered to this hub (IDs are resolved at final_validate stage)
@@ -95,7 +95,7 @@ def final_validate(config: ConfigType) -> ConfigType:
parity="NONE",
stop_bits=1,
)
return schema(config)
schema(config)
FINAL_VALIDATE_SCHEMA = final_validate
+1 -2
View File
@@ -153,7 +153,7 @@ def customise_schema(config):
CONFIG_SCHEMA = customise_schema
def _final_validate(config):
def _final_validate(config) -> None:
spi.final_validate_device_schema(
"epaper_spi", require_miso=False, require_mosi=True
)(config)
@@ -170,7 +170,6 @@ def _final_validate(config):
config[CONF_SHOW_TEST_CARD] = True
elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
return config
FINAL_VALIDATE_SCHEMA = _final_validate
+45 -40
View File
@@ -570,6 +570,9 @@ def get_download_types(storage_json):
the shape stable so the download panel
doesn't have to special-case per-platform schemas.
"""
# No recorded firmware path means nothing was built; no downloads.
if storage_json.firmware_bin_path is None:
return []
return [
{
"title": "Factory format (Previously Modern)",
@@ -1070,6 +1073,26 @@ def _parse_pio_platform_version(value):
return value
def _normalize_p4_engineering_sample(value: ConfigType) -> bool:
"""Fill in CONF_ENGINEERING_SAMPLE when unset, warning that production
silicon (rev3) is assumed. Returns the normalized flag."""
if (engineering_sample := value.get(CONF_ENGINEERING_SAMPLE)) is None:
_LOGGER.warning(
"Defaulting to ESP32-P4 production silicon (rev3).\n"
"If you have an early engineering sample (pre-rev3), add this to your config:\n"
"\n"
" esp32:\n"
" engineering_sample: true\n"
"\n"
"To check your chip revision, look for 'chip revision: vX.Y' in the boot log.\n"
"Engineering samples will show a revision below v3.0.\n"
"The 'debug:' component also reports the revision (e.g. Revision: 100 = v1.0, 300 = v3.0)."
)
engineering_sample = False
value[CONF_ENGINEERING_SAMPLE] = engineering_sample
return engineering_sample
def _detect_variant(value):
board = value.get(CONF_BOARD)
variant = value.get(CONF_VARIANT)
@@ -1082,6 +1105,8 @@ def _detect_variant(value):
# name rather than carrying a PIO board name through the IDF build.
if CORE.using_toolchain_esp_idf:
value = value.copy()
if variant == VARIANT_ESP32P4:
_normalize_p4_engineering_sample(value)
value[CONF_BOARD] = VARIANT_FRIENDLY[variant].lower()
return value
if variant not in STANDARD_BOARDS:
@@ -1092,22 +1117,8 @@ def _detect_variant(value):
)
value = value.copy()
value[CONF_BOARD] = STANDARD_BOARDS[variant]
if variant == VARIANT_ESP32P4:
engineering_sample = value.get(CONF_ENGINEERING_SAMPLE)
if engineering_sample is None:
_LOGGER.warning(
"No board specified for ESP32-P4. Defaulting to production silicon (rev3).\n"
"If you have an early engineering sample (pre-rev3), add this to your config:\n"
"\n"
" esp32:\n"
" engineering_sample: true\n"
"\n"
"To check your chip revision, look for 'chip revision: vX.Y' in the boot log.\n"
"Engineering samples will show a revision below v3.0.\n"
"The 'debug:' component also reports the revision (e.g. Revision: 100 = v1.0, 300 = v3.0)."
)
elif engineering_sample:
value[CONF_BOARD] = "esp32-p4-evboard"
if variant == VARIANT_ESP32P4 and _normalize_p4_engineering_sample(value):
value[CONF_BOARD] = "esp32-p4-evboard"
elif board in BOARDS:
variant = variant or BOARDS[board][KEY_VARIANT]
if variant != BOARDS[board][KEY_VARIANT]:
@@ -1117,6 +1128,14 @@ def _detect_variant(value):
)
value = value.copy()
value[CONF_VARIANT] = variant
if variant == VARIANT_ESP32P4:
board_is_es = BOARDS[board].get("engineering_sample", False)
engineering_sample = value.setdefault(CONF_ENGINEERING_SAMPLE, board_is_es)
if engineering_sample != board_is_es:
raise cv.Invalid(
f"'{CONF_ENGINEERING_SAMPLE}' does not match board '{board}'",
path=[CONF_ENGINEERING_SAMPLE],
)
elif not variant:
raise cv.Invalid(
"This board is unknown, if you are sure you want to compile with this board selection, "
@@ -1128,6 +1147,9 @@ def _detect_variant(value):
"This board is unknown; the specified variant '%s' will be used but this may not work as expected.",
variant,
)
if variant == VARIANT_ESP32P4:
value = value.copy()
_normalize_p4_engineering_sample(value)
return value
@@ -1365,7 +1387,7 @@ def _validate_signed_ota_keys(config: ConfigType) -> ConfigType:
return config
def final_validate(config):
def final_validate(config) -> None:
# Imported locally to avoid circular import issues
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
@@ -1431,20 +1453,6 @@ def final_validate(config):
path=[CONF_ENGINEERING_SAMPLE],
)
)
if (
config[CONF_VARIANT] == VARIANT_ESP32P4
and config.get(CONF_ENGINEERING_SAMPLE) is not None
):
board_is_es = BOARDS.get(config[CONF_BOARD], {}).get(
"engineering_sample", False
)
if config[CONF_ENGINEERING_SAMPLE] != board_is_es:
errs.append(
cv.Invalid(
f"'{CONF_ENGINEERING_SAMPLE}' does not match board '{config[CONF_BOARD]}'",
path=[CONF_ENGINEERING_SAMPLE],
)
)
if advanced[CONF_EXECUTE_FROM_PSRAM]:
if config[CONF_VARIANT] not in {VARIANT_ESP32S3, VARIANT_ESP32P4}:
errs.append(
@@ -1626,8 +1634,6 @@ def final_validate(config):
if errs:
raise cv.MultipleInvalid(errs)
return config
CONF_SDKCONFIG_OPTIONS = "sdkconfig_options"
CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server"
@@ -2517,15 +2523,14 @@ async def to_code(config):
f"CONFIG_ESPTOOLPY_FLASHFREQ_{flash_frequency[:-3]}M", True
)
# ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3
# from y to n. PlatformIO uses sections.ld.in (for rev <3) or
# sections.rev3.ld.in (for rev >=3) based on board definition.
# Set the sdkconfig option to match the board's chip revision.
# ESP32-P4: pre-v3 and rev3 (v3.0+) silicon are not binary compatible.
# CONFIG_ESP32P4_SELECTS_REV_LESS_V3 selects which layout ESP-IDF links;
# validation normalizes CONF_ENGINEERING_SAMPLE from the board when unset.
if variant == VARIANT_ESP32P4:
is_eng_sample = BOARDS.get(config[CONF_BOARD], {}).get(
"engineering_sample", False
add_idf_sdkconfig_option(
"CONFIG_ESP32P4_SELECTS_REV_LESS_V3",
config.get(CONF_ENGINEERING_SAMPLE, False),
)
add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", is_eng_sample)
# Set minimum chip revision for ESP32 variant
# Setting this to 3.0 or higher reduces flash size by excluding workaround code,
+15 -14
View File
@@ -360,17 +360,6 @@ static bool has_fault_addr() {
return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause;
}
// Append both cores' backtrace addresses to buf; returns the new position.
static int append_all_backtraces(char *buf, int size, int pos) {
pos = append_addrs_to_hint(buf, size, pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count,
s_raw_crash_data.reg_frame_count);
#if SOC_CPU_CORES_NUM > 1
pos = append_addrs_to_hint(buf, size, pos, s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count,
s_raw_crash_data.other_reg_frame_count);
#endif
return pos;
}
// The record was captured by a different firmware build (it survives soft
// resets, including the OTA reboot), so symbolizing its addresses against the
// current ELF would produce misleading symbols. Print them with lowercase
@@ -443,11 +432,23 @@ void crash_handler_log() {
}
#endif
// Build addr2line hint with all captured addresses for easy copy-paste
// Build addr2line hints for easy copy-paste. One line per core: the two
// backtraces are separate stacks, and a combined list decodes as one
// impossible call chain (and can overflow the buffer, dropping addresses).
static const char *const ADDR2LINE_CMD = "addr2line -pfiaC -e firmware.elf";
char hint[256];
int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc);
append_all_backtraces(hint, sizeof(hint), pos);
int pos = snprintf(hint, sizeof(hint), "Use: %s 0x%08" PRIX32, ADDR2LINE_CMD, s_raw_crash_data.pc);
append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count,
s_raw_crash_data.reg_frame_count);
ESP_LOGE(TAG, "%s", hint);
#if SOC_CPU_CORES_NUM > 1
if (s_raw_crash_data.other_backtrace_count > 0) {
pos = snprintf(hint, sizeof(hint), "Other core: %s", ADDR2LINE_CMD);
append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace,
s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count);
ESP_LOGE(TAG, "%s", hint);
}
#endif
}
} // namespace esphome::esp32
+1 -3
View File
@@ -443,7 +443,7 @@ def validate_connection_slots(max_connections: int) -> None:
)
def final_validation(config):
def final_validation(config) -> None:
validate_variant(config)
if (name := config.get(CONF_NAME)) is not None:
full_config = fv.full_config.get()
@@ -514,8 +514,6 @@ def final_validation(config):
# For newer chips (C3/S3/etc), different configs are used automatically
add_idf_sdkconfig_option("CONFIG_BTDM_CTRL_BLE_MAX_CONN", max_connections)
return config
FINAL_VALIDATE_SCHEMA = final_validation
+2
View File
@@ -648,6 +648,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete
case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm
case ESP_GAP_BLE_LOCAL_IR_EVT: // Local identity root key generated at security init
case ESP_GAP_BLE_LOCAL_ER_EVT: // Local encryption root key generated at security init
return;
default:
@@ -307,7 +307,7 @@ def create_device_information_service(config):
return config
def final_validate_config(config):
def final_validate_config(config) -> None:
# Validate max_clients does not exceed esp32_ble max_connections
max_clients = config[CONF_MAX_CLIENTS]
if max_clients > 1:
@@ -355,7 +355,6 @@ def final_validate_config(config):
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has both a set_value action and a templated value"
)
return config
def validate_value_type(value_config):
@@ -1,5 +1,7 @@
from __future__ import annotations
import copy
from dataclasses import dataclass
import logging
from esphome import automation
@@ -8,6 +10,7 @@ from esphome.components import ble_device_base, esp32_ble, ota
from esphome.components.const import CONF_ON_SCAN_END, CONF_SCAN_PARAMETERS, CONF_WINDOW
from esphome.components.esp32 import (
add_idf_sdkconfig_option,
idf_version,
request_bluetooth,
request_software_coexistence,
)
@@ -35,10 +38,12 @@ from esphome.const import (
CONF_SERVICE_UUID,
CONF_TRIGGER_ID,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
from esphome.enum import StrEnum
from esphome.types import ConfigType
DOMAIN = "esp32_ble_tracker"
AUTO_LOAD = ["ble_device_base", "esp32_ble"]
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@bdraco"]
@@ -125,10 +130,71 @@ def validate_max_connections_deprecated(config: ConfigType) -> ConfigType:
return config
# ESP-IDF 5.5.5 fixed a coexistence bug on the ESP32 where BLE scans ran far
# longer than the configured window (espressif/esp-idf#18931). Before the fix,
# the default 30 ms window in a 320 ms interval effectively scanned at a much
# higher duty cycle than requested; with the fix, that same default only
# listens 9.4 % of the time and misses most advertisements when wifi shares
# the radio. Espressif recommends setting the window equal to the interval in
# that case: the coexistence arbiter still shares the radio with wifi, and
# BLE uses the airtime wifi does not claim.
IDF_SCAN_WINDOW_FIX_VERSION = cv.Version(5, 5, 5)
@dataclass
class TrackerData:
"""Per-run validation state, namespaced under DOMAIN in CORE.data."""
scan_window_defaulted: bool = False
def _get_data() -> TrackerData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = TrackerData()
return CORE.data[DOMAIN]
def _scan_window_default() -> TimePeriod:
"""Schema default for the scan window.
Records that the user did not set a window, so _raise_defaulted_scan_window
can tell a defaulted 30 ms from an explicit one; the raise itself must wait
for the outer schema because it depends on software_coexistence, a sibling
key not yet resolved here.
"""
_get_data().scan_window_defaulted = True
return cv.positive_time_period(ble_device_base.DEFAULT_SCAN_WINDOW)
def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType:
"""Raise a defaulted scan window to the interval where that is safe.
Only when the coexistence arbiter is compiled in (software_coexistence,
present iff wifi is configured and not disabled by the user) and the IDF
honors the window strictly (>= 5.5.5); without the arbiter a full-duty
scan would starve wifi outright, and a user-set window is never touched.
Raising to the interval cannot invalidate the already-validated
parameters, so no re-validation is needed.
"""
if (
_get_data().scan_window_defaulted
and config.get(CONF_SOFTWARE_COEXISTENCE)
and idf_version() >= IDF_SCAN_WINDOW_FIX_VERSION
):
params = config[CONF_SCAN_PARAMETERS]
# Copy so the config dump shows a plain value instead of a YAML
# anchor/alias pair pointing at the interval.
params[CONF_WINDOW] = copy.copy(params[CONF_INTERVAL])
return config
# 320 ms is the ESP-IDF reference scan interval; the shared schema also
# tightens validation to the controller's 2.5 ms .. 10240 ms range and rejects
# window/interval pairs that collapse to the same 0.625 ms unit count.
SCAN_PARAMETERS_SCHEMA = ble_device_base.scan_parameters_schema("320ms")
# The window default is conditional (see _scan_window_default above).
SCAN_PARAMETERS_SCHEMA = ble_device_base.scan_parameters_schema(
"320ms", window_default=_scan_window_default
)
# Codegen helpers are owned by ble_device_base; kept under the historical names
# here for the components that import them from this module.
@@ -183,6 +249,7 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
validate_max_connections_deprecated,
_raise_defaulted_scan_window,
)
+24 -12
View File
@@ -3,7 +3,7 @@ from pathlib import Path
from esphome import pins
from esphome.components import esp32
from esphome.components.const import CONF_USE_PSRAM
from esphome.components.const import CONF_SLOT, CONF_USE_PSRAM
import esphome.config_validation as cv
from esphome.const import (
CONF_CLK_PIN,
@@ -16,8 +16,10 @@ from esphome.const import (
CONF_VARIANT,
)
from esphome.cpp_generator import add_define
from esphome.types import ConfigType
CODEOWNERS = ["@swoboda1337"]
DEPENDENCIES = ["esp32"]
# esp32_ble raises the task watchdog around the remote BT controller bring-up
AUTO_LOAD = ["watchdog"]
@@ -33,7 +35,6 @@ CONF_DATA_READY_PIN = "data_ready_pin"
CONF_HANDSHAKE_ACTIVE_HIGH = "handshake_active_high"
CONF_HANDSHAKE_PIN = "handshake_pin"
CONF_SDIO_FREQUENCY = "sdio_frequency"
CONF_SLOT = "slot"
CONF_SPI_MODE = "spi_mode"
# Shared fields for both transport modes
@@ -125,6 +126,21 @@ CONFIG_SCHEMA = cv.typed_schema(
)
def _final_validate(config: ConfigType) -> None:
# The esp_hosted releases compatible with older ESP-IDF versions crash at
# boot with a heap double free in the SDIO RX path (fixed in esp_hosted
# 2.11.0, which requires ESP-IDF 5.3), so reject them at validation time.
if (idf_ver := esp32.idf_version()) < cv.Version(5, 3, 0):
raise cv.Invalid(
f"esp32_hosted requires ESP-IDF 5.3 or newer, got {idf_ver}. "
"Remove the framework version from your configuration to use the "
"recommended version, or pin a version at or above 5.3."
)
FINAL_VALIDATE_SCHEMA = _final_validate
def _configure_sdio(config):
slot = config[CONF_SLOT]
esp32.add_idf_sdkconfig_option(
@@ -252,18 +268,14 @@ async def to_code(config):
if config[CONF_USE_PSRAM]:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_MEMPOOL_PREFER_SPIRAM", True)
# Library versions
# Library versions; this component set requires ESP-IDF 5.3 or newer,
# which is enforced at validation time.
idf_ver = esp32.idf_version()
os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}"
if idf_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.6.3")
esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.3")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.12")
else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11")
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.6.3")
esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.3")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.12")
esp32.add_extra_script(
"post",
"esp32_hosted.py",
@@ -221,46 +221,12 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
}
light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index) const {
int32_t r = 0, g = 0, b = 0;
switch (this->rgb_order_) {
case ORDER_RGB:
r = 0;
g = 1;
b = 2;
break;
case ORDER_RBG:
r = 0;
g = 2;
b = 1;
break;
case ORDER_GRB:
r = 1;
g = 0;
b = 2;
break;
case ORDER_GBR:
r = 2;
g = 0;
b = 1;
break;
case ORDER_BGR:
r = 2;
g = 1;
b = 0;
break;
case ORDER_BRG:
r = 1;
g = 2;
b = 0;
break;
}
uint8_t multiplier = this->is_rgbw_ || this->is_wrgb_ ? 4 : 3;
uint8_t white = this->is_wrgb_ ? 0 : this->white_index_;
return {this->buf_ + (index * multiplier) + r + (white <= r),
this->buf_ + (index * multiplier) + g + (white <= g),
this->buf_ + (index * multiplier) + b + (white <= b),
this->is_rgbw_ || this->is_wrgb_ ? this->buf_ + (index * multiplier) + white : nullptr,
const light::ChannelColors &colors = this->channel_colors_;
uint8_t *led = this->buf_ + (index * colors.bytes_per_led());
return {led + colors.r,
led + colors.g,
led + colors.b,
colors.has_white() ? led + colors.w : nullptr,
&this->effect_data_[index],
&this->correction_};
}
@@ -271,46 +237,12 @@ void ESP32RMTLEDStripLightOutput::dump_config() {
" Pin: %u",
this->pin_);
ESP_LOGCONFIG(TAG, " RMT Symbols: %" PRIu32, this->rmt_symbols_);
const char *rgb_order;
switch (this->rgb_order_) {
case ORDER_RGB:
rgb_order = "RGB";
break;
case ORDER_RBG:
rgb_order = "RBG";
break;
case ORDER_GRB:
rgb_order = "GRB";
break;
case ORDER_GBR:
rgb_order = "GBR";
break;
case ORDER_BGR:
rgb_order = "BGR";
break;
case ORDER_BRG:
rgb_order = "BRG";
break;
default:
rgb_order = "UNKNOWN";
break;
}
if (this->is_rgbw_ || this->is_wrgb_) {
char rgbw_order[5];
uint8_t white = this->is_wrgb_ ? 0 : this->white_index_;
uint8_t rgb_index = 0;
for (uint8_t i = 0; i < 4; i++) {
rgbw_order[i] = i == white ? 'W' : rgb_order[rgb_index++];
}
rgbw_order[4] = '\0';
ESP_LOGCONFIG(TAG, " RGBW Order: %s", rgbw_order);
} else {
ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order);
}
char channel_colors[5];
ESP_LOGCONFIG(TAG,
" Channel colors: %s\n"
" Max refresh rate: %" PRIu32 "\n"
" Number of LEDs: %u",
this->max_refresh_rate_.value_or(0), this->num_leds_);
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_.value_or(0), this->num_leds_);
}
float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -3,6 +3,7 @@
#ifdef USE_ESP32
#include "esphome/components/light/addressable_light.h"
#include "esphome/components/light/channel_colors.h"
#include "esphome/components/light/light_output.h"
#include "esphome/core/color.h"
#include "esphome/core/component.h"
@@ -15,15 +16,6 @@
namespace esphome::esp32_rmt_led_strip {
enum RGBOrder : uint8_t {
ORDER_RGB,
ORDER_RBG,
ORDER_GRB,
ORDER_GBR,
ORDER_BGR,
ORDER_BRG,
};
struct LedParams {
rmt_symbol_word_t bit0;
rmt_symbol_word_t bit1;
@@ -39,7 +31,7 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
if (this->is_rgbw_ || this->is_wrgb_) {
if (this->channel_colors_.has_white()) {
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
} else {
traits.set_supported_color_modes({light::ColorMode::RGB});
@@ -50,13 +42,7 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_inverted(bool inverted) { this->invert_out_ = inverted; }
void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; }
void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; }
void set_rgbw_order(uint8_t white_index) {
this->is_rgbw_ = true;
this->is_wrgb_ = false;
this->white_index_ = white_index;
}
void set_channel_colors(light::ChannelColors channel_colors) { this->channel_colors_ = channel_colors; }
void set_use_dma(bool use_dma) { this->use_dma_ = use_dma; }
void set_use_psram(bool use_psram) { this->use_psram_ = use_psram; }
@@ -66,7 +52,6 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low,
uint32_t reset_time_high, uint32_t reset_time_low);
void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; }
void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; }
void clear_effect_data() override {
@@ -79,7 +64,7 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
protected:
light::ESPColorView get_view_internal(int32_t index) const override;
size_t get_buffer_size_() const { return this->num_leds_ * (this->is_rgbw_ || this->is_wrgb_ ? 4 : 3); }
size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); }
uint8_t *buf_{nullptr};
uint8_t *effect_data_{nullptr};
@@ -94,15 +79,11 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
uint32_t rmt_symbols_{48};
uint8_t pin_;
uint16_t num_leds_;
bool is_rgbw_{false};
bool is_wrgb_{false};
// An index after the RGB channels makes offset adjustment a no-op for three-channel strips.
uint8_t white_index_{3};
bool use_dma_{false};
bool use_psram_{false};
bool invert_out_{false};
RGBOrder rgb_order_{ORDER_RGB};
light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE};
uint32_t last_refresh_{0};
optional<uint32_t> max_refresh_rate_{};
+13 -52
View File
@@ -1,10 +1,9 @@
from dataclasses import dataclass
import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components import esp32, esp32_rmt, light
from esphome.components.const import CONF_USE_PSRAM
from esphome.components.const import CONF_CHANNEL_COLORS, CONF_IS_WRGB, CONF_USE_PSRAM
from esphome.components.esp32 import include_builtin_idf_component
import esphome.config_validation as cv
from esphome.const import (
@@ -22,8 +21,6 @@ from esphome.const import (
)
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"]
@@ -32,17 +29,6 @@ ESP32RMTLEDStripLightOutput = esp32_rmt_led_strip_ns.class_(
"ESP32RMTLEDStripLightOutput", light.AddressableLight
)
RGBOrder = esp32_rmt_led_strip_ns.enum("RGBOrder")
RGB_ORDERS = {
"RGB": RGBOrder.ORDER_RGB,
"RBG": RGBOrder.ORDER_RBG,
"GRB": RGBOrder.ORDER_GRB,
"GBR": RGBOrder.ORDER_GBR,
"BGR": RGBOrder.ORDER_BGR,
"BRG": RGBOrder.ORDER_BRG,
}
@dataclass
class LEDStripTimings:
@@ -62,8 +48,6 @@ CHIPSETS = {
"SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0),
}
CONF_IS_WRGB = "is_wrgb"
CONF_RGBW_ORDER = "rgbw_order"
CONF_BIT0_HIGH = "bit0_high"
CONF_BIT0_LOW = "bit0_low"
CONF_BIT1_HIGH = "bit1_high"
@@ -72,26 +56,6 @@ CONF_RESET_HIGH = "reset_high"
CONF_RESET_LOW = "reset_low"
def _validate_rgbw_order(value: str) -> str:
value = cv.string(value).upper()
if len(value) != 4 or set(value) != set("RGBW"):
raise cv.Invalid("RGBW order must be a permutation of RGBW")
return value
def _split_rgbw_order(rgbw_order: str) -> tuple[str, int]:
return rgbw_order.replace("W", ""), rgbw_order.index("W")
def _validate_rgbw_order_exclusivity(config: ConfigType) -> ConfigType:
if CONF_RGBW_ORDER in config and (config[CONF_IS_RGBW] or config[CONF_IS_WRGB]):
raise cv.Invalid(
f"'{CONF_RGBW_ORDER}' cannot be used with '{CONF_IS_RGBW}' or "
f"'{CONF_IS_WRGB}'"
)
return config
CONFIG_SCHEMA = cv.All(
esp32.only_on_variant(
unsupported=list(esp32_rmt.VARIANTS_NO_RMT),
@@ -102,8 +66,11 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
cv.Optional(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
cv.Optional(CONF_RGBW_ORDER): _validate_rgbw_order,
cv.Optional(CONF_CHANNEL_COLORS): light.validate_channel_colors,
# Deprecated in favour of CONF_CHANNEL_COLORS, remove in 2027.3.0
cv.Optional(CONF_RGB_ORDER): cv.one_of(*light.RGB_ORDERS, upper=True),
cv.Optional(CONF_IS_RGBW): cv.boolean,
cv.Optional(CONF_IS_WRGB): cv.boolean,
cv.SplitDefault(
CONF_RMT_SYMBOLS,
esp32=192,
@@ -117,8 +84,6 @@ CONFIG_SCHEMA = cv.All(
): cv.int_range(min=2),
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
cv.Optional(CONF_IS_WRGB, default=False): cv.boolean,
cv.Optional(CONF_USE_DMA): cv.All(
esp32.only_on_variant(
supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3]
@@ -153,12 +118,13 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH),
cv.has_exactly_one_key(CONF_RGB_ORDER, CONF_RGBW_ORDER),
_validate_rgbw_order_exclusivity,
light.migrate_channel_colors(
removed_in="2027.3.0", component="esp32_rmt_led_strip"
),
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_rmt")
@@ -198,14 +164,9 @@ async def to_code(config):
)
)
if (rgbw_order := config.get(CONF_RGBW_ORDER)) is not None:
rgb_order, white_index = _split_rgbw_order(rgbw_order)
cg.add(var.set_rgb_order(RGB_ORDERS[rgb_order]))
cg.add(var.set_rgbw_order(white_index))
else:
cg.add(var.set_rgb_order(config[CONF_RGB_ORDER]))
cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
cg.add(
var.set_channel_colors(light.channel_colors_struct(config[CONF_CHANNEL_COLORS]))
)
cg.add(var.set_use_psram(config[CONF_USE_PSRAM]))
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
if CONF_USE_DMA in config:
+3
View File
@@ -113,6 +113,9 @@ def get_download_types(storage_json):
the shape stable so the download panel
doesn't have to special-case per-platform schemas.
"""
# No recorded firmware path means nothing was built; no downloads.
if storage_json.firmware_bin_path is None:
return []
return [
{
"title": "Standard format",
+2 -3
View File
@@ -355,7 +355,7 @@ def _validate(config):
" clk:\n"
" mode: %s\n"
" pin: %s\n"
"Removal scheduled for 2026.9.0.",
"Removal scheduled for 2026.11.0.",
config[CONF_CLK_MODE],
mode,
pin,
@@ -767,7 +767,7 @@ def _final_validate_rmii_pins(config: ConfigType) -> None:
raise cv.Invalid(error_msg, path=pin_path)
def _final_validate(config: ConfigType) -> ConfigType:
def _final_validate(config: ConfigType) -> None:
"""Final validation for Ethernet component."""
# Allow ethernet + wifi coexistence only when both are declared in network: priority:.
if "wifi" in fv.full_config.get():
@@ -787,7 +787,6 @@ def _final_validate(config: ConfigType) -> ConfigType:
_final_validate_spi(config)
_final_validate_rmii_pins(config)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
+1 -2
View File
@@ -60,14 +60,13 @@ CONFIG_SCHEMA = cv.All(
)
def _final_validate(config):
def _final_validate(config) -> None:
if CORE.is_esp8266 and CONF_RESETS_REQUIRED in config:
fconfig = full_config.get()
if not fconfig.get_config_for_path([KEY_ESP8266, CONF_RESTORE_FROM_FLASH]):
raise cv.Invalid(
"'resets_required' needs 'restore_from_flash' to be enabled in the 'esp8266' configuration"
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
+3 -3
View File
@@ -23,6 +23,7 @@ from esphome.components.image import (
get_image_type_enum,
get_transparency_enum,
is_svg_file,
validate_byte_order,
validate_settings,
validate_transparency,
validate_type,
@@ -200,7 +201,7 @@ OPTIONS_SCHEMA = {
"NONE", "FLOYDSTEINBERG", upper=True
),
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
cv.Optional(CONF_BYTE_ORDER): validate_byte_order,
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
}
@@ -225,7 +226,7 @@ def image_schema(class_: MockObjClass = Image_) -> cv.Schema:
)
def validate_image_final(config: ConfigType) -> ConfigType:
def validate_image_final(config: ConfigType) -> None:
"""Per-entry final validation, shared by file-backed image platforms.
For LVGL 9 the default byte order for RGB565 images is little-endian, so
@@ -240,7 +241,6 @@ def validate_image_final(config: ConfigType) -> ConfigType:
)
else:
config[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
return config
async def new_image(config: ConfigType) -> MockObj:
@@ -68,10 +68,10 @@ def _pin_shared_only_with_deep_sleep(pin_num: int) -> bool:
return any(path and path[0] == "deep_sleep" for path, _, _ in pin_users)
def _final_validate(config):
def _final_validate(config) -> None:
use_interrupt = config[CONF_USE_INTERRUPT]
if not use_interrupt:
return config
return
# Expander pins (e.g. PCF8574, MCP23017) don't support direct interrupt
# attachment — only internal/native GPIO pins do.
@@ -82,7 +82,7 @@ def _final_validate(config):
config.get(CONF_NAME, config[CONF_ID]),
)
config[CONF_USE_INTERRUPT] = False
return config
return
pin_num = config[CONF_PIN][CONF_NUMBER]
@@ -96,7 +96,7 @@ def _final_validate(config):
config.get(CONF_NAME, config[CONF_ID]),
)
config[CONF_USE_INTERRUPT] = False
return config
return
# When a pin is shared, interrupts can interfere with other components
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes.
@@ -120,8 +120,6 @@ def _final_validate(config):
pin_num,
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -0,0 +1,22 @@
from esphome import pins
import esphome.config_validation as cv
from esphome.const import CONF_ALLOW_OTHER_USES, CONF_INTERRUPT_PIN, CONF_INVERTED
from esphome.types import ConfigType
def validate_interrupt_pin(value: ConfigType) -> ConfigType:
# The expander components own INT polarity (active-low, hardcoded falling-edge ISR)
# and install a single ISR per GPIO, so neither inversion nor sharing is supported.
value = pins.internal_gpio_input_pin_schema(value)
if value.get(CONF_INVERTED):
raise cv.Invalid(
f"'{CONF_INVERTED}: true' is not supported on '{CONF_INTERRUPT_PIN}'; "
"the expander INT line is fixed active-low"
)
if value.get(CONF_ALLOW_OTHER_USES):
raise cv.Invalid(
f"'{CONF_ALLOW_OTHER_USES}: true' is not supported on '{CONF_INTERRUPT_PIN}'; "
"sharing the interrupt pin between multiple components is not implemented. "
f"Remove the '{CONF_INTERRUPT_PIN}' to fall back to polling."
)
return value
+2 -2
View File
@@ -163,8 +163,8 @@ CONFIG_SCHEMA = (
)
def _final_validate(config: ConfigType) -> ConfigType:
return modbus.final_validate_modbus_device("growatt_solar", role="client")(config)
def _final_validate(config: ConfigType) -> None:
modbus.final_validate_modbus_device("growatt_solar", role="client")(config)
FINAL_VALIDATE_SCHEMA = _final_validate
+1 -2
View File
@@ -424,7 +424,7 @@ async def power_action_to_code(config, action_id, template_arg, args):
return cg.new_Pvariable(action_id, template_arg, paren)
def _final_validate(config):
def _final_validate(config) -> None:
full_config = fv.full_config.get()
if CONF_LOGGER in full_config:
_level = "NONE"
@@ -448,7 +448,6 @@ def _final_validate(config):
raise cv.Invalid(
f"No WiFi configured, if you want to use haier climate without WiFi add {CONF_WIFI_SIGNAL}: false to climate configuration"
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
+1 -2
View File
@@ -60,7 +60,7 @@ CONFIG_SCHEMA = cv.Schema(
)
def _final_validate(config):
def _final_validate(config) -> None:
full_config = fv.full_config.get()
for switch_type in [CONF_BEEPER, CONF_QUIET_MODE]:
# Check switches that are only supported for HonClimate
@@ -72,7 +72,6 @@ def _final_validate(config):
raise cv.Invalid(
f"{switch_type} switch is only supported for hon climate"
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
+2 -2
View File
@@ -217,8 +217,8 @@ CONFIG_SCHEMA = (
)
def _final_validate(config: ConfigType) -> ConfigType:
return modbus.final_validate_modbus_device("havells_solar", role="client")(config)
def _final_validate(config: ConfigType) -> None:
modbus.final_validate_modbus_device("havells_solar", role="client")(config)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -16,7 +16,7 @@
namespace esphome::http_request {
static const char *const TAG = "http_request.arduino";
static const char *const TAG = "http_request";
#ifdef USE_ESP8266
// ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM
static constexpr int ESP8266_SSL_ERR_OOM = -1000;
@@ -14,7 +14,7 @@
namespace esphome::http_request {
static const char *const TAG = "http_request.host";
static const char *const TAG = "http_request";
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method,
const std::string &body,
@@ -16,7 +16,7 @@
namespace esphome::http_request {
static const char *const TAG = "http_request.idf";
static const char *const TAG = "http_request";
static constexpr uint32_t ERROR_DURATION_MS = 1000;
void HttpRequestIDF::dump_config() {
+1 -3
View File
@@ -315,7 +315,7 @@ def _validate_config(config: ConfigType) -> ConfigType:
return config
def _final_validate(config: ConfigType) -> ConfigType:
def _final_validate(config: ConfigType) -> None:
"""Validate requirements when using HUB75 display."""
# Local imports to avoid circular dependencies
from esphome.components.esp32 import get_esp32_variant
@@ -381,8 +381,6 @@ def _final_validate(config: ConfigType) -> ConfigType:
if errs:
raise cv.MultipleInvalid(errs)
return config
FINAL_VALIDATE_SCHEMA = cv.Schema(_final_validate)
+1 -1
View File
@@ -9,7 +9,7 @@
namespace esphome::i2c {
static const char *const TAG = "i2c.arduino";
static const char *const TAG = "i2c";
// Maximum bytes to log in hex format (truncates larger transfers)
static constexpr size_t I2C_MAX_LOG_BYTES = 32;
+1 -1
View File
@@ -12,7 +12,7 @@
namespace esphome::i2c {
static const char *const TAG = "i2c.idf";
static const char *const TAG = "i2c";
// Maximum bytes to log in hex format (truncates larger transfers)
static constexpr size_t I2C_MAX_LOG_BYTES = 32;
+1 -1
View File
@@ -16,7 +16,7 @@
namespace esphome::i2c {
static const char *const TAG = "i2c.host";
static const char *const TAG = "i2c";
HostI2CBus::~HostI2CBus() {
if (this->file_descriptor_ != -1) {
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::i2c {
static const char *const TAG = "i2c.zephyr";
static const char *const TAG = "i2c";
static const char *get_speed(uint32_t dev_config) {
switch (I2C_SPEED_GET(dev_config)) {
+136 -16
View File
@@ -10,7 +10,14 @@ from PIL import Image, UnidentifiedImageError
import esphome.codegen as cg
from esphome.components.const import CONF_BYTE_ORDER, KEY_METADATA
import esphome.config_validation as cv
from esphome.const import CONF_DEFAULTS, CONF_FILE, CONF_ID, CONF_PLATFORM, CONF_TYPE
from esphome.const import (
CONF_DEFAULTS,
CONF_FILE,
CONF_FILES,
CONF_ID,
CONF_PLATFORM,
CONF_TYPE,
)
from esphome.core import CORE
from esphome.types import ConfigType
@@ -48,6 +55,9 @@ TRANSPARENCY_TYPES = (
CONF_ALPHA_CHANNEL,
)
# Shared validator for the image platform schemas and `_drop_incompatible_byte_order`.
validate_byte_order = cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True)
def get_image_type_enum(type):
return getattr(ImageType, f"IMAGE_TYPE_{type.upper()}")
@@ -404,6 +414,120 @@ def get_image_metadata(image_id: str) -> ImageMetaData | None:
return get_all_image_metadata().get(image_id)
# ---------------------------------------------------------------------------
# `defaults:`/`files:` expansion: a `platform:` entry merges shared `defaults:`
# into every `files:` entry; the platform's CONFIG_SCHEMA validates each.
# Permanent, unlike the legacy migration below.
# ---------------------------------------------------------------------------
def _drop_incompatible_byte_order(
merged: dict, explicit: dict, *, index: int | None = None
) -> dict:
"""Drop `byte_order` when the resolved type doesn't support it, unless written directly on `explicit`.
With `index`, inherited values are validated before being dropped (the legacy flattener always drops).
"""
if CONF_BYTE_ORDER in explicit:
return merged
type_class = IMAGE_TYPE.get(str(merged.get(CONF_TYPE, "")).upper())
if (
CONF_BYTE_ORDER in merged
and isinstance(type_class, type)
and issubclass(type_class, ImageEncoder)
and not type_class.is_endian()
):
if index is not None:
try:
validate_byte_order(merged[CONF_BYTE_ORDER])
except cv.Invalid as exc:
exc.prepend([index])
raise
del merged[CONF_BYTE_ORDER]
return merged
def _expand_platform_entry(index: int, entry: dict) -> list[dict]:
if CONF_FILES not in entry:
if CONF_DEFAULTS in entry:
raise cv.Invalid(
f"'{CONF_DEFAULTS}' may only be used together with '{CONF_FILES}'",
path=[index],
)
return [entry]
extra_keys = set(entry) - {CONF_PLATFORM, CONF_DEFAULTS, CONF_FILES}
if extra_keys:
raise cv.Invalid(
f"'{CONF_FILES}' cannot be combined with "
f"{', '.join(sorted(extra_keys))} on the same entry",
path=[index],
)
files = entry[CONF_FILES]
if files is None:
raise cv.Invalid(f"'{CONF_FILES}' must not be empty", path=[index])
if not isinstance(files, list):
raise cv.Invalid(f"'{CONF_FILES}' must be a list", path=[index])
if not files:
raise cv.Invalid(f"'{CONF_FILES}' must not be empty", path=[index])
defaults = entry.get(CONF_DEFAULTS, {})
if defaults is None:
defaults = {}
if not isinstance(defaults, dict):
raise cv.Invalid(f"'{CONF_DEFAULTS}' must be a mapping", path=[index])
# Neither `id:` nor `platform:` makes sense inside `defaults:`.
for disallowed in (CONF_ID, CONF_PLATFORM):
if disallowed in defaults:
raise cv.Invalid(
f"'{disallowed}' is not allowed inside '{CONF_DEFAULTS}'",
path=[index],
)
from esphome import yaml_util
platform = entry[CONF_PLATFORM]
result: list[dict] = []
for file_entry in files:
if not isinstance(file_entry, dict):
raise cv.Invalid(
f"each entry in '{CONF_FILES}' must be a mapping", path=[index]
)
# The platform is chosen by the entry's own `platform:` key, not per file.
if CONF_PLATFORM in file_entry:
raise cv.Invalid(
f"'{CONF_PLATFORM}' is not allowed inside '{CONF_FILES}'",
path=[index],
)
# Keep the `files:` item's source range so whole-entry errors anchor there;
# `make_data_base` needs a real ESPHomeDataBase, so skip it for plain dicts.
source = (
file_entry if isinstance(file_entry, yaml_util.ESPHomeDataBase) else None
)
merged = yaml_util.make_data_base(
{CONF_PLATFORM: platform, **defaults, **file_entry}, source
)
result.append(_drop_incompatible_byte_order(merged, file_entry, index=index))
return result
def expand_platform_config(config: list) -> list:
"""Expand `defaults:`/`files:` entries; the platform's own CONFIG_SCHEMA validates each result."""
result = []
for i, entry in enumerate(config):
if isinstance(entry, dict) and CONF_PLATFORM in entry:
result.extend(_expand_platform_entry(i, entry))
else:
result.append(entry)
return result
EXPAND_PLATFORM_CONFIG = expand_platform_config
# --------------------- end defaults/files expansion -------------------------
# ---------------------------------------------------------------------------
# Legacy top-level component -> `image:` platform deprecation helpers
# -- REMOVE after 2027.1.0 together with the `animation:`/`online_image:` shims.
@@ -496,11 +620,17 @@ def _is_legacy_image_format(config: object) -> bool:
proper error instead of the migration silently dropping the input.
"""
if isinstance(config, list):
# A bare list of (not-yet-platform-tagged) image dicts.
# Exclude `files:` entries -- the list branch would otherwise silently
# migrate them to `platform: file` instead of raising the missing-platform error.
return bool(config) and all(
isinstance(entry, dict) and CONF_PLATFORM not in entry for entry in config
isinstance(entry, dict)
and CONF_PLATFORM not in entry
and CONF_FILES not in entry
for entry in config
)
if not isinstance(config, dict):
if not isinstance(config, dict) or CONF_PLATFORM in config or CONF_FILES in config:
# `platform:`/`files:` dicts are new-format (left for list-wrapping +
# expansion); the legacy flattener has no `files:` branch and would drop them.
return False
# A single image dict, or the grouped `defaults:`/`images:`/type-key form.
return (
@@ -532,18 +662,8 @@ def _flatten_legacy_image_config(config: object) -> list[dict]:
def _add(entry: dict, extra: dict) -> None:
merged = {**defaults, **extra, **entry}
# The legacy `defaults:`/type-grouped forms only applied `byte_order` to
# types that support it. Replicate that so an endian default merged into
# e.g. a binary image stays valid.
type_class = IMAGE_TYPE.get(str(merged.get(CONF_TYPE, "")).upper())
if (
CONF_BYTE_ORDER in merged
and isinstance(type_class, type)
and issubclass(type_class, ImageEncoder)
and not type_class.is_endian()
):
del merged[CONF_BYTE_ORDER]
result.append(merged)
# Always drop, matching the pre-platform behavior -- see `_drop_incompatible_byte_order`.
result.append(_drop_incompatible_byte_order(merged, {}))
def _add_entries(entries: object, extra: dict) -> None:
# `entries` may be a single image dict or a list of them; non-dict
+1 -2
View File
@@ -22,7 +22,7 @@ CONFIG_SCHEMA = (
)
def validate_logger(config):
def validate_logger(config) -> None:
logger_conf = fv.full_config.get()[CONF_LOGGER]
if logger_conf[CONF_BAUD_RATE] == 0:
raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0")
@@ -33,7 +33,6 @@ def validate_logger(config):
raise cv.Invalid(
"improv_serial does not support the selected logger hardware_uart"
)
return config
FINAL_VALIDATE_SCHEMA = validate_logger
+6 -2
View File
@@ -154,8 +154,12 @@ bool Infrared::on_receive(remote_base::RemoteReceiveData data) {
// Forward received IR data to API server
#if defined(USE_API) && defined(USE_IR_RF)
if (api::global_api_server != nullptr) {
api::global_api_server->send_infrared_rf_receive_event(this->get_device_id_or_zero(), this->get_entity_key(),
&data.get_raw_data());
#ifdef USE_DEVICES
uint32_t device_id = this->get_device_id();
#else
uint32_t device_id = 0;
#endif
api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data());
}
#endif
return false; // Don't consume the event, allow other listeners to process it
+1 -2
View File
@@ -146,13 +146,12 @@ CONFIG_SCHEMA = cv.All(
)
def _validate_cpu_frequency(config):
def _validate_cpu_frequency(config) -> None:
esp32_config = fv.full_config.get()[PLATFORM_ESP32]
if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ":
raise cv.Invalid(
"Inkplate requires 240MHz CPU frequency (set in esp32 component)"
)
return config
FINAL_VALIDATE_SCHEMA = _validate_cpu_frequency
@@ -9,7 +9,7 @@ uint32_t temp_single_get_current_temperature(uint32_t *temp_value);
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.bk72xx";
static const char *const TAG = "internal_temperature";
void InternalTemperatureSensor::update() {
float temperature = NAN;
@@ -16,7 +16,7 @@ uint8_t temprature_sens_read();
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.esp32";
static const char *const TAG = "internal_temperature";
void InternalTemperatureSensor::update() {
float temperature = NAN;
@@ -16,7 +16,7 @@
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.rp2";
static const char *const TAG = "internal_temperature";
// The on-die temperature sensor sits on the last ADC channel: input 4 on RP2040
// and RP2350A, but input 8 on RP2350B, which has eight external channels rather
@@ -8,7 +8,7 @@
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.zephyr";
static const char *const TAG = "internal_temperature";
static const struct device *const DIE_TEMPERATURE_SENSOR = DEVICE_DT_GET_ONE(nordic_nrf_temp);
+1 -2
View File
@@ -336,7 +336,7 @@ def _customise_schema(config):
CONFIG_SCHEMA = _customise_schema
def _final_validate(config):
def _final_validate(config) -> None:
# IT8951 reads from SPI (DevInfo, VCOM, register reads) so MISO is required.
spi.final_validate_device_schema("it8951", require_miso=True, require_mosi=True)(
config
@@ -351,7 +351,6 @@ def _final_validate(config):
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
config[CONF_SHOW_TEST_CARD] = True
return config
FINAL_VALIDATE_SCHEMA = _final_validate
+2 -2
View File
@@ -89,8 +89,8 @@ CONFIG_SCHEMA = (
)
def _final_validate(config: ConfigType) -> ConfigType:
return modbus.final_validate_modbus_device("kuntze", role="client")(config)
def _final_validate(config: ConfigType) -> None:
modbus.final_validate_modbus_device("kuntze", role="client")(config)
FINAL_VALIDATE_SCHEMA = _final_validate
-2
View File
@@ -184,8 +184,6 @@ static int32_t get_firmware_int(const char *version_string) {
return result;
}
float LD2420Component::get_setup_priority() const { return setup_priority::BUS; }
void LD2420Component::dump_config() {
ESP_LOGCONFIG(TAG,
"LD2420:\n"
-1
View File
@@ -105,7 +105,6 @@ class LD2420Component final : public Component, public uart::UARTDevice {
void apply_config_action();
void factory_reset_action();
void revert_config_action();
float get_setup_priority() const override;
int send_cmd_from_array(CmdFrameT cmd_frame);
void report_gate_data();
void handle_cmd_error(uint16_t error);
@@ -84,7 +84,7 @@ CONFIG_SCHEMA = cv.Schema(
)
def final_validate(config: ConfigType) -> ConfigType:
def final_validate(config: ConfigType) -> None:
full_config = fv.full_config.get()
hub_id = config[CONF_LD6002B_ID]
@@ -108,8 +108,6 @@ def final_validate(config: ConfigType) -> ConfigType:
path=[CONF_WAKE],
)
return config
FINAL_VALIDATE_SCHEMA = final_validate
@@ -105,9 +105,9 @@ CONFIG_SCHEMA = cv.Schema(
)
def final_validate(config: ConfigType) -> ConfigType:
def final_validate(config: ConfigType) -> None:
if config.get(CONF_AREA_CONFIG) is None:
return config
return
full_config = fv.full_config.get()
hub_id = config[CONF_LD6002B_ID]
@@ -132,8 +132,6 @@ def final_validate(config: ConfigType) -> ConfigType:
path=[CONF_AREA_CONFIG],
)
return config
FINAL_VALIDATE_SCHEMA = final_validate
+3
View File
@@ -182,6 +182,9 @@ def get_download_types(storage_json: StorageJSON = None):
the shape stable so the download panel
doesn't have to special-case per-platform schemas.
"""
# No recorded firmware path means nothing was built; no downloads.
if storage_json.firmware_bin_path is None:
return []
types = [
{
"title": "UF2 package (recommended)",
+108 -4
View File
@@ -1,9 +1,12 @@
from collections.abc import Callable
from dataclasses import dataclass, field
import enum
import logging
import esphome.automation as auto
import esphome.codegen as cg
from esphome.components import mqtt, power_supply, web_server
from esphome.components.const import CONF_CHANNEL_COLORS, CONF_IS_WRGB
import esphome.config_validation as cv
from esphome.const import (
CONF_BLUE,
@@ -23,6 +26,7 @@ from esphome.const import (
CONF_ICON,
CONF_ID,
CONF_INITIAL_STATE,
CONF_IS_RGBW,
CONF_MQTT_ID,
CONF_NAME,
CONF_ON_STATE,
@@ -32,6 +36,7 @@ from esphome.const import (
CONF_POWER_SUPPLY,
CONF_RED,
CONF_RESTORE_MODE,
CONF_RGB_ORDER,
CONF_STATE,
CONF_TRIGGER_ID,
CONF_WARM_WHITE,
@@ -61,6 +66,7 @@ from .effects import (
from .types import ( # noqa: F401
AddressableLight,
AddressableLightState,
ChannelColors,
ColorMode,
LightOutput,
LightState,
@@ -71,6 +77,8 @@ from .types import ( # noqa: F401
light_ns,
)
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
@@ -165,7 +173,105 @@ def available_effects_str(effects: list) -> str:
return ", ".join(f"'{name}'" for name in available) if available else "none"
def _final_validate(config: ConfigType) -> ConfigType:
# Accepted values of the deprecated `rgb_order` key.
RGB_ORDERS = ("RGB", "RBG", "GRB", "GBR", "BGR", "BRG")
_RGB_CHANNELS = frozenset("RGB")
_RGBW_CHANNELS = frozenset("RGBW")
def validate_channel_colors(value: str) -> str:
"""Validate the channel order of an addressable strip, e.g. "GRB" or "WRGB"."""
value = cv.string_strict(value).upper()
channels = frozenset(value)
if len(channels) != len(value) or channels not in (_RGB_CHANNELS, _RGBW_CHANNELS):
raise cv.Invalid(
f"'{value}' is not a valid channel order. List each of R, G and B exactly "
"once, optionally with a single W, in the order the strip expects them "
"(for example GRB, GRBW or WRGB)"
)
return value
def channel_colors_struct(value: str) -> cg.StructInitializer:
"""Build the C++ `light::ChannelColors` for a validated channel order string."""
return cg.StructInitializer(
ChannelColors,
("r", value.index("R")),
("g", value.index("G")),
("b", value.index("B")),
(
"w",
value.index("W")
if "W" in value
else cg.RawExpression(f"{ChannelColors}::NO_WHITE"),
),
)
def _quote_and_join(keys: list[str]) -> str:
"""Quote each key and join them into a readable list, e.g. "'a', 'b' and 'c'"."""
quoted = [f"'{key}'" for key in keys]
if len(quoted) == 1:
return quoted[0]
return f"{', '.join(quoted[:-1])} and {quoted[-1]}"
def migrate_channel_colors(
*, removed_in: str, component: str
) -> Callable[[ConfigType], ConfigType]:
"""Fold the deprecated `rgb_order`, `is_rgbw` and `is_wrgb` keys into `channel_colors`.
This also enforces that `channel_colors` is set, which the schema cannot do on its
own while the deprecated keys are still accepted. After this runs, `to_code` only
ever sees `channel_colors`.
"""
def validator(config: ConfigType) -> ConfigType:
config = config.copy()
deprecated = [
key for key in (CONF_RGB_ORDER, CONF_IS_RGBW, CONF_IS_WRGB) if key in config
]
if CONF_CHANNEL_COLORS in config:
if deprecated:
raise cv.Invalid(
f"'{CONF_CHANNEL_COLORS}' cannot be combined with "
f"{_quote_and_join(deprecated)}"
)
return config
if CONF_RGB_ORDER not in config:
raise cv.Invalid(
f"'{CONF_CHANNEL_COLORS}' is required", path=[CONF_CHANNEL_COLORS]
)
rgb_order = config.pop(CONF_RGB_ORDER)
is_rgbw = config.pop(CONF_IS_RGBW, False)
is_wrgb = config.pop(CONF_IS_WRGB, False)
if is_rgbw and is_wrgb:
raise cv.Invalid(
f"'{CONF_IS_RGBW}' and '{CONF_IS_WRGB}' cannot both be enabled"
)
if is_wrgb:
channel_colors = f"W{rgb_order}"
elif is_rgbw:
channel_colors = f"{rgb_order}W"
else:
channel_colors = rgb_order
_LOGGER.warning(
"[%s] %s %s deprecated, use '%s: %s'. Will be removed in %s",
component,
_quote_and_join(deprecated),
"are" if len(deprecated) > 1 else "is",
CONF_CHANNEL_COLORS,
channel_colors,
removed_in,
)
config[CONF_CHANNEL_COLORS] = channel_colors
return config
return validator
def _final_validate(config: ConfigType) -> None:
"""Validate all recorded effect name references against their target lights.
This runs once per light platform instance. If no light platform is configured,
@@ -173,7 +279,7 @@ def _final_validate(config: ConfigType) -> ConfigType:
"""
data = _get_data()
if not data.effect_refs and not data.effect_cycle_refs:
return config
return
# Drain the lists so we only validate once even though
# FINAL_VALIDATE_SCHEMA runs for each light platform instance.
@@ -217,8 +323,6 @@ def _final_validate(config: ConfigType) -> ConfigType:
path=[cv.ROOT_CONFIG_PATH] + ref.component_path,
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <cstdint>
namespace esphome::light {
/// Which byte of an addressable LED's data carries each colour.
///
/// Built from a configuration string such as "GRB" or "WRGB": every field holds the
/// position that colour occupies in the bytes the strip expects. `w` is NO_WHITE when
/// the strip has no separate white channel.
struct ChannelColors {
/// Value of `w` for a strip that only has red, green and blue channels.
static constexpr uint8_t NO_WHITE = 0xFF;
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t w;
bool has_white() const { return this->w != NO_WHITE; }
uint8_t bytes_per_led() const { return this->has_white() ? 4 : 3; }
/// Write the order back out as text, e.g. "GRBW".
///
/// `buf` must have room for at least 5 characters. Returns `buf` so the result can be
/// passed straight to a log call.
const char *to_string(char *buf) const {
buf[this->r] = 'R';
buf[this->g] = 'G';
buf[this->b] = 'B';
if (this->has_white()) {
buf[this->w] = 'W';
}
buf[this->bytes_per_led()] = '\0';
return buf;
}
};
} // namespace esphome::light
+3
View File
@@ -16,6 +16,9 @@ LightColorValues = light_ns.class_("LightColorValues")
LightStateRTCState = light_ns.struct("LightStateRTCState")
LightCall = light_ns.class_("LightCall")
# Addressable strips
ChannelColors = light_ns.struct("ChannelColors")
# Color modes
ColorMode = light_ns.enum("ColorMode", is_class=True)
COLOR_MODES = {
+1 -1
View File
@@ -444,7 +444,7 @@ LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_r
lv_indev_set_type(this->drv_, LV_INDEV_TYPE_POINTER);
lv_indev_set_disp(this->drv_, parent->get_disp());
lv_indev_set_long_press_time(this->drv_, long_press_time);
// long press repeat time TBD
lv_indev_set_long_press_repeat_time(this->drv_, long_press_repeat_time);
lv_indev_set_user_data(this->drv_, this);
lv_indev_set_read_cb(this->drv_, [](lv_indev_t *d, lv_indev_data_t *data) {
auto *l = static_cast<LVTouchListener *>(lv_indev_get_user_data(d));
+1 -2
View File
@@ -1,3 +1,4 @@
from esphome.components.const import CONF_LABEL
import esphome.config_validation as cv
from esphome.const import CONF_TEXT
@@ -14,8 +15,6 @@ from ..schemas import TEXT_SCHEMA
from ..types import LvText
from . import Widget, WidgetType
CONF_LABEL = "label"
class LabelType(WidgetType):
def __init__(self):
+2 -2
View File
@@ -1,6 +1,6 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components import gpio_expander, i2c
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
@@ -25,7 +25,7 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(MCP23016),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
}
)
.extend(cv.COMPONENT_SCHEMA)
+2 -20
View File
@@ -1,8 +1,8 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import gpio_expander
import esphome.config_validation as cv
from esphome.const import (
CONF_ALLOW_OTHER_USES,
CONF_ID,
CONF_INPUT,
CONF_INTERRUPT,
@@ -32,28 +32,10 @@ MCP23XXX_INTERRUPT_MODES = {
}
def _validate_interrupt_pin(value):
# The MCP component owns INT polarity (active-low, hardcoded falling-edge ISR)
# and installs a single ISR per GPIO, so neither inversion nor sharing is supported.
value = pins.internal_gpio_input_pin_schema(value)
if value.get(CONF_INVERTED):
raise cv.Invalid(
f"'{CONF_INVERTED}: true' is not supported on '{CONF_INTERRUPT_PIN}'; "
"the MCP23xxx INT line is fixed active-low"
)
if value.get(CONF_ALLOW_OTHER_USES):
raise cv.Invalid(
f"'{CONF_ALLOW_OTHER_USES}: true' is not supported on '{CONF_INTERRUPT_PIN}'; "
"sharing the interrupt pin between multiple MCP23xxx (or other components) "
"is not implemented. Remove the interrupt_pin to fall back to polling."
)
return value
MCP23XXX_CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean,
cv.Optional(CONF_INTERRUPT_PIN): _validate_interrupt_pin,
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
}
).extend(cv.COMPONENT_SCHEMA)
@@ -34,7 +34,7 @@ CONF_NONVOLATILE_WRITE_DELAY = "nonvolatile_write_delay"
VOLATILE_CHANNELS = ("A", "B", "C", "D")
def _validate_nonvolatile(config):
def _validate_nonvolatile(config) -> None:
channel = str(config[CONF_CHANNEL])
# Channels E-H address the nonvolatile registers directly — the mirroring options only
@@ -49,7 +49,7 @@ def _validate_nonvolatile(config):
f"enabling '{CONF_NONVOLATILE}' or setting '{CONF_NONVOLATILE_WRITE_DELAY}' is only valid for the "
f"volatile channels A-D; channels E-H are the nonvolatile registers themselves"
)
return config
return
config.setdefault(CONF_NONVOLATILE, True)
if config[CONF_NONVOLATILE]:
@@ -62,7 +62,6 @@ def _validate_nonvolatile(config):
raise cv.Invalid(
f"'{CONF_NONVOLATILE_WRITE_DELAY}' requires '{CONF_NONVOLATILE}: true'"
)
return config
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
+2 -3
View File
@@ -62,7 +62,7 @@ def _consume_mdns_sockets(config: ConfigType) -> ConfigType:
return config
def _require_network_interface(config: ConfigType) -> ConfigType:
def _require_network_interface(config: ConfigType) -> None:
"""Require a network interface for mDNS on Arduino/LEAmDNS platforms.
On ESP8266 and RP2040 the C++ implementation needs at least one IP state
@@ -71,7 +71,7 @@ def _require_network_interface(config: ConfigType) -> ConfigType:
that never initializes.
"""
if config.get(CONF_DISABLED) or not (CORE.is_esp8266 or CORE.is_rp2):
return config
return
full_config = fv.full_config.get()
has_wifi = "wifi" in full_config
has_ethernet = CORE.is_rp2 and "ethernet" in full_config
@@ -81,7 +81,6 @@ def _require_network_interface(config: ConfigType) -> ConfigType:
"mdns on this platform requires a network interface — "
f"add a {options} component to your configuration."
)
return config
CONFIG_SCHEMA = cv.All(
+1 -2
View File
@@ -175,7 +175,7 @@ def _config_schema(config):
return config
def _final_validate(config):
def _final_validate(config) -> None:
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
@@ -183,7 +183,6 @@ def _final_validate(config):
if not requires_buffer(config) and LVGL_DOMAIN not in global_config:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
return config
CONFIG_SCHEMA = _config_schema
+1 -2
View File
@@ -248,7 +248,7 @@ def _config_schema(config):
CONFIG_SCHEMA = _config_schema
def _final_validate(config):
def _final_validate(config) -> None:
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
@@ -260,7 +260,6 @@ def _final_validate(config):
config = spi.final_validate_device_schema(
"mipi_rgb", require_miso=False, require_mosi=True
)(config)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -143,11 +143,11 @@ def CONFIG_SCHEMA(config: ConfigType) -> ConfigType:
# Legacy climate-owned hub compatibility. Remove in 2027.2.0.
def _legacy_final_validate(config: ConfigType) -> ConfigType:
def _legacy_final_validate(config: ConfigType) -> None:
if CONF_MITSUBISHI_CN105_ID in config:
return config
return
return uart.final_validate_device_schema(
uart.final_validate_device_schema(
DOMAIN,
require_rx=True,
require_tx=True,
+22 -11
View File
@@ -219,14 +219,25 @@ void ModbusServerHub::parse_modbus_frames() {
this->clear_rx_buffer_(LOG_STR("timeout after partial response"), true);
}
uint16_t Modbus::find_custom_frame_end_(uint16_t min_length) const {
// Custom functions could be any length - we have to rely on the CRC to determine completeness.
uint16_t Modbus::find_frame_end_by_crc_(uint16_t min_length) const {
// Unknown-length functions (user-defined codes, unimplemented management codes, unassigned values)
// could be any length - we have to rely on the CRC to determine completeness.
// If a CRC match is never found, the buffer will eventually overflow and be cleared.
const uint8_t *raw = &this->rx_buffer_[0];
const size_t size = this->rx_buffer_.size();
for (uint16_t len = min_length; len <= std::min(size, size_t(MAX_FRAME_SIZE)); len++) {
if (crc16(raw, len) == 0)
return len;
const auto max_len = static_cast<uint16_t>(std::min(size, size_t(MAX_FRAME_SIZE)));
if (min_length > max_len)
return 0;
// The Modbus CRC (poly 0xa001, refin/refout false) keeps its running state in the returned value,
// so we seed once over the first min_length bytes and extend one byte at a time instead of
// recomputing the whole prefix for every candidate length.
uint16_t crc = crc16(raw, min_length);
if (crc == 0)
return min_length;
for (uint16_t len = min_length; len < max_len; len++) {
crc = crc16(&raw[len], 1, crc);
if (crc == 0)
return len + 1;
}
return 0;
}
@@ -241,11 +252,11 @@ bool Modbus::parse_modbus_server_frame_() {
uint8_t address = this->rx_buffer_[0];
uint8_t function_code = this->rx_buffer_[1];
if (helpers::is_function_code_custom(function_code)) {
frame_length = this->find_custom_frame_end_(frame_length);
if (helpers::is_function_code_unknown_length(function_code)) {
frame_length = this->find_frame_end_by_crc_(frame_length);
if (frame_length == 0)
return size < MAX_FRAME_SIZE; // Continue to parse until we hit max size
ESP_LOGD(TAG, "User-defined function %02X found", function_code);
ESP_LOGD(TAG, "Unknown-length function %02X found", function_code);
} else {
if (crc16(&this->rx_buffer_[0], frame_length) != 0)
return false;
@@ -272,11 +283,11 @@ bool ModbusServerHub::parse_modbus_client_frame_() {
uint8_t address = this->rx_buffer_[0];
uint8_t function_code = this->rx_buffer_[1];
if (helpers::is_function_code_custom(function_code)) {
frame_length = this->find_custom_frame_end_(frame_length);
if (helpers::is_function_code_unknown_length(function_code)) {
frame_length = this->find_frame_end_by_crc_(frame_length);
if (frame_length == 0)
return size < MAX_FRAME_SIZE; // Continue to parse until we hit max size
ESP_LOGD(TAG, "User-defined function %02X found", function_code);
ESP_LOGD(TAG, "Unknown-length function %02X found", function_code);
} else {
if (crc16(&this->rx_buffer_[0], frame_length) != 0)
return false;
+1 -1
View File
@@ -82,7 +82,7 @@ class Modbus : public uart::UARTDevice, public Component {
bool send_frame_(const ModbusFrame &frame);
// Scans forward from min_length to find a frame boundary by CRC match for custom function codes.
// Returns the matched frame length, or 0 if no valid CRC was found within MAX_FRAME_SIZE.
uint16_t find_custom_frame_end_(uint16_t min_length) const;
uint16_t find_frame_end_by_crc_(uint16_t min_length) const;
uint32_t last_modbus_byte_{0};
uint32_t last_receive_check_{0};

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