Compare commits

...

615 Commits

Author SHA1 Message Date
kbx81
f8bec0813d fix 2026-03-13 16:48:56 -05:00
kbx81
84762e6ae0 oops 2026-03-13 16:46:13 -05:00
kbx81
2edf313ee3 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-03-13 16:45:23 -05:00
Thomas SAMTER
1eed1adfa0 [pid] Replace std::deque with FixedRingBuffer (#14733)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-13 11:38:45 -10:00
J. Nick Koston
a6c08576be [sensor] Use FixedRingBuffer in SlidingWindowFilter, add window_size limit (#14736) 2026-03-13 10:17:40 -10:00
dependabot[bot]
f41aa8b18c Bump ruff from 0.15.5 to 0.15.6 (#14774)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-13 19:35:10 +00:00
Jonathan Swoboda
6700347a48 [wifi] Fix ESP-IDF 6.0 compatibility (#14766)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:47:12 -04:00
Jonathan Swoboda
b147830ef9 [core] Fix std::isnan conflict with picolibc on ESP-IDF 6.0 (#14768)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:24:39 -04:00
J. Nick Koston
bd844fcd0a [template] Fix misleading 'Text value too long to save' warning (#14753) 2026-03-13 07:37:44 -10:00
J. Nick Koston
8936be628f [api] Increase log Nagle coalescing on all platforms except ESP8266 (#14752) 2026-03-13 07:37:30 -10:00
J. Nick Koston
5920fa97e4 [select] Fix -Wmaybe-uninitialized warnings on ESP8266 (#14759) 2026-03-13 09:20:50 -04:00
Kjell Braden
326769e43c [runtime_image] fix BMP parsing (#14762) 2026-03-13 09:18:42 -04:00
Thomas SAMTER
7524590bcf [const] Add CONF_CLIMATE_ID for climate component sub-entities (#14764) 2026-03-13 09:17:11 -04:00
Michael Kerscher
15ec46abfe [vbus] add DeltaSol CS4 (Citrin Solar 1.3) (#12477) 2026-03-12 22:31:16 -07:00
J. Nick Koston
920af91db6 [rp2040] Fix compiler warnings in crash_handler and mdns (#14739) 2026-03-13 01:37:46 +00:00
J. Nick Koston
a744261934 [mdns] Fix RP2040 mDNS not restarting after WiFi reconnect (#14737)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-13 01:12:22 +00:00
J. Nick Koston
59c1368440 [i2c] Fix RP2040 I2C bus selection based on pin assignment (#14745) 2026-03-12 14:53:46 -10:00
J. Nick Koston
7e8e085a04 [light] Fix binary light spamming 'brightness not supported' warning with strobe effect (#14735) 2026-03-12 14:49:07 -10:00
J. Nick Koston
22b25724ae [wifi] Reject EAP/WPA2 Enterprise config on unsupported platforms (#14746) 2026-03-12 14:48:55 -10:00
J. Nick Koston
89719cf4b2 [water_heater] Set OPERATION_MODE feature flag when modes are configured (#14748) 2026-03-12 14:48:41 -10:00
J. Nick Koston
e15b19b223 [captive_portal] Fix captive portal inaccessible when web_server auth is configured (#14734)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 14:48:29 -10:00
J. Nick Koston
2ca13972b9 [debug] Fix missing reset reason for RP2040/RP2350 (#14740) 2026-03-12 14:48:06 -10:00
J. Nick Koston
7bb4e75459 [rp2040] Use full flash for sketch in testing mode (#14747)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:47:16 -10:00
J. Nick Koston
fd8e510745 [light] Fix ambiguous set_effect overload for const char* (#14732) 2026-03-12 18:28:25 -05:00
Brian Kaufman
25c74c8f99 [OTA] Stage exact uploaded size for ESP8266 web OTA (gzip fix) (#14741) 2026-03-12 13:23:29 -10:00
J. Nick Koston
05d285ba86 [api] Fix heap-buffer-overflow in protobuf message dump for StringRef (#14721) 2026-03-12 07:16:53 -10:00
J. Nick Koston
186ca4e458 [uart] Allow hardware UART with single pin on RP2040 (#14725) 2026-03-12 07:16:38 -10:00
J. Nick Koston
618312f0ee [api] Fix undefined behavior in noise handshake with empty rx buffer (#14722) 2026-03-12 07:16:23 -10:00
J. Nick Koston
70d188202a [adc] Fix PICO_VSYS_PIN compile error on RP2350 boards (#14724) 2026-03-12 07:16:08 -10:00
J. Nick Koston
4a21afe7ce [ota][socket] Fix ESP8266/RP2040 OTA timeout by using SO_RCVTIMEO instead of polling (#14675) 2026-03-12 07:15:48 -10:00
J. Nick Koston
fd1d016795 [time] Fix settimeofday() failure on ESP8266 (#14707)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 07:15:34 -10:00
J. Nick Koston
03c091adfc [esp32_ble_client] Fix disconnect race that causes stuck connections (#14211)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:15:21 -10:00
J. Nick Koston
a3a88acfcf [socket] Fast path for TCP_NODELAY bypasses lwip_setsockopt overhead (#14693) 2026-03-12 07:15:04 -10:00
J. Nick Koston
07f8ae6c82 [socket] Fix use-after-free in LWIP PCB close/abort path (#14706) 2026-03-12 07:14:49 -10:00
Matthias König
25c30ac5bb [mqtt] Fixed permission denied error for client certificates on Windows (#13525)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-12 12:00:08 -04:00
guillempages
a76767a0ab [runtime_image] Update jpegdec lib version (#14726)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-12 10:15:20 -04:00
Kevin Ahrendt
511d185772 [audio] Bump microOpus to v0.3.5 (#14727) 2026-03-12 08:56:01 -04:00
Brian Kaufman
c4c19c8a6c [web_server] use DETAIL_ALL in update_all_json_generator (#14711) 2026-03-11 23:07:26 -10:00
Massimo Antonello
fe2d60ccec [one_wire] allow changing address at runtime (#12150) 2026-03-12 01:52:58 -07:00
Keith Burzinski
657890695f [ledc] Fix high-pressure crash & recovery (#14720) 2026-03-12 03:16:02 -05:00
Adam DeMuri
8a5f008aee [modbus] Fix buffer overflow in modbus (#14719)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-11 22:00:26 -10:00
J. Nick Koston
f8a22b87b8 [rp2040] Fix crash handler design flaws (#14716) 2026-03-12 18:23:01 +13:00
Keith Burzinski
7f38d95424 [ethernet] ESP32-S3 Ethernet compilation fix (#14717) 2026-03-11 23:48:27 -05:00
Javier Peletier
bb7d96b954 [const] Add UNIT_METER_PER_SECOND, UNIT_MILLILITRE, UNIT_POUND to const.py (#14713) 2026-03-11 16:31:17 -10:00
J. Nick Koston
8daa946afa [esp32] Add crash handler to capture and report backtrace across reboots (#14709) 2026-03-12 14:00:20 +13:00
Keith Burzinski
ddc40f44fa [ethernet] ESP32-P4 Ethernet compilation fix (#14714)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-11 19:56:25 -05:00
Jonathan Swoboda
409640c0ee [esp32_hosted] Bump esp_hosted to 2.12.1 (#14708)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:30:44 -04:00
Jesse Hills
822c9161c6 Merge branch 'beta' into dev 2026-03-12 09:15:50 +13:00
Jesse Hills
ad198fd77b Merge pull request #14702 from esphome/bump-2026.3.0b1
2026.3.0b1
2026-03-12 09:15:11 +13:00
dependabot[bot]
a060f175ad Bump actions/download-artifact from 8.0.0 to 8.0.1 (#14705)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 09:28:46 -10:00
dependabot[bot]
73f305ff9c Bump tornado from 6.5.4 to 6.5.5 (#14704)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 09:28:19 -10:00
Jesse Hills
b6ff7185e7 [ci] Dont run codeowners workflows on release or beta PRs (#14703) 2026-03-12 08:04:07 +13:00
J. Nick Koston
928f6f1866 [ci] Add PR title check for unescaped angle brackets (#14701) 2026-03-12 07:57:43 +13:00
Jesse Hills
02f7aee680 Bump version to 2026.3.0b1 2026-03-12 07:34:53 +13:00
Jesse Hills
e7c3277eeb Bump version to 2026.4.0-dev 2026-03-12 07:34:53 +13:00
Kevin Ahrendt
bef5e4de9c [speaker_source] Add announcement pipeline (#14654) 2026-03-11 08:29:17 -10:00
Jonathan Swoboda
04bcd9f56b [dashboard] Use sys.executable for dashboard subprocess commands (#14698)
Co-authored-by: Jonathan Swoboda <swoboda1337@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-11 14:25:36 -04:00
Jonathan Swoboda
03c0ce704b Bump pyupgrade to v3.21.2 for Python 3.14 compatibility (#14699)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 08:22:48 -10:00
Kevin Ahrendt
b27165a842 [speaker_source] Add shuffle support (#14653) 2026-03-11 08:11:00 -10:00
Big Mike
3d4ebe74ce [sensirion_common] Use SmallBufferWithHeapFallback helper (#14270)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-11 08:00:42 -10:00
Kevin Ahrendt
4e16f270a3 [speaker_source] Add playlist management (#14652) 2026-03-11 07:47:58 -10:00
Jonathan Swoboda
c52a48ed38 [multiple] Convert static function locals to member variables (#14689)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-03-11 07:11:46 +00:00
Robert Resch
236f6b1935 [micronova] Add command queue (#12268)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: edenhaus <edenhaus@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-03-11 19:52:43 +13:00
J. Nick Koston
e8e700a683 [socket] Fix RP2040 heap corruption from malloc in lwip accept callback (#14687) 2026-03-11 19:51:54 +13:00
Adam DeMuri
4df3d3554e Enable the address and behavior sanitizers for C++ component unit tests (#13490) 2026-03-10 19:44:05 -10:00
Keith Burzinski
d0f37ae694 [logger] Fix UART selection not applied before pre_setup() (#14690) 2026-03-11 04:31:27 +00:00
Keith Burzinski
6561c9bc95 [core] Fix waiting for port indefinitely (#14688) 2026-03-10 17:32:29 -10:00
J. Nick Koston
794098de99 [rp2040] Add HardFault crash handler with backtrace (#14685) 2026-03-10 16:40:45 -10:00
CFlix
b84d773bec [bme280] Change communication error message to include "no response" hint. (#14686) 2026-03-10 20:24:46 -04:00
Thomas Rupprecht
dcbf3c8728 [esp32] gpio type improvements (#14517) 2026-03-10 18:18:35 -04:00
J. Nick Koston
30c8c68703 [socket] Fix RP2040 TCP race condition between lwip callbacks and main loop (#14679) 2026-03-10 11:22:23 -10:00
CFlix
9513edc468 [dew_point] Add dew_point sensor component (#14441)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-10 17:17:13 -04:00
J. Nick Koston
6356e3def9 [core] Warn on crystal frequency mismatch during serial upload (#14582)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 20:42:38 +00:00
Jonathan Swoboda
8d988723cd [config] Allow !extend/!remove on components without id in schema (#14682)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:38:50 -04:00
Kevin Ahrendt
8ca6ee4349 [speaker_source] Add new media player (#14649)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-10 20:25:26 +00:00
mahumpula
780e009bf4 [runtime_image] Add support for 8bit BMPs and fix existing issues (#10733)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-10 16:23:49 -04:00
Jonathan Swoboda
04d80cfb75 [esp32_hosted] Bump esp_wifi_remote and esp_hosted versions (#14680)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 15:17:30 -04:00
J. Nick Koston
9404eadaf8 [rp2040_ble] Add BLE component for RP2040/RP2350 (#14603) 2026-03-10 09:12:28 -10:00
J. Nick Koston
4d2ef09a29 [log] Detect early log calls before logger init and optimize hot path (#14538) 2026-03-10 09:12:10 -10:00
J. Nick Koston
89bb5d9e42 [core] Require explicit synchronous= for register_action (#14606) 2026-03-10 09:11:45 -10:00
J. Nick Koston
9dd3ec258c [scheduler] Replace unique_ptr with raw pointers, add leak detection (#14620) 2026-03-10 09:11:28 -10:00
J. Nick Koston
c709010c4c [api] Replace std::vector<uint8_t> with APIBuffer to skip zero-fill (#14593) 2026-03-10 09:11:12 -10:00
J. Nick Koston
6e468936ec [api] Inline ProtoVarInt::parse fast path and return consumed in struct (#14638)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:10:55 -10:00
J. Nick Koston
2c7ef4f758 [rp2040] Use picotool for BOOTSEL upload and improve upload UX (#14483) 2026-03-10 09:10:33 -10:00
Diorcet Yann
06a127f64b [core] ESP-IDF compilation fixes (#14541) 2026-03-10 11:52:48 -04:00
Anunay Kulshrestha
fba21e6dd4 [bl0940] Fix reset_calibration() declaration missing from header (#14676)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-10 10:44:19 -04:00
J. Nick Koston
4b50d14496 [serial_proxy] Reduce loop() overhead by disabling when idle and splitting read path (#14673) 2026-03-10 02:10:03 -05:00
Javier Peletier
e82f0f4432 [cpptests] support testing platform components (#13075)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-10 02:41:02 +00:00
Tobias Stanzel
00f809f5f0 [sen6x] fix memory leak issue (#14623)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 21:45:20 -04:00
Jonathan Swoboda
c31ac662bd [multiple] Fix crashes from malformed external input (#14643)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-09 20:39:58 -04:00
J. Nick Koston
d6ce5dda81 [ci] Skip YAML anchor keys in integration fixture component extraction (#14670) 2026-03-09 22:54:56 +00:00
J. Nick Koston
dadbdd0f7b [ci] Make codeowner label update non-fatal for fork PRs (#14668) 2026-03-09 12:34:31 -10:00
Jonathan Swoboda
d96be88ff5 [multiple] Fix reliability issues in 5 components (#14655)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-09 18:32:57 -04:00
Jonathan Swoboda
d2686b49be [canbus] Fix multiple MCP component bugs (#14461)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:15:33 -04:00
Keith Burzinski
468ce74c8e [api][serial_proxy] Fix dangling pointer (#14640) 2026-03-09 17:04:47 -05:00
Jonathan Swoboda
b3fc43c13c [multiple] Fix wrong behavior in sensor calculations and drivers (#14644)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:00:17 -04:00
Jonathan Swoboda
308e8e78cd [ble_scanner] Escape special characters in JSON output (#14664)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:59:36 -04:00
Jonathan Swoboda
470d9160a5 [demo] Fix alarm control panel auth bypass when code is omitted (#14645)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:57:02 -04:00
Jonathan Swoboda
9902447834 [multiple] Fix minor bugs in 8 components (#14650)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:51:50 -04:00
Jonathan Swoboda
7c1b9f0cb4 [multiple] Fix wrong behavior in 5 components (#14647)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:22:06 -04:00
Jonathan Swoboda
fecedeb018 [multiple] Fix crashes from malformed external input (batch 2) (#14651)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:20:09 -04:00
Jonathan Swoboda
9418f35cc3 [multiple] Remove unnecessary heap allocations in 4 components (#14656)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:18:44 -04:00
Jonathan Swoboda
08a0608a48 [wifi][captive_portal][heatpumpir][es8388] Fix wrong behavior in 4 components (#14657)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:18:21 -04:00
Jonathan Swoboda
b721cd48e5 [hmc5883l][mmc5603][honeywellabp2][xgzp68xx][max9611] Fix uninitialized members (#14659)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:18:07 -04:00
Jonathan Swoboda
75f55adbfa [api][at581x][vl53l0x] Fix bounds check issues in 3 components (#14660)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:17:31 -04:00
Jonathan Swoboda
a379e5a635 [runtime_image][st7701s] Fix BMP decoder and LCD init bugs (#14663)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:16:29 -04:00
dependabot[bot]
019db74582 Bump setuptools from 82.0.0 to 82.0.1 (#14665)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 20:44:27 +00:00
Jonathan Swoboda
31f4b4d00d [multiple] Fix undefined behavior across components (#14639)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 07:33:08 -04:00
Jonathan Swoboda
0db9137d91 [multiple] Add division by zero guards (#14634)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-03-09 00:10:48 -04:00
Clyde Stubbs
f3ca86b670 [ci-custom] Directions on constant hoisting (#14637) 2026-03-08 23:48:03 -04:00
J. Nick Koston
088a8a4338 [ci] Match symbols with changed signatures in memory impact analysis (#14600)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 17:23:58 -10:00
Jonathan Swoboda
5d3893368d [multiple] Add array bounds checks (#14635)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:16:32 -04:00
Jonathan Swoboda
5b9cab02be [multiple] Add default initializers to uninitialized member variables (#14636)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 22:37:54 -04:00
Edward Firmo
cac751e9e8 [nextion] Add configurable HTTP parameters for TFT upload (#14234)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-09 01:29:41 +00:00
J. Nick Koston
6ba5c9a705 [api] Skip state_action_() call in noise data path (#14629)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 01:22:39 +00:00
J. Nick Koston
c681dc8872 [socket] Add socket wake support for RP2040 (#14498) 2026-03-08 15:11:24 -10:00
J. Nick Koston
d0285cdc41 [core] Pack entity flags into configure_entity_() and protect setters (#14564)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:11:15 -10:00
J. Nick Koston
9547a54fac [const] Move CONF_ENABLE_FULL_PRINTF to const.py (#14633) 2026-03-08 14:52:40 -10:00
J. Nick Koston
b05dbfccd3 [api] Bump noise-c to 0.1.11 (#14632) 2026-03-08 14:52:21 -10:00
J. Nick Koston
aef2d74e41 [ld2450] Add integration tests with mock UART (#14611) 2026-03-08 14:32:59 -10:00
J. Nick Koston
e1c849d5d2 [esp8266] Wrap printf/vprintf/fprintf to eliminate _vfiprintf_r (~1.6 KB flash) (#14621) 2026-03-08 14:32:47 -10:00
J. Nick Koston
c11ad7f0e6 [rp2040] Wrap printf/vprintf/fprintf to eliminate _vfprintf_r (~9.2 KB flash) (#14622) 2026-03-08 14:32:35 -10:00
J. Nick Koston
88536ff72b [modbus] Fix timeout for non-hardware UARTs (e.g., USB UART) (#14614)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-03-08 14:31:42 -10:00
J. Nick Koston
93d7ec4d72 [esp32_ble] Inline ble_addr_to_uint64 to eliminate call overhead (#14591) 2026-03-08 14:07:59 -10:00
J. Nick Koston
66a5ad0d75 [core] Skip zero-initialization of StaticVector data array (#14592) 2026-03-08 14:06:55 -10:00
J. Nick Koston
771404668d [api] Inline fast path of try_to_clear_buffer (#14630) 2026-03-08 14:01:01 -10:00
J. Nick Koston
76c567a71c [scheduler] Use std::atomic<uint8_t> instead of std::atomic<bool> for remove flag (#14626) 2026-03-08 14:00:04 -10:00
J. Nick Koston
e7730cff00 [esp32_ble] Optimize BLE event hot path performance (#14627) 2026-03-08 13:59:40 -10:00
Jonathan Swoboda
d5dc4a39cb [i2s_audio] Fix mono sample swap and block 8-bit mono on ESP32 (#14516)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-08 12:10:43 -10:00
Keith Burzinski
50b3f9d25c [mixer_speaker] Add task debounce (#14581) 2026-03-08 18:09:06 -04:00
J. Nick Koston
ad5811280a [ci] Add medium-pr label for PRs with ≤100 lines changed (#14628) 2026-03-08 10:59:43 -10:00
tomaszduda23
9be1876fae [ble_nus] make ble_nus timeout shorter than watchdog (#14619)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-03-08 10:52:16 -10:00
dependabot[bot]
1b3a7f0b6a Bump aioesphomeapi from 44.5.0 to 44.5.1 (#14624)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 18:18:14 +00:00
Diorcet Yann
3f143d9f19 [ethernet] Fix commit 3f700bac1c (#14618) 2026-03-08 09:50:32 -04:00
Oliver Kleinecke
a9b5f95c76 [usb_uart] ch34x chip-type & port-count enumeration (#14544) 2026-03-08 21:24:39 +11:00
Keith Burzinski
0c4a44566f [serial_proxy] New component (#13944)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-08 03:55:49 -05:00
tomaszduda23
2c705810cd [nrf52] allow to update OTA via cmd (#12344)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-08 08:40:52 +00:00
J. Nick Koston
a530aeec22 [api] Inline varint and encode_varint_raw fast paths for hot loop performance (#14607)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:09:12 -10:00
dependabot[bot]
d9e76da806 Bump aioesphomeapi from 44.4.0 to 44.5.0 (#14617)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 07:59:25 +00:00
tomaszduda23
e4b89a69d4 [nrf52, ota] ble and serial OTA based on mcumgr (#11932)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-07 20:32:20 -10:00
Keith Burzinski
04cff1c916 [usb_uart] Return flush result, expose timeout via config (#14616) 2026-03-08 00:04:14 -06:00
Keith Burzinski
5e842a8b20 [uart] Return flush result, expose timeout via config (#14608)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-08 05:23:13 +00:00
J. Nick Koston
be6c3c52ac [api] Add force proto field option to skip zero checks on hot path (#14610)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 18:59:13 -10:00
Jonathan Swoboda
9fea8fe01b [vbus][rf_bridge][sensirion_common] Add buffer size guards (#14597)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 18:50:36 -10:00
J. Nick Koston
d55fe9a34b [api] Fix value-initialization of DeviceInfoResponse (#14615)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-03-07 18:34:35 -10:00
J. Nick Koston
66919ef969 [i2s_audio] Include legacy driver IDF component when use_legacy is set (#14613) 2026-03-07 22:33:54 -06:00
dependabot[bot]
ea7cfffdda Bump aioesphomeapi from 44.3.1 to 44.4.0 (#14609)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 02:40:13 +00:00
J. Nick Koston
888f3d804b [ld2420] Add integration tests with mock UART (#14471) 2026-03-07 13:22:50 -10:00
J. Nick Koston
545395a6f0 [ci] Add RP2350 to PR template test environment (#14599) 2026-03-07 13:16:19 -10:00
Oliver Kleinecke
f2dfb5e1dc [uart][usb_uart] Add debug_prefix option to distinguish multiple defined uarts in log (#14525) 2026-03-08 10:16:12 +11:00
Diorcet Yann
3f700bac1c [component] Fix components for compatibility with stricter compilers (#14545)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-07 18:50:44 +00:00
J. Nick Koston
a0cd35c5fc [core] Inline status_clear_warning/error fast path (#14571) 2026-03-07 07:27:08 -10:00
J. Nick Koston
e7b8ec18f1 [api] Inline APIServer::is_connected() for common no-arg path (#14574) 2026-03-07 07:26:50 -10:00
J. Nick Koston
77f2c371b2 [api] Single-pass protobuf encode for BLE proxy advertisements (#14575) 2026-03-07 07:26:34 -10:00
J. Nick Koston
45f20d9c06 [core] Merge set_name + set_entity_strings into configure_entity_ (#14444)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:26:01 -10:00
J. Nick Koston
f57fa4cc8d [bluetooth_proxy] Add BLE connection parameters API (#14577) 2026-03-07 07:25:33 -10:00
J. Nick Koston
abc870006c [captive_portal] Enable support for RP2040 (#14505) 2026-03-07 07:25:13 -10:00
puddly
15ffbb0b05 [uart] Fully enable raw mode with host serial (#14573) 2026-03-07 11:51:02 -05:00
Simon Redman
8b62c35ea7 [uart] Add error message when initializing UART with unsupported configuration (#13229) 2026-03-07 11:41:37 -05:00
tomaszduda23
0e106d843c [nrf52][zephyr] support for multi on rate callbacks (#14557) 2026-03-07 11:18:21 -05:00
rwrozelle
cbebb81196 [openthread] move esp functions into correct file (#14588) 2026-03-07 11:12:27 -05:00
J. Nick Koston
05ae69b766 [api] Sync api.proto from aioesphomeapi (#14579) 2026-03-06 19:00:37 -10:00
dependabot[bot]
9b489c9eba Bump aioesphomeapi from 44.2.0 to 44.3.1 (#14580)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 03:52:51 +00:00
Ricardo Sanz
df11e2765e [climate][haier][template][core] Relocate CONF_CURRENT_TEMPERATURE to general const file (#14503) 2026-03-07 01:00:52 +00:00
AndreKR
f53ee70caa [http_request] Make TLS buffer configurable on ESP8266 (#14009)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-06 14:29:20 -10:00
Jonathan Swoboda
d8deb2255d [mipi_rgb] Fix byte order and dirty bounds in fill() (#14537)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:18:09 -10:00
dependabot[bot]
035f985693 Bump ruff from 0.15.4 to 0.15.5 (#14565)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-06 22:16:36 +00:00
dependabot[bot]
086c1bb505 Bump docker/build-push-action from 6.19.2 to 7.0.0 in /.github/actions/build-image (#14567)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 12:12:44 -10:00
dependabot[bot]
c26c5935b6 Bump github/codeql-action from 4.32.5 to 4.32.6 (#14566)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 12:12:19 -10:00
Jonathan Swoboda
de7572bd3e [lightwaverf] Fix ISR safety issues (#14563)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:04:12 -10:00
Jonathan Swoboda
5777908da7 [iaqcore][scd30][sen21231][beken_spi_led_strip] Fix uninitialized variables and missing error checks (#14568)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:03:53 -10:00
Jonathan Swoboda
587bf68091 [ltr501][pvvx_mithermometer][smt100] Convert static locals to instance members (#14569)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-06 11:03:30 -10:00
Jonathan Swoboda
2c83c6a79f [shelly_dimmer][lvgl][seeed_mr60fda2][packet_transport] Fix buffer bounds checks (#14534)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:47:56 -10:00
Jonathan Swoboda
4f4b2bfdec [bmp581_base][bl0906] Fix 24-bit sign extension bugs (#14558)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:14:35 +00:00
Jonathan Swoboda
0469612d07 [multiple] Fix assorted medium-severity bugs (#14555)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:02:17 -05:00
Jonathan Swoboda
8f3db96291 [esp32_ble_server][weikai][ade7880] Fix copy-paste bugs (#14552)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:50:26 -10:00
Jonathan Swoboda
a9cceebb33 [pid][nextion][pn532_i2c][pipsolar] Fix copy-paste and logic bugs (#14551)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:48:50 -10:00
Jonathan Swoboda
9ab5f5d451 [light] Fix unsigned underflow in addressable scan effect (#14546)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:42:05 -10:00
Jonathan Swoboda
219d5170e0 [noblex] Fix IR receive losing decoded bytes between calls (#14533)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:15:54 -05:00
Jonathan Swoboda
7b8ba9bf20 [multiple] Fix cast/operator precedence bugs (#14560)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:14:12 -10:00
Jonathan Swoboda
3db436e48e [esp32_ble_server][espnow][time] Fix logic bugs (#14553)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:05:34 -10:00
Jonathan Swoboda
3c7956e72d [multiple] Add default initializers to uninitialized member variables (#14556)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:04:00 -10:00
J. Nick Koston
42dbb51022 [api] Devirtualize protobuf encode/calculate_size (#14449)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 19:03:54 +00:00
Jonathan Swoboda
9654140c00 [tm1638][rp2040_pio_led_strip][atm90e32] Fix bounds checks and off-by-one (#14559)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:00:46 -10:00
J. Nick Koston
8a915dcbbe [core] Move device class strings to PROGMEM on ESP8266 (#14443)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 08:34:27 -10:00
Thomas Rupprecht
b2378e830e [rtttl] Add AudioStreamInfo and set volume (#14439)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-06 18:11:52 +00:00
J. Nick Koston
65b7c73bf3 [sgp4x] Fix undefined behavior from mutating entity config at runtime (#14562)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:02:34 +00:00
J. Nick Koston
6e3bc7b1dd [ci] Use pull_request_target for codeowner approved label workflow (#14561) 2026-03-06 07:33:05 -10:00
J. Nick Koston
82629c397f [hlk_fm22x] Fix oversized response rejection breaking GET_ALL_FACE_IDS (#14506) 2026-03-06 07:01:50 -10:00
J. Nick Koston
a16b8fc0ac [rp2040] Fix Pico W LED pin and auto-generate board definitions for arduino-pico 5.5.x (#14528) 2026-03-06 07:00:31 -10:00
J. Nick Koston
74e4b69654 [core] Replace Application name/friendly_name std::string with StringRef (#14532)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-03-06 06:58:13 -10:00
J. Nick Koston
07e51886f3 [core] Move entity icon strings to PROGMEM on ESP8266 (#14437) 2026-03-06 06:57:52 -10:00
tomaszduda23
e59a2b3ede [nrf52] prepare for usb cdc (#14174)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-06 16:25:44 +00:00
Thomas Rupprecht
5084c32f3c [esp32] Fix ESP32-S3 pin validation error message (#14540) 2026-03-06 07:22:11 -05:00
Thomas Rupprecht
c0b7f41397 [esp32] Fix wrong variable usage in P4 pin validation error msg (#14539) 2026-03-06 07:21:44 -05:00
Jonathan Swoboda
6c07c15c50 [mipi_dsi][e131] Fix semaphore cast, missing return, and light count overread (#14530)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:18:56 -05:00
Jonathan Swoboda
666fb7cf39 [sx127x][sx126x][max6956] Fix null deref, unterminated string, and pin bounds check (#14529)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:18:28 -05:00
tomaszduda23
a2c0d70c2c [ble_nus] Add uart support (#14320)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-05 21:00:17 -10:00
Jonathan Swoboda
80fe54ed69 [bluetooth_proxy] Add null checks for api_connection (#14536)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:30:39 -05:00
Jonathan Swoboda
44870323da [host] Add null checks for getenv and fopen in preferences (#14531)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:47:47 -05:00
Gnuspice
58ab630965 [ethernet] add get_eth_handle() function (#14527) 2026-03-05 23:37:07 +00:00
Kevin Ahrendt
64098122e7 [audio_file] Add media source platform (#14436)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-05 12:30:13 -10:00
Jonathan Swoboda
8a8f6824a2 [openthread][ethernet][wifi] Add IPv6 address array bounds assert (#14488)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:29:44 -10:00
tomaszduda23
b2c12d88fe [uart] init tx_pin, rx_pin, flow control, rx_buffer_size (#14524) 2026-03-05 12:24:11 -10:00
Jonathan Swoboda
e8b1dce67b [st7735][st7789v][st7920] Fix display buffer overflows and dead code (#14511)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:28:41 -05:00
J. Nick Koston
fbf63d8e3b [rp2040] Update arduino-pico to 5.5.1 and fix WiFi AP fallback (#14500) 2026-03-05 21:23:00 +00:00
Kevin Ahrendt
06d6322fe3 [audio] Extract detect_audio_file_type helper (#14507)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 21:19:45 +00:00
Jonathan Swoboda
de14e7055e [cse7761][ads1115][tmp1075][matrix_keypad][seeed_mr60bha2] Fix assorted bugs (#14518)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 21:10:26 +00:00
Jonathan Swoboda
3392e4d73b [usb_uart][nextion][feedback][whirlpool][packet_transport][he60r][hc8][runtime_stats] Fix millis() wrapping bugs (#14474)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 16:08:58 -05:00
Bonne Eggleston
b0be02e16d [modbus] Fix timing bugs and better adhere to spec (#8032)
Co-authored-by: brambo123 <52667932+brambo123@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-05 20:54:17 +00:00
Jonathan Swoboda
d11e7cab46 [xiaomi_ble][pvvx_mithermometer][atc_mithermometer] Add BLE service data bounds checks (#14514)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:18:54 +00:00
J. Nick Koston
e25d740968 [wifi] Cache is_connected() for cheap inline access (#14463)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-05 09:58:58 -10:00
Jonathan Swoboda
291679126f [nfc] Fix off-by-one in NDEF message parsing (#14485)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:55:54 -10:00
dependabot[bot]
99a805cba6 Bump the docker-actions group across 1 directory with 2 updates (#14520)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 09:52:10 -10:00
Jonathan Swoboda
bb37887a8c [wled][lcd_base][touchscreen][ee895] Fix off-by-one, buffer overrun, empty deref, and uninitialized pointers (#14513)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 09:51:32 -10:00
Kevin Ahrendt
5c5ea8824e [audio_file] New component for embedding files into firmware (#14434)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 09:51:08 -10:00
Jonathan Swoboda
22d90d702d [usb_cdc_acm][scd4x][pulse_counter][mopeka_std_check][ruuvi_ble] Fix assorted one-liner bugs (#14495)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:47:55 -10:00
Jonathan Swoboda
9961c8180a [alpha3][mpu6886][emc2101] Fix copy-paste bugs (#14492)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 09:47:32 -10:00
Jonathan Swoboda
d6f3186b3d [haier][bedjet][vbus][lightwaverf] Fix buffer overflow bugs (#14493)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 09:47:10 -10:00
Jonathan Swoboda
05ddc85412 [rc522][sml][kamstrup_kmp] Fix buffer bounds checks (#14515)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 09:46:56 -10:00
Jonathan Swoboda
6f0460b0ee [sim800l][tormatic][tx20] Fix OOB access, div-by-zero, and off-by-one (#14512)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 09:46:47 -10:00
Olivier ARCHER
44d314d069 [GPS] fix component Python declaration to match C++ implementation (#14519) 2026-03-05 09:22:37 -10:00
J. Nick Koston
e210e414bd [ota] Devirtualize OTA backend calls (#14473) 2026-03-05 19:15:02 +00:00
Jonathan Swoboda
cce7a09fa9 [pn532_spi] Fix preamble check logic and OOB access when full_len is zero (#14486)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 14:09:34 -05:00
Jonathan Swoboda
e1d0c6da09 [dfplayer][ufire_ise][ufire_ec][qmp6988][atm90e26] Fix wrong operators and masks (#14491)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 14:09:23 -05:00
rwrozelle
9518d88a2a [openthread] static log level code quality improvement (#14456)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-05 08:35:20 -10:00
Jonathan Swoboda
4a5d8449fd [sht4x][grove_tb6612fng] Fix logic bugs (#14497)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:33:12 -10:00
Jonathan Swoboda
3df4ef9362 [ssd1322][ssd1325][ssd1327] Fix nibble mask bug in grayscale draw_pixel (#14496)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:31:26 -10:00
Jonathan Swoboda
01f4275202 [veml7700] Fix initial settling timeout using raw enum instead of milliseconds (#14487)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:16:33 -05:00
Jonathan Swoboda
a061397469 [dfrobot_sen0395][sx1509] Fix structural bugs (#14494)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:16:06 -05:00
J. Nick Koston
2777d35990 [api] Devirtualize frame helper calls when protocol is fixed at compile time (#14468) 2026-03-05 07:21:44 -10:00
J. Nick Koston
c49c23d5d9 [network] Inline network::is_connected() and ethernet is_connected() (#14464)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 07:21:04 -10:00
rwrozelle
0e2a10c5f0 [openthread] Cache is_connected() for cheap inline access (#14484)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-04 17:34:13 -10:00
Clyde Stubbs
f5c37bf486 [packet_transport] Minimise heap allocations (#14482) 2026-03-05 14:24:01 +11:00
J. Nick Koston
0ff5270632 [ci] Fix codeowner approval label workflow for fork PRs (#14490)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:57:19 -10:00
J. Nick Koston
5df4fd0a27 [tests] Fix flaky uart_mock integration tests (#14476) 2026-03-04 15:51:51 -10:00
Brandon Harvey
c0143ac6d6 [ai] Add docs note about keeping component index pages in sync (#14465)
Co-authored-by: Brandon Harvey <bharvey88@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-04 15:46:40 -10:00
Jonathan Swoboda
c8e7f78a25 [zwave_proxy] Fix uint8_t overflow for buffer index and frame end (#14480)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:32:50 -05:00
Jonathan Swoboda
b6d7e8e14d [sgp30] Fix serial number truncation from 48-bit to 24-bit (#14478)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:18:28 -05:00
Jonathan Swoboda
55103c0652 [ds2484] Fix read64() using uint8_t accumulator instead of uint64_t (#14479)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:18:14 -05:00
J. Nick Koston
61ea6c3b2f [ci] Add missing issues: write permission to codeowner approval workflow (#14477)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 17:46:26 -05:00
Jonathan Swoboda
e11a91411b [esp32_improv][rf_bridge][esp32_ble_server][display][lvgl][pipsolar] Fix unsigned integer underflows (#14466)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-03-04 16:36:52 -05:00
Jonathan Swoboda
0c883b80c4 [inkplate][ezo_pmp][ezo][packet_transport] Fix use-after-free bugs (#14467)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:05:49 -05:00
Kevin Ahrendt
4928e678d1 [mixer][resampler][speaker] Use core static task manager (#14454) 2026-03-04 14:37:22 -05:00
Jonathan Swoboda
22fc3aab39 [ld2420] Fix buffer overflows in simple mode, energy mode, and calibration (#14458)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 08:19:46 -10:00
Jonathan Swoboda
c37ab1de84 [fingerprint_grow] Fix OOB write and uint16 overflow (#14462)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:58:52 -05:00
Jonathan Swoboda
246a8bff0c [pn7160][pn7150][pn532] Fix tag purge skipping, NDEF bounds check, and NDEF length byte order (#14460)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:58:42 -05:00
Jonathan Swoboda
9abba79c54 [remote_base][remote_receiver] Fix OOB access in pronto comparison and RMT buffer allocation (#14459)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:58:24 -05:00
Jonathan Swoboda
5ba880f19b [sx127x] Fix preamble MSB register always written as zero (#14457)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:57:45 -05:00
J. Nick Koston
b2e8544c58 [ld2412] Add integration tests with mock UART (#14448) 2026-03-04 07:18:31 -10:00
J. Nick Koston
ac19d05db2 [core] Call loop() directly in main loop, bypass call() indirection (#14451) 2026-03-04 07:17:41 -10:00
J. Nick Koston
065773ed4c [runtime_stats] Use micros() for accurate per-component timing (#14452) 2026-03-04 07:17:28 -10:00
JiriPrchal
37146ff565 [integration] Add set method to publish and save sensor value (#13316)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-04 09:00:09 -05:00
Tilman Vogel
cba34e770e [core] improve help text for --device option, mention OTA (#14445)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-03-03 21:18:36 -05:00
Kevin Ahrendt
8911d9d28f [media_source] Clarify threading contract (#14433) 2026-03-03 20:42:36 -05:00
J. Nick Koston
9371159a7e [core] Replace custom esphome::optional with std::optional (#14368)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 01:14:05 +00:00
Kevin Ahrendt
43a6fe9b6c [core] add a StaticTask helper to manage task lifecycles (#14446) 2026-03-04 01:06:36 +00:00
Jesse Hills
989330d6bc [globals] Fix handling of string booleans in yaml (#14447) 2026-03-03 22:54:40 +00:00
Jonathan Swoboda
ee78d7a0c0 [tests] Fix integration test race condition in PlatformIO cache init (#14435)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:42:41 -05:00
Thomas Rupprecht
96793a99ce [rtttl] add new codeowner (#14440) 2026-03-03 10:55:56 -10:00
Clyde Stubbs
380c0db020 [usb_uart] Don't claim interrupt interface for ch34x (#14431) 2026-03-03 14:49:38 -06:00
J. Nick Koston
95544dddf8 [ci] Add code-owner-approved label workflow (#14421) 2026-03-03 07:11:47 -10:00
J. Nick Koston
b209c903bb [core] Inline trivial Component state accessors (#14425) 2026-03-03 07:05:15 -10:00
J. Nick Koston
4f69c487da [bk72xx] Fix ~100ms loop stalls by raising main task priority (#14420) 2026-03-03 07:04:12 -10:00
J. Nick Koston
78602ccacb [ci] Add lint check to prevent powf in core and base entity platforms (#14126) 2026-03-03 07:03:50 -10:00
J. Nick Koston
1f1b20f4fe [core] Pack entity string properties into PROGMEM-indexed uint8_t fields (#14171) 2026-03-03 07:03:24 -10:00
J. Nick Koston
d53ff7892a [socket] Cache lwip_sock pointers and inline ready() chain (#14408) 2026-03-03 07:03:02 -10:00
rwrozelle
b6f0bb9b6b [speaker] Add off on capability to media player (#9295)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org>
2026-03-03 10:59:01 -05:00
Clyde Stubbs
cfde0613bb [const][uart][usb_uart][weikai][core] Move constants to components/const (#14430) 2026-03-03 07:53:18 -05:00
Jesse Hills
903c67c994 Revert "[wifi] Revert cyw43_wifi_link_status change for RP2040"
This reverts commit 1b5bf2c848.
2026-03-03 22:16:54 +13:00
Jesse Hills
d8d479cef7 Merge branch 'release' into dev 2026-03-03 22:15:06 +13:00
schrob
60d66ca2dc [openthread] Add tx power option (#14200)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-02 23:28:01 -05:00
J. Nick Koston
db15b94cd7 [core] Inline HighFrequencyLoopRequester::is_high_frequency() (#14423) 2026-03-03 03:17:20 +00:00
Cody Cutrer
ae49b67321 [ld2450] Clear all related sensors when a target is not being tracked (#13602)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-02 15:47:40 -10:00
melak
c77241940b [lps22] Add support for the LPS22DF variant (#14397) 2026-03-02 15:24:00 -10:00
Kevin Ahrendt
97d713ee64 [media_source] Add new Media Source platform component (#14417)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 15:16:38 -10:00
Jesse Hills
bc04a1a0ff Merge branch 'release' into dev 2026-03-03 11:35:53 +13:00
Jonathan Swoboda
7a87348855 [ci] Skip PR title check for dependabot PRs (#14418)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 16:49:14 -05:00
J. Nick Koston
2e623fd6c3 [tests] Fix flaky log assertion race in oversized payload tests (#14414) 2026-03-02 11:48:50 -10:00
dependabot[bot]
727fa07377 Bump github/codeql-action from 4.32.4 to 4.32.5 (#14416)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 11:44:53 -10:00
Lino Schmidt
5510b45f3b [const] Move CONF_WATCHDOG (#14415) 2026-03-02 11:43:06 -10:00
J. Nick Koston
3615a7b90c [core] Eliminate __udivdi3 in millis() on ESP32 and RP2040 (#14409) 2026-03-02 11:42:25 -10:00
J. Nick Koston
d1de50c0e5 [core] Add ESP8266 support to wake_loop_any_context() (#14392) 2026-03-02 21:11:04 +00:00
Jonathan Swoboda
38f671a923 [uart] Fix flow_control_pin inverted flag ignored on ESP-IDF (#14410)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:52:06 +13:00
J. Nick Koston
cb232d8288 [core] Fix compile-time loop() detection for multiple inheritance (#14411) 2026-03-02 19:11:47 +00:00
J. Nick Koston
2fa244715d [socket] Fix pre-existing bugs found during socket devirtualization review (#14404) 2026-03-02 08:54:54 -10:00
netixx
b9b1af1c3d [mcp23016] Fix register access to use 16-bit paired transactions (#13676)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-02 17:38:28 +00:00
J. Nick Koston
a1d91ac779 [core] Compile-time detection of loop() overrides (#14405) 2026-03-02 06:59:23 -10:00
J. Nick Koston
585e195044 [socket] Devirtualize socket abstraction layer (#14398) 2026-03-02 06:58:31 -10:00
J. Nick Koston
39572d9628 [light] Resolve effect names to indices at codegen time (#14265) 2026-03-02 06:58:15 -10:00
J. Nick Koston
f278250740 [core] Deduplicate ControllerRegistry notify dispatch loop (#14394) 2026-03-02 06:57:59 -10:00
J. Nick Koston
54f410901f [web_server] Avoid temporary std::string allocations in request parameter parsing (#14366)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 06:57:39 -10:00
J. Nick Koston
00242443e1 [light] Replace powf gamma with pre-computed lookup tables (LUT) (#14123)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 06:57:23 -10:00
J. Nick Koston
82da4935b6 [core] Auto-wrap static strings in PROGMEM on ESP8266 via TemplatableValue (#13885) 2026-03-02 06:57:08 -10:00
J. Nick Koston
1c5fd8bbd4 [core] Move millis_64 rollover tracking out of Scheduler (#14360)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 06:56:51 -10:00
Copilot
77a7cbcffd Use cached files on network errors in external_files (#14055)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jesserockz <3060199+jesserockz@users.noreply.github.com>
2026-03-02 11:41:20 -05:00
Bonne Eggleston
3160457ca6 Create integration tests for modbus (#14395)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-01 22:51:27 -10:00
schrob
590ee81f7a [openthread] Disable default enabled OT console build (#14390)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-02 06:16:36 +00:00
J. Nick Koston
9d4357c619 [core] Wake main loop from ISR in enable_loop_soon_any_context() (#14383) 2026-03-01 18:20:14 -10:00
J. Nick Koston
80a2acca4f [ld2410] Add UART mock integration test for LD2410 component (#14377) 2026-03-01 18:19:32 -10:00
J. Nick Koston
f68a3ed15d [api] Remove virtual destructor from ProtoMessage (#14393) 2026-03-01 18:09:00 -10:00
schrob
6d3d8970a6 [openthread] Disable default enabled OT diag code (#14399) 2026-03-01 22:47:02 -05:00
Thomas Rupprecht
073ca63f60 [rtttl] improve comments Part 2 (#13971)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-01 22:44:02 -05:00
J. Nick Koston
0e18e4461e [time,api] Send pre-parsed timezone struct over protobuf (#14233)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:52:53 +13:00
J. Nick Koston
3e7424b307 [preferences] Reduce heap churn with small inline buffer optimization (#13259) 2026-03-02 14:22:55 +13:00
J. Nick Koston
48b5cae6c4 [api] Use StringRef for user service string arguments (#13974) 2026-03-02 10:32:44 +13:00
J. Nick Koston
a1760a1980 [improv_serial] Add missing USE_IMPROV_SERIAL define to fix WiFi scan filtering (#14359) 2026-03-02 09:23:10 +13:00
kbx81
ae9c999052 fix 2026-02-28 23:21:30 -06:00
kbx81
7d2f6fbf55 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-28 23:12:31 -06:00
J. Nick Koston
19bbd39e33 [uart] Enable wake-on-RX by default on ESP32 (#14391) 2026-02-28 21:06:46 -06:00
J. Nick Koston
3f97b3b706 [core] Extract set_status_flag_ helper to deduplicate status_set methods (#14384)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 14:27:06 -10:00
J. Nick Koston
82e620bcf5 [ld2450] Single-pass zone target counting (#14387) 2026-02-28 14:26:55 -10:00
J. Nick Koston
80e0761bf1 [ld2450] Use integer dedup for direction text sensor updates (#14386) 2026-02-28 14:26:31 -10:00
J. Nick Koston
c0781d3680 [ld2450] Use atan2f for angle calculation (#14388) 2026-02-28 14:26:08 -10:00
J. Nick Koston
b7cb65ec49 [ci] Fix TypeError in ci-custom.py when POST lint checks fail (#14378) 2026-02-28 14:23:20 -10:00
J. Nick Koston
fdbfac15db [uart] Replace wake-on-RX task+queue with direct ISR callback (#14382) 2026-03-01 00:21:08 +00:00
J. Nick Koston
28424d6acd [ld2410][ld2412] Fix signed char causing incorrect distance values (#14380)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 23:50:12 +00:00
J. Nick Koston
b679b04d14 [core] Move CONF_STOP_BITS, CONF_DATA_BITS, CONF_PARITY to const.py (#14379) 2026-02-28 13:27:33 -10:00
Jonathan Swoboda
b7d651dd17 [core] Defer entity automation codegen to prevent sibling ID deadlocks (#14381)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-28 13:10:53 -10:00
J. Nick Koston
757e8d90e6 [core] Inline set_component_state_ and use it in Application (#14369) 2026-02-28 07:20:34 -10:00
J. Nick Koston
7d52a9587f [api] Outline keepalive ping logic from APIConnection::loop() (#14374) 2026-02-28 07:20:20 -10:00
J. Nick Koston
067d773aac [core] Make register_component protected, remove runtime checks (#14371) 2026-02-28 07:19:55 -10:00
Clyde Stubbs
089d1e55e7 [mipi_dsi] Fix Waveshare P4 7B board config (#14372) 2026-02-28 20:37:04 +11:00
Raymond Richmond
6c0998f220 [gt911] Support for interrupt signal via IO Expander (#14358) 2026-02-28 18:26:06 +11:00
J. Nick Koston
49cc389bf0 [esp32] Wrap printf/vprintf/fprintf to eliminate _vfprintf_r (~11 KB flash) (#14362)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:28:05 -10:00
J. Nick Koston
8480e8df9f [uart] Revert UART0 default pin workarounds (fixed in ESP-IDF 5.5.2) (#14363) 2026-02-27 17:27:51 -10:00
J. Nick Koston
e7d4f2608b [sen6x] Fix test sensor ID collisions with sen5x (#14367) 2026-02-27 16:01:17 -10:00
Ryan Wagoner
d1b4813197 [web_server] Add climate preset, fan mode, and humidity support (#14061) 2026-02-27 14:20:13 -10:00
Jonathan Swoboda
298ee7b92e [gps] Fix codegen deadlock when automations reference sibling sensors (#14365)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-27 19:08:42 -05:00
J. Nick Koston
5c56b99742 [ci] Fix C++ unit tests missing time component dependency (#14364) 2026-02-27 13:19:11 -10:00
Martin Ebner
b9d70dcda2 [sen6x] Add SEN6x sensor support (#12553)
Co-authored-by: Martin Ebner <martinebner@me.com>
Co-authored-by: Tobias Stanzel <tobi.stanzel@gmail.com>
Co-authored-by: Big Mike <mike@bigmike.land>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-27 16:41:28 -05:00
dependabot[bot]
5e3857abf7 Bump click from 8.1.7 to 8.3.1 (#11955)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 16:25:36 -05:00
Laura Wratten
bb567827a1 [sht3xd] Allow sensors that don't support serial number read (#14224)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-27 16:23:32 -05:00
J. Nick Koston
280f874edc [rp2040] Use native time_us_64() for millis_64() (#14356)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 11:18:02 -10:00
Jonathan Swoboda
f6755aabae [ci] Add PR title format check (#14345)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-27 15:18:07 -05:00
J. Nick Koston
52af4bced0 [component] Devirtualize call_dump_config (#14355) 2026-02-27 10:01:23 -10:00
J. Nick Koston
63e757807e [zephyr] Use native k_uptime_get() for millis_64() (#14350)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 10:01:09 -10:00
dependabot[bot]
edd63e3d2d Bump actions/download-artifact from 7.0.0 to 8.0.0 (#14327)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 14:43:10 -05:00
dependabot[bot]
32133e2f46 Bump ruff from 0.15.3 to 0.15.4 (#14357)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 14:42:20 -05:00
Clyde Stubbs
2255c68377 [esp32] Enable execute_from_psram for P4 (#14329) 2026-02-28 06:40:55 +11:00
J. Nick Koston
9c1d1a0d9f [color] Use integer math in Color::gradient to reduce code size (#14354) 2026-02-27 19:25:13 +00:00
J. Nick Koston
8698b01bc7 [host] Use native clock_gettime for millis_64() (#14340)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:54:13 +00:00
J. Nick Koston
3411ce2150 [core] Fix Application asm label for Mach-O using __USER_LABEL_PREFIX__ (#14334)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-27 08:49:57 -10:00
J. Nick Koston
29e1e8bdfd [wifi] Add LibreTiny component test configs (#14351) 2026-02-27 18:45:20 +00:00
J. Nick Koston
317dd5b2da [ci] Skip memory impact target branch build when tests don't exist (#14316) 2026-02-27 08:42:08 -10:00
Michael Cassaniti
4ae7633418 [safe_mode] Add feature to explicitly mark a boot as successful (#14306)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-27 13:23:02 -05:00
J. Nick Koston
c3a0eeceec [wifi] Use direct SDK APIs for LibreTiny SSID retrieval (#14349) 2026-02-27 18:17:17 +00:00
J. Nick Koston
4fe173b644 [wifi] Remove stale TODO comment for ESP8266 callback deferral (#14347) 2026-02-27 17:56:57 +00:00
J. Nick Koston
1c7f769ec7 [core] Add millis_64() HAL function with native ESP32 implementation (#14339) 2026-02-27 07:48:21 -10:00
deirdreobyrne
72ca514cc2 [esp32_hosted] Add configurable SDIO clock frequency (#14319)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Deirdre <obyrne@rk1.lan>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-27 17:25:53 +00:00
J. Nick Koston
20314b4d63 [mdns] Update espressif/mdns to v1.10.0 (#14338) 2026-02-27 17:20:08 +00:00
Kevin Ahrendt
017d1b2872 [audio] Bump microOpus to v0.3.4 (#14346) 2026-02-27 12:12:50 -05:00
Jonathan Swoboda
ef9fc87351 [zigbee] Fix codegen ordering for basic/identify attribute lists (#14343)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-27 11:17:04 -05:00
J. Nick Koston
0f7ac1726d [core] Extend fast select optimization to LibreTiny platforms (#14254)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 06:03:37 -10:00
whitty
bd3f8e006c [esp32_ble] allow setting of min/max key_size and auth_req_mode (#7138)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-27 06:02:29 -10:00
dependabot[bot]
07406c96e1 Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#14326)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 21:35:15 -10:00
Jonathan Swoboda
4044520ccc [esp32_touch] Migrate to new unified touch sensor driver (esp_driver_touch_sens) (#14033)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-26 20:38:36 -10:00
kbx81
608bef86cc Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-26 23:42:43 -06:00
Keith Burzinski
656389f215 [usb_uart] Performance, correctness and reliability improvements (#14333) 2026-02-26 23:41:35 -06:00
J. Nick Koston
04db37a34a [esp8266] Remove forced scanf linkage to save ~8KB flash (#13678)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 17:38:38 -10:00
kbx81
6514dc2fe1 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-26 20:55:50 -06:00
J. Nick Koston
15846137a6 [rp2040] Update arduino-pico framework from 3.9.4 to 5.5.0 (#14328) 2026-02-26 15:17:52 -10:00
J. Nick Koston
50e7571f4c [web_server_idf] Prefer make_unique_for_overwrite for noninit recv buffer (#14279) 2026-02-26 15:17:25 -10:00
J. Nick Koston
1ccfcfc8d8 [time] Eliminate libc timezone bloat (~9.5KB flash ESP32, ~2% RAM on ESP8266) (#13635)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:12:44 -10:00
George Joseph
527d4964f6 [mipi_dsi] Add more Waveshare panels and comments (#14023) 2026-02-27 11:38:07 +11:00
esphomebot
67ba68a1a0 Update webserver local assets to 20260226-220330 (#14330)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-26 22:21:40 +00:00
kbx81
240afd23b3 ... 2026-02-26 14:31:17 -06:00
kbx81
156c2a8cb0 optimize 2026-02-26 14:30:31 -06:00
lyubomirtraykov
8bd474fd01 [api] Add DEFROSTING to ClimateAction (#13976)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-02-26 10:27:18 -10:00
Oliver Kleinecke
54edc46c7f [esp_ldo] Add channels 1&2 support and passthrough mode (#14177) 2026-02-27 07:12:52 +11:00
J. Nick Koston
08035261b8 [libretiny] Use C++17 nested namespace syntax (#14325) 2026-02-26 10:02:36 -10:00
J. Nick Koston
e8b45e53fd [libretiny] Use -Os optimization for ESPHome source on BK72xx (SDK remains at -O1) (#14322) 2026-02-26 10:02:25 -10:00
Jonathan Swoboda
d325890148 [cc1101] Transition through IDLE in begin_tx/begin_rx for reliable state changes (#14321)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-26 14:48:05 -05:00
dependabot[bot]
8da1e3ce21 Bump ruff from 0.15.2 to 0.15.3 (#14323)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-26 19:32:53 +00:00
dependabot[bot]
c149be20fc Bump aioesphomeapi from 44.1.0 to 44.2.0 (#14324)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 19:31:47 +00:00
J. Nick Koston
4c3bb1596e [wifi] Use memcpy-based insertion sort for scan results (#13960) 2026-02-27 08:14:46 +13:00
J. Nick Koston
1912dcf03d [core] Use placement new for global Application instance (#14052) 2026-02-27 08:07:42 +13:00
J. Nick Koston
ae16c3bae7 Add socket compile tests for libretiny platforms (#14314) 2026-02-26 08:25:36 -10:00
J. Nick Koston
be000eab4e [ci] Add undocumented C++ API change checkbox and auto-label (#14317) 2026-02-26 08:02:52 -10:00
J. Nick Koston
a05d0202e6 [core] ESP32: massively reduce main loop socket polling overhead by replacing select() (#14249) 2026-02-26 06:21:27 -10:00
Jonathan Swoboda
6c253f0c71 [sprinkler] Fix millis overflow and underflow bugs (#14299)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-26 08:40:43 -05:00
kbx81
908c47bb5e preen, tune 2026-02-25 23:28:44 -06:00
J. Nick Koston
962cbfb9d8 [safe_mode] Mark SafeModeComponent and SafeModeTrigger as final (#14282) 2026-02-25 22:14:53 -05:00
J. Nick Koston
d52f8c9c6f [web_server] Mark classes as final (#14283) 2026-02-25 22:14:33 -05:00
J. Nick Koston
ee4d67930f [api] Mark ListEntitiesIterator and InitialStateIterator as final (#14284) 2026-02-25 22:14:16 -05:00
J. Nick Koston
cced0a82b5 [ota] Mark OTA backend and component leaf classes as final (#14287) 2026-02-25 22:14:04 -05:00
J. Nick Koston
478a876b01 [mdns] Mark MDNSComponent as final (#14290) 2026-02-25 22:13:51 -05:00
J. Nick Koston
789da5fdf8 [logger] Mark Logger and LoggerMessageTrigger as final (#14291) 2026-02-25 22:13:44 -05:00
Jesse Hills
bd08a56210 Merge branch 'release' into dev 2026-02-26 15:17:16 +13:00
Jonathan Swoboda
0d5b7df77d [sensor] Fix delta filter percentage mode regression (#14302)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 19:32:02 -05:00
kbx81
6df3a30740 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-25 17:33:27 -06:00
Jonathan Swoboda
534857db9c [wled] Fix millis overflow in blank timeout (#14300)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-02-25 18:01:49 -05:00
Jonathan Swoboda
0a81a7a50b [mcp2515] Fix millis overflow in set_mode_ timeout (#14298)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 18:01:32 -05:00
Jonathan Swoboda
23ef233b60 [gp8403] Fix enum size mismatch in voltage register write (#14296)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 17:21:50 -05:00
Jonathan Swoboda
24fb74f78b [ld2420] Fix buffer overflows in command response parsing (#14297)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 17:21:33 -05:00
Jonathan Swoboda
2e167835ea [pn532] Replace millis zero sentinel with optional (#14295)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 15:15:49 -05:00
Jonathan Swoboda
a60e5c5c4f [lightwaverf] Fix millis overflow in send timeout check (#14294)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 15:11:52 -05:00
Jonathan Swoboda
3dcc9ab765 [ble_presence] Fix millis overflow in presence timeout check (#14293)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 15:08:04 -05:00
Jonathan Swoboda
d61e2f9c29 [light] Fix millis overflow in transition progress and flash timing (#14292)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 15:06:13 -05:00
Jonathan Swoboda
5dffceda59 [hmc5883l] Fix wrong gain for 88uT range (#14281)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 14:35:27 -05:00
Jonathan Swoboda
d1a636a5c3 [rtttl] Fix speaker playback bugs (#14280)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 14:34:38 -05:00
Jonathan Swoboda
3f558f63d8 [bl0942] Fix millis overflow in packet timeout check (#14285)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 19:28:47 +00:00
Jonathan Swoboda
df77213f2c [shelly_dimmer] Fix millis overflow in ACK timeout check (#14288)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 19:27:00 +00:00
Jonathan Swoboda
e601162cdd [lcd_base] Fix millis() truncation to uint8_t (#14289)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 19:21:00 +00:00
Jonathan Swoboda
62da60df47 [ld2420] Fix sizeof vs value bug in register memcpy (#14286)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 19:19:20 +00:00
J. Nick Koston
8bb577de64 [api] Split ProtoVarInt::parse into 32-bit and 64-bit phases (#14039) 2026-02-25 12:23:13 -06:00
Thomas Rupprecht
ede8235aae [core] more accurate check for leap year and valid day_of_month (#14197)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 16:46:28 +00:00
Szpadel
37a0cec53d [ac_dimmer] Use a shared ESP32 GPTimer for multiple dimmers (#13523)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-25 16:12:03 +00:00
esphomebot
78ab63581b Update webserver local assets to 20260225-155043 (#14275)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-25 16:09:45 +00:00
J. Nick Koston
1beeb9ab5c [web_server] Fix uptime display overflow after ~24.8 days (#13739) 2026-02-25 08:54:32 -07:00
Jonathan Swoboda
228874a52b [config] Improve dimensions validation and fix online_image resize aspect ratio (#14274)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 10:45:50 -05:00
Big Mike
bb05cfb711 [sensirion_common] Move sen5x's sensirion_convert_to_string_in_place() function to sensirion_common (#14269) 2026-02-25 07:34:58 -05:00
J. Nick Koston
b134c4679c [light] Replace std::lerp with lightweight lerp_fast in LightColorValues::lerp (#14238) 2026-02-24 22:33:57 -06:00
Jonathan Swoboda
2e705a919f [pid] Fix deadband threshold conversion for Fahrenheit (#14268)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-24 23:26:00 -05:00
J. Nick Koston
1dac501b04 [light] Add additional light effect test cases (#14266) 2026-02-24 21:39:51 -06:00
Jesse Hills
905e81330e Don't get stuck forever on a failed component can_proceed (#14267) 2026-02-25 03:28:19 +00:00
J. Nick Koston
3460a8c922 [dlms_meter/kamstrup_kmp] Replace powf with pow10_int (#14125)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:44:50 +13:00
J. Nick Koston
2ff876c629 [core] Use custom deleter for SchedulerItem unique_ptr to prevent destructor inlining (#14258)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-25 13:18:44 +13:00
J. Nick Koston
08dc487b5b [core] Pass std::function by rvalue reference in scheduler (#14260) 2026-02-25 13:08:07 +13:00
J. Nick Koston
4dc6b12ec5 [api] Pass std::function by rvalue reference in state subscriptions (#14261) 2026-02-25 12:56:43 +13:00
J. Nick Koston
cca4777f64 [web_server_idf] Pass std::function by rvalue reference (#14262) 2026-02-25 12:51:01 +13:00
kbx81
0aaf59dbed Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-24 16:51:04 -06:00
Andrew Rankin
af00d601be [esp32_ble_server] add max_clients option for multi-client support (#14239) 2026-02-25 08:19:13 +11:00
Jonathan Swoboda
fe3c2ba555 [http_request.ota] Percent-encode credentials in URL (#14257)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-24 14:15:22 -05:00
J. Nick Koston
6554ad7c7e [core] Prevent inlining of mark_matching_items_removed_locked_ on Thumb-1 (#14256) 2026-02-24 12:08:51 -06:00
Clyde Stubbs
4abbed0cd4 [mipi_dsi] Allow transform disable; fix warnings (#14216) 2026-02-24 08:33:33 -05:00
H. Árkosi Róbert
72263eda85 [version] text sensor add option hide_hash to restore the pre-2026.1 behavior (#14251)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-24 07:31:58 -06:00
Jonathan Swoboda
abf7074518 [esp32] Improve ESP32-P4 engineering sample warning message (#14252)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-24 08:27:48 -05:00
J. Nick Koston
ad2da0af52 [network] Use C++17 nested namespace syntax (#14248) 2026-02-24 02:00:21 +00:00
J. Nick Koston
7d9d90d3f8 [cse7766] Use C++17 nested namespace syntax (#14247) 2026-02-23 20:50:22 -05:00
J. Nick Koston
70e47f301d [ethernet] Use C++17 nested namespace syntax (#14246) 2026-02-23 20:50:11 -05:00
J. Nick Koston
1614eb9c9c [i2c] Use C++17 nested namespace syntax (#14245) 2026-02-23 20:50:00 -05:00
J. Nick Koston
a694003fe3 [usb_host] Use C++17 nested namespace syntax (#14244) 2026-02-23 20:49:48 -05:00
J. Nick Koston
500aa7bf1d [text_sensor] Use C++17 nested namespace syntax (#14243) 2026-02-23 20:49:35 -05:00
J. Nick Koston
63c1496115 [text] Use C++17 nested namespace syntax (#14242) 2026-02-23 20:49:25 -05:00
J. Nick Koston
843d06df3f [switch] Use C++17 nested namespace syntax (#14241) 2026-02-23 20:49:15 -05:00
J. Nick Koston
30cc51eac9 [version] Use C++17 nested namespace syntax (#14240) 2026-02-23 20:49:00 -05:00
kbx81
249c5bb724 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-23 18:01:56 -06:00
Jonathan Swoboda
ebf1047da7 [core] Move build_info_data.h out of application.h to fix incremental rebuilds (#14230)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-23 18:51:56 -05:00
J. Nick Koston
869678953d [core] Add pow10_int helper, replace powf in normalize_accuracy and sensor filters (#14114)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 12:03:24 +13:00
James Myatt
4a52900352 [nfc] Fix logging tag for nfc helpers (#14235) 2026-02-23 21:16:32 +00:00
tomaszduda23
02c37bb6d6 [nrf52,logger] generate crash magic in python (#14173) 2026-02-23 20:23:40 +00:00
dependabot[bot]
918bbfb0d3 Bump aioesphomeapi from 44.0.0 to 44.1.0 (#14232)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 20:22:32 +00:00
tomaszduda23
063c6a9e45 [esp32,core] Move CONF_ENABLE_OTA_ROLLBACK to core (#14231) 2026-02-23 20:06:20 +00:00
Jonathan Swoboda
daee71a2c1 [http_request] Retry update check on startup until network is ready (#14228)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-23 14:21:29 -05:00
Jonathan Swoboda
0d32a5321c [remote_transmitter/remote_receiver] Rename _esp32.cpp to _rmt.cpp (#14226)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-23 13:46:53 -05:00
J. Nick Koston
e199145f1c [core] Avoid expensive modulo in LockFreeQueue for non-power-of-2 sizes (#14221) 2026-02-23 12:20:55 -06:00
Joshua Sing
1f945a334a [hdc302x] Add new component (#10160)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-23 12:01:23 -05:00
Jonathan Swoboda
fb6c7d81d5 [core] Fix multiline log continuations without leading whitespace (#14217)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 05:08:40 +00:00
J. Nick Koston
417f4535af [logger] Use subtraction-based line number formatting to avoid division (#14219) 2026-02-23 04:25:20 +00:00
schrob
ee94bc4715 [openthread] Refactor to optimize and match code rules (#14156) 2026-02-22 22:43:42 -05:00
schrob
6801604533 [openthread] Add Thread version DEBUG trace (#14196) 2026-02-22 22:40:40 -05:00
schrob
5c388a5200 [openthread_info] Optimize: Devirtualize/unify (#14208) 2026-02-22 22:39:36 -05:00
J. Nick Koston
d239a2400d [text_sensor] Conditionally compile filter infrastructure (#14213) 2026-02-22 21:36:21 -06:00
J. Nick Koston
93ce582ad3 [sensor] Conditionally compile filter infrastructure (#14214)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:35:51 -06:00
J. Nick Koston
6e70987451 [binary_sensor] Conditionally compile filter infrastructure (#14215) 2026-02-22 21:35:30 -06:00
schrob
263fff0ba2 Move CONF_OUTPUT_POWER into const.py (#14201) 2026-02-22 22:35:00 -05:00
J. Nick Koston
ded457c2c1 [libretiny] Tune oversized lwIP defaults for ESPHome (#14186)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-23 13:52:46 +13:00
J. Nick Koston
b539a5aa51 [water_heater] Fix device_id missing from state responses (#14212) 2026-02-22 23:07:56 +00:00
J. Nick Koston
5fddce6638 [logger] Make tx_buffer_ compile-time sized (#14205) 2026-02-23 12:02:05 +13:00
J. Nick Koston
ee1f521325 [http_request] Replace std::list<Header> with std::vector in perform() chain (#14027) 2026-02-23 11:59:54 +13:00
J. Nick Koston
ede2da2fbc [core] Conditionally compile get_loop_priority with USE_LOOP_PRIORITY (#14210) 2026-02-23 10:57:23 +13:00
J. Nick Koston
509f06afac [network] Improve IPAddress::str() deprecation warning with usage example (#14195) 2026-02-23 10:56:09 +13:00
J. Nick Koston
1753074eef [web_server_base] Remove unnecessary Component inheritance and modernize (#14204) 2026-02-23 10:01:07 +13:00
Edward Firmo
e013b48675 [nextion] Add error log for failed HTTP status during TFT upload (#14190) 2026-02-21 23:44:06 -06:00
J. Nick Koston
49e4ae54be [bme68x_bsec2] Fix compilation on ESP32 Arduino (#14194) 2026-02-22 05:22:59 +00:00
J. Nick Koston
d5c9c56fdf [platformio] Add exponential backoff and session reset to download retries (#14191) 2026-02-21 19:41:43 -06:00
J. Nick Koston
a468261523 [scheduler] De-template and consolidate scheduler helper functions (#14164) 2026-02-21 19:41:26 -06:00
J. Nick Koston
462ac29563 [scheduler] Use relaxed memory ordering for atomic reads under lock (#14140) 2026-02-21 17:29:41 -06:00
J. Nick Koston
6f198adb0c [scheduler] Reduce lock acquisitions in process_defer_queue_ (#14107) 2026-02-21 14:29:50 -06:00
J. Nick Koston
e521522b38 [haier] Fix uninitialized HonSettings causing API connection failures (#14188) 2026-02-21 13:54:43 -06:00
J. Nick Koston
5a07908dfa [api] Fix build error when lambda returns StringRef in homeassistant.event data (#14187) 2026-02-21 13:54:20 -06:00
J. Nick Koston
9571a979eb [ci] Suggest StringRef instead of std::string_view (#14183) 2026-02-21 13:53:45 -06:00
tomaszduda23
48ba007c22 [nrf52] print line number after crash in logs (#14165) 2026-02-21 14:16:20 -05:00
Clyde Stubbs
6ff17fbf7c [epaper_spi] Fix color mapping for weact (#14134) 2026-02-22 04:17:54 +11:00
J. Nick Koston
416b97311b [mqtt] Remove broken ESP8266 ssl_fingerprints option (#14182) 2026-02-21 11:12:35 -06:00
J. Nick Koston
7fb09da7cf [dsmr] Add deprecated std::string overload for set_decryption_key (#14180) 2026-02-21 11:08:13 -06:00
Sxt Fov
6ecb01dedc [cc1101] actions to change general and tuner settings (#14141)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-21 08:45:15 -05:00
Clyde Stubbs
518f08b909 [mipi_dsi] Disallow swap_xy (#14124) 2026-02-21 19:51:13 +11:00
Rodrigo Martín
2eac106f11 [mqtt] add missing precision in HA autodiscovery (#14010) 2026-02-20 23:20:27 -05:00
J. Nick Koston
f77da803c9 [api] Write protobuf encode output to pre-sized buffer directly (#14018) 2026-02-20 21:39:18 -06:00
J. Nick Koston
f8f98bf428 [logger] Reduce UART driver heap waste on ESP32 (#14168) 2026-02-21 03:16:49 +00:00
J. Nick Koston
abe37c9841 [uptime] Use scheduler millis_64() for rollover-safe uptime tracking (#14170) 2026-02-21 03:08:49 +00:00
J. Nick Koston
8589f80d8f [api,ota,captive_portal] Fix fd leaks and clean up socket_ip_loop_monitored setup paths (#14167) 2026-02-20 20:59:49 -06:00
J. Nick Koston
0e38acd67a [api] Warn when clients connect with outdated API version (#14145) 2026-02-20 19:21:56 -06:00
J. Nick Koston
a3f279c1cf [usb_host] Implement disable_loop/enable_loop pattern for USB components (#14163) 2026-02-20 19:21:14 -06:00
J. Nick Koston
35037d1a5b [core] Deduplicate base64 encode/decode logic (#14143)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:20:58 -06:00
J. Nick Koston
d206c75b0b [logger] Fix loop disable optimization using wrong preprocessor guard (#14158) 2026-02-20 19:20:44 -06:00
tomaszduda23
1d3054ef5e [nrf52,logger] Early debug (#11685) 2026-02-20 16:12:50 -06:00
Jonathan Swoboda
db6aa58f40 [max7219digit] Fix typo in action names (#14162)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 16:06:46 -05:00
Pawelo
48115eca18 [safe_mode] Extract RTC_KEY constant for shared use (#14121)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-02-20 14:08:31 -06:00
dependabot[bot]
edfc3e3501 Bump ruff from 0.15.1 to 0.15.2 (#14159)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-20 19:32:41 +00:00
dependabot[bot]
1a37632891 Bump pylint from 4.0.4 to 4.0.5 (#14160)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 19:27:45 +00:00
dependabot[bot]
b85a49cdb3 Bump github/codeql-action from 4.32.3 to 4.32.4 (#14161)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 13:27:15 -06:00
Jonathan Swoboda
9c0eed8a67 [e131] Remove dead LWIP TCP code path from loop() (#14155)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-20 17:03:39 +00:00
Jonathan Swoboda
887375ebef Merge branch 'release' into dev 2026-02-20 11:24:25 -05:00
Jonathan Swoboda
403235e2d4 [wifi] Add band_mode configuration for ESP32-C5 dual-band WiFi (#14148)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:20:29 -05:00
Jonathan Swoboda
9ce01fc369 [esp32] Add engineering_sample option for ESP32-P4 (#14139)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:20:05 -05:00
J. Nick Koston
b0a35559b3 [esp32] Bump ESP-IDF to 5.5.3.1, revert GATTS workaround (#14147) 2026-02-20 10:19:01 -06:00
Jonathan Swoboda
efe54e3b5e [ld2410/ld2450] Replace header sync with buffer size increase for frame resync (#14138)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 23:25:25 -05:00
Jonathan Swoboda
5af871acce [ld2420] Increase MAX_LINE_LENGTH to allow footer-based resync (#14137)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 22:36:12 -05:00
Jonathan Swoboda
a2f0607c1e [ld2410] Add frame header synchronization to readline_() (#14136)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 03:04:38 +00:00
Jonathan Swoboda
b67b2cc3ab [ld2450] Add frame header synchronization to fix initialization regression (#14135)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-20 02:56:20 +00:00
Jonathan Swoboda
7a2a149061 [esp32] Bump ESP-IDF to 5.5.3 (#14122)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:43:29 -05:00
J. Nick Koston
afbc45bf32 [e131] Drain all queued packets per loop iteration (#14133) 2026-02-19 20:35:42 -06:00
J. Nick Koston
c1265a9490 [core] Use constexpr for hand-written PROGMEM arrays in C++ (#14129)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:54:57 -06:00
J. Nick Koston
94712b3961 [esp8266][web_server] Use constexpr for PROGMEM arrays in codegen (#14128)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:54:44 -06:00
J. Nick Koston
d29288547e [core] Use constexpr for PROGMEM arrays (#14127)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:54:33 -06:00
kbx81
54ea8dd207 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-19 18:31:15 -06:00
Jonathan Swoboda
1b4de55efd [pulse_counter] Fix PCNT glitch filter calculation off by 1000x (#14132)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 00:12:37 +00:00
J. Nick Koston
cceb109303 [uart] Always call pin setup for UART0 default pins on ESP-IDF (#14130) 2026-02-19 23:48:18 +00:00
puddly
4cfb794b62 WIP 2026-02-19 18:22:03 -05:00
kbx81
917af8ff31 [zigbee_proxy] New component 2026-02-19 14:34:29 -06:00
Jonathan Swoboda
17a810b939 [wifi] Sync output_power with PHY max TX power to prevent brownout (#14118)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:14:48 -05:00
J. Nick Koston
4aa8f57d36 [json] Add SerializationBuffer for stack-first JSON serialization (#13625) 2026-02-19 14:08:44 -06:00
Jonathan Swoboda
f2c98d6126 [safe_mode] Log brownout as reset reason on OTA rollback (#14113)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:45:04 +00:00
J. Nick Koston
7a5c3cee0d [esp32_ble] Enable CONFIG_BT_RELEASE_IRAM on ESP32-C2 (#14109)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:41:00 +00:00
Jonathan Swoboda
9aa17984df [pulse_counter] Fix build failure when use_pcnt is false (#14111)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:25:26 +00:00
Jonathan Swoboda
da616e0557 [ethernet] Improve clk_mode deprecation warning with actionable YAML (#14104)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:00:05 +00:00
Kevin Ahrendt
d2026b4cd7 [audio] Disable FLAC CRC validation to improve decoding efficiency (#14108) 2026-02-19 11:56:34 -05:00
Jonathan Swoboda
ed74790eed [i2c] Remove deprecated stop parameter overloads and readv/writev methods (#14106)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:56:06 +00:00
Jonathan Swoboda
bf2e22da4f [esp32] Remove deprecated add_idf_component() parameters and IDF component refresh option (#14105)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:55:03 +00:00
Jonathan Swoboda
bd50b80882 [opentherm] Remove deprecated opentherm_version config option (#14103)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:34:40 -05:00
Kevin Ahrendt
b11ad26c4f [audio] Support decoding audio directly from flash (#14098)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-19 11:20:19 -05:00
J. Nick Koston
f7459670d3 [core] Optimize WarnIfComponentBlockingGuard::finish() hot path (#14040)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:10:22 +00:00
Jonathan Swoboda
5304750215 [socket] Fix IPv6 compilation error on host platform (#14101)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:00:34 +00:00
J. Nick Koston
a8171da003 [web_server] Reduce set_json_id flash and stack usage (#14029) 2026-02-19 09:38:57 -06:00
J. Nick Koston
916cf0d8b7 [e131] Replace std::map with std::vector for universe tracking (#14087) 2026-02-19 09:28:00 -06:00
J. Nick Koston
0484b2852d [e131] Fix E1.31 on ESP8266 and RP2040 by restoring WiFiUDP support (#14086) 2026-02-19 09:27:05 -06:00
J. Nick Koston
b5a8e1c94c [ci] Update lint message to recommend constexpr over static const (#14099) 2026-02-19 09:06:46 -06:00
dependabot[bot]
01a46f665f Bump esptool from 5.1.0 to 5.2.0 (#14058)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 09:42:22 -05:00
J. Nick Koston
535980b9bd [cse7761] Use constexpr for compile-time constants (#14081) 2026-02-19 08:40:41 -06:00
J. Nick Koston
b0085e21f7 [core] Devirtualize call_loop() and mark_failed() in Component (#14083) 2026-02-19 08:40:23 -06:00
J. Nick Koston
6daca09794 [logger] Replace LogListener virtual interface with LogCallback struct (#14084) 2026-02-19 08:40:08 -06:00
J. Nick Koston
7b53a98950 [socket] Log error when UDP socket requested on LWIP TCP-only platforms (#14089) 2026-02-19 08:39:44 -06:00
Rodrigo Martín
4cc1e6a910 [esp32_ble_server] add test for lambda characteristic (#14091) 2026-02-19 09:23:22 -05:00
Marc Hörsken
4d05e4d576 [esp32_camera] Add support for sensors without JPEG support (#9496)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-02-18 21:52:38 -06:00
Kevin Ahrendt
eefad194d0 [audio, speaker] Add support for decoding Ogg Opus files (#13967) 2026-02-18 21:51:33 -06:00
Kevin Ahrendt
ba7134ee3f [mdns] add Sendspin advertisement support (#14013)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-18 21:51:16 -06:00
Kevin Ahrendt
264c8faedd [media_player] Add more commands to support Sendspin (#12258)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-02-18 21:51:01 -06:00
Kevin Ahrendt
3c227eeca4 [audio] Add support for sinking via an arbitrary callback (#14035) 2026-02-18 21:50:39 -06:00
J. Nick Koston
c8598fe620 [bluetooth_proxy] Use constexpr for remaining compile-time constants (#14080) 2026-02-18 21:34:25 -06:00
J. Nick Koston
2f9b76f129 [pn7160] Use constexpr for compile-time constants (#14078) 2026-02-18 21:33:39 -06:00
J. Nick Koston
9a8b00a428 [nfc] Use constexpr for compile-time constants (#14077) 2026-02-18 21:33:23 -06:00
J. Nick Koston
eaf0d03a37 [ld2420] Use constexpr for compile-time constants (#14079) 2026-02-18 21:32:37 -06:00
J. Nick Koston
e7f2021864 [http_request] Replace std::map with std::vector in action template (#14026) 2026-02-18 21:32:24 -06:00
J. Nick Koston
dff9780d3a [core] Use constexpr for compile-time constants (#14071) 2026-02-19 03:19:48 +00:00
J. Nick Koston
20239d1bb3 [remote_base] Use constexpr for compile-time constants (#14076) 2026-02-19 03:16:09 +00:00
J. Nick Koston
ee7d63f73a [packet_transport] Use constexpr for compile-time constants (#14074) 2026-02-18 21:09:49 -06:00
J. Nick Koston
76c151c6e6 [api] Use constexpr for compile-time constant (#14072) 2026-02-18 21:07:38 -06:00
J. Nick Koston
9c9365c146 [bluetooth_proxy][esp32_ble_client][esp32_ble_server] Use constexpr for compile-time constants (#14073) 2026-02-18 21:07:06 -06:00
J. Nick Koston
7e118178b3 [web_server] Fix water_heater JSON key names and move traits to DETAIL_ALL (#14064) 2026-02-18 21:00:24 -06:00
J. Nick Koston
66d2ac8cb9 [web_server] Move climate static traits to DETAIL_ALL only (#14066) 2026-02-18 21:00:09 -06:00
J. Nick Koston
e4c233b6ce [mqtt] Use constexpr for compile-time constants (#14075) 2026-02-18 20:59:31 -06:00
J. Nick Koston
be853afc24 [core] Conditionally compile setup_priority override infrastructure (#14057) 2026-02-18 20:57:56 -06:00
J. Nick Koston
565443b710 [pulse_counter] Fix compilation on ESP32-C6/C5/H2/P4 (#14070) 2026-02-18 19:08:53 -06:00
J. Nick Koston
3b869f1720 [web_server] Double socket allocation to prevent connection exhaustion (#14067) 2026-02-18 19:01:37 -06:00
J. Nick Koston
5f82017a31 [udp] Register socket consumption for CONFIG_LWIP_MAX_SOCKETS (#14068) 2026-02-18 19:01:00 -06:00
J. Nick Koston
bd055e75b9 [core] Shrink Application::dump_config_at_ from size_t to uint16_t (#14053)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 16:49:37 -06:00
J. Nick Koston
d90754dc0a [http_request] Replace heavy STL containers with std::vector for headers (#14024) 2026-02-18 16:49:19 -06:00
J. Nick Koston
387f615dae [api] Add handshake timeout to prevent connection slot exhaustion (#14050) 2026-02-18 16:48:30 -06:00
J. Nick Koston
02e310f2c9 [core] Remove unnecessary IRAM_ATTR from yield(), delay(), feed_wdt(), and arch_feed_wdt() (#14063) 2026-02-18 16:48:13 -06:00
Jesse Hills
d83738df87 Merge branch 'release' into dev 2026-02-19 11:43:58 +13:00
J. Nick Koston
09fc028895 [core] Remove dead global_state variable (#14060) 2026-02-18 15:16:26 -06:00
J. Nick Koston
82cfa00a97 [tlc59208f] Make mode constants inline constexpr (#14043) 2026-02-18 15:04:30 -06:00
J. Nick Koston
4a038978d2 [pca9685] Make mode constants inline constexpr (#14042) 2026-02-18 15:04:14 -06:00
Jesse Hills
bd38041d04 Merge branch 'beta' into dev 2026-02-19 09:05:23 +13:00
Jonathan Swoboda
9cd7b0b32b [external_components] Clean up incomplete clone on failed ref fetch (#14051)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 18:09:33 +00:00
dependabot[bot]
f73bcc0e7b Bump cryptography from 45.0.1 to 46.0.5 (#14049)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 09:08:12 -06:00
dependabot[bot]
652c669777 Bump pillow from 11.3.0 to 12.1.1 (#14048)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 09:08:02 -06:00
J. Nick Koston
fb89900c64 [core] Make setup_priority and component state constants constexpr (#14041) 2026-02-18 08:22:36 -06:00
J. Nick Koston
fb35ddebb9 [display] Make COLOR_OFF and COLOR_ON inline constexpr (#14044) 2026-02-18 08:22:07 -06:00
Jesse Hills
a3d7e76992 Merge branch 'beta' into dev 2026-02-18 13:29:11 +13:00
dependabot[bot]
5bb863f7da Bump actions/stale from 10.1.1 to 10.2.0 (#14036)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 13:24:39 -06:00
Rodrigo Martín
81ed70325c [esp32_ble_server] fix infinitely large characteristic value (#14011) 2026-02-17 07:45:21 -10:00
schrob
e826d71bd8 [openthread] Fix compiler format warning (#14030) 2026-02-17 10:16:57 -05:00
J. Nick Koston
4cd3f6c36a [api] Remove unused reserve from APIServer constructor (#14017) 2026-02-17 16:30:57 +13:00
Jesse Hills
6b4b8cb2f9 Merge branch 'beta' into dev 2026-02-17 16:22:46 +13:00
J. Nick Koston
0c4827d348 [json, core] Remove stored RAMAllocator, make constructors constexpr (#14000) 2026-02-16 08:09:53 -06:00
J. Nick Koston
81872d9822 [camera, camera_encoder] Remove stored RAMAllocator member (#13997) 2026-02-16 08:09:26 -06:00
J. Nick Koston
ffb9a00e26 [online_image] Remove stored RAMAllocator member from DownloadBuffer (#13999) 2026-02-16 08:09:13 -06:00
J. Nick Koston
f2c827f9a2 [runtime_image] Remove stored RAMAllocator member (#13998) 2026-02-16 08:08:43 -06:00
Cornelius A. Ludmann
f2cb5db9e0 [epaper_spi] Add Waveshare 7.5in e-Paper (H) (#13991) 2026-02-16 13:44:30 +11:00
Kevin Ahrendt
066419019f [audio] Support reallocating non-empty AudioTransferBuffer (#13979) 2026-02-15 16:09:35 -05:00
Pawelo
15da6d0a0b [epaper_spi] Add WeAct 3-color e-paper display support (#13894) 2026-02-16 07:58:51 +11:00
Jonathan Swoboda
6303bc3e35 [esp32_rmt] Handle ESP32 variants without RMT hardware (#14001)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:23:06 -05:00
Jonathan Swoboda
0f4dc6702d [fan] Fix preset_mode not restored on boot (#14002)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:11:50 -05:00
Jonathan Swoboda
f48c8a6444 [combination] Fix 'coeffecient' typo with backward-compatible deprecation (#14004)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:11:36 -05:00
dependabot[bot]
38404b2013 Bump ruff from 0.15.0 to 0.15.1 (#13980)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-14 15:11:17 -07:00
AndreKR
5a6d64814a [http_request] Improve TLS logging on ESP8266 (#13985) 2026-02-14 10:08:26 -07:00
J. Nick Koston
36776b40c2 [wifi] Fix ESP8266 DHCP state corruption from premature dhcp_renew() (#13983)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 08:21:04 -07:00
Jesse Hills
58c3ba7ac6 Merge branch 'beta' into dev 2026-02-14 16:03:25 +13:00
dependabot[bot]
931b47673c Bump github/codeql-action from 4.32.2 to 4.32.3 (#13981)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 16:22:26 -06:00
J. Nick Koston
79d9fbf645 [nfc] Replace constant std::vector with static constexpr std::array (#13978) 2026-02-13 16:22:05 -06:00
J. Nick Koston
f24e7709ac [core] Make LOG_ENTITY_ICON a no-op when icons are compiled out (#13973) 2026-02-13 16:21:50 -06:00
Kevin Ahrendt
903971de12 [runtime_image, online_image] Create runtime_image component to decode images (#10212) 2026-02-13 11:25:43 -05:00
J. Nick Koston
b04e427f01 [usb_host] Extract cold path from loop(), replace std::string with buffer API (#13957)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-13 06:39:00 -06:00
J. Nick Koston
e0c03b2dfa [api] Fix ESP8266 noise API handshake deadlock and prompt socket cleanup (#13972) 2026-02-12 18:20:58 -06:00
J. Nick Koston
7dff631dcb [core] Flatten single-callsite vector realloc functions (#13970) 2026-02-12 18:20:39 -06:00
J. Nick Koston
36aba385af [web_server] Flatten deq_push_back_with_dedup_ to inline vector realloc (#13968) 2026-02-12 18:20:21 -06:00
Jonathan Swoboda
136d17366f [docker] Suppress git detached HEAD advice (#13962)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:12:17 -05:00
Jonathan Swoboda
db7870ef5f [alarm_control_panel] Fix flaky integration test race condition (#13964)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:04:39 -05:00
dependabot[bot]
bbc88d92ea Bump docker/build-push-action from 6.19.1 to 6.19.2 in /.github/actions/build-image (#13965)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 14:31:43 -06:00
Jesse Hills
1604b5d6e4 Merge branch 'beta' into dev 2026-02-13 07:11:49 +13:00
J. Nick Koston
7fd535179e [helpers] Add heap warnings to format_hex_pretty, deprecate ethernet/web_server std::string APIs (#13959) 2026-02-12 17:47:44 +00:00
Lukáš Maňas
e3a457e402 [pulse_meter] Fix early edge detection (#12360)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-12 17:20:54 +00:00
J. Nick Koston
0dcff82bb4 [wifi] Deprecate wifi_ssid() in favor of wifi_ssid_to() (#13958) 2026-02-12 17:14:36 +00:00
J. Nick Koston
cde8b66719 [web_server] Switch from getParam to arg API to eliminate heap allocations (#13942) 2026-02-12 11:04:41 -06:00
J. Nick Koston
0e1433329d [api] Extract cold code from APIServer::loop() hot path (#13902) 2026-02-12 11:04:23 -06:00
J. Nick Koston
60fef5e656 [analyze_memory] Fix mDNS packet buffer miscategorized as wifi_config (#13949)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 10:26:54 -06:00
J. Nick Koston
725e774fe7 [web_server] Guard icon JSON field with USE_ENTITY_ICON (#13948) 2026-02-12 10:26:36 -06:00
J. Nick Koston
9aa98ed6c6 [uart] Remove redundant mutex, fix flush race, conditional event queue (#13955) 2026-02-12 10:26:10 -06:00
Guillermo Ruffino
7b251dcc31 [schema-gen] fix Windows: ensure UTF-8 encoding when reading component files (#13952) 2026-02-12 11:23:59 -05:00
schrob
8a08c688f6 [mipi_spi] Add Waveshare 1.83 v2 panel (#13680) 2026-02-12 23:25:51 +11:00
Jesse Hills
d6461251f9 Bump version to 2026.3.0-dev 2026-02-12 23:04:19 +13:00
1297 changed files with 50441 additions and 18374 deletions

View File

@@ -286,6 +286,7 @@ This document provides essential context for AI models interacting with this pro
* **Documentation Contributions:**
* Documentation is hosted in the separate `esphome/esphome-docs` repository.
* The contribution workflow is the same as for the codebase.
* When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync.
* **Best Practices:**
* **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests.

View File

@@ -1 +1 @@
ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e
8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a

View File

@@ -6,8 +6,9 @@
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Developer breaking change (an API change that could break external components)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — [policy](https://developers.esphome.io/contributing/code/#what-constitutes-a-c-breaking-change)
- [ ] Developer breaking change (an API change that could break external components) — [policy](https://developers.esphome.io/contributing/code/#what-is-considered-public-c-api)
- [ ] Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on) — [policy](https://developers.esphome.io/contributing/code/#c-user-expectations)
- [ ] Code quality improvements to existing code or addition of tests
- [ ] Other
@@ -24,7 +25,7 @@
- [ ] ESP32
- [ ] ESP32 IDF
- [ ] ESP8266
- [ ] RP2040
- [ ] RP2040/RP2350
- [ ] BK72xx
- [ ] RTL87xx
- [ ] LN882x

View File

@@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false

View File

@@ -14,6 +14,7 @@ module.exports = {
'chained-pr',
'core',
'small-pr',
'medium-pr',
'dashboard',
'github-actions',
'by-code-owner',
@@ -27,6 +28,7 @@ module.exports = {
'new-feature',
'breaking-change',
'developer-breaking-change',
'undocumented-api-change',
'code-quality',
'deprecated-component'
],

View File

@@ -1,5 +1,13 @@
const fs = require('fs');
const { DOCS_PR_PATTERNS } = require('./constants');
const {
COMPONENT_REGEX,
detectComponents,
hasCoreChanges,
hasDashboardChanges,
hasGitHubActionsChanges,
} = require('../detect-tags');
const { loadCodeowners, getEffectiveOwners } = require('../codeowners');
// Strategy: Merge branch detection
async function detectMergeBranch(context) {
@@ -20,15 +28,13 @@ async function detectMergeBranch(context) {
// Strategy: Component and platform labeling
async function detectComponentPlatforms(changedFiles, apiData) {
const labels = new Set();
const componentRegex = /^esphome\/components\/([^\/]+)\//;
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
for (const file of changedFiles) {
const componentMatch = file.match(componentRegex);
if (componentMatch) {
labels.add(`component: ${componentMatch[1]}`);
}
for (const comp of detectComponents(changedFiles)) {
labels.add(`component: ${comp}`);
}
for (const file of changedFiles) {
const platformMatch = file.match(targetPlatformRegex);
if (platformMatch) {
labels.add(`platform: ${platformMatch[1]}`);
@@ -90,20 +96,14 @@ async function detectNewPlatforms(prFiles, apiData) {
// Strategy: Core files detection
async function detectCoreChanges(changedFiles) {
const labels = new Set();
const coreFiles = changedFiles.filter(file =>
file.startsWith('esphome/core/') ||
(file.startsWith('esphome/') && file.split('/').length === 2)
);
if (coreFiles.length > 0) {
if (hasCoreChanges(changedFiles)) {
labels.add('core');
}
return labels;
}
// Strategy: PR size detection
async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
const labels = new Set();
if (totalChanges <= SMALL_PR_THRESHOLD) {
@@ -111,6 +111,11 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange
return labels;
}
if (totalChanges <= MEDIUM_PR_THRESHOLD) {
labels.add('medium-pr');
return labels;
}
const testAdditions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0), 0);
@@ -131,80 +136,33 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange
// Strategy: Dashboard changes
async function detectDashboardChanges(changedFiles) {
const labels = new Set();
const dashboardFiles = changedFiles.filter(file =>
file.startsWith('esphome/dashboard/') ||
file.startsWith('esphome/components/dashboard_import/')
);
if (dashboardFiles.length > 0) {
if (hasDashboardChanges(changedFiles)) {
labels.add('dashboard');
}
return labels;
}
// Strategy: GitHub Actions changes
async function detectGitHubActionsChanges(changedFiles) {
const labels = new Set();
const githubActionsFiles = changedFiles.filter(file =>
file.startsWith('.github/workflows/')
);
if (githubActionsFiles.length > 0) {
if (hasGitHubActionsChanges(changedFiles)) {
labels.add('github-actions');
}
return labels;
}
// Strategy: Code owner detection
async function detectCodeOwner(github, context, changedFiles) {
const labels = new Set();
const { owner, repo } = context.repo;
try {
const { data: codeownersFile } = await github.rest.repos.getContent({
owner,
repo,
path: 'CODEOWNERS',
});
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
const codeownersPatterns = loadCodeowners();
const prAuthor = context.payload.pull_request.user.login;
const codeownersLines = codeownersContent.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
const codeownersRegexes = codeownersLines.map(line => {
const parts = line.split(/\s+/);
const pattern = parts[0];
const owners = parts.slice(1);
let regex;
if (pattern.endsWith('*')) {
const dir = pattern.slice(0, -1);
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
} else if (pattern.includes('*')) {
// First escape all regex special chars except *, then replace * with .*
const regexPattern = pattern
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*');
regex = new RegExp(`^${regexPattern}$`);
} else {
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
}
return { regex, owners };
});
for (const file of changedFiles) {
for (const { regex, owners } of codeownersRegexes) {
if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) {
labels.add('by-code-owner');
return labels;
}
}
// Check if PR author is a codeowner of any changed file
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
if (effective.users.has(prAuthor)) {
labels.add('by-code-owner');
}
} catch (error) {
console.log('Failed to read or parse CODEOWNERS file:', error.message);
@@ -238,6 +196,7 @@ async function detectPRTemplateCheckboxes(context) {
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
{ pattern: /- \[x\] Undocumented C\+\+ API change \(removal or change of undocumented public methods that lambda users may depend on\)/i, label: 'undocumented-api-change' },
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
];
@@ -258,7 +217,7 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
const { owner, repo } = context.repo;
// Compile regex once for better performance
const componentFileRegex = /^esphome\/components\/([^\/]+)\//;
const componentFileRegex = COMPONENT_REGEX;
// Get files that are modified or added in components directory
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));

View File

@@ -35,6 +35,7 @@ async function fetchApiData() {
module.exports = async ({ github, context }) => {
// Environment variables
const SMALL_PR_THRESHOLD = parseInt(process.env.SMALL_PR_THRESHOLD);
const MEDIUM_PR_THRESHOLD = parseInt(process.env.MEDIUM_PR_THRESHOLD);
const MAX_LABELS = parseInt(process.env.MAX_LABELS);
const TOO_BIG_THRESHOLD = parseInt(process.env.TOO_BIG_THRESHOLD);
const COMPONENT_LABEL_THRESHOLD = parseInt(process.env.COMPONENT_LABEL_THRESHOLD);
@@ -120,7 +121,7 @@ module.exports = async ({ github, context }) => {
detectNewComponents(prFiles),
detectNewPlatforms(prFiles, apiData),
detectCoreChanges(changedFiles),
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD),
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD),
detectDashboardChanges(changedFiles),
detectGitHubActionsChanges(changedFiles),
detectCodeOwner(github, context, changedFiles),

227
.github/scripts/codeowners.js vendored Normal file
View File

@@ -0,0 +1,227 @@
// Shared CODEOWNERS parsing and matching utilities.
//
// Used by:
// - codeowner-review-request.yml
// - codeowner-approved-label-update.yml
// - auto-label-pr/detectors.js (detectCodeOwner)
/**
* Convert a CODEOWNERS glob pattern to a RegExp.
*
* Handles **, *, and ? wildcards after escaping regex-special characters.
*/
function globToRegex(pattern) {
let regexStr = pattern
.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1')
.replace(/\*\*/g, '\x00GLOBSTAR\x00') // protect ** from next replace
.replace(/\*/g, '[^/]*') // single star
.replace(/\x00GLOBSTAR\x00/g, '.*') // restore globstar
.replace(/\?/g, '.');
return new RegExp('^' + regexStr + '$');
}
/**
* Parse raw CODEOWNERS file content into an array of
* { pattern, regex, owners } objects.
*
* Each `owners` entry is the raw string from the file (e.g. "@user" or
* "@esphome/core").
*/
function parseCodeowners(content) {
const lines = content
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
const patterns = [];
for (const line of lines) {
const parts = line.split(/\s+/);
if (parts.length < 2) continue;
const pattern = parts[0];
const owners = parts.slice(1);
const regex = globToRegex(pattern);
patterns.push({ pattern, regex, owners });
}
return patterns;
}
/**
* Fetch and parse the CODEOWNERS file via the GitHub API.
*
* @param {object} github - octokit instance from actions/github-script
* @param {string} owner - repo owner
* @param {string} repo - repo name
* @param {string} [ref] - git ref (SHA / branch) to read from
* @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>}
*/
async function fetchCodeowners(github, owner, repo, ref) {
const params = { owner, repo, path: 'CODEOWNERS' };
if (ref) params.ref = ref;
const { data: file } = await github.rest.repos.getContent(params);
const content = Buffer.from(file.content, 'base64').toString('utf8');
return parseCodeowners(content);
}
/**
* Classify raw owner strings into individual users and teams.
*
* @param {string[]} rawOwners - e.g. ["@user1", "@esphome/core"]
* @returns {{ users: string[], teams: string[] }}
* users login names without "@"
* teams team slugs without the "org/" prefix
*/
function classifyOwners(rawOwners) {
const users = [];
const teams = [];
for (const o of rawOwners) {
const clean = o.startsWith('@') ? o.slice(1) : o;
if (clean.includes('/')) {
teams.push(clean.split('/')[1]);
} else {
users.push(clean);
}
}
return { users, teams };
}
/**
* For each file, find its effective codeowners using GitHub's
* "last match wins" semantics, then union across all files.
*
* @param {string[]} files - list of file paths
* @param {Array} codeownersPatterns - from parseCodeowners / fetchCodeowners
* @returns {{ users: Set<string>, teams: Set<string>, matchedFileCount: number }}
*/
function getEffectiveOwners(files, codeownersPatterns) {
const users = new Set();
const teams = new Set();
let matchedFileCount = 0;
for (const file of files) {
// Last matching pattern wins for each file
let effectiveOwners = null;
for (const { regex, owners } of codeownersPatterns) {
if (regex.test(file)) {
effectiveOwners = owners;
}
}
if (effectiveOwners) {
matchedFileCount++;
const classified = classifyOwners(effectiveOwners);
for (const u of classified.users) users.add(u);
for (const t of classified.teams) teams.add(t);
}
}
return { users, teams, matchedFileCount };
}
/**
* Read and parse the CODEOWNERS file from disk.
*
* Use this when the repo is already checked out (avoids an API call).
*
* @param {string} [repoRoot='.'] - path to the repo root
* @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>}
*/
function loadCodeowners(repoRoot = '.') {
const fs = require('fs');
const path = require('path');
const content = fs.readFileSync(path.join(repoRoot, 'CODEOWNERS'), 'utf8');
return parseCodeowners(content);
}
/** Possible label actions returned by determineLabelAction. */
const LabelAction = Object.freeze({
ADD: 'add',
REMOVE: 'remove',
NONE: 'none',
});
/**
* Determine what label action is needed for a PR based on codeowner approvals.
*
* Checks changed files against CODEOWNERS patterns, reviews, and current labels
* to decide if the label should be added, removed, or left unchanged.
*
* @param {object} github - octokit instance from actions/github-script
* @param {string} owner - repo owner
* @param {string} repo - repo name
* @param {number} pr_number - pull request number
* @param {Array} codeownersPatterns - from loadCodeowners / fetchCodeowners
* @param {string} labelName - label to manage
* @returns {Promise<LabelAction>}
*/
async function determineLabelAction(github, owner, repo, pr_number, codeownersPatterns, labelName) {
// Get the list of changed files in this PR
const prFiles = await github.paginate(
github.rest.pulls.listFiles,
{ owner, repo, pull_number: pr_number }
);
const changedFiles = prFiles.map(file => file.filename);
console.log(`Found ${changedFiles.length} changed files`);
if (changedFiles.length === 0) {
console.log('No changed files found');
return LabelAction.NONE;
}
// Get effective owners using last-match-wins semantics
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
const componentCodeowners = effective.users;
console.log(`Component-specific codeowners: ${Array.from(componentCodeowners).join(', ') || '(none)'}`);
// Get current labels
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner, repo, issue_number: pr_number
});
const hasLabel = currentLabels.some(label => label.name === labelName);
if (componentCodeowners.size === 0) {
console.log('No component-specific codeowners found');
return hasLabel ? LabelAction.REMOVE : LabelAction.NONE;
}
// Get all reviews and find latest per user
const reviews = await github.paginate(
github.rest.pulls.listReviews,
{ owner, repo, pull_number: pr_number }
);
const latestReviewByUser = new Map();
for (const review of reviews) {
if (!review.user || review.user.type === 'Bot' || review.state === 'COMMENTED') continue;
latestReviewByUser.set(review.user.login, review);
}
// Check if any component-specific codeowner has an active approval
let hasCodeownerApproval = false;
for (const [login, review] of latestReviewByUser) {
if (review.state === 'APPROVED' && componentCodeowners.has(login)) {
console.log(`Codeowner '${login}' has approved`);
hasCodeownerApproval = true;
break;
}
}
if (hasCodeownerApproval && !hasLabel) return LabelAction.ADD;
if (!hasCodeownerApproval && hasLabel) return LabelAction.REMOVE;
console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`);
return LabelAction.NONE;
}
module.exports = {
globToRegex,
parseCodeowners,
fetchCodeowners,
loadCodeowners,
classifyOwners,
getEffectiveOwners,
LabelAction,
determineLabelAction
};

66
.github/scripts/detect-tags.js vendored Normal file
View File

@@ -0,0 +1,66 @@
/**
* Shared tag detection from changed file paths.
* Used by pr-title-check and auto-label-pr workflows.
*/
const COMPONENT_REGEX = /^esphome\/components\/([^\/]+)\//;
/**
* Detect component names from changed files.
* @param {string[]} changedFiles - List of changed file paths
* @returns {Set<string>} Set of component names
*/
function detectComponents(changedFiles) {
const components = new Set();
for (const file of changedFiles) {
const match = file.match(COMPONENT_REGEX);
if (match) {
components.add(match[1]);
}
}
return components;
}
/**
* Detect if core files were changed.
* Core files are in esphome/core/ or top-level esphome/ directory.
* @param {string[]} changedFiles - List of changed file paths
* @returns {boolean}
*/
function hasCoreChanges(changedFiles) {
return changedFiles.some(file =>
file.startsWith('esphome/core/') ||
(file.startsWith('esphome/') && file.split('/').length === 2)
);
}
/**
* Detect if dashboard files were changed.
* @param {string[]} changedFiles - List of changed file paths
* @returns {boolean}
*/
function hasDashboardChanges(changedFiles) {
return changedFiles.some(file =>
file.startsWith('esphome/dashboard/') ||
file.startsWith('esphome/components/dashboard_import/')
);
}
/**
* Detect if GitHub Actions files were changed.
* @param {string[]} changedFiles - List of changed file paths
* @returns {boolean}
*/
function hasGitHubActionsChanges(changedFiles) {
return changedFiles.some(file =>
file.startsWith('.github/workflows/')
);
}
module.exports = {
COMPONENT_REGEX,
detectComponents,
hasCoreChanges,
hasDashboardChanges,
hasGitHubActionsChanges,
};

View File

@@ -12,6 +12,7 @@ permissions:
env:
SMALL_PR_THRESHOLD: 30
MEDIUM_PR_THRESHOLD: 100
MAX_LABELS: 15
TOO_BIG_THRESHOLD: 1000
COMPONENT_LABEL_THRESHOLD: 10

View File

@@ -62,7 +62,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: generated-proto-files
path: |

View File

@@ -49,7 +49,7 @@ jobs:
with:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Set TAG
run: |

View File

@@ -686,7 +686,7 @@ jobs:
ram_usage: ${{ steps.extract.outputs.ram_usage }}
flash_usage: ${{ steps.extract.outputs.flash_usage }}
cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }}
skip: ${{ steps.check-script.outputs.skip }}
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -705,10 +705,39 @@ jobs:
echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis"
fi
# All remaining steps only run if script exists
# Check if test files exist on the target branch for the requested
# components and platform. When a PR adds new test files for a platform,
# the target branch won't have them yet, so skip instead of failing.
# This check must be done here (not in determine-jobs.py) because
# determine-jobs runs on the PR branch and cannot see what the target
# branch has.
- name: Check for test files on target branch
id: check-tests
if: steps.check-script.outputs.skip != 'true'
run: |
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
found=false
for component in $(echo "$components" | jq -r '.[]'); do
# Check for test files matching the platform (test.platform.yaml or test-*.platform.yaml)
for f in tests/components/${component}/test*.${platform}.yaml; do
if [ -f "$f" ]; then
found=true
break 2
fi
done
done
if [ "$found" = false ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "::warning::No test files found on target branch for platform ${platform}, skipping memory impact analysis"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
# All remaining steps only run if script and tests exist
- name: Generate cache key
id: cache-key
if: steps.check-script.outputs.skip != 'true'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
run: |
# Get the commit SHA of the target branch
target_sha=$(git rev-parse HEAD)
@@ -735,14 +764,14 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
- name: Cache status
if: steps.check-script.outputs.skip != 'true'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
run: |
if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then
echo "✓ Cache hit! Using cached memory analysis results."
@@ -752,21 +781,21 @@ jobs:
fi
- name: Restore Python
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
- name: Build, compile, and analyze memory
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
id: build
run: |
. venv/bin/activate
@@ -800,7 +829,7 @@ jobs:
--platform "$platform"
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: memory-analysis-target.json
@@ -808,7 +837,7 @@ jobs:
- name: Extract memory usage for outputs
id: extract
if: steps.check-script.outputs.skip != 'true'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
run: |
if [ -f memory-analysis-target.json ]; then
ram=$(jq -r '.ram_bytes' memory-analysis-target.json)
@@ -822,7 +851,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -886,7 +915,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -916,13 +945,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: memory-analysis-pr
path: ./memory-analysis

View File

@@ -0,0 +1,81 @@
# Adds/removes a 'code-owner-approved' label when a component-specific
# codeowner approves (or dismisses) a PR.
#
# Uses pull_request_target so that fork PRs do not require workflow approval.
# The label is reconciled on every PR update; for review events specifically,
# this means the label is applied on the next push after a codeowner review.
name: Codeowner Approved Label
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
branches-ignore:
- release
- beta
permissions:
issues: write
pull-requests: read
contents: read
jobs:
codeowner-approved:
name: Run
if: ${{ github.repository == 'esphome/esphome' }}
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
.github/scripts/codeowners.js
CODEOWNERS
- name: Check codeowner approval and update label
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
with:
script: |
const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js');
const owner = context.repo.owner;
const repo = context.repo.repo;
const pr_number = parseInt(process.env.PR_NUMBER, 10);
const LABEL_NAME = 'code-owner-approved';
console.log(`Processing PR #${pr_number} for codeowner approval label`);
const codeownersPatterns = loadCodeowners();
const action = await determineLabelAction(
github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME
);
if (action === LabelAction.NONE) {
console.log('No label change needed');
return;
}
try {
if (action === LabelAction.ADD) {
await github.rest.issues.addLabels({
owner, repo, issue_number: pr_number, labels: [LABEL_NAME]
});
console.log(`Added '${LABEL_NAME}' label`);
} else if (action === LabelAction.REMOVE) {
await github.rest.issues.removeLabel({
owner, repo, issue_number: pr_number, name: LABEL_NAME
});
console.log(`Removed '${LABEL_NAME}' label`);
}
} catch (error) {
if (error.status === 403) {
console.log(`Warning: insufficient permissions to update label (expected for fork PRs)`);
} else if (error.status === 404) {
console.log(`Label '${LABEL_NAME}' not present, nothing to remove`);
} else {
throw error;
}
}

View File

@@ -13,6 +13,9 @@ on:
# Needs to be pull_request_target to get write permissions
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
branches-ignore:
- release
- beta
permissions:
pull-requests: write
@@ -24,10 +27,17 @@ jobs:
if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }}
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Request reviews from component codeowners
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
const owner = context.repo.owner;
const repo = context.repo.repo;
const pr_number = context.payload.pull_request.number;
@@ -38,12 +48,15 @@ jobs:
const BOT_COMMENT_MARKER = '<!-- codeowner-review-request-bot -->';
try {
// Get the list of changed files in this PR
const { data: files } = await github.rest.pulls.listFiles({
owner,
repo,
pull_number: pr_number
});
// Get the list of changed files in this PR (with pagination)
const files = await github.paginate(
github.rest.pulls.listFiles,
{
owner,
repo,
pull_number: pr_number
}
);
const changedFiles = files.map(file => file.filename);
console.log(`Found ${changedFiles.length} changed files`);
@@ -53,32 +66,10 @@ jobs:
return;
}
// Fetch CODEOWNERS file from root
const { data: codeownersFile } = await github.rest.repos.getContent({
owner,
repo,
path: 'CODEOWNERS',
ref: context.payload.pull_request.base.sha
});
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
// Parse CODEOWNERS from the checked-out base branch
const codeownersPatterns = loadCodeowners();
// Parse CODEOWNERS file to extract all patterns and their owners
const codeownersLines = codeownersContent.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
const codeownersPatterns = [];
// Convert CODEOWNERS pattern to regex (robust glob handling)
function globToRegex(pattern) {
// Escape regex special characters except for glob wildcards
let regexStr = pattern
.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars
.replace(/\*\*/g, '.*') // globstar
.replace(/\*/g, '[^/]*') // single star
.replace(/\?/g, '.'); // question mark
return new RegExp('^' + regexStr + '$');
}
console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
// Helper function to create comment body
function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) {
@@ -93,50 +84,11 @@ jobs:
}
}
for (const line of codeownersLines) {
const parts = line.split(/\s+/);
if (parts.length < 2) continue;
const pattern = parts[0];
const owners = parts.slice(1);
// Use robust glob-to-regex conversion
const regex = globToRegex(pattern);
codeownersPatterns.push({ pattern, regex, owners });
}
console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
// Match changed files against CODEOWNERS patterns
const matchedOwners = new Set();
const matchedTeams = new Set();
const fileMatches = new Map(); // Track which files matched which patterns
for (const file of changedFiles) {
for (const { pattern, regex, owners } of codeownersPatterns) {
if (regex.test(file)) {
console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`);
if (!fileMatches.has(file)) {
fileMatches.set(file, []);
}
fileMatches.get(file).push({ pattern, owners });
// Add owners to the appropriate set (remove @ prefix)
for (const owner of owners) {
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
if (cleanOwner.includes('/')) {
// Team mention (org/team-name)
const teamName = cleanOwner.split('/')[1];
matchedTeams.add(teamName);
} else {
// Individual user
matchedOwners.add(cleanOwner);
}
}
}
}
}
// Match changed files against CODEOWNERS patterns using last-match-wins semantics
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
const matchedOwners = effective.users;
const matchedTeams = effective.teams;
const matchedFileCount = effective.matchedFileCount;
if (matchedOwners.size === 0 && matchedTeams.size === 0) {
console.log('No codeowners found for any changed files');
@@ -170,11 +122,14 @@ jobs:
}
// Check for completed reviews to avoid re-requesting users who have already reviewed
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: pr_number
});
const reviews = await github.paginate(
github.rest.pulls.listReviews,
{
owner,
repo,
pull_number: pr_number
}
);
const reviewedUsers = new Set();
reviews.forEach(review => {
@@ -247,7 +202,7 @@ jobs:
}
const totalReviewers = reviewersList.length + teamsList.length;
console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`);
console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${matchedFileCount} matched files`);
// Request reviews
try {
@@ -279,7 +234,7 @@ jobs:
// Only add a comment if there are new codeowners to mention (not previously pinged)
if (reviewersList.length > 0 || teamsList.length > 0) {
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, true);
await github.rest.issues.createComment({
owner,
@@ -297,7 +252,7 @@ jobs:
// Only try to add a comment if there are new codeowners to mention
if (reviewersList.length > 0 || teamsList.length > 0) {
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, false);
try {
await github.rest.issues.createComment({

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
category: "/language:${{matrix.language}}"

93
.github/workflows/pr-title-check.yml vendored Normal file
View File

@@ -0,0 +1,93 @@
name: PR Title Check
on:
pull_request:
types: [opened, edited, synchronize, reopened]
permissions:
contents: read
pull-requests: read
jobs:
check:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const {
detectComponents,
hasCoreChanges,
hasDashboardChanges,
hasGitHubActionsChanges,
} = require('./.github/scripts/detect-tags.js');
const title = context.payload.pull_request.title;
const author = context.payload.pull_request.user.login;
// Skip bot PRs (e.g. dependabot) - they have their own title format
if (author === 'dependabot[bot]') {
return;
}
// Block titles starting with "word:" or "word(scope):" patterns
const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/;
if (commitStylePattern.test(title)) {
core.setFailed(
`PR title should not start with a "prefix:" style format.\n` +
`Please use the format: [component] Brief description\n`
);
return;
}
// Get changed files to detect tags
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const filenames = files.map(f => f.filename);
// Detect tags from changed files using shared logic
const tags = new Set();
for (const comp of detectComponents(filenames)) {
tags.add(comp);
}
if (hasCoreChanges(filenames)) tags.add('core');
if (hasDashboardChanges(filenames)) tags.add('dashboard');
if (hasGitHubActionsChanges(filenames)) tags.add('ci');
if (tags.size === 0) {
return;
}
// Check for angle brackets not wrapped in backticks.
// Astro docs MDX treats bare < as JSX component opening tags.
const stripped = title.replace(/`[^`]*`/g, '');
if (/[<>]/.test(stripped)) {
core.setFailed(
'PR title contains `<` or `>` not wrapped in backticks.\n' +
'Astro docs MDX interprets bare `<` as JSX components.\n' +
'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support'
);
return;
}
// Check title starts with [tag] prefix
const bracketPattern = /^\[\w+\]/;
if (!bracketPattern.test(title)) {
const suggestion = [...tags].map(c => `[${c}]`).join('');
// Skip if the suggested prefix would be too long for a readable title
if (suggestion.length > 40) {
return;
}
core.setFailed(
`PR modifies: ${[...tags].join(', ')}\n` +
`Title must start with a [tag] prefix.\n` +
`Suggested: ${suggestion} <description>`
);
}

View File

@@ -99,15 +99,15 @@ jobs:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to docker hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -138,7 +138,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -171,24 +171,24 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download digests
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

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

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.0
rev: v0.15.6
hooks:
# Run the linter.
- id: ruff
@@ -37,7 +37,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py311-plus]

View File

@@ -54,6 +54,8 @@ esphome/components/atm90e32/* @circuitsetup @descipher
esphome/components/audio/* @kahrendt
esphome/components/audio_adc/* @kbx81
esphome/components/audio_dac/* @kbx81
esphome/components/audio_file/* @kahrendt
esphome/components/audio_file/media_source/* @kahrendt
esphome/components/axs15231/* @clydebarrow
esphome/components/b_parasite/* @rbaron
esphome/components/ballu/* @bazuchan
@@ -130,6 +132,7 @@ esphome/components/dashboard_import/* @esphome/core
esphome/components/datetime/* @jesserockz @rfdarter
esphome/components/debug/* @esphome/core
esphome/components/delonghi/* @grob6000
esphome/components/dew_point/* @CFlix
esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber
esphome/components/dht/* @OttoWinter
@@ -213,6 +216,7 @@ esphome/components/hbridge/light/* @DotNetDann
esphome/components/hbridge/switch/* @dwmw2
esphome/components/hc8/* @omartijn
esphome/components/hdc2010/* @optimusprimespace @ssieb
esphome/components/hdc302x/* @joshuasing
esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
@@ -315,6 +319,7 @@ esphome/components/mcp9808/* @k7hpn
esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz
esphome/components/media_source/* @kahrendt
esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @edenhaus @jorre05
esphome/components/microphone/* @jesserockz @kahrendt
@@ -406,11 +411,13 @@ esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz
esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_ble/* @bdraco
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet
esphome/components/rtttl/* @glmnet @ximex
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
esphome/components/runtime_stats/* @bdraco
esphome/components/rx8130/* @beormund
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
@@ -427,8 +434,10 @@ esphome/components/select/* @esphome/core
esphome/components/sen0321/* @notjj
esphome/components/sen21231/* @shreyaskarnik
esphome/components/sen5x/* @martgras
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core
esphome/components/serial_proxy/* @kbx81
esphome/components/sfa30/* @ghsensdev
esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sgp4x/* @martgras @SenexCrenshaw
@@ -451,6 +460,7 @@ esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt
esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/speaker_source/* @kahrendt
esphome/components/spi/* @clydebarrow @esphome/core
esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow
@@ -581,6 +591,7 @@ esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zigbee/* @tomaszduda23
esphome/components/zio_ultrasonic/* @kahrendt

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.2.4
PROJECT_NUMBER = 2026.4.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

View File

@@ -1,6 +1,7 @@
# PYTHON_ARGCOMPLETE_OK
import argparse
from collections.abc import Callable
from contextlib import suppress
from datetime import datetime
import functools
import getpass
@@ -9,6 +10,8 @@ import logging
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import time
from typing import Protocol
@@ -23,6 +26,7 @@ import esphome.codegen as cg
from esphome.config import iter_component_configs, read_config, strip_default_ids
from esphome.const import (
ALLOWED_NAME_CHARS,
ARGUMENT_HELP_DEVICE,
CONF_API,
CONF_BAUD_RATE,
CONF_BROKER,
@@ -43,7 +47,9 @@ from esphome.const import (
CONF_SUBSTITUTIONS,
CONF_TOPIC,
ENV_NOGITIGNORE,
KEY_CORE,
KEY_NATIVE_IDF,
KEY_TARGET_PLATFORM,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
@@ -55,7 +61,11 @@ from esphome.helpers import get_bool_env, indent, is_ip_address
from esphome.log import AnsiFore, color, setup_log
from esphome.types import ConfigType
from esphome.util import (
PICOTOOL_PACKAGE,
detect_rp2040_bootsel,
get_picotool_path,
get_serial_ports,
is_picotool_usb_permission_error,
list_yaml_files,
run_external_command,
run_external_process,
@@ -64,9 +74,26 @@ from esphome.util import (
_LOGGER = logging.getLogger(__name__)
ESPHOME_COMMAND = [sys.executable, "-m", "esphome"]
# Maximum buffer size for serial log reading to prevent unbounded memory growth
SERIAL_BUFFER_MAX_SIZE = 65536
_RP2040_BOOTSEL_INSTRUCTIONS = (
"To enter BOOTSEL mode:\n"
" 1. Unplug the device\n"
" 2. Hold the BOOT/BOOTSEL button\n"
" 3. Plug in the USB cable while holding the button\n"
" 4. Release the button - the device should appear as a USB drive (RPI-RP2)\n"
"Then run the upload command again."
)
_RP2040_UDEV_HINT = (
"You may need to add a udev rule for RP2040 devices. "
"See: https://github.com/raspberrypi/picotool"
"/blob/master/udev/60-picotool.rules"
)
# Special non-component keys that appear in configs
_NON_COMPONENT_KEYS = frozenset(
{
@@ -162,6 +189,7 @@ class PortType(StrEnum):
NETWORK = "NETWORK"
MQTT = "MQTT"
MQTTIP = "MQTTIP"
BOOTSEL = "BOOTSEL"
# Magic MQTT port types that require special handling
@@ -240,6 +268,19 @@ def choose_upload_log_host(
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
]
# Add RP2040 BOOTSEL device option when uploading
bootsel_permission_error = False
if (
purpose == Purpose.UPLOADING
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
and (picotool := _find_picotool()) is not None
):
bootsel = detect_rp2040_bootsel(picotool)
if bootsel.device_count > 0:
options.append(("RP2040 BOOTSEL (via picotool)", "BOOTSEL"))
elif bootsel.permission_error:
bootsel_permission_error = True
if purpose == Purpose.LOGGING:
if has_mqtt_logging():
mqtt_config = CORE.config[CONF_MQTT]
@@ -257,6 +298,25 @@ def choose_upload_log_host(
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
# Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found
if (
purpose == Purpose.UPLOADING
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options)
):
if bootsel_permission_error:
_LOGGER.warning(
"An RP2040 device in BOOTSEL mode was detected but could "
"not be accessed due to USB permissions."
)
if sys.platform.startswith("linux"):
_LOGGER.warning(_RP2040_UDEV_HINT)
if not options:
raise EsphomeError(
f"No RP2040 device found. {_RP2040_BOOTSEL_INSTRUCTIONS}"
)
_LOGGER.info("Tip: %s", _RP2040_BOOTSEL_INSTRUCTIONS)
if check_default is not None and check_default in [opt[1] for opt in options]:
return [check_default]
return [choose_prompt(options, purpose=purpose)]
@@ -403,10 +463,13 @@ def get_port_type(port: str) -> PortType:
Returns:
PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.)
PortType.BOOTSEL for RP2040 BOOTSEL upload via picotool
PortType.MQTT for MQTT logging
PortType.MQTTIP for MQTT IP lookup
PortType.NETWORK for IP addresses, hostnames, or mDNS names
"""
if port == "BOOTSEL":
return PortType.BOOTSEL
if port.startswith("/") or port.startswith("COM"):
return PortType.SERIAL
if port == "MQTT":
@@ -431,6 +494,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
return 1
_LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate)
process_stacktrace = None
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
process_stacktrace = getattr(module, "process_stacktrace")
except AttributeError:
pass
backtrace_state = False
ser = serial.Serial()
ser.baudrate = baud_rate
@@ -472,9 +543,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
)
safe_print(parser.parse_line(line, time_str))
backtrace_state = platformio_api.process_stacktrace(
config, line, backtrace_state=backtrace_state
)
if process_stacktrace:
backtrace_state = process_stacktrace(
config, line, backtrace_state
)
else:
backtrace_state = platformio_api.process_stacktrace(
config, line, backtrace_state=backtrace_state
)
except serial.SerialException:
_LOGGER.error("Serial port closed!")
return 0
@@ -614,6 +690,47 @@ def _check_and_emit_build_info() -> None:
)
def _get_configured_xtal_freq() -> int | None:
"""Read the configured crystal frequency from the sdkconfig file."""
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
if not sdkconfig_path.is_file():
return None
with suppress(OSError, ValueError):
content = sdkconfig_path.read_text()
for line in content.splitlines():
if line.startswith("CONFIG_XTAL_FREQ="):
return int(line.split("=", 1)[1])
return None
def _make_crystal_freq_callback(
configured_freq: int,
) -> Callable[[str], str | None]:
"""Create a callback that checks esptool crystal frequency output."""
crystal_re = re.compile(r"Crystal frequency:\s+(\d+)\s*MHz")
def check_crystal_line(line: str) -> str | None:
if not (match := crystal_re.search(line)):
return None
detected = int(match.group(1))
if detected == configured_freq:
return None
return (
f"\n\033[33mWARNING: Crystal frequency mismatch! "
f"Device reports {detected}MHz but firmware is configured "
f"for {configured_freq}MHz.\n"
f"UART logging and other clock-dependent features will not "
f"work correctly.\n"
f"Set the correct crystal frequency with sdkconfig_options:\n"
f" esp32:\n"
f" framework:\n"
f" sdkconfig_options:\n"
f" CONFIG_XTAL_FREQ_{detected}: 'y'\033[0m\n\n"
)
return check_crystal_line
def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int
) -> str | int:
@@ -642,6 +759,14 @@ def upload_using_esptool(
mcu = get_esp32_variant().lower()
line_callbacks: list[Callable[[str], str | None]] = []
if (
CORE.is_esp32
and file is None
and (configured_freq := _get_configured_xtal_freq()) is not None
):
line_callbacks.append(_make_crystal_freq_callback(configured_freq))
def run_esptool(baud_rate):
cmd = [
"esptool",
@@ -666,9 +791,13 @@ def upload_using_esptool(
if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
import esptool
return run_external_command(esptool.main, *cmd) # pylint: disable=no-member
return run_external_command(
esptool.main, # pylint: disable=no-member
*cmd,
line_callbacks=line_callbacks,
)
return run_external_process(*cmd)
return run_external_process(*cmd, line_callbacks=line_callbacks)
rc = run_esptool(first_baudrate)
if rc == 0 or first_baudrate == 115200:
@@ -681,15 +810,140 @@ def upload_using_esptool(
return run_esptool(115200)
def upload_using_platformio(config: ConfigType, port: str):
def upload_using_platformio(config: ConfigType, port: str) -> int:
from esphome import platformio_api
# RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for
# the upload target, but 'nobuild' skips the build phase that creates it.
# Create it here so the upload doesn't fail.
if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040:
idedata = platformio_api.get_idedata(config)
build_dir = Path(idedata.firmware_elf_path).parent
firmware_bin = build_dir / "firmware.bin"
signed_bin = build_dir / "firmware.bin.signed"
if firmware_bin.is_file() and not signed_bin.is_file():
shutil.copy2(firmware_bin, signed_bin)
upload_args = ["-t", "upload", "-t", "nobuild"]
if port is not None:
upload_args += ["--upload-port", port]
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
def _find_picotool() -> Path | None:
"""Find the picotool binary from PlatformIO packages."""
from esphome import platformio_api
try:
idedata = platformio_api.get_idedata(CORE.config)
except Exception: # noqa: BLE001 # pylint: disable=broad-except
return None
return get_picotool_path(idedata.cc_path)
def upload_using_picotool(config: ConfigType) -> int:
"""Upload firmware to RP2040 in BOOTSEL mode using picotool.
Uses picotool to load the ELF firmware directly via USB, avoiding
the mass storage copy approach that causes "disk not ejected properly"
warnings on macOS.
"""
from esphome import platformio_api
idedata = platformio_api.get_idedata(config)
firmware_elf = Path(idedata.firmware_elf_path)
if not firmware_elf.is_file():
_LOGGER.error(
"Firmware ELF file not found at %s. "
"Make sure the project has been compiled first.",
firmware_elf,
)
return 1
picotool = get_picotool_path(idedata.cc_path)
if picotool is None:
_LOGGER.error(
"picotool not found. Ensure the RP2040 PlatformIO platform "
"is installed (%s).",
PICOTOOL_PACKAGE,
)
return 1
_LOGGER.info("Uploading firmware to RP2040 via picotool...")
try:
# Don't capture stdout — let picotool write directly to the terminal
# so progress bars display in real-time with \r updates.
# Capture stderr only so we can detect permission errors.
result = subprocess.run(
[str(picotool), "load", "-v", "-x", str(firmware_elf)],
stderr=subprocess.PIPE,
timeout=60,
check=False,
)
except subprocess.TimeoutExpired:
_LOGGER.error("picotool upload timed out after 60 seconds.")
return 1
except OSError as err:
_LOGGER.error("Failed to run picotool: %s", err)
return 1
if result.returncode != 0:
stderr = result.stderr.decode("utf-8", errors="replace").strip()
if stderr:
for line in stderr.splitlines():
safe_print(line)
if is_picotool_usb_permission_error(stderr):
msg = "Permission denied accessing USB device."
if sys.platform.startswith("linux"):
msg += f" {_RP2040_UDEV_HINT}"
_LOGGER.error(msg)
else:
_LOGGER.error("picotool upload failed (exit code %d).", result.returncode)
return 1
return 0
def _wait_for_serial_port(
port: str | None = None,
timeout: float = 30.0,
known_ports: set[str] | None = None,
) -> None:
"""Wait for a serial port to appear, e.g. after a device reboot.
USB-CDC devices disappear briefly after flashing while the device
reboots and re-enumerates on the USB bus.
If port is given, wait for that specific path. If known_ports is
given, wait for a new port that wasn't in the set. Otherwise wait
for any serial port to appear.
"""
def _port_found() -> bool:
if port is not None:
if os.name == "posix":
return os.path.exists(port)
return any(p.path == port for p in get_serial_ports())
ports = get_serial_ports()
if known_ports is not None:
return any(p.path not in known_ports for p in ports)
return bool(ports)
if _port_found():
return
if port is not None:
_LOGGER.info("Waiting for %s to come online...", port)
else:
_LOGGER.info("Waiting for device to reboot...")
start = time.monotonic()
while time.monotonic() - start < timeout:
time.sleep(0.05)
if _port_found():
time.sleep(0.05)
return
def check_permissions(port: str):
if os.name == "posix" and get_port_type(port) == PortType.SERIAL:
# Check if we can open selected serial port
@@ -719,7 +973,15 @@ def upload_program(
except AttributeError:
pass
if get_port_type(host) == PortType.SERIAL:
port_type = get_port_type(host)
if port_type == PortType.BOOTSEL:
exit_code = upload_using_picotool(config)
# Return None for device - BOOTSEL can't be used for logging,
# so command_run will show the interactive chooser for log source
return exit_code, None
if port_type == PortType.SERIAL:
check_permissions(host)
exit_code = 1
@@ -773,6 +1035,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
port_type = get_port_type(port)
if port_type == PortType.SERIAL:
_wait_for_serial_port(port)
check_permissions(port)
return run_miniterm(config, port, args)
@@ -911,6 +1174,9 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
purpose=Purpose.UPLOADING,
)
# Snapshot current serial ports before upload so we can detect new ones
pre_upload_ports = {p.path for p in get_serial_ports()}
exit_code, successful_device = upload_program(config, args, devices)
if exit_code == 0:
_LOGGER.info("Successfully uploaded program.")
@@ -921,6 +1187,19 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
if args.no_logs:
return 0
# After BOOTSEL upload, wait for a new serial port to appear
# so it shows up in the log chooser
if (
successful_device is None
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
):
_wait_for_serial_port(known_ports=pre_upload_ports)
# If exactly one new serial port appeared, use it directly
serial_ports = get_serial_ports()
new_ports = [p for p in serial_ports if p.path not in pre_upload_ports]
if len(new_ports) == 1:
successful_device = new_ports[0].path
# For logs, prefer the device we successfully uploaded to
devices = choose_upload_log_host(
default=successful_device,
@@ -1030,9 +1309,8 @@ def command_update_all(args: ArgsProtocol) -> int | None:
files = list_yaml_files(args.configuration)
def build_command(f):
if CORE.dashboard:
return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"]
return ["esphome", "run", f, "--no-logs", "--device", "OTA"]
dashboard = ["--dashboard"] if CORE.dashboard else []
return [*ESPHOME_COMMAND, *dashboard, "run", f, "--no-logs", "--device", "OTA"]
return run_multiple_configs(files, build_command)
@@ -1181,7 +1459,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
new_path.write_text(new_raw, encoding="utf-8")
rc = run_external_process("esphome", "config", str(new_path))
rc = run_external_process(*ESPHOME_COMMAND, "config", str(new_path))
if rc != 0:
print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
new_path.unlink()
@@ -1199,7 +1477,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
cli_args.insert(0, "--dashboard")
try:
rc = run_external_process("esphome", *cli_args)
rc = run_external_process(*ESPHOME_COMMAND, *cli_args)
except KeyboardInterrupt:
rc = 1
if rc != 0:
@@ -1354,7 +1632,7 @@ def parse_args(argv):
parser_upload.add_argument(
"--device",
action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
help=ARGUMENT_HELP_DEVICE,
)
parser_upload.add_argument(
"--upload_speed",
@@ -1377,7 +1655,7 @@ def parse_args(argv):
parser_logs.add_argument(
"--device",
action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
help=ARGUMENT_HELP_DEVICE,
)
parser_logs.add_argument(
"--reset",
@@ -1407,7 +1685,7 @@ def parse_args(argv):
parser_run.add_argument(
"--device",
action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
help=ARGUMENT_HELP_DEVICE,
)
parser_run.add_argument(
"--upload_speed",
@@ -1596,7 +1874,7 @@ def run_esphome(argv):
# argv[0] is the program path, skip it since we prefix with "esphome"
def build_command(f):
return (
["esphome"]
[*ESPHOME_COMMAND]
+ [arg for arg in argv[1:] if arg not in args.configuration]
+ [str(f)]
)

View File

@@ -256,7 +256,7 @@ SYMBOL_PATTERNS = {
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
# Order matters! More specific categories must come before general ones.
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
"mdns_lib": ["mdns"],
"mdns_lib": ["mdns", "packet$"],
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
"memory_mgmt": [
"mem_",
@@ -794,7 +794,6 @@ SYMBOL_PATTERNS = {
"s_dp",
"s_ni",
"s_reg_dump",
"packet$",
"d_mult_table",
"K",
"fcstab",

View File

@@ -1,3 +1,5 @@
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@@ -57,8 +59,42 @@ def maybe_conf(conf, *validators):
return validate
def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
return ACTION_REGISTRY.register(name, action_type, schema)
_LOGGER = logging.getLogger(__name__)
def register_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool | None = None,
):
"""Register an action type.
All callers must pass ``synchronous`` explicitly.
``synchronous=True`` — the action never defers ``play_next_()`` to a
later point (callback, timer, or ``loop()``). Trigger arguments are
only used during the initial call, so string args can use non-owning
StringRef for zero-copy access.
``synchronous=False`` — the action defers ``play_next_()`` via a
callback, timer, or ``Component::loop()``. Trigger arguments must
outlive the initial call, so string args use owning std::string to
prevent dangling references.
"""
if synchronous is None:
_LOGGER.warning(
"register_action('%s', ...) is missing the synchronous= parameter. "
"Defaulting to synchronous=False (safe but prevents StringRef "
"optimization). Check the C++ class: use synchronous=False if "
"play_next_() is deferred to a callback, timer, or loop(); "
"use synchronous=True if play_next_() always runs before the "
"initial play/play_complex call returns",
name,
)
synchronous = False
return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous)
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
@@ -335,7 +371,10 @@ async def component_is_idle_condition_to_code(
@register_action(
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
"delay",
DelayAction,
cv.templatable(cv.positive_time_period_milliseconds),
synchronous=False,
)
async def delay_action_to_code(
config: ConfigType,
@@ -366,6 +405,7 @@ async def delay_action_to_code(
cv.has_at_least_one_key(CONF_THEN, CONF_ELSE),
cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL),
),
synchronous=True,
)
async def if_action_to_code(
config: ConfigType,
@@ -394,6 +434,7 @@ async def if_action_to_code(
cv.Required(CONF_THEN): validate_action_list,
}
),
synchronous=True,
)
async def while_action_to_code(
config: ConfigType,
@@ -417,6 +458,7 @@ async def while_action_to_code(
cv.Required(CONF_THEN): validate_action_list,
}
),
synchronous=True,
)
async def repeat_action_to_code(
config: ConfigType,
@@ -445,7 +487,7 @@ _validate_wait_until = cv.maybe_simple_value(
)
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
@register_action("wait_until", WaitUntilAction, _validate_wait_until, synchronous=False)
async def wait_until_action_to_code(
config: ConfigType,
action_id: ID,
@@ -461,7 +503,12 @@ async def wait_until_action_to_code(
return var
@register_action("lambda", LambdaAction, cv.lambda_)
# Lambda executes user C++ inline and returns — synchronous by execution model.
# User code could theoretically store the StringRef for deferred use, but StringRef
# is a view type and storing views beyond their scope is always unsafe regardless
# of this optimization. Marking non-synchronous would disable StringRef for nearly
# all user services since most use lambda.
@register_action("lambda", LambdaAction, cv.lambda_, synchronous=True)
async def lambda_action_to_code(
config: ConfigType,
action_id: ID,
@@ -480,6 +527,7 @@ async def lambda_action_to_code(
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
synchronous=True,
)
async def component_update_action_to_code(
config: ConfigType,
@@ -499,6 +547,7 @@ async def component_update_action_to_code(
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
synchronous=True,
)
async def component_suspend_action_to_code(
config: ConfigType,
@@ -521,6 +570,7 @@ async def component_suspend_action_to_code(
),
}
),
synchronous=True,
)
async def component_resume_action_to_code(
config: ConfigType,
@@ -578,6 +628,27 @@ async def build_condition_list(
return conditions
def has_non_synchronous_actions(actions: ConfigType) -> bool:
"""Check if a validated action list contains any non-synchronous actions.
Non-synchronous actions (delay, wait_until, script.wait, etc.) store
trigger args for later execution, making non-owning types like StringRef
unsafe.
"""
if isinstance(actions, list):
return any(has_non_synchronous_actions(item) for item in actions)
if isinstance(actions, dict):
for key in actions:
if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous:
return True
return any(
has_non_synchronous_actions(v)
for v in actions.values()
if isinstance(v, (list, dict))
)
return False
async def build_automation(
trigger: MockObj, args: TemplateArgsType, config: ConfigType
) -> MockObj:

View File

@@ -72,7 +72,7 @@ def get_component_cmakelists(minimal: bool = False) -> str:
# Extract compile definitions from build flags (-DXXX -> XXX)
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
compile_defs_str = "\n ".join(compile_defs) if compile_defs else ""
compile_defs_str = "\n ".join(sorted(compile_defs)) if compile_defs else ""
# Extract compile options (-W flags, excluding linker flags)
compile_opts = [
@@ -80,11 +80,11 @@ def get_component_cmakelists(minimal: bool = False) -> str:
for flag in CORE.build_flags
if flag.startswith("-W") and not flag.startswith("-Wl,")
]
compile_opts_str = "\n ".join(compile_opts) if compile_opts else ""
compile_opts_str = "\n ".join(sorted(compile_opts)) if compile_opts else ""
# Extract linker options (-Wl, flags)
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
link_opts_str = "\n ".join(link_opts) if link_opts else ""
link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else ""
return f"""\
# Auto-generated by ESPHome

View File

@@ -11,6 +11,7 @@
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
Expression,
FlashStringLiteral,
LineComment,
LogStringLiteral,
MockObj,

View File

@@ -92,10 +92,7 @@ void AbsoluteHumidityComponent::loop() {
// Calculate absolute humidity
const float absolute_humidity = vapor_density(es, hr, temperature_k);
ESP_LOGD(TAG,
"Saturation vapor pressure %f kPa\n"
"Publishing absolute humidity %f g/m³",
es, absolute_humidity);
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity);
// Publish absolute humidity
this->status_clear_warning();

View File

@@ -199,12 +199,19 @@ void AcDimmer::setup() {
setTimer1Callback(&timer_interrupt);
#endif
#ifdef USE_ESP32
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
if (dimmer_timer == nullptr) {
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
if (dimmer_timer == nullptr) {
ESP_LOGE(TAG, "Failed to create GPTimer for AC dimmer");
this->mark_failed();
return;
}
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
}
#endif
}

View File

@@ -8,6 +8,13 @@
#endif // CYW43_USES_VSYS_PIN
#include <hardware/adc.h>
// PICO_VSYS_PIN is defined in pico-sdk board headers (e.g. boards/pico2.h),
// but the Arduino framework's config_autogen.h includes a generic board header
// that doesn't define it. Provide the standard value (pin 29) as a fallback.
#ifndef PICO_VSYS_PIN
#define PICO_VSYS_PIN 29 // NOLINT(cppcoreguidelines-macro-usage)
#endif
namespace esphome {
namespace adc {

View File

@@ -58,7 +58,10 @@ void HOT AddressableLightDisplay::draw_absolute_pixel_internal(int x, int y, Col
if (this->pixel_mapper_f_.has_value()) {
// Params are passed by reference, so they may be modified in call.
this->addressable_light_buffer_[(*this->pixel_mapper_f_)(x, y)] = color;
int index = (*this->pixel_mapper_f_)(x, y);
if (index < 0 || static_cast<size_t>(index) >= this->addressable_light_buffer_.size())
return;
this->addressable_light_buffer_[index] = color;
} else {
this->addressable_light_buffer_[y * this->get_width_internal() + x] = color;
}

View File

@@ -33,7 +33,7 @@ class AddressableLightDisplay : public display::DisplayBuffer {
// - Save the current effect index.
this->last_effect_index_ = light_state_->get_current_effect_index();
// - Disable any current effect.
light_state_->make_call().set_effect(0).perform();
light_state_->make_call().set_effect(uint32_t{0}).perform();
}
}
enabled_ = enabled;

View File

@@ -121,7 +121,7 @@ void ADE7880::update() {
this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) {
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
}
@@ -137,7 +137,7 @@ void ADE7880::update() {
this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) {
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
}
@@ -153,7 +153,7 @@ void ADE7880::update() {
this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) {
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
}

View File

@@ -85,6 +85,9 @@ constexpr uint16_t CWATTHR = 0xE402;
constexpr uint16_t AFWATTHR = 0xE403;
constexpr uint16_t BFWATTHR = 0xE404;
constexpr uint16_t CFWATTHR = 0xE405;
constexpr uint16_t ARWATTHR = 0xE406;
constexpr uint16_t BRWATTHR = 0xE407;
constexpr uint16_t CRWATTHR = 0xE408;
constexpr uint16_t AFVARHR = 0xE409;
constexpr uint16_t BFVARHR = 0xE40A;
constexpr uint16_t CFVARHR = 0xE40B;

View File

@@ -173,19 +173,8 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1
}
if (resolution == ADS1015_12_BITS) {
bool negative = (raw_conversion >> 15) == 1;
// shift raw_conversion as it's only 12-bits, left justified
raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS);
// check if number was negative in order to keep the sign
if (negative) {
// the number was negative
// 1) set the negative bit back
raw_conversion |= 0x8000;
// 2) reset the former (shifted) negative bit
raw_conversion &= 0xF7FF;
}
// ADS1015 returns 12-bit value left-justified in 16 bits; shift right and sign-extend
raw_conversion = static_cast<uint16_t>(static_cast<int16_t>(raw_conversion) >> (16 - ADS1015_12_BITS));
}
auto signed_conversion = static_cast<int16_t>(raw_conversion);

View File

@@ -92,6 +92,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
"ags10.new_i2c_address",
AGS10NewI2cAddressAction,
AGS10_NEW_I2C_ADDRESS_SCHEMA,
synchronous=True,
)
async def ags10newi2caddress_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -121,6 +122,7 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
"ags10.set_zero_point",
AGS10SetZeroPointAction,
AGS10_SET_ZERO_POINT_SCHEMA,
synchronous=True,
)
async def ags10setzeropoint_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)

View File

@@ -34,7 +34,10 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value(
@automation.register_action(
"aic3204.set_auto_mute_mode", SetAutoMuteAction, SET_AUTO_MUTE_ACTION_SCHEMA
"aic3204.set_auto_mute_mode",
SetAutoMuteAction,
SET_AUTO_MUTE_ACTION_SCHEMA,
synchronous=True,
)
async def aic3204_set_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])

View File

@@ -186,8 +186,8 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
)
@setup_entity("alarm_control_panel")
async def setup_alarm_control_panel_core_(var, config):
await setup_entity(var, config, "alarm_control_panel")
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
@@ -243,7 +243,10 @@ async def new_alarm_control_panel(config, *args):
@automation.register_action(
"alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.arm_away",
ArmAwayAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -255,7 +258,10 @@ async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.arm_home",
ArmHomeAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -267,7 +273,10 @@ async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.arm_night", ArmNightAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.arm_night",
ArmNightAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_arm_night_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -279,7 +288,10 @@ async def alarm_action_arm_night_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.disarm",
DisarmAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -291,7 +303,10 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.pending",
PendingAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_pending_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -299,7 +314,10 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.triggered",
TriggeredAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -307,7 +325,10 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.chime",
ChimeAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -315,7 +336,10 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.ready",
ReadyAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_condition(
"alarm_control_panel.ready",

View File

@@ -12,7 +12,14 @@ AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
if (code != nullptr) {
this->code_ = std::string(code);
return this->set_code(code, strlen(code));
}
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code, size_t len) {
if (code != nullptr) {
this->code_ = std::string(code, len);
}
return *this;
}

View File

@@ -15,7 +15,8 @@ class AlarmControlPanelCall {
AlarmControlPanelCall(AlarmControlPanel *parent);
AlarmControlPanelCall &set_code(const char *code);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
AlarmControlPanelCall &set_code(const char *code, size_t len);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str(), code.size()); }
AlarmControlPanelCall &arm_away();
AlarmControlPanelCall &arm_home();
AlarmControlPanelCall &arm_night();

View File

@@ -125,7 +125,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
this->current_sensor_->publish_state(NAN);
if (this->speed_sensor_ != nullptr)
this->speed_sensor_->publish_state(NAN);
if (this->speed_sensor_ != nullptr)
if (this->voltage_sensor_ != nullptr)
this->voltage_sensor_->publish_state(NAN);
break;
}

View File

@@ -63,8 +63,9 @@ void Am43Component::control(const CoverCall &call) {
ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status);
}
}
if (call.get_position().has_value()) {
auto pos = *call.get_position();
auto opt_pos = call.get_position();
if (opt_pos.has_value()) {
auto pos = *opt_pos;
if (this->invert_position_)
pos = 1 - pos;

View File

@@ -69,9 +69,15 @@ SET_FRAME_SCHEMA = cv.Schema(
)
@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA)
@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA)
@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA)
@automation.register_action(
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
)
async def animation_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)

View File

@@ -24,8 +24,9 @@ void Anova::loop() {
}
void Anova::control(const ClimateCall &call) {
if (call.get_mode().has_value()) {
ClimateMode mode = *call.get_mode();
auto mode_val = call.get_mode();
if (mode_val.has_value()) {
ClimateMode mode = *mode_val;
AnovaPacket *pkt;
switch (mode) {
case climate::CLIMATE_MODE_OFF:
@@ -45,8 +46,9 @@ void Anova::control(const ClimateCall &call) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
if (call.get_target_temperature().has_value()) {
auto *pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature());
auto target_temp = call.get_target_temperature();
if (target_temp.has_value()) {
auto *pkt = this->codec_->get_set_target_temp_request(*target_temp);
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
@@ -67,10 +69,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG,
"[%s] No control service found at device, not an Anova..?\n"
"[%s] Note, this component does not currently support Anova Nano.",
this->get_name().c_str(), this->get_name().c_str());
ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str());
ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str());
break;
}
this->char_handle_ = chr->handle;
@@ -144,9 +144,12 @@ void Anova::update() {
return;
if (this->current_request_ < 2) {
auto *pkt = this->codec_->get_read_device_status_request();
if (this->current_request_ == 0)
this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c');
AnovaPacket *pkt;
if (this->current_request_ == 0) {
pkt = this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c');
} else {
pkt = this->codec_->get_read_device_status_request();
}
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);

View File

@@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
"bool": cg.bool_,
"int": cg.int32,
"float": cg.float_,
"string": cg.std_string,
"string": cg.StringRef,
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
@@ -233,8 +233,8 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType:
# API needs 1 listening socket + typically 3 concurrent client connections
# (not max_connections, which is the upper limit rarely reached)
sockets_needed = 1 + 3
socket.consume_sockets(sockets_needed, "api")(config)
socket.consume_sockets(3, "api")(config)
socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config)
return config
@@ -380,9 +380,18 @@ async def to_code(config: ConfigType) -> None:
if is_optional:
func_args.append((cg.bool_, "return_response"))
# Check if action chain has non-synchronous actions that would make
# non-owning StringRef dangle (rx_buf_ reused after delay)
has_non_synchronous = automation.has_non_synchronous_actions(
conf.get(CONF_THEN, [])
)
service_arg_names: list[str] = []
for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_]
# Fall back to std::string for string args if non-synchronous actions exist
if has_non_synchronous and native is cg.StringRef:
native = cg.std_string
service_template_args.append(native)
func_args.append((native, name))
service_arg_names.append(name)
@@ -444,7 +453,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.10")
cg.add_library("esphome/noise-c", "0.1.11")
else:
cg.add_define("USE_API_PLAINTEXT")
@@ -509,11 +518,13 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
"homeassistant.action",
HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"homeassistant.service",
HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
synchronous=True,
)
async def homeassistant_service_to_code(
config: ConfigType,
@@ -524,24 +535,31 @@ async def homeassistant_service_to_code(
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_ACTION], args, None)
templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
# output_type=None because lambdas can return non-string types (int,
# float, char*) that TemplatableStringValue converts via to_string.
# Static strings are manually wrapped for PROGMEM on ESP8266.
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
if on_error := config.get(CONF_ON_ERROR):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
@@ -604,29 +622,37 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
"homeassistant.event",
HomeAssistantServiceCallAction,
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
synchronous=True,
)
async def homeassistant_event_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
templ = await cg.templatable(config[CONF_EVENT], args, None)
templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
# output_type=None because lambdas can return non-string types (int,
# float, char*) that TemplatableStringValue converts via to_string.
# Static strings are manually wrapped for PROGMEM on ESP8266.
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
return var
@@ -644,16 +670,17 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
"homeassistant.tag_scanned",
HomeAssistantServiceCallAction,
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
synchronous=True,
)
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service("esphome.tag_scanned"))
cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned")))
# Initialize FixedVector with exact size (1 data field)
cg.add(var.init_data(1))
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
cg.add(var.add_data("tag_id", templ))
cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ))
return var
@@ -685,6 +712,7 @@ API_RESPOND_ACTION_SCHEMA = cv.All(
"api.respond",
APIRespondAction,
API_RESPOND_ACTION_SCHEMA,
synchronous=True,
)
async def api_respond_to_code(
config: ConfigType,

View File

@@ -58,6 +58,7 @@ service APIConnection {
rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {}
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {}
rpc bluetooth_set_connection_params(BluetoothSetConnectionParamsRequest) returns (BluetoothSetConnectionParamsResponse) {}
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {}
@@ -68,7 +69,16 @@ service APIConnection {
rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {}
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
rpc zigbee_proxy_frame(ZigbeeProxyFrame) returns (void) {}
rpc zigbee_proxy_request(ZigbeeProxyRequest) returns (void) {}
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {}
rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {}
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
}
@@ -198,6 +208,17 @@ message DeviceInfo {
uint32 area_id = 3;
}
enum SerialProxyPortType {
SERIAL_PROXY_PORT_TYPE_TTL = 0;
SERIAL_PROXY_PORT_TYPE_RS232 = 1;
SERIAL_PROXY_PORT_TYPE_RS485 = 2;
}
message SerialProxyInfo {
string name = 1; // Human-readable port name
SerialProxyPortType port_type = 2; // Port type (RS232, RS485)
}
message DeviceInfoResponse {
option (id) = 10;
option (source) = SOURCE_SERVER;
@@ -260,6 +281,13 @@ message DeviceInfoResponse {
// Indicates if Z-Wave proxy support is available and features supported
uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"];
uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"];
// Serial proxy instance metadata
repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
// Indicates if Zigbee proxy support is available and features supported
uint32 zigbee_proxy_feature_flags = 26 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
uint64 zigbee_ieee_address = 27 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
}
message ListEntitiesRequest {
@@ -834,6 +862,33 @@ message GetTimeRequest {
option (source) = SOURCE_SERVER;
}
enum DSTRuleType {
DST_RULE_TYPE_NONE = 0;
DST_RULE_TYPE_MONTH_WEEK_DAY = 1;
DST_RULE_TYPE_JULIAN_NO_LEAP = 2;
DST_RULE_TYPE_DAY_OF_YEAR = 3;
}
message DSTRule {
option (source) = SOURCE_CLIENT;
sint32 time_seconds = 1;
uint32 day = 2;
DSTRuleType type = 3;
uint32 month = 4;
uint32 week = 5;
uint32 day_of_week = 6;
}
message ParsedTimezone {
option (source) = SOURCE_CLIENT;
sint32 std_offset_seconds = 1;
sint32 dst_offset_seconds = 2;
DSTRule dst_start = 3;
DSTRule dst_end = 4;
}
message GetTimeResponse {
option (id) = 37;
option (source) = SOURCE_CLIENT;
@@ -841,6 +896,7 @@ message GetTimeResponse {
fixed32 epoch_seconds = 1;
string timezone = 2;
ParsedTimezone parsed_timezone = 3;
}
// ==================== USER-DEFINES SERVICES ====================
@@ -989,6 +1045,7 @@ enum ClimateAction {
CLIMATE_ACTION_IDLE = 4;
CLIMATE_ACTION_DRYING = 5;
CLIMATE_ACTION_FAN = 6;
CLIMATE_ACTION_DEFROSTING = 7;
}
enum ClimatePreset {
CLIMATE_PRESET_NONE = 0;
@@ -1554,11 +1611,11 @@ message BluetoothLEAdvertisementResponse {
}
message BluetoothLERawAdvertisement {
uint64 address = 1;
sint32 rssi = 2;
uint64 address = 1 [(force) = true];
sint32 rssi = 2 [(force) = true];
uint32 address_type = 3;
bytes data = 4 [(fixed_array_size) = 62];
bytes data = 4 [(fixed_array_size) = 62, (force) = true];
}
message BluetoothLERawAdvertisementsResponse {
@@ -2488,3 +2545,160 @@ message InfraredRFReceiveEvent {
fixed32 key = 2; // Key identifying the receiver instance
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
}
// ==================== SERIAL PROXY ====================
enum SerialProxyParity {
SERIAL_PROXY_PARITY_NONE = 0;
SERIAL_PROXY_PARITY_EVEN = 1;
SERIAL_PROXY_PARITY_ODD = 2;
}
// Configure UART parameters for a serial proxy instance
message SerialProxyConfigureRequest {
option (id) = 138;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
uint32 baudrate = 2; // Baud rate in bits per second
bool flow_control = 3; // Enable hardware flow control
SerialProxyParity parity = 4; // Parity setting
uint32 stop_bits = 5; // Number of stop bits (1 or 2)
uint32 data_size = 6; // Number of data bits (5-8)
}
// Data received from a serial device, forwarded to clients
message SerialProxyDataReceived {
option (id) = 139;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
option (no_delay) = true;
uint32 instance = 1; // Instance index (0-based)
bytes data = 2; // Raw data received from the serial device
}
// Write data to a serial device
message SerialProxyWriteRequest {
option (id) = 140;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
option (no_delay) = true;
uint32 instance = 1; // Instance index (0-based)
bytes data = 2; // Raw data to write to the serial device
}
// Set modem control pin states (RTS and DTR)
message SerialProxySetModemPinsRequest {
option (id) = 141;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags
}
// Request current modem control pin states
message SerialProxyGetModemPinsRequest {
option (id) = 142;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
}
// Response with current modem control pin states
message SerialProxyGetModemPinsResponse {
option (id) = 143;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags
}
enum SerialProxyRequestType {
SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance
SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent)
}
enum SerialProxyStatus {
SERIAL_PROXY_STATUS_OK = 0; // Completed successfully; TX drain confirmed
SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1; // Platform cannot confirm TX drain; success assumed
SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error
SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance
}
// Generic request message for simple serial proxy operations
message SerialProxyRequest {
option (id) = 144;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
SerialProxyRequestType type = 2; // Request type
}
// Response to a SerialProxyRequest (e.g. flush completion or failure)
message SerialProxyRequestResponse {
option (id) = 147;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
SerialProxyRequestType type = 2; // Which request type this responds to
SerialProxyStatus status = 3; // Result status
string error_message = 4; // Additional detail on failure (optional)
}
// ==================== BLUETOOTH CONNECTION PARAMS ====================
message BluetoothSetConnectionParamsRequest {
option (id) = 145;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_BLUETOOTH_PROXY";
uint64 address = 1;
uint32 min_interval = 2; // units of 1.25ms
uint32 max_interval = 3; // units of 1.25ms
uint32 latency = 4;
uint32 timeout = 5; // units of 10ms
}
message BluetoothSetConnectionParamsResponse {
option (id) = 146;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BLUETOOTH_PROXY";
uint64 address = 1;
int32 error = 2;
}
// ==================== ZIGBEE ====================
message ZigbeeProxyFrame {
option (id) = 148;
option (source) = SOURCE_BOTH;
option (ifdef) = "USE_ZIGBEE_PROXY";
option (no_delay) = true;
bytes data = 1;
}
enum ZigbeeProxyRequestType {
ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0;
ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1;
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 2;
}
message ZigbeeProxyRequest {
option (id) = 149;
option (source) = SOURCE_BOTH;
option (ifdef) = "USE_ZIGBEE_PROXY";
ZigbeeProxyRequestType type = 1;
bytes data = 2;
}

View File

@@ -0,0 +1,13 @@
#include "api_buffer.h"
namespace esphome::api {
void APIBuffer::grow_(size_t n) {
auto new_data = make_buffer(n);
if (this->size_)
std::memcpy(new_data.get(), this->data_.get(), this->size_);
this->data_ = std::move(new_data);
this->capacity_ = n;
}
} // namespace esphome::api

View File

@@ -0,0 +1,67 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <memory>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
namespace esphome::api {
/// Helper to use make_unique_for_overwrite where available (skips zero-fill),
/// falling back to make_unique on older GCC (ESP8266, LibreTiny).
inline std::unique_ptr<uint8_t[]> make_buffer(size_t n) {
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
return std::make_unique<uint8_t[]>(n);
#else
return std::make_unique_for_overwrite<uint8_t[]>(n);
#endif
}
/// Byte buffer that skips zero-initialization on resize().
///
/// std::vector<uint8_t>::resize() zero-fills new bytes via memset. For the
/// shared protobuf write buffer, every byte is overwritten by the encoder,
/// making the zero-fill pure waste. For the receive buffer, bytes are
/// overwritten by socket reads.
///
/// Designed for bulk clear/resize/overwrite patterns. grow_() allocates
/// exactly the requested size (no growth factor) since callers resize to
/// known sizes rather than appending incrementally.
///
/// Safe because: callers always write exactly the number of bytes they
/// resize for. In the protobuf write path, debug_check_bounds_ validates
/// writes in debug builds.
class APIBuffer {
public:
void clear() { this->size_ = 0; }
inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE {
if (n > this->capacity_)
this->grow_(n);
}
inline void resize(size_t n) ESPHOME_ALWAYS_INLINE {
this->reserve(n);
this->size_ = n; // no zero-fill
}
uint8_t *data() { return this->data_.get(); }
const uint8_t *data() const { return this->data_.get(); }
size_t size() const { return this->size_; }
bool empty() const { return this->size_ == 0; }
uint8_t &operator[](size_t i) { return this->data_[i]; }
const uint8_t &operator[](size_t i) const { return this->data_[i]; }
/// Release all memory (equivalent to std::vector swap trick).
void release() {
this->data_.reset();
this->size_ = 0;
this->capacity_ = 0;
}
protected:
void grow_(size_t n);
std::unique_ptr<uint8_t[]> data_;
size_t size_{0};
size_t capacity_{0};
};
} // namespace esphome::api

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,23 @@
#include "esphome/core/defines.h"
#ifdef USE_API
#include "api_frame_helper.h"
#ifdef USE_API_NOISE
#include "api_frame_helper_noise.h"
#endif
#ifdef USE_API_PLAINTEXT
#include "api_frame_helper_plaintext.h"
#endif
#include "api_pb2.h"
#include "api_pb2_service.h"
#include "api_server.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#ifdef USE_ESP32_CRASH_HANDLER
#include "esphome/components/esp32/crash_handler.h"
#endif
#ifdef USE_RP2040_CRASH_HANDLER
#include "esphome/components/rp2040/crash_handler.h"
#endif
#include "esphome/core/entity_base.h"
#include "esphome/core/string_ref.h"
@@ -123,7 +135,7 @@ class APIConnection final : public APIServerConnectionBase {
void send_homeassistant_action(const HomeassistantActionRequest &call) {
if (!this->flags_.service_call_subscription)
return;
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
this->send_message(call);
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
@@ -142,12 +154,13 @@ class APIConnection final : public APIServerConnectionBase {
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
void on_subscribe_bluetooth_connections_free_request() override;
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) override;
#endif
#ifdef USE_HOMEASSISTANT_TIME
void send_time_request() {
GetTimeRequest req;
this->send_message(req, GetTimeRequest::MESSAGE_TYPE);
this->send_message(req);
}
#endif
@@ -167,6 +180,12 @@ class APIConnection final : public APIServerConnectionBase {
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
#endif
#ifdef USE_ZIGBEE_PROXY
void on_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) override;
void on_zigbee_proxy_request(const ZigbeeProxyRequest &msg) override;
void send_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) { this->send_message(msg); }
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
@@ -182,6 +201,15 @@ class APIConnection final : public APIServerConnectionBase {
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) override;
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) override;
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) override;
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) override;
void on_serial_proxy_request(const SerialProxyRequest &msg) override;
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
#endif
#ifdef USE_EVENT
void send_event(event::Event *event);
#endif
@@ -219,6 +247,12 @@ class APIConnection final : public APIServerConnectionBase {
this->flags_.log_subscription = msg.level;
if (msg.dump_config)
App.schedule_dump_config();
#ifdef USE_ESP32_CRASH_HANDLER
esp32::crash_handler_log();
#endif
#ifdef USE_RP2040_CRASH_HANDLER
rp2040::crash_handler_log();
#endif
}
#ifdef USE_API_HOMEASSISTANT_SERVICES
void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; }
@@ -247,6 +281,7 @@ class APIConnection final : public APIServerConnectionBase {
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED ||
this->is_authenticated();
}
bool is_marked_for_removal() const { return this->flags_.remove; }
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
// Get client API version for feature detection
@@ -257,9 +292,21 @@ class APIConnection final : public APIServerConnectionBase {
void on_fatal_error() override;
void on_no_setup_connection() override;
bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) override;
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) {
// Function pointer type for type-erased message encoding
using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &);
// Function pointer type for type-erased size calculation
using CalculateSizeFn = uint32_t (*)(const void *);
template<typename T> bool send_message(const T &msg) {
if constexpr (T::ESTIMATED_SIZE == 0) {
return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg);
} else {
return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg<T>, &msg);
}
}
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) {
shared_buf.clear();
// Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
@@ -270,13 +317,19 @@ class APIConnection final : public APIServerConnectionBase {
}
// Convenience overload - computes frame overhead internally
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t payload_size) {
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) {
const uint8_t header_padding = this->helper_->frame_header_padding();
const uint8_t footer_size = this->helper_->frame_footer_size();
this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size);
}
bool try_to_clear_buffer(bool log_out_of_space);
bool try_to_clear_buffer(bool log_out_of_space) {
if (this->flags_.remove)
return false;
if (this->helper_->can_write_without_blocking())
return true;
return this->try_to_clear_buffer_slow_(log_out_of_space);
}
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const char *get_name() const { return this->helper_->get_client_name(); }
@@ -286,6 +339,8 @@ class APIConnection final : public APIServerConnectionBase {
}
protected:
bool try_to_clear_buffer_slow_(bool log_out_of_space);
// Helper function to handle authentication completion
void complete_authentication_();
@@ -312,50 +367,67 @@ class APIConnection final : public APIServerConnectionBase {
void process_state_subscriptions_();
#endif
// Non-template helper to encode any ProtoMessage
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
uint32_t remaining_size);
// Helper to fill entity state base and encode message
static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type,
APIConnection *conn, uint32_t remaining_size) {
msg.key = entity->get_object_id_hash();
#ifdef USE_DEVICES
msg.device_id = entity->get_device_id();
#endif
return encode_message_to_buffer(msg, message_type, conn, remaining_size);
// Size thunk — converts void* back to concrete type for direct calculate_size() call
template<typename T> static uint32_t calc_size(const void *msg) {
return static_cast<const T *>(msg)->calculate_size();
}
// Helper to fill entity info base and encode message
static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type,
APIConnection *conn, uint32_t remaining_size) {
// Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash();
// Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0)
static void encode_msg_noop(const void *, ProtoWriteBuffer &) {}
// API 1.14+ clients compute object_id client-side from the entity name
// For older clients, we must send object_id for backward compatibility
// See: https://github.com/esphome/backlog/issues/76
// TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then
// Buffer must remain in scope until encode_message_to_buffer is called
char object_id_buf[OBJECT_ID_MAX_LEN];
if (!conn->client_supports_api_version(1, 14)) {
msg.object_id = entity->get_object_id_to(object_id_buf);
// Non-template buffer management for send_message
bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
// Non-template buffer management for batch encoding
static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
APIConnection *conn, uint32_t remaining_size);
// Thin template wrapper — computes size, delegates buffer work to non-template helper
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
if constexpr (T::ESTIMATED_SIZE == 0) {
return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size);
} else {
return encode_to_buffer(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
}
}
if (entity->has_own_name()) {
msg.name = entity->get_name();
}
// Non-template core — fills state fields and encodes
static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg,
CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn,
uint32_t remaining_size);
// Set common EntityBase properties
#ifdef USE_ENTITY_ICON
msg.icon = entity->get_icon_ref();
#endif
msg.disabled_by_default = entity->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
#ifdef USE_DEVICES
msg.device_id = entity->get_device_id();
#endif
return encode_message_to_buffer(msg, message_type, conn, remaining_size);
// Thin template wrapper
template<typename T>
static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn,
uint32_t remaining_size) {
return fill_and_encode_entity_state(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
}
// Non-template core — fills info fields, allocates buffers, and encodes
static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg,
CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn,
uint32_t remaining_size);
// Thin template wrapper
template<typename T>
static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn,
uint32_t remaining_size) {
return fill_and_encode_entity_info(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
}
// Non-template core — fills device_class, then delegates to fill_and_encode_entity_info
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
StringRef &device_class_field, CalculateSizeFn size_fn,
MessageEncodeFn encode_fn, APIConnection *conn,
uint32_t remaining_size);
// Thin template wrapper
template<typename T>
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg,
StringRef &device_class_field, APIConnection *conn,
uint32_t remaining_size) {
return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size<T>,
&proto_encode_msg<T>, conn, remaining_size);
}
#ifdef USE_VOICE_ASSISTANT
@@ -370,6 +442,10 @@ class APIConnection final : public APIServerConnectionBase {
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
}
// Send keepalive ping or disconnect unresponsive client.
// Cold path — extracted from loop() to reduce instruction cache pressure.
void __attribute__((noinline)) check_keepalive_(uint32_t now);
// Process active iterator (list_entities/initial_state) during connection setup.
// Extracted from loop() — only runs during initial handshake, NONE in steady state.
void __attribute__((noinline)) process_active_iterator_();
@@ -485,7 +561,13 @@ class APIConnection final : public APIServerConnectionBase {
// === Optimal member ordering for 32-bit systems ===
// Group 1: Pointers (4 bytes each on 32-bit)
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
std::unique_ptr<APIFrameHelper> helper_;
#elif defined(USE_API_NOISE)
std::unique_ptr<APINoiseFrameHelper> helper_;
#elif defined(USE_API_PLAINTEXT)
std::unique_ptr<APIPlaintextFrameHelper> helper_;
#endif
APIServer *parent_;
// Group 2: Iterator union (saves ~16 bytes vs separate iterators)
@@ -541,6 +623,8 @@ class APIConnection final : public APIServerConnectionBase {
uint8_t aux_data_index = AUX_DATA_UNUSED);
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
// Single push_back site to avoid duplicate _M_realloc_insert instantiation
void push_item(const BatchItem &item);
// Clear all items
void clear() {
@@ -621,8 +705,8 @@ class APIConnection final : public APIServerConnectionBase {
bool schedule_batch_();
void process_batch_();
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) __attribute__((noinline));
void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size)
__attribute__((noinline));
void clear_batch_() {
this->deferred_batch_.clear();
this->flags_.batch_scheduled = false;

View File

@@ -5,10 +5,10 @@
#include <memory>
#include <span>
#include <utility>
#include <vector>
#include "esphome/core/defines.h"
#ifdef USE_API
#include "esphome/components/api/api_buffer.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -29,6 +29,10 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
#endif
// Extra byte reserved in rx_buf_ beyond the message size so protobuf
// StringRef fields can be null-terminated in-place after decode.
static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
// Maximum number of messages to batch in a single write operation
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
@@ -130,12 +134,16 @@ class APIFrameHelper {
//
// For log messages: Use Nagle to coalesce multiple small log packets into
// fewer larger packets, reducing WiFi overhead. However, we limit batching
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into
// shared pbufs, but holding data too long waiting for Nagle's timer causes
// buffer exhaustion and dropped messages.
// to avoid excessive LWIP buffer pressure on memory-constrained devices.
// LWIP's TCP_OVERSIZE option coalesces the data into shared pbufs, but
// holding data too long waiting for Nagle's timer causes buffer exhaustion
// and dropped messages.
//
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
// ESP32 (TCP_SND_BUF=4×MSS+) / RP2040 (8×MSS) / LibreTiny (4×MSS): 4 logs per cycle
// ESP8266 (2×MSS): 3 logs per cycle (tightest buffers)
//
// Flow (ESP32/RP2040/LT): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush)
// Flow (ESP8266): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all)
//
void set_nodelay_for_message(bool is_log_message) {
if (!is_log_message) {
@@ -146,7 +154,7 @@ class APIFrameHelper {
return;
}
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
// Log messages: state transitions -1 -> 1 -> ... -> LOG_NAGLE_COUNT -> -1 (flush)
if (this->nodelay_state_ == NODELAY_ON) {
this->set_nodelay_raw_(false);
this->nodelay_state_ = 1;
@@ -174,8 +182,7 @@ class APIFrameHelper {
// rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
// and clearing would lose partially received data.
if (this->rx_buf_len_ == 0) {
// Use swap trick since shrink_to_fit() is non-binding and may be ignored
std::vector<uint8_t>().swap(this->rx_buf_);
this->rx_buf_.release();
}
}
@@ -202,9 +209,6 @@ class APIFrameHelper {
// Common socket write error handling
APIError handle_socket_write_error_();
template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;
@@ -228,9 +232,20 @@ class APIFrameHelper {
EXPLICIT_REJECT = 8, // Noise only
};
// Fast inline state check for read_packet/write_protobuf_messages hot path.
// Returns OK only in DATA state; maps CLOSED/FAILED to BAD_STATE and any
// other intermediate state to WOULD_BLOCK.
inline APIError ESPHOME_ALWAYS_INLINE check_data_state_() const {
if (this->state_ == State::DATA)
return APIError::OK;
if (this->state_ == State::CLOSED || this->state_ == State::FAILED)
return APIError::BAD_STATE;
return APIError::WOULD_BLOCK;
}
// Containers (size varies, but typically 12+ bytes on 32-bit)
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
std::vector<uint8_t> rx_buf_;
APIBuffer rx_buf_;
// Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
@@ -244,10 +259,16 @@ class APIFrameHelper {
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
// (immediate send). Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
// ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching.
// ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more.
static constexpr int8_t NODELAY_ON = -1;
#ifdef USE_ESP8266
static constexpr int8_t LOG_NAGLE_COUNT = 2;
#else
static constexpr int8_t LOG_NAGLE_COUNT = 3;
#endif
int8_t nodelay_state_{NODELAY_ON};
// Internal helper to set TCP_NODELAY socket option

View File

@@ -19,7 +19,7 @@ namespace esphome::api {
static const char *const TAG = "api.noise";
#ifdef USE_ESP8266
static const char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
#else
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
#endif
@@ -194,17 +194,20 @@ APIError APINoiseFrameHelper::try_read_frame_() {
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
// Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
bool is_data = (state_ == State::DATA);
uint16_t limit = is_data ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
if (msg_size > limit) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
return is_data ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// Reserve space for body
if (this->rx_buf_.size() != msg_size) {
this->rx_buf_.resize(msg_size);
}
// Reserve space for body (+ null terminator in DATA state so protobuf
// StringRef fields can be safely null-terminated in-place after decode.
// During handshake, rx_buf_.size() is used in prologue construction, so
// the buffer must be exactly msg_size to avoid prologue mismatch.)
uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0);
this->rx_buf_.resize(alloc_size);
if (rx_buf_len_ < msg_size) {
// more data to read
@@ -255,16 +258,19 @@ APIError APINoiseFrameHelper::state_action_() {
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = this->prologue_.size();
this->prologue_.resize(old_size + 2 + this->rx_buf_.size());
this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8);
this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size();
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size());
size_t rx_size = this->rx_buf_.size();
this->prologue_.resize(old_size + 2 + rx_size);
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
this->prologue_[old_size + 1] = (uint8_t) rx_size;
if (rx_size > 0) {
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
}
state_ = State::SERVER_HELLO;
}
if (state_ == State::SERVER_HELLO) {
// send server hello
const std::string &name = App.get_name();
const auto &name = App.get_name();
char mac[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac);
@@ -370,6 +376,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
#ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
reason_len = std::min(reason_len, sizeof(data) - 1);
if (reason_len > 0) {
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
@@ -377,6 +384,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
// Normal memory access
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
reason_len = std::min(reason_len, sizeof(data) - 1);
if (reason_len > 0) {
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
std::memcpy(data + 1, reason_str, reason_len);
@@ -392,14 +400,9 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr = this->state_action_();
if (aerr != APIError::OK) {
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
}
if (this->state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
aerr = this->try_read_frame_();
if (aerr != APIError::OK)
@@ -407,7 +410,18 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size());
// read_packet() must only be called in DATA state; the extra
// RX_BUF_NULL_TERMINATOR byte is only allocated in DATA state
// (see try_read_frame_), so calling this during handshake would
// underflow the size calculation below.
#ifdef ESPHOME_DEBUG_API
assert(this->state_ == State::DATA);
#endif
// rx_buf_ has RX_BUF_NULL_TERMINATOR extra byte for null termination
// (only added in DATA state — see try_read_frame_), so subtract it
// to get the actual encrypted data size for decryption.
size_t encrypted_size = this->rx_buf_.size() - RX_BUF_NULL_TERMINATOR;
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), encrypted_size, encrypted_size);
int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf);
APIError decrypt_err =
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
@@ -445,14 +459,9 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuff
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
APIError aerr = state_action_();
if (aerr != APIError::OK) {
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
if (messages.empty()) {
return APIError::OK;
@@ -474,7 +483,7 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
// buf_start[1], buf_start[2] to be set after encryption
// Write message header (to be encrypted)
const uint8_t msg_offset = 3;
constexpr uint8_t msg_offset = 3;
buf_start[msg_offset] = static_cast<uint8_t>(msg.message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(msg.message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(msg.payload_size >> 8); // data_len high byte
@@ -563,8 +572,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
// Use swap idiom to actually release memory (= {} only clears size, not capacity)
std::vector<uint8_t>().swap(prologue_);
prologue_.release();
err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
@@ -574,7 +582,9 @@ APIError APINoiseFrameHelper::init_handshake_() {
}
APIError APINoiseFrameHelper::check_handshake_finished_() {
#ifdef ESPHOME_DEBUG_API
assert(state_ == State::HANDSHAKE);
#endif
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)

View File

@@ -43,8 +43,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Reference to noise context (4 bytes on 32-bit)
APINoiseContext &ctx_;
// Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_;
// Buffer for noise handshake prologue (released after handshake)
APIBuffer prologue_;
// NoiseProtocolId (size depends on implementation)
NoiseProtocolId nid_;

View File

@@ -128,45 +128,44 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
// Skip indicator byte at position 0
uint8_t varint_pos = 1;
uint32_t consumed = 0;
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
// rx_header_buf_pos_ >= 3 and varint_pos == 1, so len >= 2
auto msg_size_varint = ProtoVarInt::parse_non_empty(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos);
if (!msg_size_varint.has_value()) {
// not enough data there yet
continue;
}
if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
if (msg_size_varint.value > MAX_MESSAGE_SIZE) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
MAX_MESSAGE_SIZE);
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u",
static_cast<uint32_t>(msg_size_varint.value), MAX_MESSAGE_SIZE);
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_len_ = msg_size_varint->as_uint16();
rx_header_parsed_len_ = static_cast<uint16_t>(msg_size_varint.value);
// Move to next varint position
varint_pos += consumed;
varint_pos += msg_size_varint.consumed;
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos);
if (!msg_type_varint.has_value()) {
// not enough data there yet
continue;
}
if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
if (msg_type_varint.value > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
std::numeric_limits<uint16_t>::max());
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u",
static_cast<uint32_t>(msg_type_varint.value), std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_type_ = msg_type_varint->as_uint16();
rx_header_parsed_type_ = static_cast<uint16_t>(msg_type_varint.value);
rx_header_parsed_ = true;
}
// header reading done
// Reserve space for body
if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
this->rx_buf_.resize(this->rx_header_parsed_len_);
}
// Reserve space for body (+ null terminator so protobuf StringRef fields
// can be safely null-terminated in-place after decode)
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read
@@ -194,11 +193,11 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
}
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
if (this->state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
APIError aerr = this->try_read_frame_();
aerr = this->try_read_frame_();
if (aerr != APIError::OK) {
if (aerr == APIError::BAD_INDICATOR) {
// Make sure to tell the remote that we don't
@@ -243,9 +242,9 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
std::span<const MessageInfo> messages) {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
if (messages.empty()) {
return APIError::OK;

View File

@@ -90,4 +90,10 @@ extend google.protobuf.FieldOptions {
// - uint16_t <field>_length_{0};
// - uint16_t <field>_count_{0};
optional bool packed_buffer = 50015 [default=false];
// force: Always encode this field, even when its value equals the proto3 default.
// Skips the zero/empty check in calculate_size() and encode(), using the _force
// variant of the calc_ method. Use on fields that are almost always non-default
// to eliminate dead branches on hot paths.
optional bool force = 50016 [default=false];
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
// This file was automatically generated with a tool.
// See script/api_protobuf/api_protobuf.py
#pragma once
#include "esphome/core/defines.h"
#ifndef USE_API_VARINT64
#define USE_API_VARINT64
#endif
namespace esphome::api {} // namespace esphome::api

View File

@@ -13,7 +13,7 @@ namespace esphome::api {
static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) {
out.append("'");
if (!ref.empty()) {
out.append(ref.c_str());
out.append(ref.c_str(), ref.size());
}
out.append("'");
}
@@ -100,6 +100,18 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
out.append(hex_buf).append("\n");
}
template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::SerialProxyPortType value) {
switch (value) {
case enums::SERIAL_PROXY_PORT_TYPE_TTL:
return "SERIAL_PROXY_PORT_TYPE_TTL";
case enums::SERIAL_PROXY_PORT_TYPE_RS232:
return "SERIAL_PROXY_PORT_TYPE_RS232";
case enums::SERIAL_PROXY_PORT_TYPE_RS485:
return "SERIAL_PROXY_PORT_TYPE_RS485";
default:
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::EntityCategory value) {
switch (value) {
case enums::ENTITY_CATEGORY_NONE:
@@ -208,6 +220,20 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::DSTRuleType>(enums::DSTRuleType value) {
switch (value) {
case enums::DST_RULE_TYPE_NONE:
return "DST_RULE_TYPE_NONE";
case enums::DST_RULE_TYPE_MONTH_WEEK_DAY:
return "DST_RULE_TYPE_MONTH_WEEK_DAY";
case enums::DST_RULE_TYPE_JULIAN_NO_LEAP:
return "DST_RULE_TYPE_JULIAN_NO_LEAP";
case enums::DST_RULE_TYPE_DAY_OF_YEAR:
return "DST_RULE_TYPE_DAY_OF_YEAR";
default:
return "UNKNOWN";
}
}
#ifdef USE_API_USER_DEFINED_ACTIONS
template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
switch (value) {
@@ -321,6 +347,8 @@ template<> const char *proto_enum_to_string<enums::ClimateAction>(enums::Climate
return "CLIMATE_ACTION_DRYING";
case enums::CLIMATE_ACTION_FAN:
return "CLIMATE_ACTION_FAN";
case enums::CLIMATE_ACTION_DEFROSTING:
return "CLIMATE_ACTION_DEFROSTING";
default:
return "UNKNOWN";
}
@@ -736,6 +764,62 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
}
}
#endif
#ifdef USE_SERIAL_PROXY
template<> const char *proto_enum_to_string<enums::SerialProxyParity>(enums::SerialProxyParity value) {
switch (value) {
case enums::SERIAL_PROXY_PARITY_NONE:
return "SERIAL_PROXY_PARITY_NONE";
case enums::SERIAL_PROXY_PARITY_EVEN:
return "SERIAL_PROXY_PARITY_EVEN";
case enums::SERIAL_PROXY_PARITY_ODD:
return "SERIAL_PROXY_PARITY_ODD";
default:
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums::SerialProxyRequestType value) {
switch (value) {
case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE:
return "SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE";
case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
return "SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE";
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH:
return "SERIAL_PROXY_REQUEST_TYPE_FLUSH";
default:
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::SerialProxyStatus value) {
switch (value) {
case enums::SERIAL_PROXY_STATUS_OK:
return "SERIAL_PROXY_STATUS_OK";
case enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS:
return "SERIAL_PROXY_STATUS_ASSUMED_SUCCESS";
case enums::SERIAL_PROXY_STATUS_ERROR:
return "SERIAL_PROXY_STATUS_ERROR";
case enums::SERIAL_PROXY_STATUS_TIMEOUT:
return "SERIAL_PROXY_STATUS_TIMEOUT";
case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED:
return "SERIAL_PROXY_STATUS_NOT_SUPPORTED";
default:
return "UNKNOWN";
}
}
#endif
#ifdef USE_ZIGBEE_PROXY
template<> const char *proto_enum_to_string<enums::ZigbeeProxyRequestType>(enums::ZigbeeProxyRequestType value) {
switch (value) {
case enums::ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE:
return "ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE";
case enums::ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
return "ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE";
case enums::ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO:
return "ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO";
default:
return "UNKNOWN";
}
}
#endif
const char *HelloRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "HelloRequest");
@@ -785,6 +869,14 @@ const char *DeviceInfo::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_SERIAL_PROXY
const char *SerialProxyInfo::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyInfo");
dump_field(out, "name", this->name);
dump_field(out, "port_type", static_cast<enums::SerialProxyPortType>(this->port_type));
return out.c_str();
}
#endif
const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "DeviceInfoResponse");
dump_field(out, "name", this->name);
@@ -845,6 +937,19 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
#endif
#ifdef USE_ZWAVE_PROXY
dump_field(out, "zwave_home_id", this->zwave_home_id);
#endif
#ifdef USE_SERIAL_PROXY
for (const auto &it : this->serial_proxies) {
out.append(" serial_proxies: ");
it.dump_to(out);
out.append("\n");
}
#endif
#ifdef USE_ZIGBEE_PROXY
dump_field(out, "zigbee_proxy_feature_flags", this->zigbee_proxy_feature_flags);
#endif
#ifdef USE_ZIGBEE_PROXY
dump_field(out, "zigbee_ieee_address", this->zigbee_ieee_address);
#endif
return out.c_str();
}
@@ -1252,10 +1357,35 @@ const char *GetTimeRequest::dump_to(DumpBuffer &out) const {
out.append("GetTimeRequest {}");
return out.c_str();
}
const char *DSTRule::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "DSTRule");
dump_field(out, "time_seconds", this->time_seconds);
dump_field(out, "day", this->day);
dump_field(out, "type", static_cast<enums::DSTRuleType>(this->type));
dump_field(out, "month", this->month);
dump_field(out, "week", this->week);
dump_field(out, "day_of_week", this->day_of_week);
return out.c_str();
}
const char *ParsedTimezone::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "ParsedTimezone");
dump_field(out, "std_offset_seconds", this->std_offset_seconds);
dump_field(out, "dst_offset_seconds", this->dst_offset_seconds);
out.append(" dst_start: ");
this->dst_start.dump_to(out);
out.append("\n");
out.append(" dst_end: ");
this->dst_end.dump_to(out);
out.append("\n");
return out.c_str();
}
const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "GetTimeResponse");
dump_field(out, "epoch_seconds", this->epoch_seconds);
dump_field(out, "timezone", this->timezone);
out.append(" parsed_timezone: ");
this->parsed_timezone.dump_to(out);
out.append("\n");
return out.c_str();
}
#ifdef USE_API_USER_DEFINED_ACTIONS
@@ -2469,6 +2599,91 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_SERIAL_PROXY
const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyConfigureRequest");
dump_field(out, "instance", this->instance);
dump_field(out, "baudrate", this->baudrate);
dump_field(out, "flow_control", this->flow_control);
dump_field(out, "parity", static_cast<enums::SerialProxyParity>(this->parity));
dump_field(out, "stop_bits", this->stop_bits);
dump_field(out, "data_size", this->data_size);
return out.c_str();
}
const char *SerialProxyDataReceived::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyDataReceived");
dump_field(out, "instance", this->instance);
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
return out.c_str();
}
const char *SerialProxyWriteRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyWriteRequest");
dump_field(out, "instance", this->instance);
dump_bytes_field(out, "data", this->data, this->data_len);
return out.c_str();
}
const char *SerialProxySetModemPinsRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxySetModemPinsRequest");
dump_field(out, "instance", this->instance);
dump_field(out, "line_states", this->line_states);
return out.c_str();
}
const char *SerialProxyGetModemPinsRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyGetModemPinsRequest");
dump_field(out, "instance", this->instance);
return out.c_str();
}
const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyGetModemPinsResponse");
dump_field(out, "instance", this->instance);
dump_field(out, "line_states", this->line_states);
return out.c_str();
}
const char *SerialProxyRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyRequest");
dump_field(out, "instance", this->instance);
dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type));
return out.c_str();
}
const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyRequestResponse");
dump_field(out, "instance", this->instance);
dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type));
dump_field(out, "status", static_cast<enums::SerialProxyStatus>(this->status));
dump_field(out, "error_message", this->error_message);
return out.c_str();
}
#endif
#ifdef USE_BLUETOOTH_PROXY
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "BluetoothSetConnectionParamsRequest");
dump_field(out, "address", this->address);
dump_field(out, "min_interval", this->min_interval);
dump_field(out, "max_interval", this->max_interval);
dump_field(out, "latency", this->latency);
dump_field(out, "timeout", this->timeout);
return out.c_str();
}
const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "BluetoothSetConnectionParamsResponse");
dump_field(out, "address", this->address);
dump_field(out, "error", this->error);
return out.c_str();
}
#endif
#ifdef USE_ZIGBEE_PROXY
const char *ZigbeeProxyFrame::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "ZigbeeProxyFrame");
dump_bytes_field(out, "data", this->data, this->data_len);
return out.c_str();
}
const char *ZigbeeProxyRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "ZigbeeProxyRequest");
dump_field(out, "type", static_cast<enums::ZigbeeProxyRequestType>(this->type));
dump_bytes_field(out, "data", this->data, this->data_len);
return out.c_str();
}
#endif
} // namespace esphome::api

View File

@@ -634,6 +634,94 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_infrared_rf_transmit_raw_timings_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyConfigureRequest::MESSAGE_TYPE: {
SerialProxyConfigureRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_configure_request"), msg);
#endif
this->on_serial_proxy_configure_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyWriteRequest::MESSAGE_TYPE: {
SerialProxyWriteRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_write_request"), msg);
#endif
this->on_serial_proxy_write_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxySetModemPinsRequest::MESSAGE_TYPE: {
SerialProxySetModemPinsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_set_modem_pins_request"), msg);
#endif
this->on_serial_proxy_set_modem_pins_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyGetModemPinsRequest::MESSAGE_TYPE: {
SerialProxyGetModemPinsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_get_modem_pins_request"), msg);
#endif
this->on_serial_proxy_get_modem_pins_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyRequest::MESSAGE_TYPE: {
SerialProxyRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_request"), msg);
#endif
this->on_serial_proxy_request(msg);
break;
}
#endif
#ifdef USE_BLUETOOTH_PROXY
case BluetoothSetConnectionParamsRequest::MESSAGE_TYPE: {
BluetoothSetConnectionParamsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_bluetooth_set_connection_params_request"), msg);
#endif
this->on_bluetooth_set_connection_params_request(msg);
break;
}
#endif
#ifdef USE_ZIGBEE_PROXY
case ZigbeeProxyFrame::MESSAGE_TYPE: {
ZigbeeProxyFrame msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_zigbee_proxy_frame"), msg);
#endif
this->on_zigbee_proxy_frame(msg);
break;
}
#endif
#ifdef USE_ZIGBEE_PROXY
case ZigbeeProxyRequest::MESSAGE_TYPE: {
ZigbeeProxyRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_zigbee_proxy_request"), msg);
#endif
this->on_zigbee_proxy_request(msg);
break;
}
#endif
default:
break;

View File

@@ -19,14 +19,6 @@ class APIServerConnectionBase : public ProtoService {
public:
#endif
bool send_message(const ProtoMessage &msg, uint8_t message_type) {
#ifdef HAS_PROTO_MESSAGE_DUMP
DumpBuffer dump_buf;
this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));
#endif
return this->send_message_impl(msg, message_type);
}
virtual void on_hello_request(const HelloRequest &value){};
virtual void on_disconnect_request(){};
@@ -224,6 +216,34 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_request(const SerialProxyRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
#endif
#ifdef USE_ZIGBEE_PROXY
virtual void on_zigbee_proxy_frame(const ZigbeeProxyFrame &value){};
#endif
#ifdef USE_ZIGBEE_PROXY
virtual void on_zigbee_proxy_request(const ZigbeeProxyRequest &value){};
#endif
protected:
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
};

View File

@@ -28,10 +28,12 @@ static const char *const TAG = "api";
// APIServer
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
APIServer::APIServer() {
global_api_server = this;
// Pre-allocate shared write buffer
shared_write_buffer_.reserve(64);
APIServer::APIServer() { global_api_server = this; }
void APIServer::socket_failed_(const LogString *msg) {
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
this->destroy_socket_();
this->mark_failed();
}
void APIServer::setup() {
@@ -52,22 +54,20 @@ void APIServer::setup() {
#endif
#endif
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket");
this->mark_failed();
this->socket_failed_(LOG_STR("creation"));
return;
}
int enable = 1;
int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno);
// we can still continue
}
err = this->socket_->setblocking(false);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
this->mark_failed();
this->socket_failed_(LOG_STR("nonblocking"));
return;
}
@@ -75,28 +75,28 @@ void APIServer::setup() {
socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
if (sl == 0) {
ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno);
this->mark_failed();
this->socket_failed_(LOG_STR("set sockaddr"));
return;
}
err = this->socket_->bind((struct sockaddr *) &server, sl);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
this->mark_failed();
this->socket_failed_(LOG_STR("bind"));
return;
}
err = this->socket_->listen(this->listen_backlog_);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
this->mark_failed();
this->socket_failed_(LOG_STR("listen"));
return;
}
#ifdef USE_LOGGER
if (logger::global_logger != nullptr) {
logger::global_logger->add_log_listener(this);
logger::global_logger->add_log_callback(
this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
static_cast<APIServer *>(self)->on_log(level, tag, message, message_len);
});
}
#endif
@@ -199,7 +199,7 @@ void APIServer::remove_client_(size_t client_index) {
#endif
}
void APIServer::accept_new_connections_() {
void __attribute__((flatten)) APIServer::accept_new_connections_() {
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
@@ -359,11 +359,11 @@ void APIServer::on_update(update::UpdateEntity *obj) {
#endif
#ifdef USE_ZWAVE_PROXY
void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) {
void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
// We could add code to manage a second subscription type, but, since this message type is
// very infrequent and small, we simply send it to all clients
for (auto &c : this->clients_)
c->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE);
c->send_message(msg);
}
#endif
@@ -433,8 +433,8 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper to add subscription (reduces duplication)
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
bool once) {
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
std::function<void(StringRef)> &&f, bool once) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
@@ -443,7 +443,7 @@ void APIServer::add_state_subscription_(const char *entity_id, const char *attri
// Helper to add subscription with heap-allocated strings (reduces duplication)
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f, bool once) {
std::function<void(StringRef)> &&f, bool once) {
HomeAssistantStateSubscription sub;
// Allocate heap storage for the strings
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
@@ -463,29 +463,29 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
// New const char* overload (for internal components - zero allocation)
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
}
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
}
// std::string overload with StringRef callback (zero-allocation callback)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
// Legacy helper: wraps std::string callback and delegates to StringRef version
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once) {
std::function<void(const std::string &)> &&f, bool once) {
// Wrap callback to convert StringRef -> std::string, then delegate
this->add_state_subscription_(std::move(entity_id), std::move(attribute),
std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
@@ -494,12 +494,12 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
std::function<void(const std::string &)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
std::function<void(const std::string &)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
@@ -531,7 +531,7 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
this->set_noise_psk(active_psk);
for (auto &c : this->clients_) {
DisconnectRequest req;
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
c->send_message(req);
}
});
}
@@ -582,11 +582,7 @@ void APIServer::request_time() {
}
#endif
bool APIServer::is_connected(bool state_subscription_only) const {
if (!state_subscription_only) {
return !this->clients_.empty();
}
bool APIServer::is_connected_with_state_subscription() const {
for (const auto &client : this->clients_) {
if (client->flags_.state_subscription) {
return true;
@@ -623,10 +619,7 @@ void APIServer::on_shutdown() {
this->shutting_down_ = true;
// Close the listening socket to prevent new connections
if (this->socket_) {
this->socket_->close();
this->socket_ = nullptr;
}
this->destroy_socket_();
// Change batch delay to 5ms for quick flushing during shutdown
this->batch_delay_ = 5;
@@ -634,7 +627,7 @@ void APIServer::on_shutdown() {
// Send disconnect requests to all connected clients
for (auto &c : this->clients_) {
DisconnectRequest req;
if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) {
if (!c->send_message(req)) {
// If we can't send the disconnect request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority
c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE);

View File

@@ -2,6 +2,7 @@
#include "esphome/core/defines.h"
#ifdef USE_API
#include "api_buffer.h"
#include "api_noise_context.h"
#include "api_pb2.h"
#include "api_pb2_service.h"
@@ -37,10 +38,6 @@ struct SavedNoisePsk {
class APIServer : public Component,
public Controller
#ifdef USE_LOGGER
,
public logger::LogListener
#endif
#ifdef USE_CAMERA
,
public camera::CameraListener
@@ -56,7 +53,7 @@ class APIServer : public Component,
void on_shutdown() override;
bool teardown() override;
#ifdef USE_LOGGER
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override;
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len);
#endif
#ifdef USE_CAMERA
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
@@ -69,7 +66,7 @@ class APIServer : public Component,
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
// Get reference to shared buffer for API connections
std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; }
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
#ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true);
@@ -183,13 +180,14 @@ class APIServer : public Component,
void on_update(update::UpdateEntity *obj) override;
#endif
#ifdef USE_ZWAVE_PROXY
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
#endif
#ifdef USE_IR_RF
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
#endif
bool is_connected(bool state_subscription_only = false) const;
bool is_connected() const { return !this->clients_.empty(); }
bool is_connected_with_state_subscription() const;
#ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription {
@@ -205,20 +203,20 @@ class APIServer : public Component,
};
// New const char* overload (for internal components - zero allocation)
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f);
// std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f);
std::function<void(StringRef)> &&f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f);
std::function<void(StringRef)> &&f);
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
std::function<void(const std::string &)> &&f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
std::function<void(const std::string &)> &&f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
#endif
@@ -245,16 +243,23 @@ class APIServer : public Component,
#endif // USE_API_NOISE
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper methods to reduce code duplication
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute, std::function<void(StringRef)> f,
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> &&f, bool once);
// Legacy helper: wraps std::string callback and delegates to StringRef version
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once);
std::function<void(const std::string &)> &&f, bool once);
#endif // USE_API_HOMEASSISTANT_STATES
// No explicit close() needed — listen sockets have no active connections on
// failure/shutdown. Destructor handles fd cleanup (close or abort per platform).
inline void destroy_socket_() {
delete this->socket_;
this->socket_ = nullptr;
}
void socket_failed_(const LogString *msg);
// Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr;
socket::ListenSocket *socket_{nullptr};
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> client_connected_trigger_;
#endif
@@ -268,7 +273,11 @@ class APIServer : public Component,
// Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_;
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
// Shared proto write buffer for all connections.
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
// since the buffer would almost always reallocate on first use.
APIBuffer shared_write_buffer_;
#ifdef USE_API_HOMEASSISTANT_STATES
std::vector<HomeAssistantStateSubscription> state_subs_;
#endif
@@ -316,7 +325,10 @@ template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
TEMPLATABLE_VALUE(bool, state_subscription_only)
public:
bool check(const Ts &...x) override {
return global_api_server->is_connected(this->state_subscription_only_.value(x...));
if (this->state_subscription_only_.value(x...)) {
return global_api_server->is_connected_with_state_subscription();
}
return global_api_server->is_connected();
}
};

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
from datetime import datetime
import importlib
import logging
from typing import TYPE_CHECKING, Any
import warnings
@@ -18,6 +19,7 @@ import contextlib
from esphome.const import CONF_KEY, CONF_PORT, __version__
from esphome.core import CORE
from esphome.platformio_api import process_stacktrace
from . import CONF_ENCRYPTION
@@ -55,9 +57,19 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
addresses=addresses, # Pass all addresses for automatic retry
)
dashboard = CORE.dashboard
backtrace_state = False
# Try platform-specific stacktrace handler first, fall back to generic
platform_process_stacktrace = None
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
platform_process_stacktrace = getattr(module, "process_stacktrace")
except (AttributeError, ImportError):
pass
def on_log(msg: SubscribeLogsResponse) -> None:
"""Handle a new log message."""
nonlocal backtrace_state
time_ = datetime.now()
message: bytes = msg.message
text = message.decode("utf8", "backslashreplace")
@@ -67,6 +79,15 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
)
for parsed_msg in parse_log_message(text, timestamp):
print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg)
for raw_line in text.splitlines():
if platform_process_stacktrace:
backtrace_state = platform_process_stacktrace(
config, raw_line, backtrace_state
)
else:
backtrace_state = process_stacktrace(
config, raw_line, backtrace_state=backtrace_state
)
stop = await async_run(cli, on_log, name=name)
try:

View File

@@ -130,6 +130,20 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
this->add_kv_(this->variables_, key, std::forward<V>(value));
}
#ifdef USE_ESP8266
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template;
@@ -221,7 +235,32 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
Ts... x) {
dest.init(source.size());
// Count non-static strings to allocate exact storage needed
#ifdef USE_ESP8266
// On ESP8266, all static strings from codegen are FLASH_STRING (PROGMEM),
// so is_static_string() is always false — the zero-copy STATIC_STRING fast
// path from the non-ESP8266 branch cannot trigger. We copy all keys and
// values unconditionally: keys via _P functions (may be in PROGMEM), values
// via value() which handles FLASH_STRING internally.
value_storage.init(source.size() * 2);
for (auto &it : source) {
auto &kv = dest.emplace_back();
// Key: copy from possible PROGMEM
{
size_t key_len = strlen_P(it.key);
value_storage.push_back(std::string(key_len, '\0'));
memcpy_P(value_storage.back().data(), it.key, key_len);
kv.key = StringRef(value_storage.back());
}
// Value: value() handles FLASH_STRING via _P functions internally
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
#else
// On non-ESP8266, strings are directly readable from flash-mapped memory.
// Count non-static strings to allocate exact storage needed.
size_t lambda_count = 0;
for (const auto &it : source) {
if (!it.value.is_static_string()) {
@@ -235,14 +274,15 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.key = StringRef(it.key);
if (it.value.is_static_string()) {
// Static string from YAML - zero allocation
// Static string — pointer directly readable, zero allocation
kv.value = StringRef(it.value.get_static_string());
} else {
// Lambda evaluation - store result, reference it
// Lambda evaluate and store result
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
}
#endif
}
APIServer *parent_;

View File

@@ -94,7 +94,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
#ifdef USE_API_USER_DEFINED_ACTIONS
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response();
return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE);
return this->client_->send_message(resp);
}
#endif

View File

@@ -15,7 +15,7 @@ class APIConnection;
return this->client_->schedule_message_(entity, ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \
}
class ListEntitiesIterator : public ComponentIterator {
class ListEntitiesIterator final : public ComponentIterator {
public:
ListEntitiesIterator(APIConnection *client);
#ifdef USE_BINARY_SENSOR

View File

@@ -1,5 +1,6 @@
#include "proto.h"
#include <cinttypes>
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -7,24 +8,71 @@ namespace esphome::api {
static const char *const TAG = "api.proto";
uint32_t ProtoSize::varint_slow(uint32_t value) { return varint_wide(value); }
void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) {
do {
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
} while (value > 0x7F);
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value);
}
ProtoVarIntResult ProtoVarInt::parse_slow(const uint8_t *buffer, uint32_t len) {
// Multi-byte varint: first byte already checked to have high bit set
uint32_t result32 = buffer[0] & 0x7F;
#ifdef USE_API_VARINT64
uint32_t limit = std::min(len, uint32_t(4));
#else
uint32_t limit = std::min(len, uint32_t(5));
#endif
for (uint32_t i = 1; i < limit; i++) {
uint8_t val = buffer[i];
result32 |= uint32_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
return {result32, i + 1};
}
}
#ifdef USE_API_VARINT64
return parse_wide(buffer, len, result32);
#else
return {0, PROTO_VARINT_PARSE_FAILED};
#endif
}
#ifdef USE_API_VARINT64
ProtoVarIntResult ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) {
uint64_t result64 = result32;
uint32_t limit = std::min(len, uint32_t(10));
for (uint32_t i = 4; i < limit; i++) {
uint8_t val = buffer[i];
result64 |= uint64_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
return {result64, i + 1};
}
}
return {0, PROTO_VARINT_PARSE_FAILED};
}
#endif
uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
uint32_t count = 0;
const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length;
while (ptr < end) {
uint32_t consumed;
// Parse field header (tag)
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
// Parse field header (tag) - ptr < end guarantees len >= 1
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
if (!res.has_value()) {
break; // Invalid data, stop counting
}
uint32_t tag = res->as_uint32();
uint32_t tag = static_cast<uint32_t>(res.value);
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += consumed;
ptr += res.consumed;
// Count if this is the target field
if (field_id == target_field_id) {
@@ -34,20 +82,20 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
// Skip field data based on wire type
switch (field_type) {
case WIRE_TYPE_VARINT: { // VarInt - parse and skip
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
return count; // Invalid data, return what we have
}
ptr += consumed;
ptr += res.consumed;
break;
}
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
return count;
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
uint32_t field_length = static_cast<uint32_t>(res.value);
ptr += res.consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
return count; // Out of bounds
}
@@ -70,46 +118,130 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
return count;
}
// Single-pass encode for repeated submessage elements (non-template core).
// Writes field tag, reserves 1 byte for length varint, encodes the submessage body,
// then backpatches the actual length. For the common case (body < 128 bytes), this is
// just a single byte write with no memmove — all current repeated submessage types
// (BLE advertisements at ~47B, GATT descriptors at ~24B, service args, etc.) take
// this fast path.
//
// The memmove fallback for body >= 128 bytes exists only for correctness (e.g., a GATT
// characteristic with many descriptors). It is safe because calculate_size() already
// reserved space for the full multi-byte varint — the shift fills that reserved space:
//
// calculate_size() allocates per element: tag + varint_size(body) + body_size
//
// After encode, before memmove (1 byte reserved, body written):
// [tag][__][body ..... body][??]
// ^ ^-- unused byte (v2 space from calculate_size)
// len_pos
//
// After memmove(body_start+1, body_start, body_size):
// [tag][__][__][body ..... body]
// ^ ^-- body shifted forward, fills v2 space exactly
// len_pos
//
// After writing 2-byte varint at len_pos:
// [tag][v1][v2][body ..... body]
// ^-- pos_ = element end, within buffer
void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
this->encode_field_raw(field_id, 2);
// Reserve 1 byte for length varint (optimistic: submessage < 128 bytes)
uint8_t *len_pos = this->pos_;
this->debug_check_bounds_(1);
this->pos_++;
uint8_t *body_start = this->pos_;
encode_fn(value, *this);
uint32_t body_size = static_cast<uint32_t>(this->pos_ - body_start);
if (body_size < 128) [[likely]] {
// Common case: 1-byte varint, just backpatch
*len_pos = static_cast<uint8_t>(body_size);
return;
}
// Compute extra bytes needed for varint beyond the 1 already reserved
uint8_t extra = ProtoSize::varint(body_size) - 1;
// Shift body forward to make room for the extra varint bytes
this->debug_check_bounds_(extra);
std::memmove(body_start + extra, body_start, body_size);
uint8_t *end = this->pos_ + extra;
// Write the full varint at len_pos
this->pos_ = len_pos;
this->encode_varint_raw(body_size);
this->pos_ = end;
}
// Non-template core for encode_optional_sub_message.
void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
if (nested_size == 0)
return;
this->encode_field_raw(field_id, 2);
this->encode_varint_raw(nested_size);
#ifdef ESPHOME_DEBUG_API
uint8_t *start = this->pos_;
encode_fn(value, *this);
if (static_cast<uint32_t>(this->pos_ - start) != nested_size)
this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start);
#else
encode_fn(value, *this);
#endif
}
#ifdef ESPHOME_DEBUG_API
void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) {
if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) {
ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes,
this->pos_ - this->buffer_->data(), this->buffer_->size());
abort();
}
}
void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual) {
ESP_LOGE(TAG, "encode_message: size mismatch for field %" PRIu32 ": calculated=%" PRIu32 " actual=%td", field_id,
expected, actual);
abort();
}
#endif
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length;
while (ptr < end) {
uint32_t consumed;
// Parse field header
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
// Parse field header - ptr < end guarantees len >= 1
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t tag = res->as_uint32();
uint32_t tag = static_cast<uint32_t>(res.value);
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += consumed;
ptr += res.consumed;
switch (field_type) {
case WIRE_TYPE_VARINT: { // VarInt
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
return;
}
if (!this->decode_varint(field_id, *res)) {
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32());
if (!this->decode_varint(field_id, res.value)) {
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id,
static_cast<uint64_t>(res.value));
}
ptr += consumed;
ptr += res.consumed;
break;
}
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
uint32_t field_length = static_cast<uint32_t>(res.value);
ptr += res.consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;

View File

@@ -1,5 +1,7 @@
#pragma once
#include "api_pb2_defines.h"
#include "api_buffer.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -97,75 +99,60 @@ inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) {
* within the same function scope where temporaries are created.
*/
/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit
/// Type used for decoded varint values - uint64_t when BLE needs 64-bit addresses, uint32_t otherwise
#ifdef USE_API_VARINT64
using proto_varint_value_t = uint64_t;
#else
using proto_varint_value_t = uint32_t;
#endif
/// Sentinel value for consumed field indicating parse failure
inline constexpr uint32_t PROTO_VARINT_PARSE_FAILED = 0;
/// Result of parsing a varint: value + number of bytes consumed.
/// consumed == PROTO_VARINT_PARSE_FAILED indicates parse failure (not enough data or invalid).
struct ProtoVarIntResult {
proto_varint_value_t value;
uint32_t consumed; // PROTO_VARINT_PARSE_FAILED = parse failed
constexpr bool has_value() const { return this->consumed != PROTO_VARINT_PARSE_FAILED; }
};
/// Static varint parsing methods for the protobuf wire format.
class ProtoVarInt {
public:
ProtoVarInt() : value_(0) {}
explicit ProtoVarInt(uint64_t value) : value_(value) {}
/// Parse a varint from buffer. consumed must be a valid pointer (not null).
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
/// Parse a varint from buffer. Caller must ensure len >= 1.
/// Returns result with consumed=0 on failure (truncated multi-byte varint).
static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse_non_empty(const uint8_t *buffer, uint32_t len) {
#ifdef ESPHOME_DEBUG_API
assert(consumed != nullptr);
assert(len > 0);
#endif
// Fast path: single-byte varints (0-127) are the most common case
// (booleans, small enums, field tags, small message sizes/types).
if ((buffer[0] & 0x80) == 0) [[likely]]
return {buffer[0], 1};
return parse_slow(buffer, len);
}
/// Parse a varint from buffer (safe for empty buffers).
/// Returns result with consumed=0 on failure (empty buffer or truncated varint).
static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse(const uint8_t *buffer, uint32_t len) {
if (len == 0)
return {};
// Most common case: single-byte varint (values 0-127)
if ((buffer[0] & 0x80) == 0) {
*consumed = 1;
return ProtoVarInt(buffer[0]);
}
// General case for multi-byte varints
// Since we know buffer[0]'s high bit is set, initialize with its value
uint64_t result = buffer[0] & 0x7F;
uint8_t bitpos = 7;
// A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings
// to avoid undefined behavior from shifting uint64_t by >= 64 bits.
uint32_t max_len = std::min(len, uint32_t(10));
// Start from the second byte since we've already processed the first
for (uint32_t i = 1; i < max_len; i++) {
uint8_t val = buffer[i];
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7;
if ((val & 0x80) == 0) {
*consumed = i + 1;
return ProtoVarInt(result);
}
}
return {}; // Incomplete or invalid varint
}
constexpr uint16_t as_uint16() const { return this->value_; }
constexpr uint32_t as_uint32() const { return this->value_; }
constexpr uint64_t as_uint64() const { return this->value_; }
constexpr bool as_bool() const { return this->value_; }
constexpr int32_t as_int32() const {
// Not ZigZag encoded
return static_cast<int32_t>(this->as_int64());
}
constexpr int64_t as_int64() const {
// Not ZigZag encoded
return static_cast<int64_t>(this->value_);
}
constexpr int32_t as_sint32() const {
// with ZigZag encoding
return decode_zigzag32(static_cast<uint32_t>(this->value_));
}
constexpr int64_t as_sint64() const {
// with ZigZag encoding
return decode_zigzag64(this->value_);
return {0, PROTO_VARINT_PARSE_FAILED};
return parse_non_empty(buffer, len);
}
protected:
uint64_t value_;
// Slow path for multi-byte varints (>= 128), outlined to keep fast path small
static ProtoVarIntResult parse_slow(const uint8_t *buffer, uint32_t len) __attribute__((noinline));
#ifdef USE_API_VARINT64
/// Continue parsing varint bytes 4-9 with 64-bit arithmetic.
static ProtoVarIntResult parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) __attribute__((noinline));
#endif
};
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32
// Forward declarations for decode_to_message and related encoding helpers
class ProtoDecodableMessage;
class ProtoMessage;
class ProtoSize;
@@ -217,21 +204,24 @@ class Proto32Bit {
class ProtoWriteBuffer {
public:
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
void write(uint8_t value) { this->buffer_->push_back(value); }
void encode_varint_raw(uint32_t value) {
while (value > 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
value >>= 7;
ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {}
ProtoWriteBuffer(APIBuffer *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {}
inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) {
if (value < 128) [[likely]] {
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value);
return;
}
this->buffer_->push_back(static_cast<uint8_t>(value));
this->encode_varint_raw_slow_(value);
}
void encode_varint_raw_64(uint64_t value) {
while (value > 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
}
this->buffer_->push_back(static_cast<uint8_t>(value));
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value);
}
/**
* Encode a field key (tag/wire type combination).
@@ -245,23 +235,18 @@ class ProtoWriteBuffer {
*
* Following https://protobuf.dev/programming-guides/encoding/#structure
*/
void encode_field_raw(uint32_t field_id, uint32_t type) {
uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK);
this->encode_varint_raw(val);
}
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); }
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
if (len == 0 && !force)
return;
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
this->encode_varint_raw(len);
// Using resize + memcpy instead of insert provides significant performance improvement:
// ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings
// as it avoids iterator checks and potential element moves that insert performs
size_t old_size = this->buffer_->size();
this->buffer_->resize(old_size + len);
std::memcpy(this->buffer_->data() + old_size, string, len);
// Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks
// and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings.
this->debug_check_bounds_(len);
std::memcpy(this->pos_, string, len);
this->pos_ += len;
}
void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
this->encode_string(field_id, value.data(), value.size(), force);
@@ -288,17 +273,26 @@ class ProtoWriteBuffer {
if (!value && !force)
return;
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
this->buffer_->push_back(value ? 0x01 : 0x00);
this->debug_check_bounds_(1);
*this->pos_++ = value ? 0x01 : 0x00;
}
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
// noinline: 51 call sites; inlining causes net code growth vs a single out-of-line copy
__attribute__((noinline)) void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force)
return;
this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32
this->write((value >> 0) & 0xFF);
this->write((value >> 8) & 0xFF);
this->write((value >> 16) & 0xFF);
this->write((value >> 24) & 0xFF);
this->debug_check_bounds_(4);
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
// Protobuf fixed32 is little-endian, so direct copy works
std::memcpy(this->pos_, &value, 4);
this->pos_ += 4;
#else
*this->pos_++ = (value >> 0) & 0xFF;
*this->pos_++ = (value >> 8) & 0xFF;
*this->pos_++ = (value >> 16) & 0xFF;
*this->pos_++ = (value >> 24) & 0xFF;
#endif
}
// NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
// not supported to reduce overhead on embedded systems. All ESPHome devices are
@@ -334,11 +328,33 @@ class ProtoWriteBuffer {
}
/// Encode a packed repeated sint32 field (zero-copy from vector)
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
void encode_message(uint32_t field_id, const ProtoMessage &value);
std::vector<uint8_t> *get_buffer() const { return buffer_; }
/// Single-pass encode for repeated submessage elements.
/// Thin template wrapper; all buffer work is in the non-template core.
template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
/// Encode an optional singular submessage field — skips if empty.
/// Thin template wrapper; all buffer work is in the non-template core.
template<typename T> void encode_optional_sub_message(uint32_t field_id, const T &value);
// Non-template core for encode_sub_message — backpatch approach.
void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &));
// Non-template core for encode_optional_sub_message.
void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &));
APIBuffer *get_buffer() const { return buffer_; }
protected:
std::vector<uint8_t> *buffer_;
// Slow path for encode_varint_raw values >= 128, outlined to keep fast path small
void encode_varint_raw_slow_(uint32_t value) __attribute__((noinline));
#ifdef ESPHOME_DEBUG_API
void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION());
void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual);
#else
void debug_check_bounds_([[maybe_unused]] size_t bytes) {}
#endif
APIBuffer *buffer_;
uint8_t *pos_;
};
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -414,15 +430,21 @@ class DumpBuffer {
class ProtoMessage {
public:
virtual ~ProtoMessage() = default;
// Default implementation for messages with no fields
virtual void encode(ProtoWriteBuffer buffer) const {}
// Default implementation for messages with no fields
virtual void calculate_size(ProtoSize &size) const {}
// Non-virtual defaults for messages with no fields.
// Concrete message classes hide these with their own implementations.
// All call sites use templates to preserve the concrete type, so virtual
// dispatch is not needed. This eliminates per-message vtable entries for
// encode/calculate_size, saving ~1.3 KB of flash across all message types.
void encode(ProtoWriteBuffer &buffer) const {}
uint32_t calculate_size() const { return 0; }
#ifdef HAS_PROTO_MESSAGE_DUMP
virtual const char *dump_to(DumpBuffer &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
#endif
protected:
// Non-virtual destructor is protected to prevent polymorphic deletion.
~ProtoMessage() = default;
};
// Base class for messages that support decoding
@@ -442,63 +464,44 @@ class ProtoDecodableMessage : public ProtoMessage {
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
protected:
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
~ProtoDecodableMessage() = default;
virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value) { return false; }
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
// NOTE: decode_64bit removed - wire type 1 not supported
};
class ProtoSize {
private:
uint32_t total_size_ = 0;
public:
/**
* @brief ProtoSize class for Protocol Buffer serialization size calculation
*
* This class provides methods to calculate the exact byte counts needed
* for encoding various Protocol Buffer field types. The class now uses an
* object-based approach to reduce parameter passing overhead while keeping
* varint calculation methods static for external use.
*
* Implements Protocol Buffer encoding size calculation according to:
* https://protobuf.dev/programming-guides/encoding/
*
* Key features:
* - Object-based approach reduces flash usage by eliminating parameter passing
* - Early-return optimization for zero/default values
* - Static varint methods for external callers
* - Specialized handling for different field types according to protobuf spec
*/
ProtoSize() = default;
uint32_t get_size() const { return total_size_; }
/**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
*
* @param value The uint32_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static constexpr uint32_t varint(uint32_t value) {
// Optimized varint size calculation using leading zeros
// Each 7 bits requires one byte in the varint encoding
if (value < 128)
return 1; // 7 bits, common case for small values
// For larger values, count bytes needed based on the position of the highest bit set
if (value < 16384) {
return 2; // 14 bits
} else if (value < 2097152) {
return 3; // 21 bits
} else if (value < 268435456) {
return 4; // 28 bits
} else {
return 5; // 32 bits (maximum for uint32_t)
}
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) {
if (value < 128) [[likely]]
return 1; // Fast path: 7 bits, most common case
if (__builtin_is_constant_evaluated())
return varint_wide(value);
return varint_slow(value);
}
private:
// Slow path for varint >= 128, outlined to keep fast path small
static uint32_t varint_slow(uint32_t value) __attribute__((noinline));
// Shared cascade for values >= 128 (used by both constexpr and noinline paths)
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) {
if (value < 16384)
return 2;
if (value < 2097152)
return 3;
if (value < 268435456)
return 4;
return 5;
}
public:
/**
* @brief Calculates the size in bytes needed to encode a uint64_t value as a varint
*
@@ -571,312 +574,77 @@ class ProtoSize {
return varint(tag);
}
/**
* @brief Common parameters for all add_*_field methods
*
* All add_*_field methods follow these common patterns:
* * @param field_id_size Pre-calculated size of the field ID in bytes
* @param value The value to calculate size for (type varies)
* @param force Whether to calculate size even if the value is default/zero/empty
*
* Each method follows this implementation pattern:
* 1. Skip calculation if value is default (0, false, empty) and not forced
* 2. Calculate the size based on the field's encoding rules
* 3. Add the field_id_size + calculated value size to total_size
*/
/**
* @brief Calculates and adds the size of an int32 field to the total message size
*/
inline void add_int32(uint32_t field_id_size, int32_t value) {
if (value != 0) {
add_int32_force(field_id_size, value);
}
// Static methods that RETURN size contribution (no ProtoSize object needed).
// Used by generated calculate_size() methods to accumulate into a plain uint32_t register.
static constexpr uint32_t calc_int32(uint32_t field_id_size, int32_t value) {
return value ? field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value))) : 0;
}
/**
* @brief Calculates and adds the size of an int32 field to the total message size (force version)
*/
inline void add_int32_force(uint32_t field_id_size, int32_t value) {
// Always calculate size when forced
// Negative values are encoded as 10-byte varints in protobuf
total_size_ += field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value)));
static constexpr uint32_t calc_int32_force(uint32_t field_id_size, int32_t value) {
return field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value)));
}
/**
* @brief Calculates and adds the size of a uint32 field to the total message size
*/
inline void add_uint32(uint32_t field_id_size, uint32_t value) {
if (value != 0) {
add_uint32_force(field_id_size, value);
}
static constexpr uint32_t calc_uint32(uint32_t field_id_size, uint32_t value) {
return value ? field_id_size + varint(value) : 0;
}
/**
* @brief Calculates and adds the size of a uint32 field to the total message size (force version)
*/
inline void add_uint32_force(uint32_t field_id_size, uint32_t value) {
// Always calculate size when force is true
total_size_ += field_id_size + varint(value);
static constexpr uint32_t calc_uint32_force(uint32_t field_id_size, uint32_t value) {
return field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a boolean field to the total message size
*/
inline void add_bool(uint32_t field_id_size, bool value) {
if (value) {
// Boolean fields always use 1 byte when true
total_size_ += field_id_size + 1;
}
static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; }
static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; }
static constexpr uint32_t calc_float(uint32_t field_id_size, float value) {
return value != 0.0f ? field_id_size + 4 : 0;
}
/**
* @brief Calculates and adds the size of a boolean field to the total message size (force version)
*/
inline void add_bool_force(uint32_t field_id_size, bool value) {
// Always calculate size when force is true
// Boolean fields always use 1 byte
total_size_ += field_id_size + 1;
static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) {
return value ? field_id_size + 4 : 0;
}
/**
* @brief Calculates and adds the size of a float field to the total message size
*/
inline void add_float(uint32_t field_id_size, float value) {
if (value != 0.0f) {
total_size_ += field_id_size + 4;
}
static constexpr uint32_t calc_sfixed32(uint32_t field_id_size, int32_t value) {
return value ? field_id_size + 4 : 0;
}
// NOTE: add_double_field removed - wire type 1 (64-bit: double) not supported
// to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a fixed32 field to the total message size
*/
inline void add_fixed32(uint32_t field_id_size, uint32_t value) {
if (value != 0) {
total_size_ += field_id_size + 4;
}
static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) {
return value ? field_id_size + varint(encode_zigzag32(value)) : 0;
}
// NOTE: add_fixed64_field removed - wire type 1 (64-bit: fixed64) not supported
// to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a sfixed32 field to the total message size
*/
inline void add_sfixed32(uint32_t field_id_size, int32_t value) {
if (value != 0) {
total_size_ += field_id_size + 4;
}
static constexpr uint32_t calc_sint32_force(uint32_t field_id_size, int32_t value) {
return field_id_size + varint(encode_zigzag32(value));
}
// NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported
// to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a sint32 field to the total message size
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
inline void add_sint32(uint32_t field_id_size, int32_t value) {
if (value != 0) {
add_sint32_force(field_id_size, value);
}
static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) {
return value ? field_id_size + varint(value) : 0;
}
/**
* @brief Calculates and adds the size of a sint32 field to the total message size (force version)
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
inline void add_sint32_force(uint32_t field_id_size, int32_t value) {
// Always calculate size when force is true
// ZigZag encoding for sint32
total_size_ += field_id_size + varint(encode_zigzag32(value));
static constexpr uint32_t calc_int64_force(uint32_t field_id_size, int64_t value) {
return field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of an int64 field to the total message size
*/
inline void add_int64(uint32_t field_id_size, int64_t value) {
if (value != 0) {
add_int64_force(field_id_size, value);
}
static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) {
return value ? field_id_size + varint(value) : 0;
}
/**
* @brief Calculates and adds the size of an int64 field to the total message size (force version)
*/
inline void add_int64_force(uint32_t field_id_size, int64_t value) {
// Always calculate size when force is true
total_size_ += field_id_size + varint(value);
static constexpr uint32_t calc_uint64_force(uint32_t field_id_size, uint64_t value) {
return field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a uint64 field to the total message size
*/
inline void add_uint64(uint32_t field_id_size, uint64_t value) {
if (value != 0) {
add_uint64_force(field_id_size, value);
}
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
}
/**
* @brief Calculates and adds the size of a uint64 field to the total message size (force version)
*/
inline void add_uint64_force(uint32_t field_id_size, uint64_t value) {
// Always calculate size when force is true
total_size_ += field_id_size + varint(value);
static constexpr uint32_t calc_length_force(uint32_t field_id_size, size_t len) {
return field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
}
// NOTE: sint64 support functions (add_sint64_field, add_sint64_field_force) removed
// sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size
*/
inline void add_length(uint32_t field_id_size, size_t len) {
if (len != 0) {
add_length_force(field_id_size, len);
}
static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) {
return value ? field_id_size + varint(encode_zigzag64(value)) : 0;
}
/**
* @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size (repeated
* field version)
*/
inline void add_length_force(uint32_t field_id_size, size_t len) {
// Always calculate size when force is true
// Field ID + length varint + data bytes
total_size_ += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
static constexpr uint32_t calc_sint64_force(uint32_t field_id_size, int64_t value) {
return field_id_size + varint(encode_zigzag64(value));
}
/**
* @brief Adds a pre-calculated size directly to the total
*
* This is used when we can calculate the total size by multiplying the number
* of elements by the bytes per element (for repeated fixed-size types like float, fixed32, etc.)
*
* @param size The pre-calculated total size to add
*/
inline void add_precalculated_size(uint32_t size) { total_size_ += size; }
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This helper function directly updates the total_size reference if the nested size
* is greater than zero.
*
* @param nested_size The pre-calculated size of the nested message
*/
inline void add_message_field(uint32_t field_id_size, uint32_t nested_size) {
if (nested_size != 0) {
add_message_field_force(field_id_size, nested_size);
}
static constexpr uint32_t calc_fixed64(uint32_t field_id_size, uint64_t value) {
return value ? field_id_size + 8 : 0;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size (force version)
*
* @param nested_size The pre-calculated size of the nested message
*/
inline void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) {
// Always calculate size when force is true
// Field ID + length varint + nested message content
total_size_ += field_id_size + varint(nested_size) + nested_size;
static constexpr uint32_t calc_sfixed64(uint32_t field_id_size, int64_t value) {
return value ? field_id_size + 8 : 0;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This version takes a ProtoMessage object, calculates its size internally,
* and updates the total_size reference. This eliminates the need for a temporary variable
* at the call site.
*
* @param message The nested message object
*/
inline void add_message_object(uint32_t field_id_size, const ProtoMessage &message) {
// Calculate nested message size by creating a temporary ProtoSize
ProtoSize nested_calc;
message.calculate_size(nested_calc);
uint32_t nested_size = nested_calc.get_size();
// Use the base implementation with the calculated nested_size
add_message_field(field_id_size, nested_size);
static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) {
return nested_size ? field_id_size + varint(nested_size) + nested_size : 0;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size (force version)
*
* @param message The nested message object
*/
inline void add_message_object_force(uint32_t field_id_size, const ProtoMessage &message) {
// Calculate nested message size by creating a temporary ProtoSize
ProtoSize nested_calc;
message.calculate_size(nested_calc);
uint32_t nested_size = nested_calc.get_size();
// Use the base implementation with the calculated nested_size
add_message_field_force(field_id_size, nested_size);
}
/**
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size
*
* This helper processes a vector of message objects, calculating the size for each message
* and adding it to the total size.
*
* @tparam MessageType The type of the nested messages in the vector
* @param messages Vector of message objects
*/
template<typename MessageType>
inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) {
// Skip if the vector is empty
if (!messages.empty()) {
// Use the force version for all messages in the repeated field
for (const auto &message : messages) {
add_message_object_force(field_id_size, message);
}
}
}
/**
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector
* version)
*
* @tparam MessageType The type of the nested messages in the FixedVector
* @param messages FixedVector of message objects
*/
template<typename MessageType>
inline void add_repeated_message(uint32_t field_id_size, const FixedVector<MessageType> &messages) {
// Skip if the fixed vector is empty
if (!messages.empty()) {
// Use the force version for all messages in the repeated field
for (const auto &message : messages) {
add_message_object_force(field_id_size, message);
}
}
}
/**
* @brief Calculate size of a packed repeated sint32 field
*/
inline void add_packed_sint32(uint32_t field_id_size, const std::vector<int32_t> &values) {
if (values.empty())
return;
size_t packed_size = 0;
for (int value : values) {
packed_size += varint(encode_zigzag32(value));
}
// field_id + length varint + packed data
total_size_ += field_id_size + varint(static_cast<uint32_t>(packed_size)) + static_cast<uint32_t>(packed_size);
static constexpr uint32_t calc_message_force(uint32_t field_id_size, uint32_t nested_size) {
return field_id_size + varint(nested_size) + nested_size;
}
};
// Implementation of methods that depend on ProtoSize being fully defined
// Implementation of encode_packed_sint32 - must be after ProtoSize is defined
inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) {
if (values.empty())
@@ -896,32 +664,19 @@ inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std:
}
}
// Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
// Encode thunk — converts void* back to concrete type for direct encode() call
template<typename T> void proto_encode_msg(const void *msg, ProtoWriteBuffer &buf) {
static_cast<const T *>(msg)->encode(buf);
}
// Calculate the message size first
ProtoSize msg_size;
value.calculate_size(msg_size);
uint32_t msg_length_bytes = msg_size.get_size();
// Thin template wrapper; delegates to non-template core in proto.cpp.
template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) {
this->encode_sub_message(field_id, &value, &proto_encode_msg<T>);
}
// Calculate how many bytes the length varint needs
uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes);
// Reserve exact space for the length varint
size_t begin = this->buffer_->size();
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
// Write the length varint directly
encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin);
// Now encode the message content - it will append to the buffer
value.encode(*this);
#ifdef ESPHOME_DEBUG_API
// Verify that the encoded size matches what we calculated
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
#endif
// Thin template wrapper; delegates to non-template core.
template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) {
this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>);
}
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
@@ -940,14 +695,6 @@ class ProtoService {
virtual void on_no_setup_connection() = 0;
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0;
/**
* Send a protobuf message by calculating its size, allocating a buffer, encoding, and sending.
* This is the implementation method - callers should use send_message() which adds logging.
* @param msg The protobuf message to send.
* @param message_type The message type identifier.
* @return True if the message was sent successfully, false otherwise.
*/
virtual bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) = 0;
// Authentication helper methods
inline bool check_connection_setup_() {

View File

@@ -16,7 +16,7 @@ class APIConnection;
return this->client_->send_##entity_type##_state(entity); \
}
class InitialStateIterator : public ComponentIterator {
class InitialStateIterator final : public ComponentIterator {
public:
InitialStateIterator(APIConnection *client);
#ifdef USE_BINARY_SENSOR

View File

@@ -1,5 +1,6 @@
#include "user_services.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
@@ -11,6 +12,8 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
}
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Zero-copy StringRef version for YAML-generated services (string_ is null-terminated after decode)
template<> StringRef get_execute_arg_value<StringRef>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
@@ -61,6 +64,8 @@ template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SER
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Zero-copy StringRef version for YAML-generated services
template<> enums::ServiceArgType to_service_arg_type<StringRef>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Legacy std::vector versions for external components using custom_api_device.h
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }

View File

@@ -230,7 +230,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; }
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void set_data(std::function<void(Ts..., JsonObject)> func) {
void set_data(std::function<void(Ts..., JsonObject)> &&func) {
this->json_builder_ = std::move(func);
this->has_data_ = true;
}
@@ -264,9 +264,9 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
// Build and send JSON response
json::JsonBuilder builder;
this->json_builder_(x..., builder.root());
std::string json_str = builder.serialize();
auto json_buf = builder.serialize();
this->parent_->send_action_response(call_id, success, StringRef(error_message),
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
return;
}
#endif

View File

@@ -307,9 +307,9 @@ void AS3935Component::tune_antenna() {
uint8_t tune_val = this->read_capacitance();
ESP_LOGI(TAG,
"Starting antenna tuning\n"
"Division Ratio is set to: %d\n"
"Internal Capacitor is set to: %d\n"
"Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio",
" Division Ratio is set to: %d\n"
" Internal Capacitor is set to: %d\n"
" Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio",
div_ratio, tune_val);
this->display_oscillator(true, ANTFREQ);
}

View File

@@ -9,6 +9,7 @@ from esphome.const import (
CONF_ID,
CONF_POWER_MODE,
CONF_RANGE,
CONF_WATCHDOG,
)
CODEOWNERS = ["@ammmze"]
@@ -57,7 +58,6 @@ FAST_FILTER = {
CONF_RAW_ANGLE = "raw_angle"
CONF_RAW_POSITION = "raw_position"
CONF_WATCHDOG = "watchdog"
CONF_SLOW_FILTER = "slow_filter"
CONF_FAST_FILTER = "fast_filter"
CONF_START_POSITION = "start_position"

View File

@@ -23,7 +23,6 @@ AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingCompone
CONF_RAW_ANGLE = "raw_angle"
CONF_RAW_POSITION = "raw_position"
CONF_WATCHDOG = "watchdog"
CONF_SLOW_FILTER = "slow_filter"
CONF_FAST_FILTER = "fast_filter"
CONF_PWM_FREQUENCY = "pwm_frequency"

View File

@@ -89,6 +89,7 @@ AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Actio
cv.Required(CONF_ID): cv.use_id(AT581XComponent),
}
),
synchronous=True,
)
async def at581x_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -160,6 +161,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
"at581x.settings",
AT581XSettingsAction,
RADAR_SETTINGS_SCHEMA,
synchronous=True,
)
async def at581x_settings_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)

View File

@@ -77,14 +77,14 @@ void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); }
bool AT581XComponent::i2c_write_config() {
ESP_LOGCONFIG(TAG,
"Writing new config for AT581X\n"
"Frequency: %dMHz\n"
"Sensing distance: %d\n"
"Power: %dµA\n"
"Gain: %d\n"
"Trigger base time: %dms\n"
"Trigger keep time: %dms\n"
"Protect time: %dms\n"
"Self check time: %dms",
" Frequency: %dMHz\n"
" Sensing distance: %d\n"
" Power: %dµA\n"
" Gain: %d\n"
" Trigger base time: %dms\n"
" Trigger keep time: %dms\n"
" Protect time: %dms\n"
" Self check time: %dms",
this->freq_, this->delta_, this->power_, this->gain_, this->trigger_base_time_ms_,
this->trigger_keep_time_ms_, this->protect_time_ms_, this->self_check_time_ms_);
@@ -135,6 +135,11 @@ bool AT581XComponent::i2c_write_config() {
}
// Set gain
if (this->gain_ < 0 || static_cast<size_t>(this->gain_) >= ARRAY_SIZE(GAIN5C_TABLE) ||
static_cast<size_t>(this->gain_ >> 1) >= ARRAY_SIZE(GAIN63_TABLE)) {
ESP_LOGE(TAG, "AT581X gain index out of range: %d", this->gain_);
return false;
}
if (!this->i2c_write_reg(GAIN_ADDR_TABLE[0], GAIN5C_TABLE[this->gain_]) ||
!this->i2c_write_reg(GAIN_ADDR_TABLE[1], GAIN63_TABLE[this->gain_ >> 1])) {
ESP_LOGE(TAG, "Failed to write AT581X gain registers");

View File

@@ -61,6 +61,10 @@ optional<ParseResult> ATCMiThermometer::parse_header_(const esp32_ble_tracker::S
}
auto raw = service_data.data;
if (raw.size() < 13) {
ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size());
return {};
}
static uint8_t last_frame_count = 0;
if (last_frame_count == raw[12]) {

View File

@@ -197,7 +197,7 @@ float ATM90E26Component::get_reactive_power_() {
float ATM90E26Component::get_power_factor_() {
const uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed
if (val & 0x8000) {
return -(val & 0x7FF) / 1000.0f;
return -(val & 0x7FFF) / 1000.0f;
} else {
return val / 1000.0f;
}

View File

@@ -619,7 +619,7 @@ void ATM90E32Component::run_gain_calibrations() {
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs,
phase_labels[phase]);
} else {
uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain);
uint32_t new_voltage_gain = static_cast<uint32_t>((ref_voltage / measured_voltage) * current_voltage_gain);
if (new_voltage_gain == 0) {
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs,
phase_labels[phase]);
@@ -644,7 +644,7 @@ void ATM90E32Component::run_gain_calibrations() {
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs,
phase_labels[phase]);
} else {
uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain);
uint32_t new_current_gain = static_cast<uint32_t>((ref_current / measured_current) * current_current_gain);
if (new_current_gain == 0) {
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs,
phase_labels[phase]);

View File

@@ -1,10 +1,14 @@
from dataclasses import dataclass
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
from esphome.core import CORE
import esphome.final_validate as fv
CODEOWNERS = ["@kahrendt"]
DOMAIN = "audio"
audio_ns = cg.esphome_ns.namespace("audio")
AudioFile = audio_ns.struct("AudioFile")
@@ -14,9 +18,38 @@ AUDIO_FILE_TYPE_ENUM = {
"WAV": AudioFileType.WAV,
"MP3": AudioFileType.MP3,
"FLAC": AudioFileType.FLAC,
"OPUS": AudioFileType.OPUS,
}
@dataclass
class AudioData:
flac_support: bool = False
mp3_support: bool = False
opus_support: bool = False
def _get_data() -> AudioData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = AudioData()
return CORE.data[DOMAIN]
def request_flac_support() -> None:
"""Request FLAC codec support for audio decoding."""
_get_data().flac_support = True
def request_mp3_support() -> None:
"""Request MP3 codec support for audio decoding."""
_get_data().mp3_support = True
def request_opus_support() -> None:
"""Request Opus codec support for audio decoding."""
_get_data().opus_support = True
CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample"
CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample"
CONF_MIN_CHANNELS = "min_channels"
@@ -173,3 +206,12 @@ async def to_code(config):
name="esphome/esp-audio-libs",
ref="2.0.3",
)
data = _get_data()
if data.flac_support:
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
if data.mp3_support:
cg.add_define("USE_AUDIO_MP3_SUPPORT")
if data.opus_support:
cg.add_define("USE_AUDIO_OPUS_SUPPORT")
add_idf_component(name="esphome/micro-opus", ref="0.3.5")

View File

@@ -1,5 +1,9 @@
#include "audio.h"
#include "esphome/core/helpers.h"
#include <cstring>
namespace esphome {
namespace audio {
@@ -46,6 +50,10 @@ const char *audio_file_type_to_string(AudioFileType file_type) {
#ifdef USE_AUDIO_MP3_SUPPORT
case AudioFileType::MP3:
return "MP3";
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
case AudioFileType::OPUS:
return "OPUS";
#endif
case AudioFileType::WAV:
return "WAV";
@@ -54,6 +62,58 @@ const char *audio_file_type_to_string(AudioFileType file_type) {
}
}
AudioFileType detect_audio_file_type(const char *content_type, const char *url) {
// Try Content-Type header first
if (content_type != nullptr && content_type[0] != '\0') {
#ifdef USE_AUDIO_MP3_SUPPORT
if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 ||
strcasecmp(content_type, "audio/mpeg") == 0) {
return AudioFileType::MP3;
}
#endif
if (strcasecmp(content_type, "audio/wav") == 0) {
return AudioFileType::WAV;
}
#ifdef USE_AUDIO_FLAC_SUPPORT
if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) {
return AudioFileType::FLAC;
}
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
// Match "audio/ogg" with a codecs parameter containing "opus"
// Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc.
// Plain "audio/ogg" without opus is not matched (almost always Ogg Vorbis)
if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) {
return AudioFileType::OPUS;
}
#endif
}
// Fallback to URL extension
if (url != nullptr && url[0] != '\0') {
if (str_endswith_ignore_case(url, ".wav")) {
return AudioFileType::WAV;
}
#ifdef USE_AUDIO_MP3_SUPPORT
if (str_endswith_ignore_case(url, ".mp3")) {
return AudioFileType::MP3;
}
#endif
#ifdef USE_AUDIO_FLAC_SUPPORT
if (str_endswith_ignore_case(url, ".flac")) {
return AudioFileType::FLAC;
}
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
if (str_endswith_ignore_case(url, ".opus")) {
return AudioFileType::OPUS;
}
#endif
}
return AudioFileType::NONE;
}
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
size_t samples_to_scale) {
// Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same.

View File

@@ -112,6 +112,9 @@ enum class AudioFileType : uint8_t {
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
MP3,
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
OPUS,
#endif
WAV,
};
@@ -127,6 +130,13 @@ struct AudioFile {
/// @return const char pointer to the readable file type
const char *audio_file_type_to_string(AudioFileType file_type);
/// @brief Detect audio file type from a Content-Type header value and/or URL extension.
/// Tries Content-Type first, then falls back to URL extension. Either parameter may be null.
/// @param content_type Content-Type header value (may be null or empty)
/// @param url URL to inspect for file extension (may be null or empty)
/// @return The detected AudioFileType, or NONE if unknown
AudioFileType detect_audio_file_type(const char *content_type, const char *url);
/// @brief Scales Q15 fixed point audio samples. Scales in place if audio_samples == output_buffer.
/// @param audio_samples PCM int16 audio samples
/// @param output_buffer Buffer to store the scaled samples

View File

@@ -3,17 +3,20 @@
#ifdef USE_ESP32
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace audio {
static const char *const TAG = "audio.decoder";
static const uint32_t DECODING_TIMEOUT_MS = 50; // The decode function will yield after this duration
static const uint32_t READ_WRITE_TIMEOUT_MS = 20; // Timeout for transferring audio data
static const uint32_t MAX_POTENTIALLY_FAILED_COUNT = 10;
AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) {
this->input_transfer_buffer_ = AudioSourceTransferBuffer::create(input_buffer_size);
AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size)
: input_buffer_size_(input_buffer_size) {
this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size);
}
@@ -26,11 +29,20 @@ AudioDecoder::~AudioDecoder() {
}
esp_err_t AudioDecoder::add_source(std::weak_ptr<RingBuffer> &input_ring_buffer) {
if (this->input_transfer_buffer_ != nullptr) {
this->input_transfer_buffer_->set_source(input_ring_buffer);
return ESP_OK;
auto source = AudioSourceTransferBuffer::create(this->input_buffer_size_);
if (source == nullptr) {
return ESP_ERR_NO_MEM;
}
return ESP_ERR_NO_MEM;
source->set_source(input_ring_buffer);
this->input_buffer_ = std::move(source);
return ESP_OK;
}
esp_err_t AudioDecoder::add_source(const uint8_t *data_pointer, size_t length) {
auto source = make_unique<ConstAudioSourceBuffer>();
source->set_data(data_pointer, length);
this->input_buffer_ = std::move(source);
return ESP_OK;
}
esp_err_t AudioDecoder::add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer) {
@@ -51,8 +63,16 @@ esp_err_t AudioDecoder::add_sink(speaker::Speaker *speaker) {
}
#endif
esp_err_t AudioDecoder::add_sink(AudioSinkCallback *callback) {
if (this->output_transfer_buffer_ != nullptr) {
this->output_transfer_buffer_->set_sink(callback);
return ESP_OK;
}
return ESP_ERR_NO_MEM;
}
esp_err_t AudioDecoder::start(AudioFileType audio_file_type) {
if ((this->input_transfer_buffer_ == nullptr) || (this->output_transfer_buffer_ == nullptr)) {
if (this->output_transfer_buffer_ == nullptr) {
return ESP_ERR_NO_MEM;
}
@@ -65,6 +85,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) {
#ifdef USE_AUDIO_FLAC_SUPPORT
case AudioFileType::FLAC:
this->flac_decoder_ = make_unique<esp_audio_libs::flac::FLACDecoder>();
// CRC check slows down decoding by 15-20% on an ESP32-S3. FLAC sources in ESPHome are either from an http source
// or built into the firmware, so the data integrity is already verified by the time it gets to the decoder,
// making the CRC check unnecessary.
this->flac_decoder_->set_crc_check_enabled(false);
this->free_buffer_required_ =
this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header
break;
@@ -79,6 +103,14 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) {
// Always reallocate the output transfer buffer to the smallest necessary size
this->output_transfer_buffer_->reallocate(this->free_buffer_required_);
break;
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
case AudioFileType::OPUS:
this->opus_decoder_ = make_unique<micro_opus::OggOpusDecoder>();
this->free_buffer_required_ =
this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header
this->decoder_buffers_internally_ = true;
break;
#endif
case AudioFileType::WAV:
this->wav_decoder_ = make_unique<esp_audio_libs::wav_decoder::WAVDecoder>();
@@ -101,6 +133,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) {
}
AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
if (this->input_buffer_ == nullptr) {
return AudioDecoderState::FAILED;
}
if (stop_gracefully) {
if (this->output_transfer_buffer_->available() == 0) {
if (this->end_of_file_) {
@@ -108,7 +144,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
return AudioDecoderState::FINISHED;
}
if (!this->input_transfer_buffer_->has_buffered_data()) {
if (!this->input_buffer_->has_buffered_data()) {
// If all the internal buffers are empty, the decoding is done
return AudioDecoderState::FINISHED;
}
@@ -158,10 +194,11 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
// Decode more audio
// Only shift data on the first loop iteration to avoid unnecessary, slow moves
size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS),
first_loop_iteration);
// If the decoder buffers internally, then never shift
size_t bytes_read = this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS),
first_loop_iteration && !this->decoder_buffers_internally_);
if (!first_loop_iteration && (this->input_transfer_buffer_->available() < bytes_processed)) {
if (!first_loop_iteration && (this->input_buffer_->available() < bytes_processed)) {
// Less data is available than what was processed in last iteration, so don't attempt to decode.
// This attempts to avoid the decoder from consistently trying to decode an incomplete frame. The transfer buffer
// will shift the remaining data to the start and copy more from the source the next time the decode function is
@@ -169,19 +206,21 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
break;
}
bytes_available_before_processing = this->input_transfer_buffer_->available();
bytes_available_before_processing = this->input_buffer_->available();
if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) {
// Failed to decode in last attempt and there is no new data
if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) {
// The input buffer is full. Since it previously failed on the exact same data, we can never recover
if ((this->input_buffer_->free() == 0) && first_loop_iteration) {
// The input buffer is full (or read-only, e.g. const flash source). Since it previously failed on the exact
// same data, we can never recover. For const sources this is correct: the entire file is already available, so
// a decode failure is genuine, not a transient out-of-data condition.
state = FileDecoderState::FAILED;
} else {
// Attempt to get more data next time
state = FileDecoderState::IDLE;
}
} else if (this->input_transfer_buffer_->available() == 0) {
} else if (this->input_buffer_->available() == 0) {
// No data to decode, attempt to get more data next time
state = FileDecoderState::IDLE;
} else {
@@ -195,6 +234,11 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
case AudioFileType::MP3:
state = this->decode_mp3_();
break;
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
case AudioFileType::OPUS:
state = this->decode_opus_();
break;
#endif
case AudioFileType::WAV:
state = this->decode_wav_();
@@ -207,7 +251,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
}
first_loop_iteration = false;
bytes_processed = bytes_available_before_processing - this->input_transfer_buffer_->available();
bytes_processed = bytes_available_before_processing - this->input_buffer_->available();
if (state == FileDecoderState::POTENTIALLY_FAILED) {
++this->potentially_failed_count_;
@@ -226,8 +270,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
FileDecoderState AudioDecoder::decode_flac_() {
if (!this->audio_stream_info_.has_value()) {
// Header hasn't been read
auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(),
this->input_transfer_buffer_->available());
auto result = this->flac_decoder_->read_header(this->input_buffer_->data(), this->input_buffer_->available());
if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
// Serrious error reading FLAC header, there is no recovery
@@ -235,7 +278,7 @@ FileDecoderState AudioDecoder::decode_flac_() {
}
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
this->input_buffer_->consume(bytes_consumed);
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
return FileDecoderState::MORE_TO_PROCESS;
@@ -256,8 +299,7 @@ FileDecoderState AudioDecoder::decode_flac_() {
}
uint32_t output_samples = 0;
auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(),
this->input_transfer_buffer_->available(),
auto result = this->flac_decoder_->decode_frame(this->input_buffer_->data(), this->input_buffer_->available(),
this->output_transfer_buffer_->get_buffer_end(), &output_samples);
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
@@ -266,7 +308,7 @@ FileDecoderState AudioDecoder::decode_flac_() {
}
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
this->input_buffer_->consume(bytes_consumed);
if (result > esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
// Corrupted frame, don't retry with current buffer content, wait for new sync
@@ -288,26 +330,25 @@ FileDecoderState AudioDecoder::decode_flac_() {
#ifdef USE_AUDIO_MP3_SUPPORT
FileDecoderState AudioDecoder::decode_mp3_() {
// Look for the next sync word
int buffer_length = (int) this->input_transfer_buffer_->available();
int32_t offset =
esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_transfer_buffer_->get_buffer_start(), buffer_length);
int buffer_length = (int) this->input_buffer_->available();
int32_t offset = esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_buffer_->data(), buffer_length);
if (offset < 0) {
// New data may have the sync word
this->input_transfer_buffer_->decrease_buffer_length(buffer_length);
this->input_buffer_->consume(buffer_length);
return FileDecoderState::POTENTIALLY_FAILED;
}
// Advance read pointer to match the offset for the syncword
this->input_transfer_buffer_->decrease_buffer_length(offset);
const uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start();
this->input_buffer_->consume(offset);
const uint8_t *buffer_start = this->input_buffer_->data();
buffer_length = (int) this->input_transfer_buffer_->available();
buffer_length = (int) this->input_buffer_->available();
int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length,
(int16_t *) this->output_transfer_buffer_->get_buffer_end(), 0);
size_t consumed = this->input_transfer_buffer_->available() - buffer_length;
this->input_transfer_buffer_->decrease_buffer_length(consumed);
size_t consumed = this->input_buffer_->available() - buffer_length;
this->input_buffer_->consume(consumed);
if (err) {
switch (err) {
@@ -339,15 +380,53 @@ FileDecoderState AudioDecoder::decode_mp3_() {
}
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
FileDecoderState AudioDecoder::decode_opus_() {
bool processed_header = this->opus_decoder_->is_initialized();
size_t bytes_consumed, samples_decoded;
micro_opus::OggOpusResult result = this->opus_decoder_->decode(
this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(),
this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded);
if (result == micro_opus::OGG_OPUS_OK) {
if (!processed_header && this->opus_decoder_->is_initialized()) {
// Header processed and stream info is available
this->audio_stream_info_ =
audio::AudioStreamInfo(this->opus_decoder_->get_bit_depth(), this->opus_decoder_->get_channels(),
this->opus_decoder_->get_sample_rate());
}
if (samples_decoded > 0 && this->audio_stream_info_.has_value()) {
// Some audio was processed
this->output_transfer_buffer_->increase_buffer_length(
this->audio_stream_info_.value().frames_to_bytes(samples_decoded));
}
this->input_buffer_->consume(bytes_consumed);
} else if (result == micro_opus::OGG_OPUS_OUTPUT_BUFFER_TOO_SMALL) {
// Reallocate to decode the packet on the next call
this->free_buffer_required_ = this->opus_decoder_->get_required_output_buffer_size();
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
// Couldn't reallocate output buffer
return FileDecoderState::FAILED;
}
} else {
ESP_LOGE(TAG, "Opus decoder failed: %" PRId8, result);
return FileDecoderState::POTENTIALLY_FAILED;
}
return FileDecoderState::MORE_TO_PROCESS;
}
#endif
FileDecoderState AudioDecoder::decode_wav_() {
if (!this->audio_stream_info_.has_value()) {
// Header hasn't been processed
esp_audio_libs::wav_decoder::WAVDecoderResult result = this->wav_decoder_->decode_header(
this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available());
esp_audio_libs::wav_decoder::WAVDecoderResult result =
this->wav_decoder_->decode_header(this->input_buffer_->data(), this->input_buffer_->available());
if (result == esp_audio_libs::wav_decoder::WAV_DECODER_SUCCESS_IN_DATA) {
this->input_transfer_buffer_->decrease_buffer_length(this->wav_decoder_->bytes_processed());
this->input_buffer_->consume(this->wav_decoder_->bytes_processed());
this->audio_stream_info_ = audio::AudioStreamInfo(
this->wav_decoder_->bits_per_sample(), this->wav_decoder_->num_channels(), this->wav_decoder_->sample_rate());
@@ -363,7 +442,7 @@ FileDecoderState AudioDecoder::decode_wav_() {
}
} else {
if (!this->wav_has_known_end_ || (this->wav_bytes_left_ > 0)) {
size_t bytes_to_copy = this->input_transfer_buffer_->available();
size_t bytes_to_copy = this->input_buffer_->available();
if (this->wav_has_known_end_) {
bytes_to_copy = std::min(bytes_to_copy, this->wav_bytes_left_);
@@ -372,9 +451,8 @@ FileDecoderState AudioDecoder::decode_wav_() {
bytes_to_copy = std::min(bytes_to_copy, this->output_transfer_buffer_->free());
if (bytes_to_copy > 0) {
std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_transfer_buffer_->get_buffer_start(),
bytes_to_copy);
this->input_transfer_buffer_->decrease_buffer_length(bytes_to_copy);
std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_buffer_->data(), bytes_to_copy);
this->input_buffer_->consume(bytes_to_copy);
this->output_transfer_buffer_->increase_buffer_length(bytes_to_copy);
if (this->wav_has_known_end_) {
this->wav_bytes_left_ -= bytes_to_copy;

View File

@@ -24,6 +24,11 @@
#endif
#include <wav_decoder.h>
// micro-opus
#ifdef USE_AUDIO_OPUS_SUPPORT
#include <micro_opus/ogg_opus_decoder.h>
#endif
namespace esphome {
namespace audio {
@@ -45,17 +50,17 @@ enum class FileDecoderState : uint8_t {
class AudioDecoder {
/*
* @brief Class that facilitates decoding an audio file.
* The audio file is read from a ring buffer source, decoded, and sent to an audio sink (ring buffer or speaker
* component).
* Supports wav, flac, and mp3 formats.
* The audio file is read from a source (ring buffer or const data pointer), decoded, and sent to an audio sink
* (ring buffer, speaker component, or callback).
* Supports wav, flac, mp3, and ogg opus formats.
*/
public:
/// @brief Allocates the input and output transfer buffers
/// @brief Allocates the output transfer buffer and stores the input buffer size for later use by add_source()
/// @param input_buffer_size Size of the input transfer buffer in bytes.
/// @param output_buffer_size Size of the output transfer buffer in bytes.
AudioDecoder(size_t input_buffer_size, size_t output_buffer_size);
/// @brief Deallocates the MP3 decoder (the flac and wav decoders are deallocated automatically)
/// @brief Deallocates the MP3 decoder (the flac, opus, and wav decoders are deallocated automatically)
~AudioDecoder();
/// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr.
@@ -75,6 +80,17 @@ class AudioDecoder {
esp_err_t add_sink(speaker::Speaker *speaker);
#endif
/// @brief Adds a const data pointer as the source for raw file data. Does not allocate a transfer buffer.
/// @param data_pointer Pointer to the const audio data (e.g., stored in flash memory)
/// @param length Size of the data in bytes
/// @return ESP_OK
esp_err_t add_source(const uint8_t *data_pointer, size_t length);
/// @brief Adds a callback as the sink for decoded audio.
/// @param callback Pointer to the AudioSinkCallback implementation
/// @return ESP_OK if successful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
esp_err_t add_sink(AudioSinkCallback *callback);
/// @brief Sets up decoding the file
/// @param audio_file_type AudioFileType of the file
/// @return ESP_OK if successful, ESP_ERR_NO_MEM if the transfer buffers fail to allocate, or ESP_ERR_NOT_SUPPORTED if
@@ -108,26 +124,33 @@ class AudioDecoder {
#ifdef USE_AUDIO_MP3_SUPPORT
FileDecoderState decode_mp3_();
esp_audio_libs::helix_decoder::HMP3Decoder mp3_decoder_;
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
FileDecoderState decode_opus_();
std::unique_ptr<micro_opus::OggOpusDecoder> opus_decoder_;
#endif
FileDecoderState decode_wav_();
std::unique_ptr<AudioSourceTransferBuffer> input_transfer_buffer_;
std::unique_ptr<AudioReadableBuffer> input_buffer_;
std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_;
AudioFileType audio_file_type_{AudioFileType::NONE};
optional<AudioStreamInfo> audio_stream_info_{};
size_t input_buffer_size_{0};
size_t free_buffer_required_{0};
size_t wav_bytes_left_{0};
uint32_t potentially_failed_count_{0};
uint32_t accumulated_frames_written_{0};
uint32_t playback_ms_{0};
bool end_of_file_{false};
bool wav_has_known_end_{false};
bool pause_output_{false};
bool decoder_buffers_internally_{false};
uint32_t accumulated_frames_written_{0};
uint32_t playback_ms_{0};
bool pause_output_{false};
};
} // namespace audio
} // namespace esphome

View File

@@ -185,21 +185,8 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
return err;
}
if (str_endswith_ignore_case(url, ".wav")) {
file_type = AudioFileType::WAV;
}
#ifdef USE_AUDIO_MP3_SUPPORT
else if (str_endswith_ignore_case(url, ".mp3")) {
file_type = AudioFileType::MP3;
}
#endif
#ifdef USE_AUDIO_FLAC_SUPPORT
else if (str_endswith_ignore_case(url, ".flac")) {
file_type = AudioFileType::FLAC;
}
#endif
else {
file_type = AudioFileType::NONE;
file_type = detect_audio_file_type(nullptr, url);
if (file_type == AudioFileType::NONE) {
this->cleanup_connection_();
return ESP_ERR_NOT_SUPPORTED;
}
@@ -227,24 +214,6 @@ AudioReaderState AudioReader::read() {
return AudioReaderState::FAILED;
}
AudioFileType AudioReader::get_audio_type(const char *content_type) {
#ifdef USE_AUDIO_MP3_SUPPORT
if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 ||
strcasecmp(content_type, "audio/mpeg") == 0) {
return AudioFileType::MP3;
}
#endif
if (strcasecmp(content_type, "audio/wav") == 0) {
return AudioFileType::WAV;
}
#ifdef USE_AUDIO_FLAC_SUPPORT
if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) {
return AudioFileType::FLAC;
}
#endif
return AudioFileType::NONE;
}
esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
// Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224
AudioReader *this_reader = (AudioReader *) evt->user_data;
@@ -252,7 +221,7 @@ esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
switch (evt->event_id) {
case HTTP_EVENT_ON_HEADER:
if (strcasecmp(evt->header_key, "Content-Type") == 0) {
this_reader->audio_file_type_ = get_audio_type(evt->header_value);
this_reader->audio_file_type_ = detect_audio_file_type(evt->header_value, nullptr);
}
break;
default:

View File

@@ -58,11 +58,6 @@ class AudioReader {
/// @brief Monitors the http client events to attempt determining the file type from the Content-Type header
static esp_err_t http_event_handler(esp_http_client_event_t *evt);
/// @brief Determines the audio file type from the http header's Content-Type key
/// @param content_type string with the Content-Type key
/// @return AudioFileType of the url, if it can be determined. If not, return AudioFileType::NONE.
static AudioFileType get_audio_type(const char *content_type);
AudioReaderState file_read_();
AudioReaderState http_read_();

View File

@@ -2,6 +2,8 @@
#ifdef USE_ESP32
#include <cstring>
#include "esphome/core/helpers.h"
namespace esphome {
@@ -75,12 +77,32 @@ bool AudioTransferBuffer::has_buffered_data() const {
}
bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
if (this->buffer_length_ > 0) {
// Buffer currently has data, so reallocation is impossible
if (this->buffer_ == nullptr) {
return this->allocate_buffer_(new_buffer_size);
}
if (new_buffer_size < this->buffer_length_) {
// New size is too small to hold existing data
return false;
}
this->deallocate_buffer_();
return this->allocate_buffer_(new_buffer_size);
// Shift existing data to the start of the buffer so realloc preserves it
if ((this->buffer_length_ > 0) && (this->data_start_ != this->buffer_)) {
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
this->data_start_ = this->buffer_;
}
RAMAllocator<uint8_t> allocator;
uint8_t *new_buffer = allocator.reallocate(this->buffer_, new_buffer_size);
if (new_buffer == nullptr) {
// Reallocation failed, but the original buffer is still valid
return false;
}
this->buffer_ = new_buffer;
this->data_start_ = this->buffer_;
this->buffer_size_ = new_buffer_size;
return true;
}
bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
@@ -115,12 +137,12 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_
if (pre_shift) {
// Shift data in buffer to start
if (this->buffer_length_ > 0) {
memmove(this->buffer_, this->data_start_, this->buffer_length_);
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
}
this->data_start_ = this->buffer_;
}
size_t bytes_to_read = this->free();
size_t bytes_to_read = AudioTransferBuffer::free();
size_t bytes_read = 0;
if (bytes_to_read > 0) {
if (this->ring_buffer_.use_count() > 0) {
@@ -143,6 +165,8 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait,
if (this->ring_buffer_.use_count() > 0) {
bytes_written =
this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait);
} else if (this->sink_callback_ != nullptr) {
bytes_written = this->sink_callback_->audio_sink_write(this->data_start_, this->available(), ticks_to_wait);
}
this->decrease_buffer_length(bytes_written);
@@ -150,7 +174,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait,
if (post_shift) {
// Shift unwritten data to the start of the buffer
memmove(this->buffer_, this->data_start_, this->buffer_length_);
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
this->data_start_ = this->buffer_;
}
@@ -169,6 +193,21 @@ bool AudioSinkTransferBuffer::has_buffered_data() const {
return (this->available() > 0);
}
size_t AudioSourceTransferBuffer::free() const { return AudioTransferBuffer::free(); }
bool AudioSourceTransferBuffer::has_buffered_data() const { return AudioTransferBuffer::has_buffered_data(); }
void ConstAudioSourceBuffer::set_data(const uint8_t *data, size_t length) {
this->data_start_ = data;
this->length_ = length;
}
void ConstAudioSourceBuffer::consume(size_t bytes) {
bytes = std::min(bytes, this->length_);
this->length_ -= bytes;
this->data_start_ += bytes;
}
} // namespace audio
} // namespace esphome

View File

@@ -15,6 +15,12 @@
namespace esphome {
namespace audio {
/// @brief Abstract interface for writing decoded audio data to a sink.
class AudioSinkCallback {
public:
virtual size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) = 0;
};
class AudioTransferBuffer {
/*
* @brief Class that facilitates tranferring data between a buffer and an audio source or sink.
@@ -26,7 +32,7 @@ class AudioTransferBuffer {
/// @brief Destructor that deallocates the transfer buffer
~AudioTransferBuffer();
/// @brief Returns a pointer to the start of the transfer buffer where available() bytes of exisiting data can be read
/// @brief Returns a pointer to the start of the transfer buffer where available() bytes of existing data can be read
uint8_t *get_buffer_start() const { return this->data_start_; }
/// @brief Returns a pointer to the end of the transfer buffer where free() bytes of new data can be written
@@ -56,6 +62,9 @@ class AudioTransferBuffer {
/// @return True if there is data, false otherwise.
virtual bool has_buffered_data() const;
/// @brief Reallocates the transfer buffer, preserving any existing data.
/// @param new_buffer_size The new size in bytes. Must be at least as large as available().
/// @return True if successful, false otherwise. On failure, the original buffer remains valid.
bool reallocate(size_t new_buffer_size);
protected:
@@ -105,6 +114,10 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer {
void set_sink(speaker::Speaker *speaker) { this->speaker_ = speaker; }
#endif
/// @brief Adds a callback as the transfer buffer's sink.
/// @param callback Pointer to the AudioSinkCallback implementation
void set_sink(AudioSinkCallback *callback) { this->sink_callback_ = callback; }
void clear_buffered_data() override;
bool has_buffered_data() const override;
@@ -113,12 +126,44 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer {
#ifdef USE_SPEAKER
speaker::Speaker *speaker_{nullptr};
#endif
AudioSinkCallback *sink_callback_{nullptr};
};
class AudioSourceTransferBuffer : public AudioTransferBuffer {
/// @brief Abstract interface for reading audio data from a buffer.
/// Provides a common read interface for both mutable transfer buffers and read-only const buffers.
class AudioReadableBuffer {
public:
virtual ~AudioReadableBuffer() = default;
/// @brief Returns a pointer to the start of readable data
virtual const uint8_t *data() const = 0;
/// @brief Returns the number of bytes available to read
virtual size_t available() const = 0;
/// @brief Returns the number of free bytes available to write. Defaults to 0 for read-only buffers.
virtual size_t free() const { return 0; }
/// @brief Advances past consumed data
/// @param bytes Number of bytes consumed
virtual void consume(size_t bytes) = 0;
/// @brief Tests if there is any buffered data
virtual bool has_buffered_data() const = 0;
/// @brief Refills the buffer from its source. No-op by default for read-only buffers.
/// @param ticks_to_wait FreeRTOS ticks to block while waiting for data
/// @param pre_shift If true, shifts existing data to the start of the buffer before reading
/// @return Number of bytes read
virtual size_t fill(TickType_t ticks_to_wait, bool pre_shift) { return 0; }
size_t fill(TickType_t ticks_to_wait) { return this->fill(ticks_to_wait, true); }
};
class AudioSourceTransferBuffer : public AudioTransferBuffer, public AudioReadableBuffer {
/*
* @brief A class that implements a transfer buffer for audio sources.
* Supports reading audio data from a ring buffer into the transfer buffer for processing.
* Implements AudioReadableBuffer for use by consumers that only need read access.
*/
public:
/// @brief Creates a new source transfer buffer.
@@ -126,7 +171,7 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer {
/// @return unique_ptr if successfully allocated, nullptr otherwise
static std::unique_ptr<AudioSourceTransferBuffer> create(size_t buffer_size);
/// @brief Reads any available data from the sink into the transfer buffer.
/// @brief Reads any available data from the source into the transfer buffer.
/// @param ticks_to_wait FreeRTOS ticks to block while waiting for the source to have enough data
/// @param pre_shift If true, any unwritten data is moved to the start of the buffer before transferring from the
/// source. Defaults to true.
@@ -136,6 +181,36 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer {
/// @brief Adds a ring buffer as the transfer buffer's source.
/// @param ring_buffer weak_ptr to the allocated ring buffer
void set_source(const std::weak_ptr<RingBuffer> &ring_buffer) { this->ring_buffer_ = ring_buffer.lock(); };
// AudioReadableBuffer interface
const uint8_t *data() const override { return this->data_start_; }
size_t available() const override { return this->buffer_length_; }
size_t free() const override;
void consume(size_t bytes) override { this->decrease_buffer_length(bytes); }
bool has_buffered_data() const override;
size_t fill(TickType_t ticks_to_wait, bool pre_shift) override {
return this->transfer_data_from_source(ticks_to_wait, pre_shift);
}
};
/// @brief A lightweight read-only audio buffer for const data sources (e.g., flash memory).
/// Does not allocate memory or transfer data from external sources.
class ConstAudioSourceBuffer : public AudioReadableBuffer {
public:
/// @brief Sets the data pointer and length for the buffer
/// @param data Pointer to the const audio data
/// @param length Size of the data in bytes
void set_data(const uint8_t *data, size_t length);
// AudioReadableBuffer interface
const uint8_t *data() const override { return this->data_start_; }
size_t available() const override { return this->length_; }
void consume(size_t bytes) override;
bool has_buffered_data() const override { return this->length_ > 0; }
protected:
const uint8_t *data_start_{nullptr};
size_t length_{0};
};
} // namespace audio

View File

@@ -23,7 +23,10 @@ SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value(
@automation.register_action(
"audio_adc.set_mic_gain", SetMicGainAction, SET_MIC_GAIN_ACTION_SCHEMA
"audio_adc.set_mic_gain",
SetMicGainAction,
SET_MIC_GAIN_ACTION_SCHEMA,
synchronous=True,
)
async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])

View File

@@ -31,15 +31,22 @@ SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value(
)
@automation.register_action("audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA)
@automation.register_action("audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA)
@automation.register_action(
"audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True
)
async def audio_dac_mute_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"audio_dac.set_volume", SetVolumeAction, SET_VOLUME_ACTION_SCHEMA
"audio_dac.set_volume",
SetVolumeAction,
SET_VOLUME_ACTION_SCHEMA,
synchronous=True,
)
async def audio_dac_set_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])

View File

@@ -0,0 +1,255 @@
from dataclasses import dataclass, field
import hashlib
import logging
from pathlib import Path
import puremagic
from esphome import external_files
import esphome.codegen as cg
from esphome.components import audio
import esphome.config_validation as cv
from esphome.const import (
CONF_FILE,
CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, ID, HexInt
from esphome.cpp_generator import MockObj
from esphome.external_files import download_content
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@kahrendt"]
AUTO_LOAD = ["audio"]
DOMAIN = "audio_file"
audio_file_ns = cg.esphome_ns.namespace("audio_file")
TYPE_LOCAL = "local"
TYPE_WEB = "web"
@dataclass
class AudioFileData:
file_ids: dict[str, ID] = field(default_factory=dict)
file_cache: dict[str, tuple[bytes, MockObj]] = field(default_factory=dict)
def _get_data() -> AudioFileData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = AudioFileData()
return CORE.data[DOMAIN]
def get_audio_file_ids() -> dict[str, ID]:
"""Get all registered audio file IDs for cross-component access."""
return _get_data().file_ids
def _compute_local_file_path(value: ConfigType) -> Path:
url = value[CONF_URL]
h = hashlib.new("sha256")
h.update(url.encode())
key = h.hexdigest()[:8]
base_dir = external_files.compute_local_file_dir(DOMAIN)
_LOGGER.debug("_compute_local_file_path: base_dir=%s", base_dir / key)
return base_dir / key
def _download_web_file(value: ConfigType) -> ConfigType:
url = value[CONF_URL]
path = _compute_local_file_path(value)
download_content(url, path)
_LOGGER.debug("download_web_file: path=%s", path)
return value
def _file_schema(value: ConfigType | str) -> ConfigType:
if isinstance(value, str):
return _validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(value)
def _validate_file_shorthand(value: str) -> ConfigType:
value = cv.string_strict(value)
if value.startswith("http://") or value.startswith("https://"):
return _file_schema(
{
CONF_TYPE: TYPE_WEB,
CONF_URL: value,
}
)
return _file_schema(
{
CONF_TYPE: TYPE_LOCAL,
CONF_PATH: value,
}
)
def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
"""Read an audio file and determine its type. Used by this component and media_source platform."""
conf_file = file_config[CONF_FILE]
file_source = conf_file[CONF_TYPE]
if file_source == TYPE_LOCAL:
path = CORE.relative_config_path(conf_file[CONF_PATH])
elif file_source == TYPE_WEB:
path = _compute_local_file_path(conf_file)
else:
raise cv.Invalid("Unsupported file source")
with open(path, "rb") as f:
data = f.read()
try:
file_type: str = puremagic.from_string(data)
file_type = file_type.removeprefix(".")
except puremagic.PureError as e:
raise cv.Invalid(
f"Unable to determine audio file type of '{path}'. "
f"Try re-encoding the file into a supported format. Details: {e}"
)
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type == "wav":
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"]
elif file_type in ("mp3", "mpeg", "mpga"):
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"]
elif file_type == "flac":
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"]
elif (
file_type == "ogg"
and len(data) >= 36
and data.startswith(b"OggS")
and data[28:36] == b"OpusHead"
):
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["OPUS"]
return data, media_file_type
LOCAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_PATH): cv.file_,
}
)
WEB_SCHEMA = cv.All(
{
cv.Required(CONF_URL): cv.url,
},
_download_web_file,
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
TYPE_LOCAL: LOCAL_SCHEMA,
TYPE_WEB: WEB_SCHEMA,
},
)
MEDIA_FILE_TYPE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(audio.AudioFile),
cv.Required(CONF_FILE): _file_schema,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
)
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType]:
for file_config in config:
data, media_file_type = read_audio_file_and_type(file_config)
if len(data) > MAX_FILE_SIZE:
file_info = file_config.get(CONF_FILE, {})
source = (
file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source"
)
raise cv.Invalid(
f"Audio file {source!r} is too large ({len(data)} bytes, max {MAX_FILE_SIZE} bytes)"
)
if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]):
file_info = file_config.get(CONF_FILE, {})
source = (
file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source"
)
raise cv.Invalid(
f"Unsupported media file from {source!r} (detected type: {media_file_type})"
)
# Cache the file data so to_code() doesn't need to re-read it
_get_data().file_cache[str(file_config[CONF_ID])] = (data, media_file_type)
media_file_type_str = str(media_file_type)
if media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["FLAC"]):
audio.request_flac_support()
elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["MP3"]):
audio.request_mp3_support()
elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["OPUS"]):
audio.request_opus_support()
return config
CONFIG_SCHEMA = cv.All(
cv.only_on_esp32,
cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
_validate_supported_local_file,
)
async def to_code(config: list[ConfigType]) -> None:
cache = _get_data().file_cache
for file_config in config:
file_id = str(file_config[CONF_ID])
data, media_file_type = cache[file_id]
rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs)
media_files_struct = cg.StructInitializer(
audio.AudioFile,
(
"data",
prog_arr,
),
(
"length",
len(rhs),
),
(
"file_type",
media_file_type,
),
)
cg.new_Pvariable(
file_config[CONF_ID],
media_files_struct,
)
# Store file ID for cross-component access
_get_data().file_ids[file_id] = file_config[CONF_ID]
# Register all files in the shared C++ registry
cg.add_define("AUDIO_FILE_MAX_FILES", len(config))
for file_config in config:
file_id = str(file_config[CONF_ID])
file_var = await cg.get_variable(file_config[CONF_ID])
cg.add(audio_file_ns.add_named_audio_file(file_var, file_id))

View File

@@ -0,0 +1,28 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef AUDIO_FILE_MAX_FILES
#include "esphome/components/audio/audio.h"
#include "esphome/core/helpers.h"
namespace esphome::audio_file {
struct NamedAudioFile {
audio::AudioFile *file;
const char *file_id;
};
inline StaticVector<NamedAudioFile, AUDIO_FILE_MAX_FILES>
named_audio_files; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
inline void add_named_audio_file(audio::AudioFile *file, const char *file_id) {
named_audio_files.push_back({file, file_id});
}
inline const StaticVector<NamedAudioFile, AUDIO_FILE_MAX_FILES> &get_named_audio_files() { return named_audio_files; }
} // namespace esphome::audio_file
#endif // AUDIO_FILE_MAX_FILES

View File

@@ -0,0 +1,38 @@
import esphome.codegen as cg
from esphome.components import media_source, psram
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM
from esphome.types import ConfigType
CODEOWNERS = ["@kahrendt"]
AUTO_LOAD = ["audio"]
DEPENDENCIES = ["audio_file"]
audio_file_ns = cg.esphome_ns.namespace("audio_file")
AudioFileMediaSource = audio_file_ns.class_(
"AudioFileMediaSource", cg.Component, media_source.MediaSource
)
CONFIG_SCHEMA = cv.All(
media_source.media_source_schema(
AudioFileMediaSource,
)
.extend(
{
cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All(
cv.boolean, cv.requires_component(psram.DOMAIN)
),
}
)
.extend(cv.COMPONENT_SCHEMA),
cv.only_on_esp32,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await media_source.register_media_source(var, config)
if CONF_TASK_STACK_IN_PSRAM in config:
cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM]))

View File

@@ -0,0 +1,283 @@
#include "audio_file_media_source.h"
#ifdef USE_ESP32
#include "esphome/components/audio/audio_decoder.h"
#include <cstring>
namespace esphome::audio_file {
namespace { // anonymous namespace for internal linkage
struct AudioSinkAdapter : public audio::AudioSinkCallback {
media_source::MediaSource *source;
audio::AudioStreamInfo stream_info;
size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) override {
return this->source->write_output(data, length, pdTICKS_TO_MS(ticks_to_wait), this->stream_info);
}
};
} // namespace
#if defined(USE_AUDIO_OPUS_SUPPORT)
static constexpr uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024;
#else
static constexpr uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024;
#endif
static const char *const TAG = "audio_file_media_source";
enum EventGroupBits : uint32_t {
// Requests to start playback (set by play_uri, handled by loop)
REQUEST_START = (1 << 0),
// Commands from main loop to decode task
COMMAND_STOP = (1 << 1),
COMMAND_PAUSE = (1 << 2),
// Decode task lifecycle signals (one-shot, cleared by loop)
TASK_STARTING = (1 << 7),
TASK_RUNNING = (1 << 8),
TASK_STOPPING = (1 << 9),
TASK_STOPPED = (1 << 10),
TASK_ERROR = (1 << 11),
// Decode task state (level-triggered, set/cleared by decode task)
TASK_PAUSED = (1 << 12),
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
void AudioFileMediaSource::dump_config() {
ESP_LOGCONFIG(TAG, "Audio File Media Source:");
ESP_LOGCONFIG(TAG, " Task Stack in PSRAM: %s", this->task_stack_in_psram_ ? "Yes" : "No");
}
void AudioFileMediaSource::setup() {
this->disable_loop();
this->event_group_ = xEventGroupCreate();
if (this->event_group_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
this->mark_failed();
return;
}
}
void AudioFileMediaSource::loop() {
EventBits_t event_bits = xEventGroupGetBits(this->event_group_);
if (event_bits & REQUEST_START) {
xEventGroupClearBits(this->event_group_, REQUEST_START);
this->decoding_state_ = AudioFileDecodingState::START_TASK;
}
switch (this->decoding_state_) {
case AudioFileDecodingState::START_TASK: {
if (!this->decode_task_.is_created()) {
xEventGroupClearBits(this->event_group_, ALL_BITS);
if (!this->decode_task_.create(decode_task, "AudioFileDec", DECODE_TASK_STACK_SIZE, this, 1,
this->task_stack_in_psram_)) {
ESP_LOGE(TAG, "Failed to create task");
this->status_momentary_error("task_create", 1000);
this->set_state_(media_source::MediaSourceState::ERROR);
this->decoding_state_ = AudioFileDecodingState::IDLE;
return;
}
}
this->decoding_state_ = AudioFileDecodingState::DECODING;
break;
}
case AudioFileDecodingState::DECODING: {
if (event_bits & TASK_STARTING) {
ESP_LOGD(TAG, "Starting");
xEventGroupClearBits(this->event_group_, TASK_STARTING);
}
if (event_bits & TASK_RUNNING) {
ESP_LOGV(TAG, "Started");
xEventGroupClearBits(this->event_group_, TASK_RUNNING);
this->set_state_(media_source::MediaSourceState::PLAYING);
}
if ((event_bits & TASK_PAUSED) && this->get_state() != media_source::MediaSourceState::PAUSED) {
this->set_state_(media_source::MediaSourceState::PAUSED);
} else if (!(event_bits & TASK_PAUSED) && this->get_state() == media_source::MediaSourceState::PAUSED) {
this->set_state_(media_source::MediaSourceState::PLAYING);
}
if (event_bits & TASK_STOPPING) {
ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, TASK_STOPPING);
}
if (event_bits & TASK_ERROR) {
// Report error so the orchestrator knows playback failed; task will have already logged the specific error
this->set_state_(media_source::MediaSourceState::ERROR);
}
if (event_bits & TASK_STOPPED) {
ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, ALL_BITS);
this->decode_task_.deallocate();
this->set_state_(media_source::MediaSourceState::IDLE);
this->decoding_state_ = AudioFileDecodingState::IDLE;
}
break;
}
case AudioFileDecodingState::IDLE: {
if (this->get_state() == media_source::MediaSourceState::ERROR && !this->status_has_error()) {
this->set_state_(media_source::MediaSourceState::IDLE);
}
break;
}
}
if ((this->decoding_state_ == AudioFileDecodingState::IDLE) &&
(this->get_state() == media_source::MediaSourceState::IDLE)) {
this->disable_loop();
}
}
// Called from the orchestrator's main loop, so no synchronization needed with loop()
bool AudioFileMediaSource::play_uri(const std::string &uri) {
if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener() ||
xEventGroupGetBits(this->event_group_) & REQUEST_START) {
return false;
}
// Check if source is already playing
if (this->get_state() != media_source::MediaSourceState::IDLE) {
ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
return false;
}
// Validate URI starts with "audio-file://"
if (!uri.starts_with("audio-file://")) {
ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
return false;
}
// Strip "audio-file://" prefix and find the file
const char *file_id = uri.c_str() + 13; // "audio-file://" is 13 characters
for (const auto &named_file : get_named_audio_files()) {
if (strcmp(named_file.file_id, file_id) == 0) {
this->current_file_ = named_file.file;
xEventGroupSetBits(this->event_group_, EventGroupBits::REQUEST_START);
this->enable_loop();
return true;
}
}
ESP_LOGE(TAG, "Unknown file: '%s'", file_id);
return false;
}
// Called from the orchestrator's main loop, so no synchronization needed with loop()
void AudioFileMediaSource::handle_command(media_source::MediaSourceCommand command) {
if (this->decoding_state_ != AudioFileDecodingState::DECODING) {
return;
}
switch (command) {
case media_source::MediaSourceCommand::STOP:
xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_STOP);
break;
case media_source::MediaSourceCommand::PAUSE:
xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_PAUSE);
break;
case media_source::MediaSourceCommand::PLAY:
xEventGroupClearBits(this->event_group_, EventGroupBits::COMMAND_PAUSE);
break;
default:
break;
}
}
void AudioFileMediaSource::decode_task(void *params) {
AudioFileMediaSource *this_source = static_cast<AudioFileMediaSource *>(params);
do { // do-while(false) ensures RAII objects are destroyed on all exit paths via break
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STARTING);
// 0 bytes for input transfer buffer makes it an inplace buffer
std::unique_ptr<audio::AudioDecoder> decoder = make_unique<audio::AudioDecoder>(0, 4096);
esp_err_t err = decoder->start(this_source->current_file_->file_type);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start decoder: %s", esp_err_to_name(err));
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR | EventGroupBits::TASK_STOPPING);
break;
}
// Add the file as a const data source
decoder->add_source(this_source->current_file_->data, this_source->current_file_->length);
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_RUNNING);
AudioSinkAdapter audio_sink;
bool has_stream_info = false;
while (true) {
EventBits_t event_bits = xEventGroupGetBits(this_source->event_group_);
if (event_bits & EventGroupBits::COMMAND_STOP) {
break;
}
bool paused = event_bits & EventGroupBits::COMMAND_PAUSE;
decoder->set_pause_output_state(paused);
if (paused) {
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_PAUSED);
vTaskDelay(pdMS_TO_TICKS(20));
} else {
xEventGroupClearBits(this_source->event_group_, EventGroupBits::TASK_PAUSED);
}
// Will stop gracefully once finished with the current file
audio::AudioDecoderState decoder_state = decoder->decode(true);
if (decoder_state == audio::AudioDecoderState::FINISHED) {
break;
} else if (decoder_state == audio::AudioDecoderState::FAILED) {
ESP_LOGE(TAG, "Decoder failed");
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
break;
}
if (!has_stream_info && decoder->get_audio_stream_info().has_value()) {
has_stream_info = true;
audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value();
ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %d", stream_info.get_bits_per_sample(),
stream_info.get_channels(), stream_info.get_sample_rate());
if (stream_info.get_bits_per_sample() != 16 || stream_info.get_channels() > 2) {
ESP_LOGE(TAG, "Incompatible audio stream. Only 16 bits per sample and 1 or 2 channels are supported");
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
break;
}
audio_sink.source = this_source;
audio_sink.stream_info = stream_info;
esp_err_t err = decoder->add_sink(&audio_sink);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to add sink: %s", esp_err_to_name(err));
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
break;
}
}
}
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPING);
} while (false);
// All RAII objects from the do-while block (decoder, audio_sink, etc.) are now destroyed.
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPED);
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
}
} // namespace esphome::audio_file
#endif // USE_ESP32

View File

@@ -0,0 +1,50 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include "esphome/components/audio/audio.h"
#include "esphome/components/audio_file/audio_file.h"
#include "esphome/components/media_source/media_source.h"
#include "esphome/core/component.h"
#include "esphome/core/static_task.h"
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
namespace esphome::audio_file {
enum class AudioFileDecodingState : uint8_t {
START_TASK,
DECODING,
IDLE,
};
class AudioFileMediaSource : public Component, public media_source::MediaSource {
public:
void setup() override;
void loop() override;
void dump_config() override;
// MediaSource interface implementation
bool play_uri(const std::string &uri) override;
void handle_command(media_source::MediaSourceCommand command) override;
bool can_handle(const std::string &uri) const override { return uri.starts_with("audio-file://"); }
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
protected:
static void decode_task(void *params);
audio::AudioFile *current_file_{nullptr};
AudioFileDecodingState decoding_state_{AudioFileDecodingState::IDLE};
EventGroupHandle_t event_group_{nullptr};
StaticTask decode_task_;
bool task_stack_in_psram_{false};
};
} // namespace esphome::audio_file
#endif // USE_ESP32

View File

@@ -38,6 +38,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
const auto &data = service_data.data;
if (data.size() < 10) {
ESP_LOGW(TAG, "Service data too short: %zu", data.size());
return false;
}
const uint8_t protocol_version = data[0] >> 4;
if (protocol_version != 1 && protocol_version != 2) {
ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version);
@@ -47,6 +52,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
// Some b-parasite versions have an (optional) illuminance sensor.
bool has_illuminance = data[0] & 0x1;
if (has_illuminance && data.size() < 18) {
ESP_LOGW(TAG, "Service data too short for illuminance: %zu", data.size());
return false;
}
// Counter for deduplicating messages.
uint8_t counter = data[1] & 0x0f;
if (last_processed_counter_ == counter) {

View File

@@ -47,7 +47,7 @@ void BalluClimate::transmit_state() {
remote_state[11] = 0x1e;
// Fan speed
switch (this->fan_mode.value()) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
case climate::CLIMATE_FAN_HIGH:
remote_state[4] |= BALLU_FAN_HIGH;
break;

View File

@@ -45,17 +45,21 @@ void BangBangClimate::setup() {
}
void BangBangClimate::control(const climate::ClimateCall &call) {
if (call.get_mode().has_value()) {
this->mode = *call.get_mode();
auto mode = call.get_mode();
if (mode.has_value()) {
this->mode = *mode;
}
if (call.get_target_temperature_low().has_value()) {
this->target_temperature_low = *call.get_target_temperature_low();
auto target_temperature_low = call.get_target_temperature_low();
if (target_temperature_low.has_value()) {
this->target_temperature_low = *target_temperature_low;
}
if (call.get_target_temperature_high().has_value()) {
this->target_temperature_high = *call.get_target_temperature_high();
auto target_temperature_high = call.get_target_temperature_high();
if (target_temperature_high.has_value()) {
this->target_temperature_high = *target_temperature_high;
}
if (call.get_preset().has_value()) {
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
auto preset = call.get_preset();
if (preset.has_value()) {
this->change_away_(*preset == climate::CLIMATE_PRESET_AWAY);
}
this->compute_state_();

View File

@@ -1,4 +1,5 @@
#include "bedjet_codec.h"
#include <algorithm>
#include <cstdio>
#include <cstring>
@@ -68,6 +69,10 @@ BedjetPacket *BedjetCodec::get_set_runtime_remaining_request(const uint8_t hour,
/** Decodes the extra bytes that were received after being notified with a partial packet. */
void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) {
if (length < 5) {
ESP_LOGVV(TAG, "Received extra: %d bytes (too short)", length);
return;
}
ESP_LOGVV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
uint8_t offset = this->last_buffer_size_;
if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) {
@@ -90,14 +95,19 @@ void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) {
* @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise.
*/
bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
if (length < 5) {
ESP_LOGW(TAG, "Received short packet: %d bytes", length);
return false;
}
ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) {
// Clear old buffer
memset(&this->buf_, 0, sizeof(BedjetStatusPacket));
// Copy new data into buffer
memcpy(&this->buf_, data, length);
this->last_buffer_size_ = length;
size_t copy_len = std::min(static_cast<size_t>(length), sizeof(BedjetStatusPacket));
memcpy(&this->buf_, data, copy_len);
this->last_buffer_size_ = copy_len;
// TODO: validate the packet checksum?
if (this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && this->buf_.target_temp_step <= 86 &&
@@ -113,13 +123,15 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
}
} else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) {
// We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself.
ESP_LOGVV(TAG,
"received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, "
"[12]=%d, [-1]=%d",
bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8],
data[9], data[10], data[11], data[12], data[length - 1]);
if (length >= 13) {
ESP_LOGVV(TAG,
"received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, "
"[12]=%d, [-1]=%d",
bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8],
data[9], data[10], data[11], data[12], data[length - 1]);
}
if (this->has_status()) {
if (this->has_status() && length >= 7) {
this->status_packet_->ambient_temp_step = data[6];
}
} else {

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