Compare commits

..
Author SHA1 Message Date
J. Nick Koston f3676e27d4 Merge remote-tracking branch 'origin/parse-float-prefix' into HEAD 2026-11-14 08:39:53 -06:00
J. Nick Koston 365bea82ad Merge remote-tracking branch 'origin/dev' into HEAD 2026-11-14 08:35:53 -06:00
J. Nick Koston eebe66da62 [i2c] Add byte and read-modify-write helpers for 16-bit register addresses (#22698) 2026-11-14 08:26:35 -06:00
J. Nick Koston c032f58c6c [libretiny] Free closed TCP connections after about 1 s instead of 120 s (#22696) 2026-11-14 08:10:14 -06:00
J. Nick Koston ff3adb407e [rp2] Bump arduino-pico framework to 6.3.2 (#22671) 2026-11-14 08:09:29 -06:00
J. Nick Koston dcf3a6eb6e [bluetooth_connection] Keep the top half of 32-bit GATT UUIDs on RP2 (#22679) 2026-11-14 08:09:09 -06:00
J. Nick Koston afbc7151d3 [async_tcp] Use the esphome-libs RPAsyncTCP fork so SSE streams do not stall on RP2040 (#22686) 2026-11-14 08:08:35 -06:00
J. Nick Koston 37b95ff544 [web_server_base] Check Basic auth without linking the library's Digest code (#22684) 2026-11-14 08:08:26 -06:00
J. Nick Koston 6c556c178a [core] Lock the scheduler against lwIP callbacks on RP2 (#22688) 2026-11-14 08:08:10 -06:00
J. Nick Koston 9f197ca8d7 [core] Provide buf_append_str_p on every platform (#22673) 2026-11-14 08:07:05 -05:00
J. Nick Koston 15291f06a5 Merge commit '090156a32e851ea237bf16e18e967fdbab20cdfe' into parse-float-components 2026-11-13 23:45:59 -06:00
J. Nick Koston 090156a32e [core] Say str_to_float does not set errno 2026-11-13 23:45:56 -06:00
J. Nick Koston db07dac851 Merge commit '543151ab1cffd9ddce66ab310d647ba907247abf' into parse-float-components 2026-11-13 23:05:30 -06:00
J. Nick Koston 543151ab1c [core] Keep the str_to_float comment within 120 columns 2026-11-13 23:05:27 -06:00
J. Nick Koston 3f8705ecbb Merge commit '3b053f1a0c5abe194d1930a0c02a8c52ac2e29b1' into parse-float-components 2026-11-13 23:05:18 -06:00
J. Nick Koston 3b053f1a0c [core] Point str_to_float only at float results of strtod and atof 2026-11-13 23:05:15 -06:00
J. Nick Koston d719b9220e Merge commit '7bed89eafc5b7bbe70131a7793a92030187d656d' into parse-float-components 2026-11-13 22:37:58 -06:00
J. Nick Koston 7bed89eafc [core] Document what parse_float accepts and rejects 2026-11-13 22:37:55 -06:00
J. Nick Koston 381d6fe7cd Merge commit '04fe5cdac96bec4161ff85b29f9a7eb4eb30f64b' into parse-float-components 2026-11-13 22:13:14 -06:00
J. Nick Koston 04fe5cdac9 [core] Stop hex digits at ':' to '?' instead of reading them as 10 to 15
picolibc 1.8.10 (and its main) read them as hex digits, so "0x1:" was 26 and
parse_number<float> accepted it; newlib and the C standard stop there.
2026-11-13 22:13:11 -06:00
Keith Burzinski c2e8c872b1 [serial_proxy] Keep received bytes when the client's connection is blocked (#22687) 2026-11-13 19:52:12 -06:00
J. Nick Koston 72555af9b6 Merge commit 'b35a450f5f9ea4bb90da86da842b5583d3f6e6a3' into parse-float-components 2026-11-13 19:43:09 -06:00
J. Nick Koston b35a450f5f [core] Say stof uses str_to_float in the StringRef helpers comment 2026-11-13 19:43:06 -06:00
J. Nick Koston 16205445bb Merge commit 'bf6d3ca470e598bffbb1573caf1118125b48b2d7' into parse-float-components 2026-11-13 19:40:00 -06:00
J. Nick Koston bf6d3ca470 [core] Note the one non-tie near a midpoint 2026-11-13 19:39:57 -06:00
J. Nick Koston 6cd7dd98f3 Merge commit '16f660b0090391b511dd06ad5c20ba9e02cefcc0' into parse-float-components 2026-11-13 19:37:24 -06:00
J. Nick Koston 16f660b009 [core] Terser parser comments and docstrings 2026-11-13 19:37:21 -06:00
J. Nick Koston ce3a282a41 Merge commit '1e127efe8e6c5f7e613c6661ba2d08ca6f1164c3' into parse-float-components 2026-11-13 19:35:10 -06:00
J. Nick Koston 1e127efe8e [core] Rename the pow10 template to pow10_exact so it cannot hide libm's pow10 2026-11-13 19:35:07 -06:00
J. Nick Koston e657d18e45 Merge commit 'cdd994455141a13f0ecb7d752f98b49072b658a3' into parse-float-components 2026-11-13 19:34:32 -06:00
J. Nick Koston cdd9944551 [core] Fewer lines in parse_float.cpp; object code identical on host, ESP8266 and RP2040 2026-11-13 19:34:29 -06:00
J. Nick Koston dacf19954f Merge commit '9e64b21eb46a2ecae9cebb3a6efacb1e42ae08c3' into parse-float-components 2026-11-13 19:21:23 -06:00
J. Nick Koston 9e64b21eb4 [core] Share the plain-decimal limit between parse_float and str_to_float; no multiply for the fast-path factor 2026-11-13 19:21:16 -06:00
J. Nick Koston ba424d1f26 Merge commit '6f7b62602e6' into parse-float-components 2026-11-13 17:46:10 -06:00
J. Nick Koston 6f7b62602e [core] Give picolibc's strtof result, with integer arithmetic only
str_to_float and parse_number<float> now return what picolibc 1.8.10's
strtof (ESP-IDF 6) returns, bits and end pointer, on every platform: a
decimal is rounded to 9 significant digits (ties to even) and those are
converted to the nearest float; hex is rounded to the nearest float.

- the 9 digits convert with one float operation when both operands are
  exact floats, otherwise with one 64-bit product against a 12-entry
  table of 10^(8k); all 85 billion (m10, e10) were checked against
  picolibc's engine, and the ties the product cannot decide alone are
  computed exactly
- no double arithmetic and no big-integer path: time linear in the
  input length, and less code
- picolibc's syntax: "x" after zero digits starts hex, "inf" and
  "infinity", "nan(" with letters, digits and '_', exponent digits
  capped at 224
- tests use picolibc's values for long input and syntax, the host
  strtof for up to 9 significant digits, and name the RP2040 and
  RP2350 libc results the bench found (flushed denormals, NaN to inf)
2026-11-13 17:40:06 -06:00
J. Nick Koston 29020748e2 [dfrobot_sen0609][hydreon_rgxx][smt100] Use str_to_float 2026-11-13 17:00:01 -06:00
J. Nick Koston 8917409887 Merge commit 'dd08b19bd51' into parse-float-components 2026-11-13 16:59:51 -06:00
J. Nick Koston dd08b19bd5 Round to the nearest float; rename parse_float_prefix to str_to_float
The parser now gives the correctly rounded float (ties to even) for decimal
and hex input on every platform, instead of newlib's decimal-to-double-to-
float double rounding. Syntax and end pointer stay newlib's; the hex exponent
saturates instead of wrapping at 32 bits.

- one integer rounding helper builds the float bits for the double guess,
  hex input and double_to_float_bits
- the exact comparison is against the float midpoint itself
- the early return checks the guess's distance to a float midpoint
  directly, without two extra conversions
- named exponent clamps folded into the EXACT_WORDS static_assert
- tests use the host's correctly rounded strtof as the reference, with
  named tests for the inputs where newlib and picolibc differ
- the unit test is host only (ESP32 clang-tidy), reproducible RNG seeds
  are marked, and int32_t replaces long
2026-11-13 16:45:41 -06:00
J. Nick Koston 7b8afeb4df [dfrobot_sen0609][hydreon_rgxx][smt100] Sort the parse_float.h include 2026-11-13 16:24:26 -06:00
J. Nick Koston 2eb25bf800 Merge branch 'parse-float-prefix' into parse-float-components 2026-11-13 16:24:14 -06:00
J. Nick Koston b4e65d51bb [core] Test the double fast path, the overflow midpoint and nan forms; bound exact rounds at two 2026-11-13 16:24:12 -06:00
J. Nick Koston da65beb404 [core] Move pow10, is_digit and is_space_c into helpers.h; integer powers of ten on the plain path 2026-11-13 16:24:12 -06:00
J. Nick Koston 491674adec [dfrobot_sen0609][hydreon_rgxx][smt100] Include parse_float.h for parse_float_prefix 2026-11-13 15:54:42 -06:00
J. Nick Koston e3a9f11525 Merge branch 'parse-float-prefix' into parse-float-components 2026-11-13 15:54:30 -06:00
J. Nick Koston 859299b94c [core] Drop the parse_float_prefix benchmarks that repeat parse_number<float>'s 2026-11-13 15:54:29 -06:00
J. Nick Koston 080bdbd53b [core] Simplify parse_float_prefix after review and declare parse_float in parse_float.h 2026-11-13 15:54:29 -06:00
Bascht74 9620899479 [tcp_uart][uart_tcp] Move CONF_DISCONNECTS to the shared component constants (#22438) 2026-11-13 15:47:32 -06:00
J. Nick Koston ba53757f7e [dfrobot_sen0609] Update a comment that still named strtof 2026-11-13 15:24:03 -06:00
J. Nick Koston df31b05804 Merge branch 'parse-float-prefix' into parse-float-components 2026-11-13 15:23:56 -06:00
J. Nick Koston 49e696555e [core] Benchmark rejected text and drop the libc comparison benchmarks 2026-11-13 15:23:45 -06:00
J. Nick Koston 10a7992f5b [core] Bound parse_float_prefix's time on every platform and speed up common inputs 2026-11-13 15:23:44 -06:00
J. Nick Koston 774cb9e9e5 Merge remote-tracking branch 'origin/parse-float-prefix' into parse-float-components 2026-11-13 14:51:40 -06:00
J. Nick Koston cbf38abdb9 [core] Benchmark parse_number<float> and parse_float_prefix against strtof 2026-11-13 14:51:26 -06:00
J. Nick Koston e5bf7c74e7 [core] Take strtod's exact double fast path in parse_float_prefix 2026-11-13 14:51:26 -06:00
J. Nick Koston c197a824e3 [dfrobot_sen0609][hydreon_rgxx][smt100] Parse floats with parse_float_prefix 2026-11-13 14:34:25 -06:00
J. Nick Koston 8cfc265b19 [core] Parse floats with parse_float_prefix so libc's strtod is not linked 2026-11-13 14:34:25 -06:00
J. Nick Koston fa08c7a305 [core] Add parse_float_prefix, a strtof that does not link libc's strtod 2026-11-13 14:27:23 -06:00
dependabot[bot] 2a6a1a050e Bump peter-evans/create-pull-request from 8.1.2 to 8.1.3 (#22672)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-13 19:27:44 +00:00
6a743eca57 [daly_bms] Finish the request cycle before starting a new one (#22284)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-13 12:56:40 -06:00
tomaszduda23andClaude 46844d3631 [i2c] Support I2C on the nRF54LM20 and nRF54L15 (#22642)
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-13 10:11:40 -06:00
Mark MayoandJ. Nick Koston 8cad9c4035 [light] Compute a color mode's bit instead of searching for it (#22647)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-13 16:09:00 +00:00
Mark MayoandJ. Nick Koston 76ff2d6c0f [api] Write repeated sint32 elements through the short varint path (#22648)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-13 15:59:31 +00:00
Mark Mayo 8b7d68b80f [core] Make FixedRingBuffer iteration cheaper (#22646) 2026-11-13 09:42:29 -06:00
Tom d40b7b12d5 [const] Move CONF_OPERATING_MODE to the shared component constants (#22650) 2026-11-13 09:35:00 -06:00
Mark Mayo 32ac9f17f1 [select] Compare the first byte before calling strncmp in index_of() (#22649) 2026-11-13 09:23:07 -06:00
Mark Mayo eee52fe419 [core] Let the scheduler's heap algorithms inline the item comparison (#22632) 2026-11-13 03:34:58 +00:00
J. Nick Koston 915b75dcb8 [core] Brace the fast loop check in the unit test main (#22640) 2026-11-12 21:14:16 -06:00
J. Nick Koston df83d20fdc [espota2] Pin the clock in the probe deadline test (#22639) 2026-11-12 22:12:02 -05:00
J. Nick Koston 63decb1d68 [epaper_spi] Stop the fast loop after the fill tests (#22638) 2026-11-12 20:54:39 -06:00
Craig Andrews 5a04f4949a [epaper_spi] Point the UC8279 gate-count error at the height (#22628) 2026-11-13 12:10:15 +11:00
esphome[bot] cc6db898e0 Bump bundled esphome-device-builder to 1.28.1 (#22630)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-11-12 18:25:11 -06:00
Chris f6fa1d8e9e [usb_host] Respect explicit control transfer size (#22629) 2026-11-12 18:20:40 -06:00
Craig Andrews 1442554df2 [epaper_spi] Mark the frame drawn after fill() (#21505) 2026-11-13 10:37:37 +11:00
Nicolas LiaudatandJ. Nick Koston dceba4fa3b [core] Add an align parameter to progmem_array() (#22620)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-12 22:13:53 +00:00
54f2f25f2d [brizlabs_led_string] Add two-wire power-line LED string light (#22531)
Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-12 16:06:25 -06:00
J. Nick Koston 9245f20b31 [wifi] Start STA on Realtek when no network or AP is configured (#22611) 2026-11-12 15:51:54 -06:00
J. Nick Koston 483a4f6a73 [uart] Stop ESP8266 software serial TX from tripping the Wi-Fi watchdog above 19200 baud (#22603) 2026-11-12 15:51:11 -06:00
Fabio Pugliese Ornellas 2ef4e52637 [ld2450] Add a button to bypass the xy text sensor filters for 10 seconds (#22285) 2026-11-12 20:47:46 +00:00
122df49ef5 [INA226] Add INA226 Alert functionality with configurable threshold and clear action (#13637)
Co-authored-by: Mashimin <msmarks@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-12 14:14:24 -06:00
e96345f5dc [text_sensor] Add detach_filters/attach_filters to take a filter chain out of use (#22621)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-12 19:54:48 +00:00
J. Nick Koston 4d758269a1 [mqtt] Check the fail-fast discovery tests without a wall clock (#22614) 2026-11-12 19:40:23 +00:00
43d61df1fc [xl9535] Cache the output registers so a pin write needs no read (#22483)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-12 19:39:36 +00:00
dependabot[bot] 9843763917 Bump filelock from 4.2.9 to 4.2.10 (#22622)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-12 19:30:47 +00:00
J. Nick Koston 0b0b9033e5 [zephyr] Add ISR pin_mode so dht links on nRF52 (#22609) 2026-11-12 13:30:36 -06:00
J. Nick Koston deeb88a835 [zephyr] Report interrupt context in in_isr_context() and is_main_loop_thread() (#22613) 2026-11-12 13:30:14 -06:00
J. Nick Koston 4d04cd6938 [logger] Reject a LibreTiny hardware_uart that has no default pins (#22608) 2026-11-12 13:29:59 -06:00
J. Nick Koston eef2ddca0d [dht] Negate sign-and-magnitude negative temperatures instead of inverting them (#22615) 2026-11-12 13:29:44 -06:00
J. Nick Koston 2bfa9d1524 [host] Add the empty ISRInternalGPIOPin::pin_mode (#22616) 2026-11-12 13:29:43 -06:00
rwrozelleandClaude Opus 5.5 610c9faca6 [nrf52][zephyr] Share the Python environment builder with nrf52 (#22617)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 14:13:49 -05:00
d09867c27e [nrf52] Add the Seeed XIAO nRF54LM20A and fix uploading to it (#22599)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-12 12:26:33 -06:00
nahumoded 20d34660b8 [homeassistant] Do not warn when a binary_sensor's state is unavailable or unknown (#22607) 2026-11-12 11:07:39 -06:00
rwrozelleandClaude Opus 5.5 c894b4514d [core][stm32] Add the stm32 platform identifier (#22605)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 11:08:21 -05:00
rwrozelleandClaude Opus 5.5 e347dd957b [nrf52][zephyr] Share the Zephyr script patches with nrf52 (#22606)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 11:08:04 -05:00
J. Nick Koston 2e60fa0f42 [gpio] Save 4 bytes per binary sensor and simplify the platforms (#22591) 2026-11-12 09:38:09 -05:00
rwrozelleandClaude Opus 5.5 7684ccee06 [nrf52][zephyr] Share the Python environment helpers with nrf52 (#22589)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 09:22:50 -05:00
J. Nick Koston dccb81b319 [logger] Fix LibreTiny task log messages landing unaligned and out of step (#22594) 2026-11-12 09:06:57 -05:00
J. Nick Koston cdafeb5734 [core] Make call_user_lambda public for builders that emit their own statements (#22588) 2026-11-12 08:46:26 -05:00
J. Nick Koston b09d62e6a3 [speaker] Generate speaker.play as an inline action (#22584) 2026-11-12 08:44:52 -05:00
J. Nick Koston c4a7b75596 [fan] Generate fan.cycle_speed as an inline action (#22585) 2026-11-12 08:44:07 -05:00
J. Nick Koston 2d0c1b07ec [sprinkler] Generate start_single_valve, shutdown and resume_or_start_full_cycle as inline actions (#22586) 2026-11-12 08:43:43 -05:00
J. Nick Koston f216936fdd [ble_device_base] Generate the BK72xx, LN882H and RP2 tracker start_scan actions as inline calls (#22587) 2026-11-12 08:42:48 -05:00
0240f57069 [packet_interface] Add addressing and fragmentation layers (#21101)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-11-12 20:14:26 +11:00
Jesse HillsandJ. Nick Koston 3248eeaf1f [api] Send the platform of each wizard entity (#22580)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-12 19:05:06 +13:00
J. Nick Koston a2ccd64257 [ci] Add ESP8266 lock load, CC1101 sleep, soft AP BLE scan and HA script switch flows to the hardware-test skill (#22575) 2026-11-11 22:11:42 -06:00
Jesse Hills c22a7a4188 Merge branch 'beta' into dev 2026-11-12 16:27:17 +13:00
Jesse Hills b03870b551 Merge pull request #22583 from esphome/bump-2026.11.0b4
2026.11.0b4
2026-11-12 16:26:56 +13:00
esphome[bot] 6e9b37ca0d Bump aioesphomeapi from 46.29.1 to 46.30.0 (#22582) 2026-11-11 21:07:27 -06:00
J. Nick KostonandJonathan Swoboda f695f41a3d [sensor][binary_sensor][text_sensor] Generate lambda filters as apply filters (#21830)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-11-12 02:54:48 +00:00
J. Nick Koston 909ffd7313 [ai] Freeze the YAML language until configs move to yaml.rocks (#22556) 2026-11-11 20:44:37 -06:00
Jesse Hills 4a965b8b32 Bump version to 2026.11.0b4 2026-11-12 15:42:05 +13:00
J. Nick Koston d826a19eb1 [esp32_ble] Sort GATT discovery responses by handle for ESP-IDF 5.5.6 and 6.0.3 (#22576) 2026-11-12 15:42:02 +13:00
J. Nick Koston fb54ec51c1 [gpio] Lock interrupts per slot on the one_wire bus (#22452) 2026-11-12 15:42:02 +13:00
J. Nick Koston 04b6c0d4ca [esp32_ble] Keep bond keys when a bonded device re-encrypts on ESP-IDF 5.5.6 (#22559) 2026-11-12 15:42:02 +13:00
J. Nick Koston abf282dd08 [esp32_ble_tracker] Scan at the short window while the wifi AP is running (#22566) 2026-11-12 15:41:58 +13:00
J. Nick Koston e097958ad9 [remote_transmitter] Lock interrupts only near each edge on ESP8266 (#22453) 2026-11-12 15:41:24 +13:00
jabla 8a8ad4d8da [zigbee] Rejoin only after the parent missed several polls (#22405) 2026-11-12 15:41:24 +13:00
J. Nick Koston 09b82388c9 [uart] Lock interrupts per bit in ESP8266 software serial TX at 19200 baud and below (#22450) 2026-11-12 15:41:23 +13:00
J. Nick Koston ea5407925a [wifi] Apply power save when the AP starts without networks (#22564) 2026-11-12 15:41:20 +13:00
J. Nick Koston 97732df3a5 [esp32_ble][ble_device_base][improv_ble] Send advertising as raw packets and keep Improv in the advertising packet (#22499) 2026-11-12 15:40:20 +13:00
Francesco Pace 04482ae0d4 [espectre] Update to SDK 3.2.0 (#22570) 2026-11-12 15:40:20 +13:00
Clyde Stubbs ca0dd8e292 [mipi_dsi] Keep the panel running while flash is written (#22535) 2026-11-12 15:40:20 +13:00
J. Nick Koston e75ac64cc9 [esp8266] Skip other devices' known mDNS answers without allocating (Arduino core 3.1.2-esphome.4) (#22533) 2026-11-12 15:40:20 +13:00
PolarGooseandJ. Nick Koston 212c0c1ea4 [dsmr] Update dsmr_parser to 1.11.0 (#22530)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-12 15:40:20 +13:00
Jonathan Swoboda 3e2f9bf296 [ota] Skip the auth request when the password is empty at run time (#22485) 2026-11-12 15:40:20 +13:00
J. Nick Koston b997866e86 [esp32_ble_server] Size the BLE2902 value buffer to its 2 byte value (#22454) 2026-11-12 15:40:20 +13:00
J. Nick Koston ae89db98e4 [core] Inline InterruptLock on ESP8266 (#22455) 2026-11-12 15:40:19 +13:00
Kevin Ahrendt e9bb102c1f [sendspin] Add bits_per_sample to the media source (#22313) 2026-11-12 15:40:19 +13:00
J. Nick Koston e95c1143af [esp32_ble] Sort GATT discovery responses by handle for ESP-IDF 5.5.6 and 6.0.3 (#22576) 2026-11-12 14:59:56 +13:00
Fabio Pugliese Ornellas 0d7a4bd87e [text_sensor] Add Filter::reset_chain to put filters back in their boot state (#22409) 2026-11-11 19:55:23 -06:00
J. Nick Koston bf6b0fd352 [gpio] Lock interrupts per slot on the one_wire bus (#22452) 2026-11-12 01:23:34 +00:00
Jesse Hills b273dfb829 [ci] Reject a new constant defined in two or more files (#22498) 2026-11-12 01:19:01 +00:00
J. Nick Koston 0b6a78d206 [binary_sensor] Generate the delayed_on_off filter as an apply filter (#21832) 2026-11-11 19:04:29 -06:00
J. Nick Koston 0cf1bad9a7 [esp32_ble] Keep bond keys when a bonded device re-encrypts on ESP-IDF 5.5.6 (#22559) 2026-11-12 00:59:50 +00:00
J. Nick Koston 9f74fcb820 [esp32_ble_tracker] Scan at the short window while the wifi AP is running (#22566) 2026-11-12 00:57:08 +00:00
J. Nick Koston 666c60b158 [core] Collect config ids once in unique_global_id and convert raw filter lambda types (#22573) 2026-11-11 18:53:58 -06:00
408a24b6a5 [nrf52][zephyr] Share the Zephyr SDK installer with nrf52 (#22562)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-11-11 19:53:22 -05:00
J. Nick Koston 047ff1aa15 [remote_transmitter] Lock interrupts only near each edge on ESP8266 (#22453) 2026-11-12 00:53:04 +00:00
Jonathan Swoboda e602b7aa53 [components] Fix misspelled identifiers (#22568) 2026-11-11 19:46:29 -05:00
Nicolas LiaudatandJ. Nick Koston 4f1e2da537 [micro_wake_word] Bump esp-tflite-micro to 1.4.1 and esp-nn to 1.5.0 (#21480)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-11 18:39:39 -06:00
jabla e3c0ddb5df [zigbee] Rejoin only after the parent missed several polls (#22405) 2026-11-12 13:35:19 +13:00
J. Nick Koston c8d038eada [uart] Lock interrupts per bit in ESP8266 software serial TX at 19200 baud and below (#22450) 2026-11-11 18:33:29 -06:00
J. Nick Koston 01d9547700 [wifi] Apply power save when the AP starts without networks (#22564) 2026-11-12 13:33:14 +13:00
J. Nick Koston 9c6fb6830d [esp32_ble][ble_device_base][improv_ble] Send advertising as raw packets and keep Improv in the advertising packet (#22499) 2026-11-11 18:32:55 -06:00
Jonathan Swoboda 3919f4fcbc [tests] Fix typos in test names and fixtures (#22567) 2026-11-11 18:23:30 -06:00
Francesco Pace f544ac247b [espectre] Update to SDK 3.2.0 (#22570) 2026-11-11 19:19:31 -05:00
Jonathan Swoboda 12f88f72bb [nrf52] Skip the testing mode flash overlay on nRF54L (#22571) 2026-11-11 19:11:58 -05:00
J. Nick Koston f88f254ba0 [core] Add a CodSpeed benchmark for naming generated globals (#22572) 2026-11-11 19:07:05 -05:00
Bascht74 10b503cb07 [store_yaml] Add manufacturer_key to the sensitive keys (#22569) 2026-11-11 17:58:14 -06:00
J. Nick Koston 9aee2ea1ef [core] Add ApplyFilter for generated sensor, binary_sensor and text_sensor filters (#21829) 2026-11-11 18:26:46 -05:00
Jonathan Swoboda 434ba13986 [components] Fix typos in comments and log messages (#22565) 2026-11-11 21:43:23 +00:00
f4c5d5f818 [zephyr] Use the new USB device stack on nRF Connect SDK 3.4.1 and newer (#22233)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-11 21:39:06 +00:00
cfb76b1bc4 [entity_transport] Add entity transport component (#21095)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-11-11 21:26:20 +00:00
rwrozelleandClaude Opus 5.5 74dee7dd17 [core] Share the install lock between framework installers (#22561)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 15:16:52 -06:00
tomaszduda23andClaude e0790a9063 [zephyr_ble_server] Fix ble on nrf sdk 3.4.1 (#22558)
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-11 22:04:35 +01:00
J. Nick Koston 9b26a15960 [network] Build the Zephyr TCP stack on nRF52 only when a component uses TCP (#21910) 2026-11-11 13:54:47 -06:00
75b586fb57 [wifi] Add on_ap_start / on_ap_stop triggers (#22154)
Co-authored-by: git-xmu999 <173232202+git-xmu999@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@home-assistant.io>
2026-11-11 13:50:49 -06:00
dependabot[bot] 6d95112b77 Bump bleak from 3.0.2 to 3.1.0 (#22557)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-11 13:32:09 -06:00
tomaszduda23andClaude ff3075cb26 [zephyr_ble_server] Fix build on sdk-nrf 3.4.1 (#22555)
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-11 20:16:49 +01:00
Clyde Stubbs 40a226f3fd [mipi_dsi] Keep the panel running while flash is written (#22535) 2026-11-12 06:06:46 +11:00
J. Nick Koston 182db18dd9 [core] Add cg.shared_static_function to emit one function per distinct body (#21837) 2026-11-11 12:57:00 -06:00
2a14d1375b [sen6x] Add forced CO2 recalibration action (#22256)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-11 12:38:50 -06:00
8e0cb460ea [cc1101] Add power-down and wake-up actions (#22519)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-11 12:25:55 -06:00
J. Nick Koston ea5c8a2fbc [store_yaml] Add esphome recover-yaml to download the stored YAML (#22536) 2026-11-11 12:04:03 -06:00
J. Nick Koston 4a6e43f9b3 [select] Store the select.is lambda as a function pointer (#22550) 2026-11-11 12:56:51 -05:00
8c2841d6f4 [socket] Let TcpListener accept into several links (#21039)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-11 11:23:51 -06:00
ac0dc6fe2e [epaper_spi] Name each model's driver class once (#20502)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-11 10:47:16 -06:00
Daniel PernoldandJ. Nick Koston dc0cb537cc [scd4x] Add automatic_self_calibration_target (#22546)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-11 10:38:17 -06:00
Ivan KruglovandJ. Nick Koston c3c4d70e0e [bl0939] Add configurable anti-creep threshold (#22501)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-11 10:36:51 -06:00
f00fa15b54 [core] Speed up component imports and config validation (#22543)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-11 10:26:00 -06:00
34c4898771 [homeassistant] Allow script to be controlled with homeassistant/switch component (#22548)
Co-authored-by: Olivier Smeesters <osmeest@gmail.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-11 10:15:23 -06:00
Mark MayoandJ. Nick Koston 19a332507d [api] Match Home Assistant state subscriptions by their stored length (#22544)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-11 15:51:02 +00:00
Jonathan Swoboda f71fcc1739 [remote_base] Don't require code for the KeeLoq binary sensor (#22541) 2026-11-11 09:45:53 -06:00
esphome[bot] 36fd800c83 Bump aioesphomeapi from 46.28.0 to 46.29.1 (#22540)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-11-11 09:35:58 -06:00
Mark Mayo 3a3c390621 [homeassistant] Do not warn when a sensor's state is unavailable or unknown (#22545) 2026-11-11 09:32:14 -06:00
Flaviu TamasandJonathan Swoboda 29875a9e03 [bmi160] Remove code owner (#22537)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-11-11 03:05:45 +00:00
Jonathan Swoboda 9abc1812d9 [script] Only import platform modules in build_codeowners.py (#22539) 2026-11-10 21:54:21 -05:00
J. Nick KostonandJonathan Swoboda a9c3c6d8d0 [ble_device_base] Match service and manufacturer data triggers from a constant (#21827)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-11-10 19:16:16 -06:00
J. Nick Koston b03e03e465 [combination] Keep sources in static storage and compute median and range without the heap (#22510) 2026-11-10 18:11:38 -06:00
J. Nick Koston 6b2a9870d0 [wled] Use the socket component instead of Arduino WiFiUDP (#22509) 2026-11-10 18:11:22 -06:00
J. Nick Koston da06b29845 [packet_interface] Keep the reliability and encryption buffers in static buffers (#22337) 2026-11-10 18:11:05 -06:00
J. Nick Koston 629dfe1c9a [esp8266] Skip other devices' known mDNS answers without allocating (Arduino core 3.1.2-esphome.4) (#22533) 2026-11-10 18:10:29 -06:00
J. Nick Koston 354f844843 [ci] Add heap allocation tracing to the hardware-test skill (#22529) 2026-11-10 19:06:29 -05:00
Bascht74 c54b3b41d3 [ci] Skip benchmark.yaml when looking for direct bus config (#22437) 2026-11-10 14:17:21 -08:00
PolarGooseandJ. Nick Koston 8412d6255a [dsmr] Update dsmr_parser to 1.11.0 (#22530)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-10 21:50:36 +00:00
dependabot[bot] 6b962b15d6 Bump zeroconf from 0.153.0 to 0.154.0 (#22527)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-10 19:31:24 +00:00
6d5400a80f [dsmr] Fix compiling on the host platform (#22524)
Co-authored-by: Bascht74 <66269310+Bascht74@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-10 11:18:44 -08:00
Zedas f6a2241eb1 [store_yaml] Find integer secrets written as quoted hex (#22516) 2026-11-10 11:49:48 -06:00
Bascht74 9d1071e2f8 [dlms_meter] Fix compiling on the host platform (#22503) 2026-11-10 17:27:01 +00:00
Bascht74 74db3c0484 [canbus][rs485_frame] Move CONF_ON_FRAME to the shared component constants (#22505) 2026-11-10 11:25:06 -06:00
J. Nick KostonandJonathan Swoboda 452b7017e6 [ezo] Pass reply text to the triggers as StringRef (#22204)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-11-10 16:57:56 +00:00
J. Nick Koston b65cdb788c [ezo] Pin the argument types of the reply triggers in tests (#22201) 2026-11-10 09:50:02 -05:00
J. Nick KostonandJonathan Swoboda 1558751b46 [mcp23008][mcp23017] Configure IOCON with update_register (#22232)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-11-10 09:49:55 -05:00
J. Nick Koston 5b4d0069d2 [ezo] Link the command text builder only when a command is queued (#21866) 2026-11-10 09:23:45 -05:00
J. Nick Koston 44e39e6bca [mcp23008][mcp23017] Add setup tests for the IOCON configuration (#22231) 2026-11-10 09:22:07 -05:00
J. Nick Koston 19e16144b6 [sprinkler] Keep the valve list in static storage (#22345) 2026-11-10 09:09:54 -05:00
Bascht74 029199b0ca [uart] Enable wake-on-RX without a network component (#22502) 2026-11-09 19:57:43 -08:00
J. Nick Koston e2b60adc2b [sensor][number] Generate the on_value_range bounds as one function (#21818) 2026-11-09 19:56:49 -08:00
J. Nick Koston 71d9a995b6 [core] Add ValueRange and new_value_range_trigger for on_value_range triggers (#21817) 2026-11-09 19:56:49 -08:00
Jonathan Swoboda 9e69326b14 [ota] Skip the auth request when the password is empty at run time (#22485) 2026-11-09 16:30:52 -05:00
942faa87ef Bump ruff from 0.17.3 to 0.17.4 (#22492)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-09 12:35:11 -08:00
dependabot[bot] 42cae195f8 Bump esphome-glyphsets from 0.2.0 to 0.2.1 (#22493)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-09 12:06:49 -08:00
dependabot[bot] f85ebd2e21 Bump prek from 0.5.9 to 0.5.10 (#22486)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-09 12:05:23 -08:00
dependabot[bot] 31af32a488 Bump docker/login-action from 4.7.0 to 4.8.0 in the docker-actions group (#22489)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-09 12:05:14 -08:00
dependabot[bot] 279e1dc48f Bump paho-mqtt from 2.1.0 to 2.2.0 (#22495)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-09 12:05:01 -08:00
dependabot[bot] dd641ff9d8 Bump filelock from 4.2.7 to 4.2.9 (#22488)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-09 12:00:44 -08:00
dependabot[bot] 4d740dc1b4 Bump click from 8.5.0 to 8.5.1 (#22487)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-09 11:45:40 -08:00
dependabot[bot] 292a646b03 Bump platformdirs from 4.14.3 to 4.14.5 (#22490)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-09 11:45:32 -08:00
dependabot[bot] 429c3586bf Bump github/codeql-action from 4.39.2 to 4.39.3 (#22491)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-09 11:45:24 -08:00
dependabot[bot] 0929b4d6be Bump esphome/workflows/.github/workflows/lock.yml from 2026.10.3 to 2026.11.0 (#22496)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-09 11:38:34 -08:00
dependabot[bot] e24fe74baa Bump peter-evans/create-pull-request from 8.1.1 to 8.1.2 (#22494)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-09 11:37:46 -08:00
1daba1b998 [core] Add the Zephyr toolchain id and tools cache (#21720)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-11-09 17:50:24 +00:00
J. Nick Koston e7c066b923 [lcd_base] Keep the character buffer in a static buffer (#22331) 2026-11-09 10:57:51 -06:00
J. Nick Koston 1add061d94 [core] Keep the looping components list in static storage (#22342) 2026-11-09 11:35:32 -05:00
J. Nick Koston b55460af99 [esp32_ble_tracker] Use the shared ble_device_base advertise triggers (#21823) 2026-11-09 11:32:48 -05:00
J. Nick Koston f25d66d7df [bp1658cj] Clean up the driver (#21845) 2026-11-09 11:28:43 -05:00
J. Nick Koston ad6b83dce4 [bp5758d] Use Apply outputs for the channels and clean up the driver (#21844) 2026-11-09 11:28:24 -05:00
J. Nick Koston 62d9f9ef26 [sm16716] Clean up the driver (#21842) 2026-11-09 11:25:49 -05:00
J. Nick Koston b3e75be525 [my9231] Clean up the driver (#21841) 2026-11-09 11:25:21 -05:00
J. Nick Koston d9797c2efb [modbus_client] Keep constant write values in shared flash tables (#20328) 2026-11-09 11:24:13 -05:00
J. Nick Koston 805392e9e2 [homeassistant] Keep the select's option storage in static buffers (#22329) 2026-11-09 11:20:11 -05:00
J. Nick Koston 9c350d0bc1 [light] Keep the LED strip buffers in static buffers (#22335) 2026-11-09 11:18:17 -05:00
J. Nick Koston 5f0e2db058 [core] Add StaticFixedVector for codegen-sized lists (#22341) 2026-11-09 11:13:53 -05:00
J. Nick Koston bd75639765 [ci] Validate changed component tests with a shared-id check (#22429) 2026-11-09 11:07:32 -05:00
J. Nick Kostonandpre-commit-ci-lite[bot] a4f844335d [ci] Catch omitted bus ids in grouped component tests (#22427)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-11-09 11:06:04 -05:00
J. Nick Koston 12117419f6 [spi] Mark the bus failed when ESP-IDF bus init fails (#22373) 2026-11-09 11:03:46 -05:00
J. Nick Koston 6cdf238568 [micro_wake_word] Act on the last start or stop request (#22393) 2026-11-09 11:02:18 -05:00
J. Nick Koston 1424c9b74b [components] Clear the comm warning after a successful read (#22424) 2026-11-09 11:00:34 -05:00
J. Nick KostonandJonathan Swoboda 62828cb550 [ci] Add BMP390, IPv6, MCP23017, PWM capture, SPI and Home Assistant wizard flows to the hardware-test skill (#22413)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-11-09 10:59:19 -05:00
J. Nick Koston 6cc96fab0d [components] Use status_comm_check for failed bus calls (#22422) 2026-11-09 10:59:06 -05:00
J. Nick Koston ee60953e67 [core] Add Component::status_comm_check (#22421) 2026-11-09 10:56:04 -05:00
J. Nick Koston a330714cf6 [ci] Add MCP2518FD, crc32 signed OTA, I2C fault and LibreTiny edge flows to the hardware-test skill (#22412) 2026-11-09 10:38:43 -05:00
J. Nick Koston 428883ed80 [nextion] Move the writer in set_writer instead of copying it (#22469) 2026-11-09 09:55:37 -05:00
J. Nick Koston 62cef67191 [display] Move the writer in set_writer instead of copying it (#22466) 2026-11-09 09:36:48 -05:00
J. Nick Koston ba423ff170 [components] Use cg.byte_span for std::span<const uint8_t> automation args (#22464) 2026-11-09 09:30:56 -05:00
esphome[bot] 2610a6a03a [ci] Refresh integration test durations (#22449)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-11-09 09:25:16 -05:00
J. Nick Koston 380660dcdb [esp32_ble_server] Size the BLE2902 value buffer to its 2 byte value (#22454) 2026-11-09 09:24:27 -05:00
J. Nick Koston be0550b1e8 [core] Inline InterruptLock on ESP8266 (#22455) 2026-11-09 09:23:06 -05:00
J. Nick Koston 935befcbe5 [esp32_ble_server] Build the write and connect triggers as callback automations (#22456) 2026-11-09 09:22:39 -05:00
J. Nick Koston 3325b16c56 [core] Add cg.byte_span for std::span<const uint8_t> (#22462) 2026-11-09 09:20:45 -05:00
J. Nick Koston d9cac9a88f [components] Define simple setters in the header (#22459) 2026-11-09 09:18:46 -05:00
J. Nick Koston 86442c8816 [components] Define simple setters in the header (sensor drivers) (#22468) 2026-11-09 09:18:10 -05:00
Jesse Hills 46f532e2bd [espidf] Remove unreachable fallback when choosing a library source directory (#22305) 2026-11-09 16:54:33 +13:00
Jesse Hills f1a1502fb6 [ci] Check stale PRs for merge conflicts every 3 hours (#22446) 2026-11-09 16:26:12 +13:00
Jesse Hills d3738e31f6 [core] Require CONF_ and UNIT_ constants to be plain string literals (#22445) 2026-11-09 02:58:49 +00:00
Jesse Hills 76d0b989e7 [core] Recommend VS Code extensions and set up the dev environment on first open (#22301) 2026-11-09 15:51:41 +13:00
Jesse Hills f7c91fad0f [ld2412] Remove unused threshold key lists (#22443) 2026-11-08 20:59:27 -05:00
Jesse Hills a7b3c34598 [ld2410] Remove obsolete threshold key errors (#22444) 2026-11-08 20:58:11 -05:00
Jesse Hills 918fe15eb8 Merge branch 'beta' into dev 2026-11-09 14:05:11 +13:00
Jesse Hills 26c4f8d40e Merge pull request #22442 from esphome/bump-2026.11.0b3
2026.11.0b3
2026-11-09 14:04:52 +13:00
Jesse Hills c097fe388e Bump version to 2026.11.0b3 2026-11-09 13:36:04 +13:00
J. Nick Koston 21267d7421 [libretiny] Interrupt on both edges for any edge pins on RTL8710B (#22403) 2026-11-09 13:36:03 +13:00
J. Nick Koston 582adc950a [libretiny] Run millis(), micros() and delays on real time on RTL8710B (#22404) 2026-11-09 13:36:03 +13:00
J. Nick Koston 613319f984 [i2c] Mask I2C interrupts after an error on classic ESP32 (#22396) 2026-11-09 13:36:02 +13:00
J. Nick Koston 4ad3cc7c0e [light] Keep power estimation state in codegen static storage (#22370) 2026-11-09 13:36:02 +13:00
J. Nick Koston a033260639 [libretiny] Read pins in interrupts without detaching the interrupt on Realtek (#22400) 2026-11-09 13:36:02 +13:00
24f23ba3db [i2s_audio] Fix APLL clock source on ESP32-P4 (#22402)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-09 13:36:02 +13:00
Roman PriesolandJ. Nick Koston 674d593f75 [wifi] Create the IPv6 link-local through lwIP on connect (#22364)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-09 13:36:02 +13:00
Jonathan Swoboda c1771e002b [ld2410][ld2412][ld2420] Drop device classes from unitless gate and light entities (#22374) 2026-11-09 13:36:02 +13:00
Jonathan Swoboda 5529e87a40 [tsl2591][seeed_mr24hpc1] Drop device classes from unitless sensors (#22375) 2026-11-09 13:36:02 +13:00
J. Nick Koston 54cae78a42 [ht16k33_char] Name the I2C bus in the component test (#22372) 2026-11-09 13:36:02 +13:00
J. Nick Koston e77a3bc54d [core] Add cg.static_buffer for config-sized static buffers (#22326) 2026-11-09 13:36:02 +13:00
guillempages af7f95a01b [ht16k33] Rename display_off method (#22352) 2026-11-09 13:36:02 +13:00
daf48ce4f2 [vl53l0x] Recover from a failed I2C read instead of stalling (#19235)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Tekla Bania <[EMAIL]>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: Tekla Bania <8668038+teklabania@users.noreply.github.com>
Signed-off-by: Tekla Bania <[EMAIL]>
2026-11-09 13:36:02 +13:00
J. Nick Koston 3de3f72e7c [libretiny] Interrupt on both edges for any edge pins on RTL8710B (#22403) 2026-11-09 13:33:53 +13:00
J. Nick Koston 1878a69187 [libretiny] Run millis(), micros() and delays on real time on RTL8710B (#22404) 2026-11-09 13:31:33 +13:00
J. Nick Koston 032c8d9618 [i2c] Mask I2C interrupts after an error on classic ESP32 (#22396) 2026-11-09 13:18:05 +13:00
Jesse Hills 36c079de78 [epaper_spi] Add Good Display GDEW075T7 7.5" model (075BN-T7-D2) (#22322) 2026-11-09 11:35:45 +13:00
J. Nick Koston bdd0a2b3b6 [core][components] Use standard helpers and ternaries for if/else blocks that pick a value (#22425) 2026-11-08 12:18:46 -06:00
David WoodhouseandJ. Nick Koston 857d437739 [tuya] Support string datapoints for fan and select devices (#22289)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-08 11:32:29 -06:00
J. Nick Koston 50d7a13e0e [gy39] Pin the test sensor to i2c_bus (#22426) 2026-11-08 10:31:33 -06:00
ishai-levy c7267c1c4a [es8388] Correct the ALC and noise gate register comments (#22096) 2026-11-08 10:18:37 -06:00
Joe f0227926a9 [canbus] Add support for MCP251xFD series of CAN FD SPI Controllers (#21951) 2026-11-08 07:07:23 -06:00
J. Nick Koston cb57120ffc [pixoo][graph][usb_audio][ld600x] Keep display and audio buffers in static buffers (#22336) 2026-11-07 22:04:53 -06:00
J. Nick Koston adcce19bba [core][esp32][esp8266][rp2][zephyr][host][libretiny] Point ISR pins at the pin instead of a heap copy (#22365) 2026-11-07 22:04:33 -06:00
J. Nick Koston 182c652ed0 [ci] Add speaker, voice and microphone flows to the hardware-test skill (#22384) 2026-11-07 22:03:22 -06:00
J. Nick Koston ff6847edc6 [tlc5947][sn74hc595] Keep output driver buffers in static or fixed storage (#22338) 2026-11-07 22:59:17 -05:00
J. Nick Koston 3a94d97b87 [fastled_base][neopixelbus] Keep the effect data in a static buffer (#22333) 2026-11-07 22:58:29 -05:00
J. Nick Koston 9dd2c8b612 [max7219] Keep the display buffer in a static buffer (#22332) 2026-11-07 22:57:45 -05:00
J. Nick Koston 1151740669 [ci] Add LED strip, sigma_delta and PIO probe flows to the hardware-test skill (#22382) 2026-11-07 21:56:25 -06:00
J. Nick Koston 44f12bec7f [ci] Add simulated I2C, SPI and parallel bus flows to the hardware-test skill (#22377) 2026-11-07 21:47:23 -06:00
J. Nick Koston af63d27de6 [ci] Add network, BLE, MQTT, HTTPS and crash report flows to the hardware-test skill (#22383) 2026-11-07 21:39:40 -06:00
J. Nick Koston 35add805c5 [tx20] Store the pulse buffer inline instead of on the heap (#22366) 2026-11-07 22:38:15 -05:00
J. Nick Koston f6d6130aca [spi] Make the null pin and null delegate static objects (#22367) 2026-11-07 22:37:37 -05:00
J. Nick Koston 4377446ab8 [pipsolar] Point polling commands at their string literals (#22368) 2026-11-07 22:34:33 -05:00
J. Nick Koston 76f55870df [light] Keep power estimation state in codegen static storage (#22370) 2026-11-07 22:34:08 -05:00
J. Nick Koston eedad14dea [i2c] Use the global Wire on ESP8266 instead of a heap TwoWire (#22371) 2026-11-07 22:31:14 -05:00
J. Nick Koston 7dd35c3217 [libretiny] Read pins in interrupts without detaching the interrupt on Realtek (#22400) 2026-11-07 21:31:05 -06:00
J. Nick Koston 7d1d940983 [voice_assistant] Fire on_end once for every requested pipeline (#22394) 2026-11-07 21:30:44 -06:00
J. Nick Koston 10410aef22 [nrf52] Add the nRF52832 and the Adafruit Feather nRF52832 (#22385) 2026-11-07 21:30:18 -06:00
J. Nick Koston e431415841 [spi] Compile the software SPI delegate only when a bus uses it (#22379) 2026-11-07 21:29:54 -06:00
J. Nick Koston 89ac55e92b [spi] Drop the heap bus object and device map (#22376) 2026-11-07 21:29:54 -06:00
J. Nick Koston dff21c88c0 [ci] Add simulated UART, RS485 and HX711 flows to the hardware-test skill (#22381) 2026-11-07 21:28:49 -06:00
J. Nick Koston 77e2f3fdf0 [ai] Keep shared code changes out of component PRs (#22408) 2026-11-07 21:17:38 -06:00
David Woodhouse c9d206f0e5 [core] Add crc32 helper (#22398) 2026-11-08 03:15:36 +00:00
David Woodhouse b938575e5f [const] Add CONF_STRING_DATAPOINT (#22411) 2026-11-07 19:52:51 -06:00
3f7373ce35 [i2s_audio] Fix APLL clock source on ESP32-P4 (#22402)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-07 19:00:46 -06:00
Stefan Kaps 7abf3fb0fb [gy39] Add the GY-39 environment sensor (#21265) 2026-11-07 15:22:13 -06:00
Bascht74andJ. Nick Koston 0c5e81236b [tcp_uart][uart_tcp] Close a quiet TCP connection (#20097)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-07 14:18:46 -06:00
Bascht74andJ. Nick Koston a9ba8e9743 [cdc_acm_uart] Claim the bridge's interfaces with uart.claim_exclusive() (#22388)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-07 13:31:03 -06:00
Roman PriesolandJ. Nick Koston 010e6eb7fc [wifi] Create the IPv6 link-local through lwIP on connect (#22364)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-07 13:05:51 -06:00
esphome[bot] 3ebcb88c5b Bump bundled esphome-device-builder to 1.28.0 (#22395) 2026-11-06 17:38:57 -06:00
45161b2e24 [mqtt] keep MQTT disconnected after mqtt.disable on ESP32 (#22321)
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-11-06 23:31:30 +00:00
f7d45afd73 [bmp3xx] Add data_rate option for continuous (normal mode) measurement (#21632)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-06 23:29:29 +00:00
5fd9273821 [rs485_frame] a generic, YAML-declarative engine for DLE/STX/ETX byte-stuffed framed serial buses (#16758)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-06 17:24:27 -06:00
jda03806andClaude Opus 5.5 ee6fbfe2f3 [bmp3xx] Warn instead of failing when a measurement cannot be started (#21633)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:21:51 -06:00
dependabot[bot] 6bb7850ed5 Bump CodSpeedHQ/action from 5.6.1 to 5.6.3 (#22386)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-06 19:30:36 +00:00
dependabot[bot] 672021db0c Bump github/codeql-action from 4.39.1 to 4.39.2 (#22387)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-06 19:28:49 +00:00
Jonathan Swoboda 417973ddf2 [ld2410][ld2412][ld2420] Drop device classes from unitless gate and light entities (#22374) 2026-11-06 14:07:05 -05:00
Jonathan Swoboda ac24fa4033 [tsl2591][seeed_mr24hpc1] Drop device classes from unitless sensors (#22375) 2026-11-06 14:06:51 -05:00
J. Nick Koston c641c903cf [ht16k33_char] Name the I2C bus in the component test (#22372) 2026-11-06 19:02:07 +00:00
awa21212andJonathan Swoboda 47ff0ebf28 [core] Accept file:// mirrors for native toolchain framework downloads (#22356)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-11-06 14:00:31 -05:00
J. Nick Koston d2d5bb623a [ci] Add BLE advertise trigger, sun, filter, LED driver, hub input, value range, EZO, TWAI and light sleep flows to the hardware-test skill (#21878) 2026-11-06 12:30:09 -06:00
e68344a30c [uart] Share uart_tcp's exclusive UART claim (#20476)
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-11-06 12:13:41 -06:00
J. Nick Koston fc685ddf96 [core] Let a static buffer fall back to a PSRAM-capable heap buffer (#22334) 2026-11-06 11:31:13 -06:00
J. Nick Koston 20de263245 [remote_receiver] Keep the pulse and RMT rings in static buffers (#22330) 2026-11-06 11:30:59 -06:00
2c2c9679bc [sx127x] Add afc_bandwidth option (#22300)
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-11-06 16:28:32 +00:00
J. Nick Koston abe85a5596 [core] Use cg.static_buffer for the in-tree static buffers (#22327) 2026-11-06 10:17:09 -06:00
J. Nick Koston 38a8e285af [thermostat] Keep the custom preset names in a flash table (#22339) 2026-11-06 11:06:21 -05:00
J. Nick Koston 343a37877b [wifi] Size the station list at code generation (#22340) 2026-11-06 11:05:15 -05:00
J. Nick Koston b2a10d4db3 [mcp23xxx_base] Clear the read warning once a read succeeds (#22067) 2026-11-06 11:00:42 -05:00
J. Nick Koston 1268e808f8 [core] Add cg.static_buffer for config-sized static buffers (#22326) 2026-11-06 10:58:02 -05:00
guillempages ad6619c006 [ht16k33] Rename display_off method (#22352) 2026-11-06 09:53:07 -06:00
J. Nick Koston d8d485d11c [ci] Flag badly named user-facing functions in the code review skill (#22323) 2026-11-05 21:50:18 -06:00
esphome[bot] 3c18fee164 Bump bundled esphome-device-builder to 1.27.0 (#22324)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-11-05 21:29:10 -06:00
Jesse Hills 9f1f52b124 [wizard] Look up the board link from a table (#22302) 2026-11-05 21:15:29 -06:00
Jesse Hills fee6a37906 [core] Remove unreachable UnicodeEncodeError handler from log formatter (#22304) 2026-11-05 21:01:22 -06:00
8adaf825bd [Ld2450] motion target icon (#22310)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-05 20:49:21 -06:00
Jesse Hills d413e5d705 Merge branch 'beta' into dev 2026-11-06 15:38:27 +13:00
Jesse Hills 3cfe8231c7 Merge pull request #22320 from esphome/bump-2026.11.0b2
2026.11.0b2
2026-11-06 15:38:06 +13:00
1c7e952354 [vl53l0x] Recover from a failed I2C read instead of stalling (#19235)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Tekla Bania <[EMAIL]>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: Tekla Bania <8668038+teklabania@users.noreply.github.com>
Signed-off-by: Tekla Bania <[EMAIL]>
2026-11-05 20:10:20 -06:00
Keith BurzinskiandClaude Fable 5.1 59c3dd35c0 [nfc] Fix out-of-bounds read in NDEF record parsing (#22276)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 14:32:54 +13:00
d83411732a [beken_irda][remote_receiver] Add BK72xx IR receiving: hardware NEC decoder and timer sampling (#21889)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-05 19:28:58 -06:00
Jesse Hills 1ff084be84 Bump version to 2026.11.0b2 2026-11-06 14:11:43 +13:00
Dckiller 041da28d53 [xiaomi_body_scale] Handle the S400 Pro 0x7FF no-weight marker (#22317) 2026-11-06 14:11:42 +13:00
J. Nick Koston 27a724c157 [mqtt] Keep retained messages from overflowing the ESP32 event queue at connect (#22248) 2026-11-06 14:11:42 +13:00
Jesse Hills 5f62432ef0 [api] Let components give wizard entities a stable role (#22295) 2026-11-06 14:11:42 +13:00
Jonathan Swoboda 21fb3c4791 [sun] Pick the next sunrise or sunset from the right day for any longitude (#22308) 2026-11-06 14:11:42 +13:00
tomaszduda23andClaude ebeb3774d3 [nrf52] Download the pinned nrfutil launcher by its versioned file name (#22314)
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-06 14:11:41 +13:00
J. Nick Koston 3bb0bc104b [esp32_ble] Silence the PACKED redefined warning from Bluedroid's private headers (#22296) 2026-11-06 14:11:41 +13:00
Jonathan Swoboda a9e3e32382 [esp32_hosted] Bump esp_hosted to 3.1.0 and fix the host restart after a coprocessor OTA (#22306) 2026-11-06 14:11:41 +13:00
J. Nick Koston e3816ac4bb [wifi] Retry at once when a scan leaves no hidden network to try (#21747) 2026-11-06 14:11:41 +13:00
J. Nick Koston e8415cdd87 [web_server_base] Apply the web server's allowed origins check to every handler (#21972) 2026-11-06 14:11:41 +13:00
J. Nick Koston b8e1699137 [wifi] Treat open networks as open in RTL87xx scan results (#21750) 2026-11-06 14:11:41 +13:00
J. Nick Koston b5fbd12b00 [wifi] Ignore the scan inside a join on RTL87xx (#21749) 2026-11-06 14:11:41 +13:00
J. Nick Koston 0acba6cc48 [wifi] Log why a join failed on RTL87xx (#21751) 2026-11-06 14:11:41 +13:00
J. Nick Koston 485b7cc4a2 [schema] Keep sibling extends when shrink inlines one schema (#22287) 2026-11-06 14:11:41 +13:00
J. Nick Koston 9a357ca9f3 [libretiny] Record abort() as a crash and reboot (#22288) 2026-11-06 14:11:41 +13:00
J. Nick Koston b9f598ff71 [core] Keep a trigger's only extra key in the language schema (#21801) 2026-11-06 14:11:41 +13:00
J. Nick Koston 183065c351 [mqtt] Cap incoming message size with max_payload_size (#22278) 2026-11-06 14:11:41 +13:00
Jonathan Swoboda 49ee4914ce [docker] Bump base image to 2026.11.0 (#22275) 2026-11-06 14:11:41 +13:00
dependabot[bot] d9dca1adf8 Bump zeroconf from 0.152.0 to 0.153.0 (#22273)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-06 14:11:41 +13:00
fb4c241bf4 [sm2135] Fix light turning off when switching color modes (#19438)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-06 14:11:41 +13:00
J. Nick Koston bfb9ca984c [logger] Report the RP2 crash record when a host opens the USB port (#22266) 2026-11-06 14:11:41 +13:00
J. Nick Koston 8d6fb44dd6 [mdns] Bump espressif/mdns to 1.15.0 (#22267) 2026-11-06 14:11:41 +13:00
J. Nick Koston ca12f6e3f3 [rp2040_pio_led_strip] Compute the PIO clock divider with its fraction (#22265) 2026-11-06 14:11:36 +13:00
J. Nick Koston 0721604da4 [rp2040_pio_led_strip] Size the DMA channel arrays for RP2350 (#22153) 2026-11-06 14:05:23 +13:00
J. Nick Koston 849aef0c10 [rp2040_pio_led_strip] Fail setup instead of panicking when no state machine or DMA channel is free (#22152) 2026-11-06 14:03:53 +13:00
J. Nick Koston 8ea8cacd95 [tfluna] Drop signal_strength device class from the signal strength sensor (#22260) 2026-11-06 14:03:53 +13:00
J. Nick Koston 5e15fef5a8 [core] Dump typed_schema types that have no keys of their own (#22258) 2026-11-06 14:03:53 +13:00
J. Nick Koston 0164f1a431 [ld2410][ld2412] Drop signal_strength device class from the gate threshold numbers (#22259) 2026-11-06 14:03:53 +13:00
J. Nick Koston 8ad61941f8 [esp32_ble] Fix build on Windows when Bluedroid's private headers are not reachable (#22257) 2026-11-06 14:03:52 +13:00
bff389208f [deep_sleep] Fix ESP32-P4 deep sleep (#22246)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-11-06 14:03:52 +13:00
Jonathan Swoboda 3ae0a0174b [bme280_base][bmp280_base] Compensate raw 0x80000 when the channel is enabled (#22236) 2026-11-06 14:03:52 +13:00
psewar 4f0b216b55 [voice_assistant] Do not treat "playback has not started" as "playback finished" (#19408) 2026-11-06 14:03:52 +13:00
Dckiller b614b0344a [xiaomi_body_scale] Handle the S400 Pro 0x7FF no-weight marker (#22317) 2026-11-05 18:04:20 -06:00
TomandJ. Nick Koston 4ad0624e0b [ds248x] Fix stale channel cache and skip buses on a failed hub (#22268)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-05 17:20:37 -06:00
J. Nick Koston 48e0af4b13 [mqtt] Keep retained messages from overflowing the ESP32 event queue at connect (#22248) 2026-11-06 11:34:41 +13:00
Jesse Hills 0918d63c83 [epaper_spi] Add GDEY0213B74 models (0213BN-B74, TTGO T5 2.13in B74) (#22129) 2026-11-06 11:21:07 +13:00
Jesse Hills b4ac701221 [api] Let components give wizard entities a stable role (#22295) 2026-11-06 11:16:09 +13:00
995fac044a [nrf52] Add nRF54LM20/NRF54L15 support and J-Link upload (#22173)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-05 22:29:40 +01:00
Ashley TheissandJ. Nick Koston 4f9812c55c [BQ25820] Added TI BQ25820 battery charge controller & MPPT component driver (#21881)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-05 15:08:09 -06:00
Jonathan Swoboda fabeb737b8 [sun] Pick the next sunrise or sunset from the right day for any longitude (#22308) 2026-11-05 15:53:40 -05:00
tomaszduda23andClaude 14b6d7fa13 [nrf52] Download the pinned nrfutil launcher by its versioned file name (#22314)
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-05 14:36:28 -06:00
Kevin Ahrendt 03e08a3f31 [sendspin] Add bits_per_sample to the media source (#22313) 2026-11-05 15:12:23 -05:00
dependabot[bot] 0ce700bfec Bump filelock from 4.2.6 to 4.2.7 (#22312)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-05 14:01:51 -06:00
J. Nick Koston ad15bf64aa [esp32_ble] Silence the PACKED redefined warning from Bluedroid's private headers (#22296) 2026-11-05 11:36:53 -06:00
Jonathan Swoboda ed456ef902 [esp32_hosted] Bump esp_hosted to 3.1.0 and fix the host restart after a coprocessor OTA (#22306) 2026-11-05 12:06:57 -05:00
J. Nick Koston 6c16a94737 [ai] Document release-cherry-pick blocks for fixes that conflict on a release branch (#22297) 2026-11-05 18:40:58 +13:00
83ace37536 [es8388] Add volume_min_db and volume_max_db (#22249)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-05 03:43:03 +00:00
J. Nick Koston 001f8e91d7 [ble_client] Fire on_notify from the BLE sensors through a callback (#21821) 2026-11-04 22:34:52 -05:00
J. Nick Koston f45c627d1e [analyze_memory] Match ROM symbol patterns only at the start of the name (#21836) 2026-11-04 22:32:57 -05:00
J. Nick Koston 9d71ae7a3b [sun] Share one elevation poll between all on_sunrise and on_sunset triggers (#21803) 2026-11-04 22:20:35 -05:00
J. Nick Koston 0955d533c0 [components] Use FrameReader in a01nyub, a02yyuw and jsn_sr04t (#21948) 2026-11-04 21:17:16 -06:00
J. Nick Koston 604e4c5ada [matrix_keypad][rc522][rdm6300][sx1509][wiegand] Keep trigger automations in an AutomationList (#21790) 2026-11-04 22:10:37 -05:00
J. Nick Koston bf611b3a18 [components] Use cg.slot_list and SLOT_LIST for existing codegen-sized registration lists (#22097) 2026-11-04 21:09:20 -06:00
J. Nick Koston 64440c0357 [core] Add cg.slot_list and SLOT_LIST for codegen-sized registration lists (#22095) 2026-11-04 21:09:20 -06:00
J. Nick Koston b1ccbd1643 [components] Read compensation inputs with sensor::source_value (#21947) 2026-11-04 22:09:04 -05:00
J. Nick Koston 00250ee6c7 [core] Inline the bit_cast fallback on toolchains without std::bit_cast (#21992) 2026-11-04 21:08:24 -06:00
J. Nick Koston 4a2ed118e8 [motion] Publish motion children through a generated hub function (#22106) 2026-11-04 21:07:13 -06:00
J. Nick KostonandJonathan Swoboda 019e173949 [thermopro_ble] Fix TP972 temperatures, TP3 battery and invalid readings, and 13-byte probe frames (#22054)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-11-05 02:48:17 +00:00
J. Nick Koston 2499ecaa51 [remote_base] Generate transmit_raw and rc_switch transmits as inline actions (#22005) 2026-11-04 20:47:10 -06:00
J. Nick Koston 82824861ae [thermopro_ble] Simplify the parser and pass the MAC address to the constructor (#22052) 2026-11-04 21:39:56 -05:00
J. Nick Koston ce35de6468 [components] Use the gpio_expander input store in every cached expander (#22057) 2026-11-04 20:29:01 -06:00
J. Nick Koston fc106564ed [bme680_bsec] Publish pending readings from fixed storage instead of a heap queue (#22193) 2026-11-04 20:16:06 -06:00
J. Nick Koston 7b380674ee [gpio_expander] Add an input store to the cached expander bases (#22056) 2026-11-04 20:14:38 -06:00
J. Nick Koston e4011bd96a [components] Publish sy6970, tca8418 and tm1638 children through cg.HubFunction (#22104) 2026-11-04 20:11:22 -06:00
J. Nick Koston 1fa3fe26bb [ezo] Replace the heap command queue with a fixed ring buffer (#21840) 2026-11-04 20:05:52 -06:00
Keith Burzinski 3de1503ec0 [api] Let clients fetch the crash recorded on the previous boot (#21596) 2026-11-05 02:04:01 +00:00
Jesse Hills c248a7d3c4 [epaper_spi] Add Good Display GDEY0583T81 5.83" model (#22143) 2026-11-04 20:58:45 -05:00
esphome[bot] 59d507197d Bump aioesphomeapi from 46.27.0 to 46.28.0 (#22290) 2026-11-05 01:38:40 +00:00
J. Nick Koston 469a459b9a [wifi] Retry at once when a scan leaves no hidden network to try (#21747) 2026-11-04 19:36:43 -06:00
J. Nick Koston f06cba700f [web_server_base] Apply the web server's allowed origins check to every handler (#21972) 2026-11-04 19:36:18 -06:00
J. Nick Koston 5b077a0a9a [wifi] Treat open networks as open in RTL87xx scan results (#21750) 2026-11-04 19:35:13 -06:00
J. Nick Koston 51b88cee9b [wifi] Ignore the scan inside a join on RTL87xx (#21749) 2026-11-04 20:20:34 -05:00
J. Nick Koston da5aba76c7 [wifi] Log why a join failed on RTL87xx (#21751) 2026-11-04 20:19:37 -05:00
J. Nick Koston 765ca4a085 [schema] Keep sibling extends when shrink inlines one schema (#22287) 2026-11-04 19:19:26 -06:00
J. Nick Koston 9b24d1afda [libretiny] Record abort() as a crash and reboot (#22288) 2026-11-04 18:55:07 -06:00
esphome[bot]andJ. Nick Koston 549b56197f Bump aioesphomeapi from 46.26.0 to 46.27.0 (#22279)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-04 18:38:46 -06:00
J. Nick Koston c390a46f1c [core] Keep a trigger's only extra key in the language schema (#21801) 2026-11-04 17:40:01 -06:00
JoeandJ. Nick Koston cdc0d69e4c [canbus] Add support for CAN FD frames (#21950)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-04 17:27:20 -06:00
Michael Hansen 92723c905c [voice_assistant] Stream buffered wake word audio for verification (#21885) 2026-11-04 16:47:29 -06:00
J. Nick Koston ca8be3e953 [bme68x_bsec2] Publish pending readings from fixed storage instead of a heap queue (#22191) 2026-11-04 17:27:31 -05:00
J. Nick Koston 3f330fcdb2 [mqtt] Cap incoming message size with max_payload_size (#22278) 2026-11-04 17:24:38 -05:00
J. Nick Koston 95278ea4d8 [pipsolar] Fix flaky switch integration test (#22282) 2026-11-04 17:23:26 -05:00
Jonathan Swoboda a6b2d20038 [docker] Bump base image to 2026.11.0 (#22275) 2026-11-04 17:15:04 -05:00
Keith BurzinskiandClaude Fable 5.1 a37e3bba26 [nfc] Fix out-of-bounds read in NDEF record parsing (#22276)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 20:31:55 +00:00
puddlyandJ. Nick Koston e83e892514 [usb_host] Read iInterface for connected interfaces (#21781)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-04 14:04:52 -06:00
esphome[bot] e1c781871b Bump aioesphomeapi from 46.25.1 to 46.26.0 (#22274) 2026-11-04 19:54:30 +00:00
dependabot[bot] f926fabbd8 Bump pytest-asyncio from 1.4.1 to 1.4.1.post1 (#22272)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-04 13:46:21 -06:00
124969267d [bthome_mithermometer] Add opt-in replay protection (#19957)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-04 13:45:33 -06:00
dependabot[bot] de24754b4d Bump zeroconf from 0.152.0 to 0.153.0 (#22273)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-04 19:32:33 +00:00
dependabot[bot] 9b138d17de Bump platformdirs from 4.14.2 to 4.14.3 (#22271)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-04 19:32:10 +00:00
J. Nick Koston c64d5f149f [ai] Prefer generated decoding for fixed register fields in the codegen skill (#22093) 2026-11-04 12:52:42 -06:00
J. Nick Koston d29268cc00 [remote_base] Dispatch received frames from a span (#22269) 2026-11-04 12:47:35 -06:00
J. Nick Koston 9969cb3de1 [bq25186] Generate the status entity decoding in code generation (#22091) 2026-11-04 12:43:38 -06:00
J. Nick Koston c9ca5ba910 [canbus] Pass received frames to callbacks without a heap vector per frame (#22008) 2026-11-04 12:41:24 -06:00
J. Nick Kostonandpre-commit-ci-lite[bot] 6d3239bf43 [store_yaml] Embed user YAML in firmware for recovery (#16445)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-11-04 12:34:44 -06:00
295de90303 [sm2135] Fix light turning off when switching color modes (#19438)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-04 12:17:55 -06:00
J. Nick Koston 2473ddcf19 [logger] Report the RP2 crash record when a host opens the USB port (#22266) 2026-11-04 12:17:40 -06:00
J. Nick Koston 26303448d7 [bq25186] Add tests for the status binary and text sensors (#22090) 2026-11-04 12:12:47 -06:00
J. Nick Koston 3755a7c970 [core] Add cg.HubFunction to generate one function per hub from child statements (#22102) 2026-11-04 12:12:46 -06:00
J. Nick Koston 76a0c75d27 [mdns] Bump espressif/mdns to 1.15.0 (#22267) 2026-11-04 13:11:22 -05:00
J. Nick Koston 55cb8ea03d [core] Let a byte payload lambda return a fixed-size buffer (#22043) 2026-11-04 12:06:39 -06:00
J. Nick Koston 58419f2207 [canbus] Register on_frame automations as callbacks instead of trigger components (#21795) 2026-11-04 11:58:57 -06:00
J. Nick Koston 1854bd12bb [rp2040_pio_led_strip] Compute the PIO clock divider with its fraction (#22265) 2026-11-04 16:45:04 +00:00
J. Nick Koston d927597771 [rp2040_pio_led_strip] Size the DMA channel arrays for RP2350 (#22153) 2026-11-04 16:44:16 +00:00
J. Nick Koston 20bd5f5316 [rp2040_pio_led_strip] Fail setup instead of panicking when no state machine or DMA channel is free (#22152) 2026-11-04 10:32:03 -06:00
J. Nick KostonandJonathan Swoboda 3b862f03f1 [rp2040_pio_led_strip] Load each PIO program once per PIO without a heap map (#22151)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-11-04 10:31:12 -06:00
J. Nick Koston c94cb79f65 [it8951] Use StaticRingBuffer for the op queue (#22263) 2026-11-04 10:31:04 -06:00
J. Nick Koston bffe06211a [core] Add push_front to StaticRingBuffer (#22262) 2026-11-04 10:31:04 -06:00
J. Nick Koston e07be6bef5 [tfluna] Drop signal_strength device class from the signal strength sensor (#22260) 2026-11-04 10:09:31 -06:00
J. Nick Koston c4100d5b35 [components] Use setup_check() in the remaining setup failure paths (#22255) 2026-11-04 10:09:00 -06:00
J. Nick Koston 781f4330ba [core] Dump typed_schema types that have no keys of their own (#22258) 2026-11-04 10:08:42 -06:00
J. Nick Koston d20e3b3bb5 [api] Make the event order backpressure test wait until the pipe is full (#22112) 2026-11-04 11:08:18 -05:00
J. Nick Koston b764b18733 [ld2410][ld2412] Drop signal_strength device class from the gate threshold numbers (#22259) 2026-11-04 11:03:21 -05:00
2d74ef7b29 [sx1509] Add configurable led_driver_frequency option (#20285)
Co-authored-by: Claude <noreply@anthropic.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>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-04 09:30:35 -06:00
J. Nick Koston 562614d290 [esp32_ble] Fix build on Windows when Bluedroid's private headers are not reachable (#22257) 2026-11-04 09:28:41 -06:00
8a5c5a51f4 [remote_base] Switch Toto to little endian interpretation (#22254)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-04 15:22:59 +00:00
J. Nick Koston b656f2f9cb [bl0942][d01][daly_bms][dht][dht12][micronova][rc522][rp2][shelly_dimmer][radon_eye_rd200] Use sum_bytes and xor_bytes for checksums (#22209) 2026-11-04 08:49:23 -06:00
Reinout Heeck af3d283825 [remote_base] update Toto to measured timings (#22253) 2026-11-04 08:33:26 -06:00
eeefe3eb14 [deep_sleep] Fix ESP32-P4 deep sleep (#22246)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-11-04 09:18:06 -05:00
J. Nick Koston 1085d1bf3a [components] Use put_be and put_le for integer splits (pca9554 to toshiba) (#22216) 2026-11-04 09:17:49 -05:00
J. Nick Koston af34c1d009 [components] Use put_be and put_le for integer splits (ecocomfort2 to modbus_controller) (#22215) 2026-11-04 09:17:26 -05:00
J. Nick Koston 6536d88837 [bl0942][daly_bms][d01][dht12] Add checksum unit tests (#22208) 2026-11-04 09:15:59 -05:00
Francesco PaceandJ. Nick Koston d7c9cc6b63 [wifi_info] Add channel sensor (#22142)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-04 08:12:21 -06:00
J. Nick Koston 6dd111b1de [components] Use put_be and put_le for integer splits (ade7953_i2c to ezsp_proxy_tap) (#22214) 2026-11-04 08:03:09 -06:00
J. Nick Koston 4969f643cd [components] Use setup_check() in setup failure paths (#22182) 2026-11-04 08:02:30 -06:00
Jesse Hills ae31989bd8 Merge branch 'beta' into dev 2026-11-04 22:33:16 +13:00
Jesse Hills 49f3133c4f Merge pull request #22247 from esphome/bump-2026.11.0b1
2026.11.0b1
2026-11-04 22:32:37 +13:00
Clyde Stubbs d07311b8f1 [core] Move min/max volume consts to components/const (#22250) 2026-11-04 04:39:20 +00:00
J. Nick Koston 75d2cd10bf [qmp6988] Use update_register for the CTRLMEAS register (#22218) 2026-11-03 22:43:19 -05:00
J. Nick Koston 5f7fd7692d [xgzp68xx] Use update_register for the oversampling config (#22221) 2026-11-03 22:43:01 -05:00
J. Nick Koston f960d99d66 [sx1509] Add a 16-bit update_register helper for the register pairs (#22223) 2026-11-03 22:42:37 -05:00
J. Nick Koston ab4e3b42f5 [opt3001] Avoid heap allocation in the read path (#22238) 2026-11-03 22:41:40 -05:00
J. Nick Koston 5c6a7a89c8 [rx8111] Keep boot retry callback in std::function inline buffer (#22239) 2026-11-03 22:39:38 -05:00
J. Nick Koston be25249209 [ai] Keep scheduler lambda captures within the std::function small buffer (#22240) 2026-11-03 22:37:53 -05:00
J. Nick Koston ffab9a81e7 [ld6002b] Keep the wake pulse frame in members so its timeout does not allocate (#22242) 2026-11-03 22:36:45 -05:00
Jonathan Swoboda bea7994c9f [bme280_base][bmp280_base] Compensate raw 0x80000 when the channel is enabled (#22236) 2026-11-03 22:33:26 -05:00
J. Nick Koston 6c8c105449 [bme68x_bsec2] Share BSEC test double types with bme680_bsec to fix the all-components C++ unit test build (#22245) 2026-11-04 16:08:58 +13:00
J. Nick Koston ca7401750d [ld6002b] Add unit tests for the wake pulse deferred write (#22241) 2026-11-03 22:02:59 -05:00
Jesse Hills cfb98dc580 Bump version to 2026.12.0-dev 2026-11-04 15:54:17 +13:00
psewar 1065312345 [voice_assistant] Do not treat "playback has not started" as "playback finished" (#19408) 2026-11-03 20:44:21 -06:00
Bascht74andJ. Nick Koston ca291a9724 [uart] Check devices on a forwarding UART against its source's settings (#20200)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-04 01:26:42 +00:00
J. Nick Koston 38ed6919f8 [bme68x_bsec2] Share BSEC test double types with bme680_bsec to fix the all-components C++ unit test build (#22245) 2026-11-03 19:25:55 -06:00
Jesse Hills 9a5a8f9822 Bump version to 2026.11.0b1 2026-11-04 14:20:07 +13:00
J. Nick Koston 9dea935499 [web_server_base] Count the HTTP server's sockets in one place (#21971) 2026-11-04 13:50:59 +13:00
J. Nick Koston 2d73971fcc [web_server_idf] Send a default header once when a response sets it (#21974) 2026-11-03 18:28:29 -06:00
J. Nick Koston 5d511a1bd1 [sim800l] Pass callback strings by const reference (#22203) 2026-11-03 18:17:08 -06:00
14b190ab8f [esp32_rmt] Check that RMT components fit the chip's RMT channels (#22172)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-03 18:11:19 -06:00
J. Nick Koston c50a5a0879 [core] Give apply action lambdas their arguments by value again (#22011) 2026-11-04 13:02:32 +13:00
J. Nick Koston 75b9f8de1c [esp32_ble_client] Ignore a disconnect event from the previous link during a new connection (#21918) 2026-11-04 12:59:16 +13:00
J. Nick Koston 7637cdc162 [web_server] Keep a custom js_url/css_url on AP only WiFi (#21879) 2026-11-04 12:58:34 +13:00
J. Nick Koston 891ebdaaaa [espota2] Make the probe deadline test deterministic (#22234) 2026-11-03 17:50:38 -05:00
J. Nick Koston a01b213ab4 [bme68x_bsec2] Add unit tests for publishing readings (#22189) 2026-11-03 17:49:42 -05:00
J. Nick Koston a21268f641 [bme680_bsec] Add unit tests for publishing readings (#22190) 2026-11-03 17:49:13 -05:00
J. Nick Koston 7813ea1c7f [sim800l] Add integration test for the on_* automation arguments (#22202) 2026-11-03 17:48:31 -05:00
J. Nick Koston 82a375907d [xl9535] Use update_register and status_comm_result for pin access (#22217) 2026-11-03 17:47:13 -05:00
J. Nick Koston 9591e34722 [lps22] Use update_register for the one-shot trigger (#22219) 2026-11-03 17:46:18 -05:00
J. Nick Koston 408bd8820a [cap1188] Use update_register and check the touch status read (#22220) 2026-11-03 17:45:32 -05:00
J. Nick Koston 07a175692a [components] Replace local byte-order write helpers with put_be and put_le (#22212) 2026-11-03 22:14:32 +00:00
J. Nick Koston 94d0113f66 [core] Add put_be and put_le helpers to write integers into byte buffers (#22211) 2026-11-03 16:02:02 -06:00
09d3b645a3 [i2s_audio] Fix sample-rate-dependent microphone DC correction (#22130)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-03 21:45:41 +00:00
Adam Matthews 940f87056d [cse7766] Log the clamped power factor and reactive power at verbose level (#22224) 2026-11-03 14:00:28 -06:00
dependabot[bot] 6f284d171a Bump docker/setup-buildx-action from 4.4.0 to 4.4.1 in the docker-actions group (#22229)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-03 13:48:16 -06:00
dependabot[bot] a7a4473b32 Bump filelock from 4.2.4 to 4.2.6 (#22228)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-03 13:48:06 -06:00
dependabot[bot] 8092a23553 Bump pytest-codspeed from 5.0.3 to 5.0.4 (#22227)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-03 13:47:57 -06:00
dependabot[bot] 1a260e4c57 Bump zeroconf from 0.151.5 to 0.152.0 (#22226)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-03 13:47:48 -06:00
dependabot[bot] 8a30af4ad1 Bump platformdirs from 4.14.0 to 4.14.2 (#22225)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-03 13:47:41 -06:00
cad297866f [speaker] Reduce startup buffering for announcements (#21091)
Co-authored-by: andytrofimov <26643796+andytrofimov@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-03 14:04:36 -05:00
Petapton ff254ae9d8 [daikin_madoka] Add new Daikin Madoka integration (#5951) 2026-11-03 16:00:39 +00:00
J. Nick Koston f5ab5febb4 [wifi] Keep manual IP settings in flash (#21708) 2026-11-02 16:39:40 -10:00
J. Nick Koston 3d95a0ba33 [libretiny] Report the previous boot's crash on RTL8710B (#21752) 2026-11-02 16:39:17 -10:00
J. Nick Koston d8bfae306f [core] Add AutomationList for hubs that trigger their own automations (#21788) 2026-11-02 16:38:22 -10:00
J. Nick Koston 78d2fdbfc0 [wifi] Keep scans working after a join with a BSSID on RTL87xx (#21754) 2026-11-02 16:37:33 -10:00
J. Nick Koston 4af8e50dca [ai] Point reviewers at existing helpers (#22178) 2026-11-02 21:24:24 -05:00
J. Nick Koston 93d00b5920 [rx8111] Keep the scheduled event poll out of the unit tests (#22179) 2026-11-02 21:24:03 -05:00
J. Nick Koston 3d4e20f574 [components] Build enum options with options(prefix=) in ld600x and rx8111 (#22180) 2026-11-02 21:23:22 -05:00
J. Nick Koston be3113e7d5 [m5stack_unit_bldc] Declare child entities with the SUB_* macros (#22181) 2026-11-02 21:23:02 -05:00
J. Nick Koston e236c741db [core] Add setup_check() (#22177) 2026-11-02 21:22:44 -05:00
iskyguy ed4db4b20e [sht4x] Expose serial number as a text sensor (#22176) 2026-11-02 14:20:42 -10:00
iskyguy 51db039f59 [sht4x] Increase serial number response wait to 10 ms (#22183) 2026-11-02 13:45:30 -10:00
dependabot[bot] a24b994a5e Bump esphome/workflows/.github/workflows/lock.yml from 2026.10.2 to 2026.10.3 (#22168)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-02 16:05:58 -06:00
dependabot[bot] 912e9dedae Bump esphome/workflows/.github/workflows/stale.yml from 2026.10.2 to 2026.10.3 (#22169)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-02 16:05:44 -06:00
dependabot[bot] 47a526d35c Bump astral-sh/setup-uv from 10.3.2 to 10.3.3 in /.github/actions/restore-python (#22167)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-02 15:40:35 -06:00
Jesse Hills 389e782e78 Merge branch 'release' into dev 2026-11-03 10:30:47 +13:00
Jesse Hills 9a78022ded Merge pull request #22174 from esphome/bump-2026.10.2
2026.10.2
2026-11-03 10:30:22 +13:00
Jesse Hills c11c788d25 Revert "[lvgl] Make a scrollable container automatically clickable (#22094)"
This reverts commit 8e87cfc17e.
2026-11-03 10:11:19 +13:00
Clyde Stubbs d516fd5c92 [http_request] Fix macOS host build with the precompiled header (#21705) 2026-11-03 10:00:14 +13:00
Clyde StubbsandClaude 8e87cfc17e [lvgl] Make a scrollable container automatically clickable (#22094)
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-03 09:59:38 +13:00
Jesse Hills f0c4dc67e7 Bump version to 2026.10.2 2026-11-03 09:51:12 +13:00
J. Nick Koston 863cb54982 [ota] Work around the LibreTiny OTA issue that bricks RTL8710B on a compressed update (#22111)
Squash of #22111 for the release branch, with the conflicts resolved:
release has no UpdateLock in the LibreTiny OTA backend, so abort() has no
lock check and a successful end() clears wrote_; release negotiates inline
in perform_ota(), so the RTL8710B check is made there.
2026-11-03 09:50:22 +13:00
J. Nick Koston 64b621f8f1 [bluetooth_proxy] Release held connections when a new client replaces the subscriber (#22077) 2026-11-03 09:43:51 +13:00
Jonathan Swoboda 7d29bdf177 [host] Offer the built program as a download in the Device Builder (#22114) 2026-11-03 09:43:51 +13:00
J. Nick Koston 86a0cdfb7a [core] Start the Apple Silicon cc1plus wrapper through sh when the Python path has a space (#21967) 2026-11-03 09:43:51 +13:00
cf5519615a [esp32][audio_http] Keep TLS 1.3 working (#21786)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-03 09:43:51 +13:00
dependabot[bot] 741310ef30 Bump prek from 0.5.7 to 0.5.9 (#22165) 2026-11-02 10:43:23 -10:00
dependabot[bot] f96c0c7d0e Bump CodSpeedHQ/action from 5.6.0 to 5.6.1 (#22166) 2026-11-02 10:40:54 -10:00
dependabot[bot] b18b7fe7a1 Bump astral-sh/setup-uv from 10.3.2 to 10.3.3 (#22170) 2026-11-02 10:40:34 -10:00
Jesse HillsandJ. Nick Koston 6ea4ed0868 [CI] Adopt gh actions-lock for workflow action pinning (#22138)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-03 08:50:25 +13:00
Jesse Hills a78a80c0d1 [epaper_spi] Fix SSD1681 partial updates alternating between two images (#22145) 2026-11-03 07:01:17 +13:00
Jakub SztanderaandJ. Nick Koston 4206de5ed0 [ledc] Correct PWM duty scaling (#22160)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-02 11:04:48 -06:00
J. Nick Koston ae54e8b696 [esp32_ble] Parse Bluedroid hex strings without sscanf (#22081) 2026-11-02 09:23:36 -06:00
J. Nick Kostonandpre-commit-ci-lite[bot] b252c90a5b [api] Add tests for Home Assistant state subscriptions (#22155)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-11-02 09:39:21 -05:00
J. Nick Koston d68674597e [const] Move shared charger config keys to components/const (#22161) 2026-11-02 09:34:38 -05:00
J. Nick Koston 57a5e54118 [uptime] Keep the text sensor format in flash (#22147) 2026-11-02 09:34:00 -05:00
J. Nick Koston 69d96a4707 [ota] Work around the LibreTiny OTA issue that bricks RTL8710B on a compressed update (#22111) 2026-11-02 04:33:18 -10:00
J. Nick Koston 52fedcbe97 [components] Log byte dumps with format_hex_pretty_to (#22149) 2026-11-02 09:01:31 -05:00
J. Nick Koston d2b635cd92 [uptime] Add unit tests for the uptime text sensor format (#22146) 2026-11-02 09:00:36 -05:00
J. Nick Koston 6d5305f1fe [ci] Show why a C++ unit test build raised (#22140) 2026-11-02 09:00:16 -05:00
esphome[bot] 354660cafc [ci] Refresh integration test durations (#22150) 2026-11-01 19:54:11 -10:00
chunckus-aurelius e5f6dbe6bf [motion] Add shared tap and double tap support (#22144) 2026-11-02 16:35:11 +11:00
Giorgio Aresu ab95d504fa [mipi_rgb] Update pins for ZX3D95CE01S-TR-4848 (#22134) 2026-11-02 11:55:40 +11:00
Francesco Pace 2c5cb6b063 [espectre] Update to SDK 3.1.0; add runtime config entities and CSI diagnostics (#22132) 2026-11-01 13:29:40 -10:00
Jesse Hills 23e66187a0 [epaper_spi] Add Good Display GDEY029T94 2.9" model (#22128) 2026-11-02 12:18:13 +13:00
esphome[bot] acaf582bf6 Bump aioesphomeapi from 46.25.0 to 46.25.1 (#22137) 2026-11-01 22:59:41 +00:00
2019ebb49f [ht16k33_char] Add support for HT16K33 based character displays (#8520)
Co-authored-by: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-01 12:38:23 -10:00
J. Nick Koston dddacfa239 [libretiny] Flag a BK7238 board layout its bootloader will not apply updates from (#21743) 2026-11-01 12:16:26 -10:00
Ardumine 174016df57 [udp] Add Zephyr/nRF52 IPv6 support (#17838) 2026-11-01 12:15:21 -10:00
J. Nick Koston 4d50e1ba22 [bluetooth_proxy] Release held connections when a new client replaces the subscriber (#22077) 2026-11-01 12:04:02 -10:00
J. Nick Koston 956a633c48 [core] Explain a failed clone of a missing or private repository (#22074) 2026-11-01 12:03:44 -10:00
J. Nick Koston 8a62328276 [core] Let module platforms inherit their component's FILTER_SOURCE_FILES (#22072) 2026-11-01 12:03:27 -10:00
J. Nick Koston 041979d406 [core] Move owning_args from nextion into automation (#21893) 2026-11-01 17:02:44 -05:00
J. Nick Koston 956fff4b44 [my9231][sm16716] Reject channels past num_channels (#21855) 2026-11-01 12:02:31 -10:00
J. Nick Koston 415eb22a2c [output] Let final_validate_unique_channel check a hub's channel count (#21854) 2026-11-01 12:02:31 -10:00
J. Nick Koston eec1a0a690 [core] Add StringRef::find for std::string and StringRef needles (#21895) 2026-11-01 12:02:00 -10:00
J. Nick Koston a4ed37bd79 [canbus] Add an integration test for on_frame filtering (#21792) 2026-11-01 17:00:08 -05:00
J. Nick Koston 9317c021f5 [interval] Compile the startup delay out of intervals that don't use it (#21799) 2026-11-01 16:59:52 -05:00
J. Nick Koston 84b4ffeddb [binary_sensor] Put the settle filter flag in the base class padding (#21800) 2026-11-01 16:58:57 -05:00
J. Nick Koston 3ac882b097 [sensor] Test delta filter with min and max together and a percentage max (#21812) 2026-11-01 16:58:23 -05:00
J. Nick Koston 4027857ab2 [cst816][ft5x06] Remove unused button listener declarations (#22065) 2026-11-01 16:52:35 -05:00
J. Nick Koston c16f6d21ef [sdp3x] Print the model once in dump_config and quiet the runtime logs (#22045) 2026-11-01 16:49:18 -05:00
J. Nick Koston 730dda3d54 [components] Publish child entities guarded by a bare pointer check with PUBLISH_STATE (#22082) 2026-11-01 21:48:54 +00:00
J. Nick Koston 7ed387b660 [thermostat] Build triggers, settings and mode checks from tables (#22050) 2026-11-01 16:48:07 -05:00
J. Nick Koston ae0850f159 [components] Declare entity pointers with the SUB_* macros (#22051) 2026-11-01 16:47:33 -05:00
J. Nick Koston bfb4ad7b02 [ade7880] Build the channels from a table in code generation (#22076) 2026-11-01 16:44:25 -05:00
J. Nick Koston b76f491eef [core] Add tests for component.update, suspend and resume codegen (#21985) 2026-11-01 16:38:35 -05:00
J. Nick Koston 0ac52f42ad [core] Add tests for how apply actions call user lambdas (#22007) 2026-11-01 16:38:23 -05:00
J. Nick Koston 481b39787f [api] Add tests for user service code generation (#22009) 2026-11-01 16:38:07 -05:00
J. Nick Koston fd0ca9316a [mqtt] Add tests for mqtt.publish_json code generation (#22025) 2026-11-01 16:37:52 -05:00
J. Nick Koston f60abbf84f [components] Publish the remaining child entities with PUBLISH_STATE (#22079) 2026-11-01 16:36:42 -05:00
J. Nick Koston e4715443b0 [components] Add tests for the touchscreen key binary sensors (#22063) 2026-11-01 16:35:47 -05:00
J. Nick Koston 5ebf08d70e [thermostat] Add tests pinning validation messages and codegen order (#22049) 2026-11-01 16:35:08 -05:00
J. Nick Koston 3a5cbb423c [light] Use cg.add_optional for optional setters in cwww, rgbww and tuya (#22048) 2026-11-01 16:34:52 -05:00
J. Nick Koston 87b7205397 [thermopro_ble] Add parser unit tests (#22047) 2026-11-01 16:34:32 -05:00
J. Nick Koston d85b41be44 [script] Add tests for script code generation and synchronous scripts (#22036) 2026-11-01 16:34:08 -05:00
J. Nick Koston efe7a5a7de [binary_sensor] Add tests for the click and double click triggers (#22031) 2026-11-01 16:33:51 -05:00
J. Nick Koston 12ae7a2487 [api] Add tests pinning homeassistant.action and event code generation (#22030) 2026-11-01 16:33:35 -05:00
J. Nick Koston 2bf52e72ec [tmp102][packet_interface] Use the global App in the test fixtures (#22083) 2026-11-01 16:31:59 -05:00
J. Nick Koston df519ca14c [pipsolar] Publish an unknown state instead of a bogus value for missing fields (#22084) 2026-11-01 16:31:34 -05:00
J. Nick Koston c22f1d64c5 [components] Use the global App in the remaining test fixtures (#22085) 2026-11-01 16:29:41 -05:00
J. Nick Koston f3c828b71f [socket] Size Zephyr's poll table from the socket budget (#22089) 2026-11-01 16:29:11 -05:00
J. Nick Koston 506cafd7ef [core] Build the follow entity publish statement from codegen expressions (#22103) 2026-11-01 16:24:02 -05:00
Jesse Hills 5c780ed5f5 [mipi_spi] Remove redundant pca9554 requires from Waveshare SPD2010 model (#22125) 2026-11-02 10:19:26 +13:00
Jonathan Swoboda 19ea009546 [host] Offer the built program as a download in the Device Builder (#22114) 2026-11-01 15:25:54 -05:00
Jonathan Swoboda 4219a48408 [host] Add use_address so OTA and logs can reach a host device (#22115) 2026-11-01 15:25:17 -05:00
tomaszduda23andClaude 2cdcdd82f8 [nrf52] Deprecate framework versions older than 2.9.2 (#22122)
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-01 21:11:26 +01:00
Craig AndrewsandScott Gusler 45116a8f3e [epaper_spi] - Add support for the XSRUPB-7.5in-bwr panel (#19654)
Co-authored-by: Scott Gusler <812172+mutilator@users.noreply.github.com>
2026-11-02 07:06:34 +11:00
tomaszduda23andClaude 3bd76e1176 [nrf52] Warn when the Adafruit bootloader is older than 0.11.0 (#22119)
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-01 21:01:22 +01:00
tomaszduda23andClaude dc324dd2cb [nrf52][zephyr] Fix building LVGL on nRF52 (#21554)
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-01 21:00:34 +01:00
Jesse Hills e5e068ff20 [mipi_spi] Add generic SPD2010 model and SenseCAP Watcher board (#21354) 2026-11-02 08:56:23 +13:00
Clyde StubbsandClaude 0548ad7a72 [lvgl] Make a scrollable container automatically clickable (#22094)
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-01 08:47:34 -10:00
Giorgio Aresu 27aa91bb9d [mipi_rgb] Add GC9503V and ZX3D95CE01S-TR-4848 (#21588) 2026-11-01 20:19:31 +11:00
Remco van Essenandclydebarrow 7ae4ff99ce [runtime_image] Decode progressive and SOF1 JPEGs with JPEGDEC (#21784)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-11-01 20:08:43 +11:00
Mark Dietzer 36c666c8a9 [usb_host] Size the USB FIFO for periodic packets on the ESP32-P4 (#21739) 2026-10-31 18:24:20 -10:00
J. Nick Koston bf86c11e44 [lis3dh][esp32_hosted] Move CONF_ACTIVE_HIGH to the shared component constants (#22088) 2026-10-31 17:48:18 -10:00
chunckus-aurelius e5f2060262 [lis2dw12][msa3xx] Move the tap constants to the shared component constants (#22087) 2026-10-31 17:08:19 -10:00
ishai-levy 107a573ffb [es8388] Add configurable ADC PGA gain (#21363) 2026-10-31 17:07:23 -10:00
david-collett d902d1e84f [esp32_ble_server] Add secure connection support (#17562) 2026-10-31 17:05:56 -10:00
andytrofimov da6ab5ec1e [audio] Release partial HTTP audio reads during synthesis gaps (#21090) 2026-10-31 17:02:13 -10:00
Jeff Brown e8fe6984b7 [light] Tidy up addressable light setup (#22078) 2026-10-31 15:38:15 -10:00
William HowellandJ. Nick Koston d4d3df819c [fujitsu_general] Add buttons for the vane step and economy commands (#14271)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-01 00:25:22 +00:00
Clyde Stubbs fc921d0a96 [rtttl] Fade speaker notes in and out to stop clicks (#22075) 2026-11-01 11:25:09 +11:00
Clyde Stubbs 910c42a8bc [mipi_dsi] fix fill(), minimise DMA underruns (#21638) 2026-11-01 11:06:37 +11:00
fea23e7dec [light] Refactor ESPColorView and related types (#21577)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-31 13:59:23 -10:00
Clyde Stubbs 76912c3a9b [rtttl] Build speaker samples from a sine table (#22073) 2026-11-01 10:58:16 +11:00
195749fb02 [ledc] Add hardware duty dithering for supported ESP32 variants (#21320)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-31 13:54:32 -10:00
J. Nick Koston cc65a82a1e [components] Turn platform packages with only __init__.py into modules (#22061) 2026-10-31 13:24:00 -10:00
4a16cf1783 [m5stack_unit_bldc] Add M5Stack M5Unit-BLDC platform support (#18086)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-31 13:22:11 -10:00
9dd043160a [rtttl] Build speaker samples in single precision (#22059)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-31 13:21:03 -10:00
J. Nick Koston 62264c9695 [ms8607] Use the global App in the lifecycle test (#22071) 2026-10-31 13:09:10 -10:00
90e3f57adb Bump aioesphomeapi from 46.24.0 to 46.25.0 (#22046)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: bdraco <663432+bdraco@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-31 11:47:00 -10:00
Greg CormierandJ. Nick Koston c81754be78 [xgzp68xx] Add XGZP6899DC variant (#21779)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-31 10:01:44 -10:00
ccf437271e [ms8607] Fix a crash without a humidity sensor and skip unconfigured readings (#16487)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-31 09:56:46 -10:00
dependabot[bot] 63d2800aae Bump pytest from 9.1.1 to 9.2.0 (#22040)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 19:54:56 +00:00
dependabot[bot] 942ca93b6d Bump platformdirs from 4.13.6 to 4.14.0 (#22042)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 19:54:53 +00:00
dependabot[bot] bb05854474 Update tzdata requirement from >=2026.5 to >=2026.6 (#22041)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 19:54:31 +00:00
Mikkel Jeppesen 5f5a81434d [panasonic_ir] Add Panasonic AC IR climate component (#21631) 2026-10-31 07:02:55 -10:00
Jonathan Swoboda b576063d15 [ethernet] Bump ESP-IDF ethernet driver components (#21982) 2026-10-31 10:25:11 -04:00
J. Nick Koston c1a068220f [core] Add tests for on_boot code generation and ordering (#21988) 2026-10-31 08:47:46 -04:00
J. Nick Koston 9aceee001e [display] Add tests for display.page.show code generation (#21995) 2026-10-31 08:47:13 -04:00
J. Nick Koston 176ab8433e [deep_sleep] Add tests for deep_sleep.enter code generation (#21997) 2026-10-31 08:46:31 -04:00
J. Nick Koston b6460a1904 [light] Add tests for dim_relative and effect next/previous code generation (#21999) 2026-10-31 08:46:12 -04:00
J. Nick Koston 05949b7e3a [remote_base] Add tests for transmit_raw and rc_switch transmit code generation (#22000) 2026-10-31 08:45:49 -04:00
John 1f70f86b47 [atm90e32] Add configurable thresholds (#15244) 2026-10-30 22:50:06 -10:00
c50d09ee1d [atm90e32] Verify gain calibration writes (#18731)
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>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-30 22:05:01 -10:00
Rick van Hattem e87e7abbcd [ld6004] Add switch platform (#19845) 2026-10-30 19:45:28 -10:00
Rick van Hattem f60408208e [ld6004] Add number platform (#19843) 2026-10-30 19:35:36 -10:00
Rick van Hattem 1d94b351ff [ld6004] Add button platform (#19846) 2026-10-31 05:16:37 +00:00
esphome[bot] 563ba82a95 Bump aioesphomeapi from 46.23.0 to 46.24.0 (#21983) 2026-10-30 19:14:17 -10:00
Rick van Hattem 7995cecca9 [ld6004] Add select platform (#19844) 2026-10-30 19:09:37 -10:00
Clyde StubbsandClaude Opus 5.5 d47582b0c9 [core] Generate if, choose, while and repeat as C++ control flow (#21980)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:53:33 +11:00
Clyde StubbsandClaude Opus 5.5 3a0520245d [core] Run callback automations directly when every action is inline (#21979)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:53:33 +11:00
Clyde StubbsandClaude Opus 5.5 ea40fba54e [core] Merge adjacent simple actions into one generated function (#21978)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:53:33 +11:00
2563605fe7 [ld6004] Add text_sensor platform (#19842)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-30 18:03:18 -10:00
J. Nick Koston 96dc1fbaf3 [sm10bit_base] Clean up the driver base (#21843) 2026-10-30 17:41:53 -10:00
7ee82fbc90 [ld6004] Add sensor platform (#19841)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-30 17:40:09 -10:00
J. Nick Koston c82e275eee [sensor][binary_sensor] Pin the output of stateless sensor filters and binary sensor delay filters (#21828) 2026-10-30 23:24:40 -04:00
J. Nick Koston f999c4ed1b [ruuvitag][atc_mithermometer][pvvx_mithermometer][thermopro_ble] Publish optional parse results with publish_optional (#21924) 2026-10-30 23:20:49 -04:00
J. Nick Koston 867b51941c [components] Use PROGMEM_STRING_TABLE for pressure sensor oversampling and filter names (#21931) 2026-10-30 23:20:27 -04:00
J. Nick Koston 1297771d5b [gcja5] Do not publish frames that fail the checks (#21936) 2026-10-30 23:19:18 -04:00
J. Nick Koston fe0e67bdd7 [xiaomi] Publish optional parse results with publish_optional in the plant, motion and other sensors and binary sensors (#21946) 2026-10-30 23:18:33 -04:00
0af702a0af [ld6004] Add Hi-Link LD6004 radar with binary_sensor platform (#19840)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-30 17:05:22 -10:00
J. Nick Koston 09d8477557 [xiaomi] Publish optional parse results with publish_optional in the thermometers (#21923) 2026-10-30 16:03:16 -10:00
J. Nick Koston bb458503c9 [core] Add publish_optional for optional entity values (#21921) 2026-10-30 16:03:16 -10:00
J. Nick Koston c01bc17bc8 [cm1106] Drop the duplicate helpers.h include (#21976) 2026-10-30 15:26:31 -10:00
Rick van Hattem 135943911b [ld600x] Share platform schemas and code generation with ld6002b (#19839) 2026-10-30 15:17:08 -10:00
J. Nick Koston 3870ca0031 [output] Use ApplyButton for the output button (#21907) 2026-10-30 14:44:54 -10:00
J. Nick Koston e5871655b2 [mitsubishi_cn105] Make the unit tests build with Apple clang (#21900) 2026-10-30 14:21:38 -10:00
J. Nick Koston a5b6c69201 [hte501][tee501] Check I2C transfer results (#21929) 2026-10-30 14:21:25 -10:00
J. Nick Koston c08aff9300 [bp5758d] Keep the channel values in fixed arrays (#21905) 2026-10-30 14:20:24 -10:00
J. Nick Koston f6fc942c4c [network] Fix inet_ntop link failure on Zephyr (#21909) 2026-10-30 14:19:48 -10:00
J. Nick Koston 7d750279ef [components] Add parser tests for a01nyub, a02yyuw, jsn_sr04t, gcja5, pm1006 and sds011 (#21920) 2026-10-30 19:53:45 -04:00
J. Nick Koston 2b0bcf646e [output] Let final_validate_unique_channel take the channel key (#21899) 2026-10-30 19:53:09 -04:00
J. Nick Koston 2913537baf [ota] Add tests for the OTA on_* automations (#21902) 2026-10-30 19:52:18 -04:00
J. Nick Koston ba25efc791 [uart] Use ApplyButton for the UART button (#21906) 2026-10-30 19:49:52 -04:00
J. Nick Koston 0f1192feb2 [components] Declare child entities with SUB_* macros (4/8) (#21773) 2026-10-30 19:47:55 -04:00
J. Nick Koston 02c4512749 [ezo] Add tests for the command text, reply callbacks and address change (#21865) 2026-10-30 19:47:09 -04:00
J. Nick Koston ad7d246a45 [syslog] Pack the settings into one word (#21896) 2026-10-30 19:46:22 -04:00
J. Nick Koston 89360efb38 [hte501][tee501] Decode temperature as a signed 16-bit value (#21930) 2026-10-30 19:43:04 -04:00
J. Nick Koston c2a2ddb490 [tee501] Drop the stray read before the identification command (#21928) 2026-10-30 19:41:43 -04:00
J. Nick Koston 471b67079f [ccs811] Round environment data once (#21927) 2026-10-30 19:39:34 -04:00
J. Nick Koston 844dd58b03 [mhz19] Keep reading after the uptime counter wraps (#21926) 2026-10-30 19:38:20 -04:00
J. Nick Koston 8b35e2ad16 [sensor] Add source_value for reading a compensation input (#21937) 2026-10-30 19:31:40 -04:00
J. Nick Koston 918488c8d6 [climate] Use celsius_to_fahrenheit in toshiba and daikin_brc (#21944) 2026-10-30 19:29:18 -04:00
J. Nick Koston ae85d85bc4 [senseair] Use discard_available to drain after a bad preamble (#21945) 2026-10-30 19:28:29 -04:00
J. Nick Koston 96836eab3d [components] Pass the failure reason to mark_failed in ee895, hte501, tee501 and mt6701_i2c (#21943) 2026-10-30 19:25:05 -04:00
J. Nick Koston 02e4d6d8b1 [components] Use sum_bytes for checksums in ufm01, bq76972, cm1106, pzem004t and pd_pioneer (#21942) 2026-10-30 19:23:50 -04:00
4b79dc5c99 [ld600x] Move the LD6002B component into a shared LD600XComponent base (#19837)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-30 23:16:02 +00:00
J. Nick Koston 67456e9dd6 [core] Start the Apple Silicon cc1plus wrapper through sh when the Python path has a space (#21967) 2026-10-30 13:09:24 -10:00
J. Nick Koston 7ac9a86861 [components] Use encode_uint16 and encode_uint24 in more drivers (#21941) 2026-10-30 18:56:05 -04:00
J. Nick Koston 2589cc6aea [components] Use encode_uint16 in sensor drivers (#21940) 2026-10-30 18:55:42 -04:00
J. Nick Koston 801e36f666 [components] Use encode_uint16 and encode_uint24 in 13 more drivers (#21949) 2026-10-30 18:47:26 -04:00
Clyde Stubbs 8a4d76be32 [mipi_rgb] Update pins for Lilygo boards (#21917) 2026-10-30 12:19:06 -10:00
5f916e3d49 [ld600x] Add shared TinyFrame parser and use it in ld6002b (#19836)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-30 12:17:36 -10:00
aronkandClaude Opus 5.5 81b8e36e96 [xiaomi_ble] Add LYWSD02MMC product ID 0x16E4 (#21955)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:04:04 -10:00
huangkongshengandJ. Nick Koston 6e5d9c1ad7 [core] Reject empty domain/object parts in cv.entity_id (#21964)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: huangkongsheng <huangkongsheng@users.noreply.github.com>
2026-10-30 10:56:14 -10:00
huangkongshengandJ. Nick Koston 7d2a039696 [core] Measure cv.ssid length in UTF-8 bytes, not characters (#21965)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: huangkongsheng <huangkongsheng@users.noreply.github.com>
2026-10-30 10:55:49 -10:00
huangkongshengandJ. Nick Koston c79e0b8f95 [sun] Reject garbage and truncated latitude/longitude instead of parsing 0.0 (#21876)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: huangkongsheng <huangkongsheng@users.noreply.github.com>
2026-10-30 10:55:24 -10:00
huangkongsheng e3888e2fa1 [time] Fix cron ranges wrapping around the end of the scale (#21961) 2026-10-30 10:51:16 -10:00
huangkongshengandJ. Nick Koston f35a0c4fef [core] Validate strings in cv.percentage_int instead of passing them through (#21877)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: huangkongsheng <huangkongsheng@users.noreply.github.com>
2026-10-30 10:46:14 -10:00
huangkongshengandJ. Nick Koston bf8ae4e306 [mipi] Document that init sequence delays require a time unit (#21873)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: huangkongsheng <huangkongsheng@users.noreply.github.com>
2026-10-30 10:41:08 -10:00
huangkongshengandJ. Nick Koston 07a3f39760 [core] Reject trailing garbage in cv.dimensions (#21963)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: huangkongsheng <huangkongsheng@users.noreply.github.com>
2026-10-30 10:40:37 -10:00
huangkongshengandJ. Nick Koston 9b2fcced64 [time] Reject invalid '/' step and offset in cron expressions (#21962)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Signed-off-by: huangkongsheng <huangkongsheng@users.noreply.github.com>
2026-10-30 10:28:07 -10:00
dependabot[bot] a897f55e36 Bump platformdirs from 4.13.5 to 4.13.6 (#21970)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-30 10:03:45 -10:00
dependabot[bot]andesphome[bot] 2f84fd81a0 Bump ruff from 0.17.2 to 0.17.3 (#21969)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-30 10:03:33 -10:00
Clyde Stubbs 044fd267b6 [ufire_ec][ufire_ise] Move CONF_TEMPERATURE_SENSOR to the shared component constants (#21919) 2026-10-30 07:10:56 -10:00
Sebastian Muszynski a178954b11 [mqtt] Add value and command templates to time, date and datetime dis… (#21956) 2026-10-30 06:56:25 -10:00
00c15ddda2 [zephyr_mcumgr] Add SMP UDP OTA transport for nRF52 (#17245)
Co-authored-by: Copilot Autofix powered by AI <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: tomaszduda23 <tomaszduda23@gmail.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-30 00:45:31 -10:00
b4048bfb6f [wifi] Handle intentional ESP32 disconnects during roaming (#21758)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-29 19:43:17 -10:00
848231f59e [airton_serial] Add Airton serial climate component (#21872)
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-10-29 19:31:37 -10:00
5d2961e96b [mitsubishi_cn105] Hold the climate action through wind-down and keep the AUTO direction (#20819)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-29 19:01:09 -10:00
08e41587ae [ledc] Fix pulse truncation when setting zero duty (#21179)
Co-authored-by: Tomas Matheson <tomas.matheson@icloud.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-10-29 22:16:44 -05:00
129b3f248a [mitsubishi_cn105] Add climate action support (#21650)
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>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-29 17:05:46 -10:00
FuNK3YandClaude Opus 5.5 1a325304ed [dlms_meter] Allow multiple dlms_meter instances (#21585)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:42:25 -10:00
Boris KrivonogandJ. Nick Koston 957fe02ebc [mitsubishi_cn105] Add the horizontal vane to the vane trigger and action (#21870)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-29 15:56:40 -10:00
96d6627daf [ir_rf_base] Reject infrared and radio_frequency entities sharing a key (#21886)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-29 15:54:50 -10:00
Jonathan Swoboda 349945225c [esp32][ota][esp32_ble_tracker] Prepare for ESP-IDF 6.2 (#21891) 2026-10-29 19:50:31 -04:00
be857de642 [ld2410] Add automatic background noise correction (#21460)
Co-authored-by: Anunay Kulshrestha <1544855+anunayk@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-29 12:36:51 -10:00
ee9ea7e62f [mitsubishi_cn105] Add horizontal vane direction select (#21863)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-29 10:46:35 -10:00
dependabot[bot] b155e4ba0b Bump clang-tidy from 23.1.3 to 23.1.4 (#21884)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-29 19:42:39 +00:00
dependabot[bot] 68b01ba754 Bump pytest-asyncio from 1.4.0 to 1.4.1 (#21883)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-29 09:32:06 -10:00
J. Nick Koston 95973f0d38 [matrix_keypad][rc522][rdm6300][sx1509][wiegand] Build hub triggers with two automations each in tests (#21787) 2026-10-29 11:07:47 -04:00
J. Nick Koston 2d41bd5cea [time] Add tests for on_time schedules and time jump handling (#21793) 2026-10-29 11:06:39 -04:00
J. Nick Koston 5d2772c216 [sun] Add unit tests for on_sunrise and on_sunset crossing (#21802) 2026-10-29 11:05:44 -04:00
J. Nick KostonandJonathan Swoboda 2ce14405ec [components] Publish child entities with PUBLISH_STATE (5/5) (#21852)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-29 11:04:43 -04:00
J. Nick KostonandJonathan Swoboda 8fd654e295 [components] Publish child entities with PUBLISH_STATE (4/5) (#21851)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-29 11:03:43 -04:00
J. Nick KostonandJonathan Swoboda fa28726789 [components] Publish child entities with PUBLISH_STATE (3/5) (#21810)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-29 11:02:26 -04:00
J. Nick KostonandJonathan Swoboda 8401d59ca1 [components] Publish child entities with PUBLISH_STATE (2/5) (#21809)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-29 10:51:21 -04:00
J. Nick KostonandJonathan Swoboda f838978a70 [components] Publish child entities with PUBLISH_STATE (1/5) (#21808)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-29 10:38:39 -04:00
J. Nick Koston f5a97e61bf [ld2420] Remove the unfinished gate energy sensor plumbing (#21850) 2026-10-29 10:15:53 -04:00
J. Nick Koston d8eec3c460 [ble_client] Cover text sensor on_notify in the build test (#21820) 2026-10-29 10:13:28 -04:00
J. Nick Koston dedccca940 [ci] Add GPIO hold, BLE, light sleep, manual IP, web handler, LD2450, BK72xx flash, Prometheus and OTA upload flows to the hardware-test skill (#21724) 2026-10-29 10:12:45 -04:00
J. Nick Koston 7cde8beb0d [wifi] Run the connection retry phases in a host unit test (#21746) 2026-10-29 10:12:22 -04:00
J. Nick Koston b59018147e [sensor] Fix codegen crash for a delta baseline given as a number (#21811) 2026-10-29 10:11:56 -04:00
J. Nick Koston 807dfa3abb [sensor][number] Add tests for the on_value_range trigger (#21814) 2026-10-29 10:08:53 -04:00
J. Nick Koston 9aa035a01e [ble_device_base] Cover the service and manufacturer data advertise triggers (#21822) 2026-10-29 10:08:12 -04:00
J. Nick Koston f3cb6039f2 [esp32_rmt_led_strip] Reorder members to fill base class tail padding (#21838) 2026-10-29 10:06:05 -04:00
J. Nick Koston d7e6051489 [modbus_controller] Drop the unused Component base from the outputs (#21857) 2026-10-29 10:04:12 -04:00
J. Nick Koston ce7b2506bb [vbus] Drop the unused Component base from the custom sub-sensors (#21859) 2026-10-29 10:03:29 -04:00
J. Nick Koston 585cc99331 [template] Drop the unused Component base from the template event (#21860) 2026-10-29 10:03:07 -04:00
J. Nick Koston 4cfa8780ee [hydreon_rgxx] Remove the empty binary sensor component (#21862) 2026-10-29 10:02:43 -04:00
J. Nick Koston a0e317d9f9 [core] Add PUBLISH_STATE (#21805) 2026-10-29 10:02:06 -04:00
J. Nick Koston a8aa4e6497 [components] Declare child entities with SUB_* macros (9/10) (#21848) 2026-10-29 10:01:06 -04:00
J. Nick Koston 4e725ea6d3 [components] Declare child entities with SUB_* macros (10/10) (#21849) 2026-10-29 10:00:45 -04:00
Jonathan Swoboda 5071968eb9 [ci] Compile each build path ESPHome ships on Windows on a Windows runner (#21791) 2026-10-28 22:46:10 -04:00
Jonathan Swoboda e6bebc298f [nrf52] Patch west build so a project on another Windows drive than the SDK builds (#21798) 2026-10-28 22:00:56 -04:00
Jonathan Swoboda 8831c0dc0d [platformio] Use ccache for compiles PlatformIO moved to a response file on Windows (#21819) 2026-10-28 21:59:54 -04:00
J. Nick Koston 9166980740 [remote_base] Drop the unused Component base from RawTrigger (#21796) 2026-10-28 21:09:41 -04:00
J. Nick Koston 7e575ef16d [http_request] Remove the unused HttpRequestResponseTrigger (#21797) 2026-10-28 21:09:29 -04:00
J. Nick Koston 6d9f4421a8 [components] Declare child entities with SUB_* macros (1/8) (#21770) 2026-10-28 20:34:55 -04:00
J. Nick Koston 8854e1eb94 [components] Declare child entities with SUB_* macros (2/8) (#21771) 2026-10-28 20:34:25 -04:00
J. Nick Koston d597be77ac [components] Declare child entities with SUB_* macros (3/8) (#21772) 2026-10-28 20:34:04 -04:00
J. Nick Koston f56a4db60f [components] Declare child entities with SUB_* macros (5/8) (#21774) 2026-10-28 20:33:25 -04:00
J. Nick Koston 362e73313c [components] Declare child entities with SUB_* macros (6/8) (#21775) 2026-10-28 20:33:03 -04:00
J. Nick Koston 6e92bb301d [components] Declare child entities with SUB_* macros (7/8) (#21776) 2026-10-28 20:32:50 -04:00
J. Nick Koston 644bd8995a [components] Declare child entities with SUB_* macros (8/8) (#21777) 2026-10-28 20:32:47 -04:00
J. Nick Koston 3caff2b80b [hdc1080][hdc2010][hdc2080][hdc302x] Declare child sensors with SUB_SENSOR (#21778) 2026-10-28 20:32:16 -04:00
TomandJ. Nick Koston 4814377805 [pcf85063] Read 12-hour mode and set 24-hour mode on every write (#21221)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-28 13:51:07 -10:00
Kevin AhrendtandJ. Nick Koston d0bafef28c [sendspin] Add the source role, streaming a microphone to the server (#21696)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-28 23:41:38 +00:00
Bascht74 e8cf0cfd06 [core][socket] Add an idle timeout to TcpClientLink (#21365) 2026-10-28 13:23:59 -10:00
88f6135171 [esp32][audio_http] Keep TLS 1.3 working (#21786)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-28 23:22:43 +00:00
Kevin Ahrendt 055faab7e3 [sound_level] Allow more than one microphone channel (#21760) 2026-10-28 13:22:13 -10:00
Kevin Ahrendt f74d8b3ca2 [i2s_audio] Let stop() cancel a microphone start that has not run yet (#21759) 2026-10-28 13:21:37 -10:00
Jonathan Swoboda d5539d358a [core][nrf52] Drop build flags holding ';', keep linker groups whole, and quote nRF52 build flags (#21699) 2026-10-28 18:14:36 -04:00
d1c1f10be8 [binary_sensor] Add on_raw_value trigger (#21126)
Co-authored-by: Philip Poten <philip@poten.at>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-28 10:06:32 -10:00
tomaszduda23 185d2a4fe2 [nrf52] clean-up comments (#21769) 2026-10-28 19:36:37 +00:00
techdregs 3d3c624309 [rx8111] Add optional binary sensors (#21744) 2026-10-28 09:30:19 -10:00
tomaszduda23 3e884e1227 [nrf52] apply ThreadModel.MULTI_ATOMICS (#21761) 2026-10-28 20:03:03 +01:00
lobo67 f762e397be [cs5460a] Fix nested SPI enable() deadlock on ESP32 (#21753) 2026-10-28 08:48:37 -10:00
Arthur Komatsu fceec6cf95 [http_request] Close the connection when an OTA download is refused (#21762) 2026-10-28 18:46:13 +00:00
Wei-Lun LiandClaude Opus 5.5 23fa94bee1 [core] Reject a 0 K color temperature instead of crashing (#21756)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:18:54 -10:00
tomaszduda23andClaude 01fc4432ef [nrf52] do not show cdc_acm in device builder (#21737)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-28 16:33:28 +01:00
Javier Torres 0544ec9cd2 [voice_assistant] Ensure microphone is stopped at STT_END (#21755) 2026-10-28 07:26:14 -04:00
Kevin AhrendtandJ. Nick Koston f565ad0286 [i2s_audio] Stamp microphone buffers with the DMA capture time (#21664)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-28 07:05:19 -04:00
n-IA-hane 10ce6b9faa [speaker_source] Stop pending sources without advancing the playlist (#19501) 2026-10-27 19:46:17 -10:00
Raphael Hehl a99bca0a23 [esp32_camera] Add capture_delay and capture triggers for single images (#21614) 2026-10-27 19:45:01 -10:00
Nicolas Liaudat 647c9b8b28 [esp32_camera] Add sensor windowing (digital zoom) (#20480) 2026-10-27 18:39:55 -10:00
Bogdan Dumitru 1acd6a64d4 [ld2450] Detect a lost radar link (#21734) 2026-10-28 04:27:58 +00:00
Petar Petrov d58c809e9b [vnc] Add VNC password authentication (#20626) 2026-10-27 18:14:42 -10:00
68fdc2c1bc [ring_buffer] Respect waits for oversized partial writes (#21058)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-28 03:47:49 +00:00
f8fbcb8886 [ecocomfort2] Add the configuration entities (Part 3) (#20783)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-27 17:46:54 -10:00
J. Nick KostonandJonathan Swoboda 28fb5125a6 [binary_sensor] Reset filters when the state becomes unknown (#21740)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-27 17:15:18 -10:00
J. Nick Koston 02eab7b269 [libretiny] Report the previous boot's crash on LN882H (#21728) 2026-10-27 16:56:46 -10:00
441d7ec20c [rx8111] Add event timestamp text sensor and capture storage modes (#21604)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-27 16:52:50 -10:00
J. Nick Koston 3d7ec081f8 [i2c] Print the bus scan results from one shared helper (#21718) 2026-10-27 22:50:05 -04:00
J. Nick Koston 3f1ca2fc9a [bluetooth_connection] Hold the RP2 service table in a RAMUniquePtr (#21730) 2026-10-27 22:49:27 -04:00
J. Nick Koston 6309f63a44 [core] Read the OS from sys.platform instead of querying it at import (#21736) 2026-10-27 22:48:31 -04:00
J. Nick Koston f2880d2a37 [components] Declare hub entity pointers with the SUB_* macros (#21726) 2026-10-27 22:47:47 -04:00
Jonathan SwobodaandJ. Nick Koston 6f9e92a9e3 [zephyr_ble_server] Keep the stored connection right when disconnect runs first (#21723)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-27 16:19:47 -10:00
Kevin Ahrendt 5e51abb438 [resampler] Carry capture times through the resampler microphone (#21682) 2026-10-27 16:04:45 -10:00
ce2e025d84 [delta_wallbox] Swapped the power and energy readings (#21628)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-27 14:54:45 -10:00
072c9556eb [core] Allow esphome upload without a configuration (#21738)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-27 14:48:47 -10:00
Jonathan Swoboda 02502b7af2 [core][scheduler] Log a new interval before taking the scheduler lock (#21721) 2026-10-27 19:58:36 -04:00
Jordan Zucker 205ef9407f [prometheus] Add prometheus datetime metrics (#9941) 2026-10-27 13:09:01 -10:00
Adam Vigneaux d552396650 [tinyusb] Only declare the IAD device class when a class needs it (#21719) 2026-10-27 22:20:50 +00:00
J. Nick Koston a101d57476 [runtime_image] Hold the JPEG decoder state in a RAMUniquePtr (#21717) 2026-10-27 11:37:24 -10:00
Bogdan Dumitru 195cb3b2b4 [ld2450] Allow a presence_timeout of 0 (#21733) 2026-10-27 11:23:59 -10:00
Bohdan 8529b7a59c [bk72xx_ble] Add BK7238 support (#21735) 2026-10-27 21:22:24 +00:00
Bogdan Dumitru 4e63342350 [ld2450] Write an edited polygon zone to flash right away (#21732) 2026-10-27 10:51:57 -10:00
Fabio Pugliese Ornellas 3784a3d56a [ld2450] Add a text sensor with the position of each target (#21416) 2026-10-27 10:50:35 -10:00
Bogdan Dumitru 848d1ac4e8 [ld24xx] Show radar MAC addresses that have the multicast bit set (#21731) 2026-10-27 10:45:00 -10:00
c3348789eb [gree] Add IR receiver support and YB1FA model (#18435)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Leonardo Rivera <leodrivera@gmail.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-27 09:28:11 -10:00
6ea4d976c2 [tas58xx] Add mixer mode select (#21637)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-27 09:20:00 -10:00
ebd5a8bb18 [debug] Log per-thread CPU usage on nRF52 (#13938)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-27 08:46:22 -10:00
J. Nick Koston 6ac63ac495 [pcm5122] Use Apply switches for the power switches (#21677) 2026-10-27 08:44:38 -10:00
J. Nick Koston 7d5bfb8fa3 [sendspin] Use an Apply switch for the enabled switch (#21673) 2026-10-27 08:33:58 -10:00
Jesse Hills 4cd4ddb981 Merge branch 'release' into dev 2026-10-28 07:33:31 +13:00
Jesse Hills 4ea26ed65f Merge pull request #21707 from esphome/bump-2026.10.1
2026.10.1
2026-10-28 07:33:05 +13:00
J. Nick Koston 747921faa8 [bq25186] Use an Apply switch for the PG/GPO switch (#21675) 2026-10-27 08:29:48 -10:00
J. Nick Koston f8056c3a30 [bq25186] Test the PG/GPO switch (#21654) 2026-10-27 08:29:48 -10:00
J. Nick Koston 85c241391e [core] Add publish="on_success" for Apply entities whose hub call can fail (#21663) 2026-10-27 08:29:47 -10:00
J. Nick Koston 9cccb42c6f [ci] Add the Xiaomi BLE, WPA3 AP, BLE terminate, I2C slave, OTA timing and RTL8720C crash flows to the hardware-test skill (#21636) 2026-10-27 08:08:52 -10:00
J. Nick Koston 81d451aa63 [text_sensor] Add throttle, throttle_with_priority and timeout filters (#21669) 2026-10-27 08:00:44 -10:00
J. Nick Koston c3506bd7b9 [ci] Persist a bounded integration test ccache seed (#21701) 2026-10-27 08:00:22 -10:00
J. Nick Koston 449c511606 [ci] Build integration tests without debug symbols, re-run failures with them (#21702) 2026-10-27 08:00:21 -10:00
J. Nick Koston 0ec8e58772 [libretiny] Report the previous boot's crash on RTL8720C (#21648) 2026-10-27 10:04:19 -04:00
J. Nick Koston 1ee8762006 [esp32_ble] Warn once for repeated connection parameter update failures (#21698) 2026-10-27 09:43:27 -04:00
J. Nick Koston c865b551ee [web_server] Make web OTA's request handler part of its component (#21711) 2026-10-27 09:29:46 -04:00
J. Nick Koston 6e416952c8 [ethernet] Keep manual IP settings in flash (#21712) 2026-10-27 09:28:19 -04:00
J. Nick Koston 96b8bae876 [tests] Drive the clock in the UDP OTA ping test (#21715) 2026-10-27 09:22:00 -04:00
3b1bf1dcba [core] Move shared BLE pairing constants to components/const (#21710)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-26 22:15:04 -10:00
72221aa0d3 [esp32_ble] Keep LL_TERMINATE_IND going until acknowledged on ESP32-C3/S3 (#20232)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-26 21:55:50 -10:00
J. Nick Koston 1e8f084741 [ci] Stop saving an empty apt cache when the install fails (#21713) 2026-10-27 19:34:58 +13:00
J. Nick Koston 70448afb87 [ci] Stop saving an empty apt cache when the install fails (#21713) 2026-10-27 19:33:01 +13:00
J. Nick Koston 6ba8862ad8 [sendspin] Test the enabled switch codegen (#21671) 2026-10-26 19:54:36 -10:00
J. Nick Koston ca69cd06e9 [pipsolar] Use Apply switches (#21659) 2026-10-26 19:53:42 -10:00
J. Nick Koston c965c5ee70 [ld2410s] Use Apply entities for the select and numbers (#21662) 2026-10-26 19:53:04 -10:00
J. Nick Koston 97bbe58c8c [ld2410s] Test the select and number writes (#21661) 2026-10-26 19:53:04 -10:00
Jesse Hills 6595df0cab Bump version to 2026.10.1 2026-10-27 16:17:54 +13:00
J. Nick Koston 73894b2490 [core] Turn off the precompiled header with ccache older than 4.7 (#21627) 2026-10-27 16:17:53 +13:00
Jonathan Swoboda 9bef20d8dc [core] Avoid division by zero in Color::random_color() (#21623) 2026-10-27 16:17:53 +13:00
Jonathan Swoboda 0e440500a2 [core] Generate valid C++ for infinite float values (#21625) 2026-10-27 16:17:53 +13:00
minimicro34 badfa84fe0 [esp32_ble] Ignore unhandled BLE key GAP events (#21457) 2026-10-27 16:17:53 +13:00
J. Nick Koston e686ecf76e [core] Resolve version-less library dependencies like PlatformIO (#21317) 2026-10-27 16:17:53 +13:00
Jonathan Swoboda 64fb4e06b5 [ledc] Reset the peripheral through the HAL on targets without the legacy periph API (#21397) 2026-10-27 16:17:53 +13:00
Jonathan Swoboda cb82c9031a [core] Keep the color codes of a filtered build output line (#21396) 2026-10-27 16:17:53 +13:00
J. Nick KostonandJonathan Swoboda ca74ab9873 [climate] Add StringRef overloads for ClimateCall::set_fan_mode and set_preset (#21359)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-27 16:17:53 +13:00
J. Nick Koston 86fc740b65 [http_request] Copy a manifest URL set at run time and keep the config literal by pointer (#21373) 2026-10-27 16:17:53 +13:00
J. Nick Koston 450bb52f65 [mqtt] Resend the fan state when its first publish fails (#21328) 2026-10-27 16:17:35 +13:00
Clyde StubbsandClaude Opus 5.5 1cc6c9d1dd [core] Split AGENTS.md detail into skills, add schema extraction rule (#21606)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 17:16:25 -10:00
J. Nick Koston ed3615ec69 [light] Add a StringRef overload for LightCall::set_effect (#21358) 2026-10-27 16:15:01 +13:00
J. Nick Koston a5f25ebe2d [core] Emit generated lambda code after the user includes (#21307) 2026-10-27 16:12:58 +13:00
Clyde Stubbs f188d0c450 [http_request] Fix macOS host build with the precompiled header (#21705) 2026-10-27 14:11:05 +11:00
Jonathan Swoboda 7a4775de3d [esp32_hosted] Keep the OTA watchdog timeout through the coprocessor end call (#21300) 2026-10-27 15:48:16 +13:00
Jonathan Swoboda d4064d3f3d [core][nrf52] Make sdk-nrf a native toolchain (#21704) 2026-10-26 21:49:29 -04:00
Jonathan Swoboda 72c429bb0f [core] Fix a pylint false positive in idedata.py (#21706) 2026-10-27 01:30:39 +00:00
a9f970ca67 [modbus_monitor] Add modbus_monitor with on_request and on_response for server and client hubs (#20256)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Dale Klein <49771139+dalklein@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@home-assistant.io>
2026-10-26 15:28:03 -10:00
1a50e1394a [vnc] Add input actions and touchscreen forwarding (#20627)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-26 12:48:25 -10:00
luar123andJ. Nick Koston ae1f912a5f [esp32] fix rtcio handling (#21576)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-26 22:42:56 +00:00
916798184d [nextion] Keep a queued command when an unrelated response arrives (#19019)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-26 12:41:48 -10:00
Adam Matthews 1e49616bc5 [api] Fix compile of responding actions with array args (#21672) 2026-10-26 12:32:11 -10:00
esphome[bot] c66f71bb96 Bump aioesphomeapi from 46.22.0 to 46.23.0 (#21700) 2026-10-26 22:31:36 +00:00
8db0774d67 [integration] Test state_class on the integration sensor (#21489)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-26 12:28:18 -10:00
Jonathan Swoboda f735c4634e [esp32] Split and quote build flags in native ESP-IDF builds (#21680) 2026-10-26 17:54:37 -04:00
fe125726ee [template] Stop a running position action on the next cover command (#21599)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-26 11:39:25 -10:00
e05f2fc74e [copy] Forward cover toggle and lock open to the source (#21598)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-26 11:22:58 -10:00
PanoplianandJ. Nick Koston 4e68f1d324 [display] Warn when show_test_card is used with update_interval: never (#20887)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-26 11:12:41 -10:00
Bascht74andJ. Nick Koston 98706c594b [demo] Set the cover to idle when it is stopped (#21600)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-26 11:09:08 -10:00
Jonathan Swoboda 633c2b7f71 [core] Load !include_dir_* files in a fixed order (#21678) 2026-10-26 16:44:24 -04:00
dependabot[bot] 516511b346 Bump actions/setup-python from 7.0.0 to 7.1.0 (#21694)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 20:27:44 +00:00
dependabot[bot] f72dc7c329 Bump astral-sh/setup-uv from 10.3.0 to 10.3.2 (#21695)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:27:07 -10:00
dependabot[bot] f1695ac0b0 Bump CodSpeedHQ/action from 5.5.0 to 5.6.0 (#21692)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:27:01 -10:00
dependabot[bot] 24c64a8efd Bump prek from 0.5.6 to 0.5.7 (#21689)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:26:54 -10:00
dependabot[bot]andesphome[bot] d3638336ae Bump ruff from 0.17.1 to 0.17.2 (#21683)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:26:47 -10:00
dependabot[bot] 53bffb560b Bump tzlocal from 5.4.4 to 5.4.5 (#21686)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:26:44 -10:00
dependabot[bot] 2af39e6643 Bump smp from 4.1.0 to 4.2.0 (#21685)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:26:40 -10:00
dependabot[bot] 533a3afebd Bump actions/setup-python from 7.0.0 to 7.1.0 in /.github/actions/restore-python (#21690)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:26:34 -10:00
dependabot[bot] ddb7a6d9aa Bump astral-sh/setup-uv from 10.3.0 to 10.3.2 in /.github/actions/restore-python (#21688)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:26:28 -10:00
dependabot[bot] ae99accace Bump cryptography from 48.0.1 to 50.0.2 (#21684)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:26:15 -10:00
dependabot[bot] 474667d1a8 Bump github/codeql-action/init from 4.39.0 to 4.39.1 (#21693)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:26:08 -10:00
dependabot[bot] dbc93bb133 Bump docker/login-action from 4.6.0 to 4.7.0 in the docker-actions group (#21687)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:26:01 -10:00
dependabot[bot] 5486ef400f Bump github/codeql-action/analyze from 4.39.0 to 4.39.1 (#21691)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-26 10:25:54 -10:00
J. Nick Koston bd723f75af [pipsolar] Test the switch commands (#21655) 2026-10-26 16:17:27 -04:00
J. Nick Koston efba19ea3a [ld2420] Switch the module to simple reporting when the select picks Simple (#21651) 2026-10-26 08:29:45 -10:00
Jonathan Swoboda 6c68985c14 [core] Report missing dependencies of a component as an import error (#21670) 2026-10-26 14:05:19 -04:00
Jonathan Swoboda 7be8a7c390 [core][esp32] Reject git refs and github:// shorthands with unmatched text (#21656) 2026-10-26 14:05:11 -04:00
PolarGoose eb8cf139d7 [dlms_meter] Update parser to latest version to fix bogus error message on some smart meters (#21657) 2026-10-26 07:45:58 -10:00
Kevin Ahrendt ab6ebac628 [microphone] Zero muted audio in place before publishing (#21646) 2026-10-26 13:27:22 -04:00
6cb622417a [tas58xx] Add fault_output and modulation options (#21242)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-26 17:18:18 +00:00
Jonathan Swoboda 960316abd6 [core] Percent-encode "/" in git credentials (#21653) 2026-10-26 07:17:44 -10:00
Jonathan Swoboda 87341dc136 [logger] Enable Log V2 only on the native ESP-IDF toolchain (#21652) 2026-10-26 13:03:15 -04:00
J. Nick Koston cfe5809654 [wifi] Hand the DPP bootstrapping key to the supplicant DER encoded (#20882) 2026-10-26 16:37:25 +00:00
J. Nick Koston 9c54f96520 [libretiny] Send only the UF2 images the device writes over OTA (#21642) 2026-10-26 06:28:54 -10:00
J. Nick Kostonandpre-commit-ci-lite[bot] 0cc5b372fb [core] Defer integration imports in ir_rf_base and media_player (#21601)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-26 06:26:00 -10:00
J. Nick Koston f7c8cba8b7 [components] Use pulse_reset for blocking reset pin pulses (#21472) 2026-10-26 06:22:11 -10:00
J. Nick Koston 79a22e9a9a [core] Add pulse_reset for active-low reset pins (#21470) 2026-10-26 06:22:10 -10:00
J. Nick Koston 103023737d [openthread] Resolve IPv6 literal addresses when the DNS64 client is enabled (#19269) 2026-10-26 16:18:12 +00:00
J. Nick Koston 6b6b70a8b6 [dfrobot_sen0395] Collect lines with LineReader (#20979) 2026-10-26 12:16:51 -04:00
J. Nick Koston d312bf1b73 [core] Let the yaml editor insert lines, write with a rollback and restore (#20602) 2026-10-26 06:16:26 -10:00
J. Nick Koston 0d2f8c0e88 [components] Set or clear entity, socket and sensor bits with assign_bits (#21188) 2026-10-26 12:10:29 -04:00
J. Nick Koston 2f8b50c5af [components] Build the remaining enum option dicts with options() (#20972) 2026-10-26 12:10:08 -04:00
Kevin Ahrendt c4b598be3e [microphone] Pass each audio buffer as an AudioCapture (#21645) 2026-10-26 06:09:53 -10:00
J. Nick Koston 0cd16df16f [components] Write constant codec register tables with write_registers (#20946) 2026-10-26 12:09:47 -04:00
J. Nick Koston a680974da2 [ld6002b] Use Apply entities and find the setting by entity pointer (#20937) 2026-10-26 12:09:03 -04:00
J. Nick Koston 78f0b1ceba [esp32_rmt_led_strip] Stop blocking the main loop while waiting for the previous frame (#19337) 2026-10-26 06:08:13 -10:00
J. Nick Koston c5a841e11e [seeed_mr60fda2] Keep the parameter value tables in flash (#20720) 2026-10-26 12:07:24 -04:00
J. Nick Koston 365965f9bb [veml7700] Keep the gain and integration time tables in flash (#20719) 2026-10-26 12:07:06 -04:00
J. Nick Koston 16011d7fce [nextion] Keep the queue type name table in flash (#20658) 2026-10-26 12:06:31 -04:00
J. Nick Koston 4486dc53d8 [tx20] Keep the wind direction table in flash (#20657) 2026-10-26 12:05:43 -04:00
8d5e3ce534 [components] Use Apply selects for hub forwarding selects (#20966)
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-10-26 16:05:20 +00:00
J. Nick Koston 9ea4d35a50 [uart] Keep debug delimiters and event patterns in flash (#20109) 2026-10-26 12:05:06 -04:00
J. Nick Koston 5cceddfbf4 [kamstrup_kmp] Keep the unit name table in flash (#20642) 2026-10-26 11:58:43 -04:00
J. Nick Koston ff6a37a052 [micronova] Keep the stove state table in flash (#20645) 2026-10-26 11:58:15 -04:00
J. Nick Koston 6990092be0 [ezo] Keep the command and calibration type tables in flash (#20655) 2026-10-26 11:57:41 -04:00
J. Nick Koston 9220c79a51 [bme680_bsec] Keep the IAQ accuracy names in flash (#20656) 2026-10-26 11:57:23 -04:00
J. Nick Koston 215fcd4f5d [pulse_counter] Keep the edge mode name table in flash (#20660) 2026-10-26 11:57:03 -04:00
J. Nick Koston 918e3b3a58 [haier] Keep the protocol phase names in flash (#20710) 2026-10-26 11:56:43 -04:00
J. Nick Koston 347eb3902e [seeed_mr24hpc1] Keep the text sensor state tables in flash (#20711) 2026-10-26 11:55:18 -04:00
J. Nick Koston 177820083f [epaper_spi] Keep the state name table in flash (#20713) 2026-10-26 11:54:07 -04:00
J. Nick Koston b0a0524091 [cc1101] Keep the modulation name table in flash (#20716) 2026-10-26 11:53:10 -04:00
93b6388cb9 [sx127x] Keep the bandwidth and ramp tables in flash (#20650)
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-10-26 11:50:47 -04:00
J. Nick Koston 8677844629 [ld2410] Format the gate threshold test (#21649) 2026-10-26 05:50:10 -10:00
c1a9c8f2f1 [sx126x] Keep the bandwidth and ramp tables in flash (#20647)
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-10-26 15:49:44 +00:00
J. Nick Koston f35e45febf [ld2412] Send the output pin level when its select changes (#21353) 2026-10-26 11:46:42 -04:00
J. Nick Koston 9f47b5c7f8 [core] Defer mqtt, web_server and zigbee imports in sixteen entity components (#21508) 2026-10-26 05:45:52 -10:00
J. Nick Koston 58b6779dbe [components] Drain UART bytes with read_available (#20948) 2026-10-26 11:42:58 -04:00
J. Nick Koston bd881b025f [ld2410] Keep the select option names in flash (#21351) 2026-10-26 05:41:57 -10:00
J. Nick Koston c41b1f8f0f [components] Use Apply switches for hub forwarding switches (#20967) 2026-10-26 11:41:28 -04:00
J. Nick Koston 87b20ffdbb [uart] Collect fixed length frames with FrameReader in five components (#21449) 2026-10-26 05:41:11 -10:00
J. Nick Koston a3f38a9d4d [components] Use Apply numbers for hub forwarding numbers (#20965) 2026-10-26 11:40:15 -04:00
J. Nick Koston 76491691f5 [components] Use Apply buttons for hub forwarding buttons (#20964) 2026-10-26 11:39:42 -04:00
6791dc7750 [i2c] Add software I2C bus for BK72xx (#21586)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-26 05:36:57 -10:00
J. Nick Koston db76f0b1cd [logger] Defer platform imports out of module scope (#18336) 2026-10-26 05:35:47 -10:00
J. Nick Koston 09f7b71efd [nfc] Keep the URI prefix table in flash (#20709) 2026-10-26 11:34:18 -04:00
J. Nick Koston fd54bf7a07 [shelly_dimmer] Drop the device table fields that are never read (#20671) 2026-10-26 11:33:27 -04:00
J. Nick Koston 011497c447 [ezo_pmp] Keep the dosing mode names in flash (#20665) 2026-10-26 11:32:48 -04:00
J. Nick Koston 31bfdcb316 [rtttl] Keep the note frequency table in flash (#20664) 2026-10-26 11:32:18 -04:00
J. Nick Koston 42f50ac3bc [bl0939] Keep the init command table in flash (#20663) 2026-10-26 11:31:58 -04:00
J. Nick KostonandOmni Flux 3edbfb47b9 [logger] Add optional boot log buffer replayed over the API (#17666)
Co-authored-by: Omni Flux <omniflux@omniflux.com>
2026-10-26 05:08:27 -10:00
J. Nick Koston b622a59d74 [usb_audio] Flush the speaker's tail when the bus idles so a stream can stop and restart (#21103) 2026-10-26 05:01:15 -10:00
J. Nick Koston 0bfdbb948e [esp32] Enable ESP-IDF Log V2 on IDF 6.1+ (#17343) 2026-10-26 10:57:43 -04:00
J. Nick Koston 840f3216b8 [wifi_info][ethernet_info] Size the IP address child sensor array from the configured addresses (#21559) 2026-10-26 10:55:48 -04:00
J. Nick Koston e257b279d5 [ld2412] Keep the select option names in flash (#21350) 2026-10-26 04:54:05 -10:00
J. Nick Koston 125bd8ccd0 [ld2410][ld2410s][ld2412][ld2450][ld2460][dfrobot_sen0609] Use sensor::SensorWithDedup (#20926) 2026-10-26 10:47:14 -04:00
J. Nick Koston cce3511112 [ld2410][ld2412][ld2450][tfluna][dfrobot_sen0609] Use the button schema presets (#20916) 2026-10-26 10:46:43 -04:00
J. Nick Kostonandpre-commit-ci-lite[bot] 836b3220a2 [components] Look up declaring configs with get_declaring_config (#20913)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-26 10:46:18 -04:00
J. Nick Koston 49f2610595 [fusb302b] Filter the state triggers with when (#20859) 2026-10-26 10:45:58 -04:00
J. Nick Koston b49f3953f9 [i2c] Move 18 components to I2CDevice::update_register (#20855) 2026-10-26 10:44:57 -04:00
J. Nick Koston de9420a625 [sensor][number] Build in_range with register_apply_condition (#20848) 2026-10-26 10:43:22 -04:00
mrtoy-meandClaude Opus 5.5 ae127f7db8 [speaker][i2s_audio][es8156] Allow a list of audio DACs on one speaker (#21260)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:43:03 -04:00
J. Nick Koston 26ba2cc234 [http_request] Build ota.http_request.flash with register_apply_action (#20830) 2026-10-26 10:42:47 -04:00
J. Nick Koston 068fe3b7ce [libretiny] Reboot and report the previous boot's crash on BK7238 and BK7231N (#21012) 2026-10-26 04:42:30 -10:00
J. Nick Koston db6c956e55 [core] Add the YAML loader helpers store_yaml builds on (#21432) 2026-10-26 04:39:25 -10:00
J. Nick Koston c95ce587c0 [components] Use Apply outputs for hub channel outputs (#21257) 2026-10-26 04:38:23 -10:00
J. Nick Koston 3540ac4d1f [esp32] Describe each variant's GPIO rules with a pin spec (#21463) 2026-10-26 14:37:43 +00:00
J. Nick Koston 180d7b31d8 [components] Use shared helpers in recently added components (#21024) 2026-10-26 10:35:25 -04:00
J. Nick Koston 3cf79df27c [components] Use update_register and write_registers in the remaining I2C components (#21083) 2026-10-26 10:34:45 -04:00
J. Nick Kostonandpre-commit-ci-lite[bot] 2beb2de8a1 [web_server] Pass entity domain names from flash (#21115)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-26 10:33:04 -04:00
J. Nick Koston 9e457b11ba [dfrobot_sen0395] Use Apply switches (#21253) 2026-10-26 10:30:39 -04:00
J. Nick Koston ee41748744 [ld2420] Use fixed size storage and keep the mode strings in flash (#21347) 2026-10-26 10:30:11 -04:00
J. Nick Koston 08d4d59b1f [ld2450] Keep the baud rate, direction and zone type names in flash (#21349) 2026-10-26 10:29:29 -04:00
J. Nick Koston e4c0ef7c95 [ld24xx] Keep the unknown MAC text and version format in flash (#21352) 2026-10-26 10:22:42 -04:00
J. Nick Koston 26ee68652f [core] Add FrameReader to collect fixed length binary frames (#21446) 2026-10-26 04:20:59 -10:00
J. Nick Koston fa88c9218b [ld2412] Split frames by their length instead of scanning for footer bytes (#21360) 2026-10-26 04:19:37 -10:00
J. Nick Koston 44e27c08d8 [ld2450] Split frames by their length instead of scanning for footer bytes (#21357) 2026-10-26 04:19:20 -10:00
J. Nick Koston f431cdfb0c [haier_water_heater] Move child entities to sensor, binary_sensor and text_sensor platforms (#21437) 2026-10-26 10:18:05 -04:00
J. Nick Koston ba488eab6e [components] Apply inverted when restoring gree, hbridge, lvgl, opentherm and haier_tundra switches (#21544) 2026-10-26 04:16:56 -10:00
J. Nick Koston f21183c160 [esp32] Add a data driven pin spec for the variant GPIO validators (#21461) 2026-10-26 10:15:09 -04:00
J. Nick Koston 60ca75baad [ld2410] Split frames by their length instead of scanning for footer bytes (#21361) 2026-10-26 04:15:00 -10:00
J. Nick Koston b668cb1e50 [usb_uart] Use FixedRingBuffer for the receive buffer (#21476) 2026-10-26 10:14:32 -04:00
J. Nick Koston ab7d0ca17d [ltr390] Pack members, drop the float tables and expand gain and resolution in the schema (#21195) 2026-10-26 14:11:02 +00:00
J. Nick Koston 43f10a7cfe [core] Turn off the precompiled header with ccache older than 4.7 (#21627) 2026-10-26 04:09:55 -10:00
J. Nick Koston d5e755e407 [dfrobot_sen0395] Queue commands by value instead of allocating them on the heap (#20906) 2026-10-26 04:09:07 -10:00
J. Nick Koston cf67610d43 [xiaomi_ble] Move the Xiaomi sensors onto XiaomiDevice and fix the shared duplicate filter (#21483) 2026-10-26 04:08:39 -10:00
J. Nick Koston bd55bdcd6c [sen6x] Drop the product and serial strings (#21199) 2026-10-26 10:05:57 -04:00
J. Nick Koston f42d1ac645 [airton] Use an Apply select for the vane direction (#21252) 2026-10-26 10:05:13 -04:00
J. Nick Koston 5198c37cd9 [bedjet] Keep the device name in a fixed buffer (#21204) 2026-10-26 10:04:47 -04:00
J. Nick Koston d9fcc292e2 [components] Read hub-restored switch levels with get_initial_write_state (#21543) 2026-10-26 04:03:48 -10:00
J. Nick Koston 8f8e615ee7 [libretiny] Apply a level written before a pin becomes an output (#21603) 2026-10-26 04:03:25 -10:00
J. Nick Koston f91ad8d43c [ltr390] Wait the full integration time at 17, 16 and 13 bit (#21194) 2026-10-26 10:03:11 -04:00
J. Nick Koston 465a8b0386 [fan] Pack the fan traits flags and narrow the speed count to 16 bits (#21340) 2026-10-26 09:52:19 -04:00
J. Nick Koston 9e692b6772 [output] Add ApplyFloatOutput and ApplyBinaryOutput (#21255) 2026-10-26 09:49:49 -04:00
J. Nick Koston 1fd4025a0e [haier] Use Apply switches (#21251) 2026-10-26 09:46:30 -04:00
J. Nick Koston 03b8bd7941 [core] Add bulk push and pop to FixedRingBuffer (#21474) 2026-10-26 09:43:54 -04:00
J. Nick Koston 0e2c906fce [i2c] Put the device address after the bus pointer so derived classes reuse its padding (#21473) 2026-10-26 09:40:37 -04:00
J. Nick Koston d260265cf5 [ld6002b] Use StaticRingBuffer for the command queue (#21479) 2026-10-26 09:40:08 -04:00
J. Nick Koston 2388c3313d [xiaomi_ble] Add a shared XiaomiDevice base for MiBeacon devices (#21481) 2026-10-26 09:39:42 -04:00
J. Nick Koston e717a21bce [restart][shutdown][factory_reset][safe_mode] Use momentary Apply switches (#21519) 2026-10-26 09:37:12 -04:00
J. Nick Koston d47a510b84 [remote_base] Use the byte helpers in roomba, dooya and magiquest (#21513) 2026-10-26 09:09:36 -04:00
J. Nick Koston a16d0c48a0 [core] Add publish="momentary" for Apply switches that trigger an action (#21518) 2026-10-26 09:08:59 -04:00
J. Nick Koston 8541104d87 [switch] Add get_initial_write_state for hubs that restore their switches (#21542) 2026-10-26 09:04:41 -04:00
J. Nick Koston 579c5a5226 [components] Publish PROGMEM table strings on text sensors directly (#21537) 2026-10-26 09:02:38 -04:00
J. Nick Koston a027785fbf [libretiny] Bump LibreTiny to 1.14.0 and build BK7231N and BK7231T with BDK 3.0.78 (#21531) 2026-10-26 09:01:50 -04:00
J. Nick Koston d7f5976525 [http_request] Keep the OTA request strings out of RAM until a flash (#21556) 2026-10-26 09:00:04 -04:00
J. Nick Koston 4c8de264fd [remote_base] Keep the stateless dumpers in flash (#21563) 2026-10-26 08:59:08 -04:00
Bascht74andJ. Nick Koston 2d37ad6836 [api] Keep an entity's events in order under backpressure (#21567)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-25 23:57:11 -10:00
Clyde Stubbs d2f6b389d4 [runtime_image] Fix LVGL widgets on image resize (#21368) 2026-10-26 20:08:06 +11:00
Raphael Hehl 875b2076c0 [esp32_camera] Fix lost single image requests (#21613) 2026-10-25 22:04:24 -10:00
c21660251c [modbus_client] Pass the request's address to the send handlers (#20461)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Dale Klein <49771139+dalklein@users.noreply.github.com>
Co-authored-by: Bonne Eggleston <bonne@exciton.com.au>
2026-10-25 21:05:10 -10:00
esphome[bot] 995acb94b4 [ci] Refresh integration test durations (#21640) 2026-10-26 05:58:30 +00:00
Manuel MauszandJ. Nick Koston 24b7eefaad [remote_base] Add support for Cardin S438 remotes (#21507)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-25 19:56:32 -10:00
Clyde StubbsandClaude Opus 5.5 2349e0be00 [esp32] Check that code is mapped when reporting a crash (#21630)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:49:16 +11:00
Clyde StubbsandClaude Opus 5.5 63349801b7 [ci] Include dependencies added by overrides of dependency components (#21635)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:18:17 -10:00
afdfa7b298 [gree] Add use_fahrenheit for Fahrenheit display on the unit's panel (#20751)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-25 17:09:04 -10:00
JoHoenk 400407b135 [runtime_image] Allocate the JPEG decoder state from PSRAM (#21629) 2026-10-26 08:28:49 +11:00
Jonathan SwobodaandJ. Nick Koston 22b32b194a [core][scheduler] Count a cancelled item on a failed component as removed (#21626)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-25 15:36:17 -04:00
J. Nick Koston 6dcd989a7f [switch][number][text] Follow the source with plain binary sensors, sensors and text sensors (#21612) 2026-10-25 18:05:42 +00:00
J. Nick Koston 2016884210 [copy] Follow the source with plain sensors and text sensors (#21610) 2026-10-25 07:52:01 -10:00
J. Nick Koston d3f4f44c7b [core] Add follow helpers for plain sensors and text sensors (#21609) 2026-10-25 07:50:58 -10:00
Jonathan Swoboda 6df15be714 [tests] Cover remote, modbus and bus action options (#21616) 2026-10-25 07:50:12 -10:00
Jonathan Swoboda a03fe3c38e [tests] Cover network and scripting action options (#21615) 2026-10-25 07:49:52 -10:00
Jonathan Swoboda afc8c51f93 [core] Keep components that failed before setup out of the loop (#21622) 2026-10-25 07:49:32 -10:00
Jonathan Swoboda ef7ed69f9d [lightwaverf] Stop requiring the unused name on send_raw (#21620) 2026-10-25 07:48:57 -10:00
Jonathan Swoboda ec32802662 [core] Avoid division by zero in Color::random_color() (#21623) 2026-10-25 07:48:34 -10:00
Jonathan Swoboda 433d8046b1 [espota2] Stop waiting when the device closes mid-reply (#21624) 2026-10-25 07:48:07 -10:00
Jonathan Swoboda 8ecb981656 [tests] Cover device and lvgl action options (#21617) 2026-10-25 07:47:36 -10:00
Jonathan Swoboda 8c990573d7 [core] Generate valid C++ for infinite float values (#21625) 2026-10-25 07:47:24 -10:00
J. Nick Koston bdec0c1485 [copy] Add an integration test for the sensor, binary sensor and text sensor platforms (#21608) 2026-10-25 10:18:37 -04:00
Jonathan Swobodaandclydebarrow 70e85d939d [lvgl] Make canvas gradient and dashed line options compile (#21580)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-10-25 08:59:32 -04:00
Clyde Stubbs 25b3c2820a [core][display] Let users take over a display's enable pins (#21605) 2026-10-25 20:47:50 +11:00
Jonathan Swoboda ef53e675ff [lvgl] Apply opa on indicator.update (#21579) 2026-10-24 21:43:38 -10:00
Anton 10b569491d [nextion] Add tft_upload_http_buffer_size_tx option for TFT upload redirects (#21435) 2026-10-24 19:51:43 -10:00
Bogdan Rădulescu 71b1b58e8d [ble_midi] Add BLE MIDI component (#18508) 2026-10-24 19:04:41 -10:00
Rodrigo Martín 4ab67d867f [mqtt] fix/implement session persistence (#12426) 2026-10-24 19:02:01 -10:00
a2a46944ab [climate][water_heater] Add unit_of_measurement to the template climate and water heater (#20759)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-24 16:16:46 -10:00
537d1643cb [delta_wallbox] Added ability to set the max current (#21587)
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-10-24 16:15:11 -10:00
07744bb53e [adafruit_ble][nrf52][zephyr_ble_server] Initial adafruit ble dfu ota support (#20481)
Co-authored-by: Copilot <223556219+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-10-24 16:03:16 -10:00
c5d69ce22b [wifi] Add ap_mode: ALWAYS_ON and allow a captive portal with ap_mode: WISP (#20077)
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>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-24 16:02:45 -10:00
21fb25a45f [rx8111] Add backup, FOUT, and EVIN events (#21490)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-24 15:10:44 -10:00
J. Nick Koston a208cf2264 [remote_base] Give each receiver an exact dumper table (#21562) 2026-10-25 01:00:17 +00:00
J. Nick Koston a57dff1ea1 [am43] Build and decode packets on the stack instead of heap objects (#21571) 2026-10-24 14:57:26 -10:00
J. Nick Koston 1f8c762f83 [core] Add ConstVector::append_copy and prepend_copy that read flash tables safely (#21561) 2026-10-24 14:48:30 -10:00
J. Nick Koston a339f2cec6 [remote_base] Test dumper dispatch and registration (#21560) 2026-10-24 14:48:30 -10:00
J. Nick Koston bbb978f8a0 [http_request] Test the OTA component's setters and getters from a lambda (#21555) 2026-10-24 14:47:50 -10:00
J. Nick Koston e6b4b80ff8 [tests] Run the integration test TCP servers through a tcp_server context manager (#21541) 2026-10-24 14:47:29 -10:00
J. Nick Koston e17c205863 [tests] Let SensorTracker wait for text sensor values (#21540) 2026-10-24 14:47:14 -10:00
J. Nick Koston 485ce83703 [bq25186] Apply inverted to the PG/GPO pin level (#21547) 2026-10-24 14:45:41 -10:00
J. Nick Koston 0bb4097de4 [libretiny] Flag a BK72xx board layout that misses the RF calibration (#21550) 2026-10-24 14:45:26 -10:00
J. Nick Koston 90a90455ed [ci] Add UART simulation, remote_base loopback, online_image decode, http_request and MQTT flows to the hardware-test skill (#21549) 2026-10-24 14:44:48 -10:00
J. Nick Koston 893b60e7a0 [wifi_info][ethernet_info] Test the IP address child sensors (#21558) 2026-10-24 14:44:31 -10:00
J. Nick Koston 978dbf9b41 [am43] Test the packet encoder and decoder (#21570) 2026-10-24 14:37:38 -10:00
J. Nick Koston b8df87350f [web_server_base] Answer captive portal DNS queries with a 60 s TTL (#21597) 2026-10-24 14:37:15 -10:00
J. Nick Koston 146107c93f [as7341] Deprecate in favour of as734x, removal in 2027.5.0 (#21569) 2026-10-24 14:37:02 -10:00
J. Nick Koston fe6f19a6f5 [wifi] Leave the AP before a BK72xx reboots (#21590) 2026-10-24 14:36:47 -10:00
J. Nick Koston 6d989d60aa [mqtt][core] Wait for the broker to confirm the shutdown message before disconnecting (#21566) 2026-10-24 14:36:29 -10:00
MarvintheboredandJ. Nick Koston 263256b548 [gree] Add yaw1f model for YAW1F remotes (#19722)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-24 22:51:36 +00:00
chorty 97fb058d60 [es7243] Add ES7243 audio ADC driver (#19335) 2026-10-24 11:12:52 -10:00
ebb170e3e2 [gree] Fix YX1FF and YAC IR transmit frames (#20881)
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>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-24 11:02:28 -10:00
c307754c62 [pcf8563] Don't trust the time after a power loss (#20735)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-24 21:00:11 +00:00
b60fc37cbc [modem] Add signal sensors and a network type text sensor (#14683)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-24 09:32:34 -10:00
J. Nick Koston 55940fac30 [const] Move CONF_DEVICE_TYPE to the shared component constants (#21592) 2026-10-24 09:32:22 -10:00
Olivier ARCHERandJ. Nick Koston 6dbd140922 [modem] [wifi] allow wifi AP for modem (WISP) (#21584)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-24 18:40:06 +00:00
Jonathan Swoboda 4da3706c55 [remote_base] Reject delta on the pronto transmit action (#21578) 2026-10-24 08:27:40 -10:00
J. Nick Koston 4a1b837ddb [const] Add CONF_MODEM (#21589) 2026-10-24 08:27:26 -10:00
Jonathan Swoboda 1488ca32b4 [esp32_hosted] Keep hosted task stacks in internal RAM with use_psram (#21583) 2026-10-24 12:58:12 -04:00
5e0e32552b [packet_interface] Add session and XXTEA encryption layers (#21088)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-24 22:09:59 +11:00
Clyde StubbsandClaude Opus 5.5 809bc71649 [packet_interface] Add layer registry and HDLC reliability layer (#21044)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:09:58 +11:00
Clyde StubbsandClaude Opus 5.5 7bec8aeb3d [udp] Add UDP packet interface link (#21042)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:09:58 +11:00
Clyde StubbsandClaude Opus 5.5 2897963905 [runtime_image] Draw BMP pixels opaque (#21424)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:52:04 +00:00
Christopher Pruijsen ace4651476 [as7341] Read channel data as little-endian (#21494) 2026-10-23 22:13:17 -10:00
Clyde StubbsandClaude Opus 5.5 b8d35764e7 [runtime_image] Write opaque RGB565 image directly (#21393)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 19:07:29 +11:00
Clyde StubbsandClaude Opus 5.5 afcf345264 [runtime_image] Limit how long one BMP or QOI decode call runs (#21392)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 07:48:01 +00:00
Clyde StubbsandClaude Opus 5.5 19ce03f4bf [runtime_image] Limit how long one PNG decode call runs (#21391)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 07:32:39 +00:00
Clyde StubbsandClaude Opus 5.5 83f460e964 [runtime_image] Work out grayscale pixels with integer maths (#21390)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 18:15:06 +11:00
Clyde StubbsandClaude Opus 5.5 e964a4d11a [runtime_image] Draw each target pixel once, with integer maths (#21387)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 18:00:14 +11:00
Peter Valkov 29a928ed5a [mqtt] mqtt_client does not honor name_add_mac_suffix for default log_topic, birth_message, will_message, shutdown_message (#9315) 2026-10-23 20:58:39 -10:00
Clyde Stubbs 2cbc1bc02d [http_request] Run requests without blocking the loop (#21486) 2026-10-24 06:14:45 +00:00
Petar Petrov 701e5746b0 [demo][lvgl][const] Move CONF_BUTTONS to the shared component constants (#21551) 2026-10-23 20:14:36 -10:00
Keith BurzinskiandClaude Opus 5.5 9d30bcc721 [core] Do not warn that ESPHome's own core is an unmatched SBOM component (#21546)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 00:52:50 -05:00
J. Nick Koston acb89a8676 [duco] Use the shared sntp_time id in the component test (#21548) 2026-10-23 19:03:42 -10:00
Keith BurzinskiandClaude Opus 5.5 08d231991b [core] Tighten SBOM source detection for local installs (#21529)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 22:55:48 -05:00
253db1e0d5 [hitachi_ac344] Add quiet and vertical swing mode (#18229)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 17:50:48 -10:00
J. Nick Koston bb07d04c90 [tests] Drive the clock in the UDP OTA no-progress test (#21192) 2026-10-23 17:41:33 -10:00
3c1e53628f [hitachi_ac344] Add mildew-proof switch (#18230)
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>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 17:32:27 -10:00
d515e8b2e3 [sim800l] Add on_dtmf_received trigger (#21495)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 17:31:36 -10:00
Keith BurzinskiandClaude Opus 5.5 49d4b83dd2 [core] Add esphome.project.sbom for SBOM supplier and contact (#21455)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 22:29:53 -05:00
Jonathan Swoboda e04dbc1881 [tests] Exercise unused options on core entity actions (#21532) 2026-10-23 22:54:18 -04:00
Jonathan Swoboda 4c62713eb2 [alarm_control_panel] Reject a code on the pending and triggered actions (#21539) 2026-10-23 22:46:33 -04:00
J. Nick Koston b3633fe53c [text_sensor] Publish PROGMEM strings without copying them at the call site (#21536) 2026-10-23 16:40:25 -10:00
b6fdccfc65 [mqtt] Honor publish_nan_as_none for climate target states (#20598)
Co-authored-by: Javier Carrasco Reyes <javier.carrasco@brightstarlottery.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 16:37:22 -10:00
Jonathan Swoboda 1a11424475 [tests] Cover remaining untested triggers and lvgl update actions (#21530) 2026-10-23 22:32:32 -04:00
J. Nick Koston f35395b584 [mqtt] Rebuild subscription topics when needed instead of storing them (#20762) 2026-10-24 02:12:06 +00:00
Jonathan Swoboda 6d110fd981 [lvgl] Skip the creation handler when updating widgets without update options (#21535) 2026-10-23 22:03:52 -04:00
J. Nick Koston 7c69d1b63f [gpio_expander] Move eight expanders onto the shared interrupt and loop base (#21469) 2026-10-23 15:59:18 -10:00
J. Nick Koston 1546ff022c [gpio_expander] Add a shared component base for cached expanders with an interrupt pin (#21468) 2026-10-23 15:59:18 -10:00
mrtoy-meandClaude Opus 5.5 c2911a51bd [tas58xx] Add an activate switch (#21458)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:19:12 -10:00
J. Nick Koston db16d86ee2 [mqtt] Reject subscribed topics longer than the ESP8266 and LibreTiny MQTT client accepts (#20768) 2026-10-23 15:16:12 -10:00
b401a8bf17 [toshiba] Add support for RAS-25J2KVSG-ND (#21413)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-23 14:49:19 -10:00
minimicro34 051978212c [esp32_ble] Ignore unhandled BLE key GAP events (#21457) 2026-10-23 14:01:16 -10:00
Keith BurzinskiandClaude Opus 5.5 971035d6d1 [core] Write esp-idf-sbom manifests into ESP-IDF builds (#21454)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 18:53:10 -05:00
b90e1e43a3 [tuya] fan optimistic mode (#18525)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 13:52:15 -10:00
Jonathan Swoboda fc6accb8d9 [core] Declare component defines missing from defines.h so clang-tidy parses the code behind them (#21521) 2026-10-23 19:45:14 -04:00
Jonathan Swoboda ade01b2c71 [midea] Clamp the follow-me temperature before rounding (#21524) 2026-10-23 19:13:33 -04:00
Jonathan Swoboda b415f86847 [lvgl] Declare the log callback's strchr result as a pointer (#21523) 2026-10-23 13:11:00 -10:00
Jonathan Swoboda 8fa9932033 [ota] Name the protected partition table method per convention and mark the cache statics (#21525) 2026-10-23 13:10:26 -10:00
Jonathan Swoboda c464ef0cc4 [safe_mode] Avoid a duplicated branch when partition access is enabled (#21526) 2026-10-23 13:09:52 -10:00
J. Nick Koston 6f091e4cf2 [light] Use the shared LED strip helpers in five addressable drivers (#21447) 2026-10-23 13:05:11 -10:00
J. Nick Koston 4a9409e299 [light] Add shared helpers for addressable LED strip drivers (#21445) 2026-10-23 13:05:11 -10:00
J. Nick Koston 53357c92f8 [light] Keep strobe and color wipe colors in flash tables (#20222) 2026-10-23 13:04:50 -10:00
J. Nick Koston ed882741bc [remote_base] Let BitTimings vary the mark per bit and send the space first (#21511) 2026-10-23 13:03:05 -10:00
Jonathan Swoboda f994fbde04 [esphome] Reject the esphome OTA platform on targets without an OTA backend (#21515) 2026-10-23 18:51:12 -04:00
Jonathan Swoboda a72adc7573 [libretiny_pwm] Reject LN882H at config time (#21498) 2026-10-23 18:23:35 -04:00
Jonathan Swoboda 55a62993c8 [ezo_pmp] Guard entity pointer uses so the hub builds without sensors (#21516) 2026-10-23 11:56:03 -10:00
Jonathan Swoboda 31a43f13d7 [ecocomfort2] Only compile the fan when a fan platform is configured (#21517) 2026-10-23 11:55:42 -10:00
3c06d41c6a [modem] Add PPPoS networking over a UART cellular modem on ESP32 (#6721)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Rapsssito <contact@rodrigomartin.dev>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 21:38:24 +00:00
Jonathan SwobodaandJ. Nick Koston 3be29eab35 [tests] Cover untested entity platforms, automations and platform-gated code (#21506)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-23 17:14:21 -04:00
J. Nick Koston 202087f919 [tests] Share the sntp id in the delta_wallbox test so it groups with other sntp users (#21510) 2026-10-23 10:53:19 -10:00
9ed59c29e2 [delta_wallbox] Delta AC wallbox (#21231)
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-10-23 10:32:09 -10:00
dependabot[bot] d9548d142c Bump setuptools from 84.0.0 to 84.2.1 (#21499)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-23 10:30:41 -10:00
dependabot[bot] 34ef64186e Bump j178/prek-action from 3.0.2 to 3.1.0 (#21500)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-23 10:30:31 -10:00
Jonathan Swoboda e8a8d6cf95 [tests] Rename test files the runner never matched and drop orphaned common files (#21504) 2026-10-23 16:26:24 -04:00
Jonathan Swoboda 1babcbf371 [pn7160_i2c] Fix test configuring pn7150_i2c instead of pn7160_i2c (#21502) 2026-10-23 16:24:38 -04:00
Jonathan Swoboda f9ca09ab30 [libretiny_pwm] Accept frequency units in set_frequency action (#21497) 2026-10-23 16:22:53 -04:00
Jonathan Swoboda ff56667929 [ads1118][libretiny_pwm][statsd] Add missing component tests and fix statsd test dir name (#21496) 2026-10-23 16:22:23 -04:00
J. Nick Kostonandpre-commit-ci-lite[bot] ebdd9fe07e [sensor] Defer integration imports out of module scope (#18353)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-23 10:16:49 -10:00
J. Nick Koston 5a4025ee12 [output] Reject two outputs on the same channel of a hub (#21467) 2026-10-23 10:11:26 -10:00
J. Nick Koston 2b024114cf [core] Use the final-validate platform helpers in components (#21465) 2026-10-23 09:36:59 -10:00
J. Nick Koston a1316e1bfa [core] Resolve version-less library dependencies like PlatformIO (#21317) 2026-10-23 09:36:16 -10:00
J. Nick Kostonandpre-commit-ci-lite[bot] 9beb41e2ff [binary_sensor] Defer integration imports out of module scope (#18337)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-23 09:34:39 -10:00
J. Nick Koston b9f973ba7f [ci] Add the store_yaml, http_request update, modem and BLE MIDI flows to the hardware-test skill (#21414) 2026-10-23 09:31:24 -10:00
dependabot[bot] 3d42180dcb Bump pypa/gh-action-pypi-publish from 1.14.2 to 1.14.3 (#21501)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-23 09:29:58 -10:00
J. Nick KostonandJonathan Swoboda 8262e336cc [tests] Use subscribe_after_initial_states and next_state in the integration tests (#21438)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-23 14:36:14 -04:00
J. Nick Koston 616100803f [core] Gate ESP32 features on the ESP-IDF version, not the framework (#21415) 2026-10-23 14:28:03 -04:00
J. Nick Koston 8fb9dba023 [tests] Use LineWaiter expect and count in the remaining integration tests (#21439) 2026-10-23 14:26:19 -04:00
J. Nick Koston c57a2b9f73 [tests] Name the I2C bus in two relay test configs so they group with multiplexers (#21485) 2026-10-23 14:16:30 -04:00
J. Nick Koston 9f2be8f195 [weikai] Use StaticRingBuffer for the receive buffer (#21478) 2026-10-23 14:16:13 -04:00
J. Nick Koston aa6c06ac03 [core] Add final-validate helpers for a platform's entries and unique keys (#21462) 2026-10-23 14:15:46 -04:00
2caf6eb6d0 [voice_assistant] Wait for microphone to stop before speaking response (#21380)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Kevin Ahrendt <kahrendt@gmail.com>
2026-10-23 11:09:29 -04:00
Bonne Eggleston 5f90fb501b [epaper_spi] Skip the refresh when the frame is unchanged (#20333) 2026-10-23 16:44:37 +11:00
J. Nick Koston dfe87daa40 [components] Set or clear GPIO expander bits with assign_bits (#21185) 2026-10-22 19:12:07 -10:00
J. Nick Koston 29cb1e9039 [components] Set or clear display, audio and climate bits with assign_bits (#21187) 2026-10-22 19:10:55 -10:00
J. Nick Koston 831f10fdeb [st7305] Put model defaults in the per-model schema (#21436) 2026-10-22 19:04:36 -10:00
Ryan Schlesinger 8a2358d0e6 [max170xx] Shared helper; retry setup and fix sleep for max17043 and max17048 (#18594) 2026-10-22 18:49:50 -10:00
Pieter Kokx f416e1edd4 [duco] Component for Duco ventilation system (#7993) 2026-10-22 17:09:05 -10:00
8156de9613 [packet_interface] Add packet interface component (#21037)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 13:28:03 +11:00
Jonathan Swoboda 89c2cdb592 [core][hmac_sha256] Compare digests in constant time (#21443) 2026-10-22 22:24:30 -04:00
Jonathan Swoboda cb57148178 [syslog] Redact LOG_SECRET values before sending a message (#21444) 2026-10-22 22:13:30 -04:00
Clyde StubbsandClaude Opus 5.5 dcfad5c439 [runtime_image] Decode at the smallest possible JPEGDEC scale (#21386)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:57:05 +11:00
Clyde StubbsandClaude Opus 5.5 e08cbc5661 [online_image] Add on_download_started and on_download_progress (#21355)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:38:13 +11:00
Clyde StubbsandClaude Opus 5.5 2b183be544 [online_image] Decode a JPEG as it downloads (#21342)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:38:13 +11:00
J. Nick Koston 6a18470ccb [tests] Let LineWaiter wait for several log patterns at once (#21153) 2026-10-22 15:05:30 -10:00
J. Nick Koston e9aaf84ee5 [tests] Add helpers to subscribe after initial states and wait for the next state (#21151) 2026-10-22 15:05:04 -10:00
4673 changed files with 203806 additions and 43161 deletions

No files matched your search

+241
View File
@@ -0,0 +1,241 @@
---
name: automations
description: Write ESPHome automation building blocks - triggers (on_* keys), actions and conditions. Use when adding or changing an on_* trigger, an action such as my_component.do_something, a condition, a callback registration method (add_on_*_callback), or code that calls build_callback_automation, build_automation, register_*_action or register_*_condition.
---
# Automations: triggers, actions and conditions
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do
something), and **Conditions** (check if something is true). The helpers named here are documented in
their docstrings in `esphome/automation.py`.
## Triggers: callback method (preferred)
Use `build_callback_automation()` for simple triggers. This eliminates the need for a C++ Trigger class by
using a lightweight pointer-sized forwarder struct registered directly as a callback. No `CONF_TRIGGER_ID`
in the schema.
**Python:**
```python
from esphome import automation
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(MyComponent),
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
for conf in config.get(CONF_ON_STATE, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [(bool, "x")], conf
)
```
`build_callback_automation` arguments: `parent`, `callback_method` (C++ method name), `args` (template args
as `[(type, name)]` tuples), `config`, and optional `forwarder` (defaults to `TriggerForwarder<Ts...>`).
For boolean filtering (e.g. `on_press`/`on_release`), use built-in forwarders with `args=[]`:
```python
for conf_key, forwarder in (
(CONF_ON_PRESS, automation.TriggerOnTrueForwarder),
(CONF_ON_RELEASE, automation.TriggerOnFalseForwarder),
):
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [], conf, forwarder=forwarder
)
```
When the callback's parameters differ from the automation's arguments, or the trigger should only fire for
some values, pass `params`, `forward` and `when`; the helper then generates a capture-less lambda (stored
inline, nothing allocated). Name the parent with `automation.parent_ref(var)` and build `forward` from
`MockObj` calls, not f-strings of C++:
```python
parent = automation.parent_ref(var)
index = cg.RawExpression("index")
await automation.build_callback_automation(
var,
"add_on_state_callback",
[(cg.StringRef, "x"), (cg.size_t, "i")],
conf,
params=[(cg.size_t, "index")],
forward=[cg.StringRef(parent.option_at(index)), index],
)
```
`build_parent_callback_automation`, `build_trigger_callback`, `build_callback_automations` and
`build_trigger_automations` cover the other shapes; see their docstrings in `esphome/automation.py`.
When every action in a callback automation is inline (see Actions below), no `Automation` or action objects
are generated: the callback calls one generated function, at the same RAM cost as a hand-written lambda. A
custom forwarder blocks this unless its filter is also given as `params`, `forward` and `when`, so always pass
them together:
```python
automation.CallbackAutomation(
CONF_ON_IDLE,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(State.IDLE),
params=[(State, "state")],
forward=[],
when=f"state == {State.IDLE}",
)
```
**C++: no trigger class needed.** The callback registration method must be templatized to accept both
`std::function` and lightweight forwarder structs (which avoid heap allocation):
```cpp
class MyComponent : public Component {
public:
// Must be a template -- accepts both std::function and pointer-sized forwarder structs
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
protected:
// Use CallbackManager when callbacks are always registered (e.g. core components)
CallbackManager<void(bool)> state_callback_;
// Use LazyCallbackManager when callbacks are often not registered -- saves 8 bytes
// (nullptr vs empty std::vector) per instance when no callbacks are added
// LazyCallbackManager<void(bool)> state_callback_;
};
```
Never type a registration method's parameter as `std::function`; that forces a heap allocation for every
forwarder struct:
```cpp
// Bad
void add_on_state_callback(std::function<void(bool)> &&callback) {
this->state_callback_.add(std::move(callback));
}
```
| Type | Idle overhead (32-bit) | When to use |
|------|----------------------|-------------|
| `CallbackManager<void(Ts...)>` | 12 bytes (empty `std::vector`) | Callbacks are always or almost always registered |
| `LazyCallbackManager<void(Ts...)>` | 4 bytes (`nullptr`) | Callbacks are often not registered (common case) |
| `AutomationList<N, Ts...>` + `build_automation_lists` | 4 bytes per automation + count, none when unused | A hub fires its own `on_*` automations with no filter (4 B per automation vs 8 B per `Callback`) |
| `automation.new_automation(args, conf)` + the parent's own fixed table | the table entry (pointer plus its own data) | Like `AutomationList`, but each automation needs a stored value of its own, e.g. a threshold the parent compares against |
Both live in `esphome/core/helpers.h` and accept `std::function`, lambdas and forwarder structs through
their templatized `add()` method. `LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first
callback is added. Prefer it for entity-level callbacks where most instances have no subscribers.
## Triggers: Trigger class method
Use `build_automation()` with a `Trigger<Ts...>` subclass only when the forwarder needs **mutable state
beyond a single `Automation*` pointer** (e.g. edge detection tracking previous state, timing logic).
**Python:**
```python
TurnOnTrigger = my_ns.class_("TurnOnTrigger", automation.Trigger.template())
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ON_TURN_ON): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)}
),
}
)
async def to_code(config):
for conf in config.get(CONF_ON_TURN_ON, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
```
**C++:**
```cpp
class TurnOnTrigger : public Trigger<> {
public:
explicit TurnOnTrigger(MyComponent *parent) : last_on_{false} {
parent->add_on_state_callback([this](bool state) {
if (state && !this->last_on_)
this->trigger();
this->last_on_ = state;
});
}
protected:
bool last_on_;
};
```
## Actions
```cpp
template<typename... Ts> class MyAction : public Action<Ts...> {
public:
explicit MyAction(MyComponent *parent) : parent_(parent) {}
void play(const Ts &...) override { this->parent_->do_something(); }
protected:
MyComponent *parent_;
};
```
Register it without writing a builder:
```python
automation.register_simple_action(
"my_component.do_something", MyAction, schema, synchronous=True
)
```
The constructor receives the object named by `config[CONF_ID]`. Use `register_bare_action` for a
no-argument constructor, `register_parented_action` for a class deriving from `Parented<T>`, and the
`@automation.register_action(...)` decorator only when the builder must also set fields.
Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use
`synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`)
or store trigger arguments for later use.
**Actions that only forward templatable values to their parent need no C++ class.** Register them with
`register_apply_action`; do not write a `TEMPLATABLE_VALUE` class or a builder for this shape.
```python
automation.register_apply_action(
"my_component.set_gains",
schema,
automation.ApplyField(CONF_KP, "set_kp", cg.float_),
automation.ApplyField(CONF_KI, "set_ki", cg.float_),
)
```
The `ApplyField`, `ApplyCall` and `register_apply_action` docstrings in `esphome/automation.py` cover the
rest; `cover.control` and `cover.template.publish` are in-tree examples. An action whose input is a byte
payload passes `automation.ApplyBytes(CONF_DATA, "send({})")` in the same call; a constant payload stays in
flash and a lambda's vector is forwarded as data and size. `TEMPLATABLE_VALUE` with `cg.templatable` stays
for actions whose `play()` has real logic beyond forwarding values.
Adjacent `register_apply_action` and `lambda:` actions in a list merge into one generated function and one
object, which saves 12 to 16 bytes of RAM per merged action. An action with its own C++ class ends the merged
run, so prefer `register_apply_action` wherever the action allows it. Because of this, `build_action_list` can
return fewer objects than the config has entries. A builder that wraps `apply_action_builder` may do other work
first, but must return its result unchanged and not use it. A builder that must generate its own statements, and
is registered with `automation.ApplyAction`, can call `apply_statements_action` and still merge; `script.execute`
is an example. `if`, `choose`, `while` and `repeat` whose bodies are all inline become C++ control flow in the
same function, and `register_apply_condition` and `lambda:` conditions become plain expressions there.
## Conditions
```cpp
template<typename... Ts> class MyCondition : public Condition<Ts...> {
public:
explicit MyCondition(MyComponent *parent) : parent_(parent) {}
bool check(const Ts &...) override { return this->parent_->is_active(); }
protected:
MyComponent *parent_;
};
```
Register with `automation.register_simple_condition("my_component.is_active", MyCondition, schema)`;
`register_bare_condition`, `register_parented_condition` and the decorator follow the action rules.
**Conditions that only test their parent need no C++ class either.** Register them with
`register_apply_condition`; the expression is applied to the parent, and an `ApplyCall` compares against
config values.
```python
automation.register_apply_condition("my_component.is_active", schema, "is_active()")
automation.register_apply_condition(
"my_component.state_is",
schema,
automation.ApplyCall("state == {}", ((CONF_STATE, cg.bool_),)),
)
```
`cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples.
+68
View File
@@ -0,0 +1,68 @@
---
name: breaking-changes
description: Decide whether an ESPHome change breaks public API and handle it - what counts as public C++ and Python API, the deprecation window, ESPDEPRECATED for C++, cv.rename_key and deprecation warnings for config keys, and the migration notes a PR needs. Use when removing, renaming or changing the signature of a config option, a public C++ member or a core Python helper.
---
# Public API and breaking changes
Full policy: https://developers.esphome.io/contributing/code/#public-api-and-breaking-changes
## What is public
- **C++ in components:** only features documented at [esphome.io](https://esphome.io) are public API.
Undocumented `public` members are internal.
- **C++ core and base classes** (`esphome/core/`, `Component`, `Sensor`, etc.): all `public` members are
public API.
- **Components with global accessors** (`global_api_server`, etc.): all `public` members are public API
(except config setters).
- **Python:** all documented configuration options are public API. Python code in `esphome/core/` that
existing core components use is stable API. Other Python code is internal unless explicitly documented
for external component use.
- Changing the signature of a lambda that code generation injects is not a breaking change.
## Policy
- Aim for a **6-month deprecation window** when possible.
- Clean breaks are allowed for signature changes, deep refactorings and resource constraints.
- The migration path goes in the PR description (it generates the release notes).
- A blog post is required for core/base class changes or significant architectural changes.
Checklist:
- [ ] Clear justification (RAM/flash savings, architectural improvement)
- [ ] Explored non-breaking alternatives
- [ ] Added deprecation warnings if possible (`ESPDEPRECATED` for C++)
- [ ] Documented migration path in the PR description with before/after examples
- [ ] Updated all internal usage and esphome.io
- [ ] Tested backward compatibility during the deprecation period
## Deprecating C++
```cpp
// Remove before 2026.6.0
ESPDEPRECATED("Use new_method() instead. Removed in 2026.6.0", "2025.12.0")
void old_method() { this->new_method(); }
```
## Deprecating config keys
For a renamed key, use the shared `cv.rename_key` validator with `removed_in` (and `component` for
context); it warns and migrates automatically:
```python
CONFIG_SCHEMA = cv.All(
cv.rename_key(
CONF_OLD_KEY, CONF_NEW_KEY, removed_in="2026.6.0", component="my_component"
),
cv.Schema({...}),
)
```
For other deprecations, warn manually during validation:
```python
# Remove before 2026.6.0
if CONF_OLD_KEY in config:
_LOGGER.warning(
f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0"
)
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
```
Cover the old syntax with a `validate.*.yaml` test (see the `component-tests` skill), and keep the language
schema correct (see the `config-validation` skill).
+82 -7
View File
@@ -9,15 +9,45 @@ ESPHome parses YAML into C++ firmware for memory-constrained microcontrollers
(ESP32, ESP8266, RP2040, LibreTiny). Review changes with that in mind: RAM and
flash are scarce, and code runs unattended for months.
`AGENTS.md` in the repository root is the full contributor guide and the
authority when it disagrees with this summary. The developer documentation at
https://developers.esphome.io explains the component lifecycle and the reasoning
behind these rules. This skill lists the concrete things worth flagging in a
review; read `AGENTS.md` for the detail behind any item.
`AGENTS.md` in the repository root is the contributor guide, and the skills it
lists (`component-codegen`, `config-validation`, `automations`,
`embedded-memory`, `component-tests`, `breaking-changes`) hold the detail. They
are the authority when they disagree with this summary. The developer
documentation at https://developers.esphome.io explains the component lifecycle
and the reasoning behind these rules. This skill lists the concrete things worth
flagging in a review; read the matching skill for the detail behind any item.
Only raise findings that the diff actually introduces or changes. Do not ask for
drive-by cleanup of pre-existing code the PR did not touch.
## Scope: shared code does not ride a component PR
Check the file list first. A PR titled for one component (`[ld2450] ...`) must
not also change code other components build on: `esphome/core/`, the entity
platform components (`esphome/components/sensor/`, `esphome/components/text_sensor/`
including its `filter.*`, `binary_sensor/`, `number/`, ...), the buses
(`esphome/components/{i2c,spi,uart}/`), `network/`, `socket/`, the shared
constants (`esphome/components/const/`, `esphome/const.py`),
`esphome/automation.py`, `esphome/cpp_generator.py` or `esphome/config_validation.py`.
Flag it and ask for the shared change to move into its own PR, which lands first
and is reviewed by that code's owners; the component PR then builds on it. This
holds even when the shared change is small or only the component uses it today.
The exception is a shared change that must update several components in the same
PR so none of them breaks in between (a `[core]` or `[components]` PR that adds a
helper and moves its callers at once).
Not shared code: the component's own platform files (`esphome/components/<c>/sensor.py`,
`text_sensor/__init__.py`, ...), its new `USE_*` lines in `esphome/core/defines.h`,
and its `CODEOWNERS` entry.
## YAML language freeze
Until configurations move to yaml.rocks (esphome/backlog#201), flag any PR that adds to the YAML language: a new tag
in `esphome/yaml_util.py`, new behaviour in the `packages` or `substitutions` components (a new entry in the Jinja
`SAFE_GLOBALS` in `substitutions/jinja.py`, or a custom Jinja filter, counts), or new merge, override or remove
semantics (`esphome/config_helpers.py`, `!extend`, `!remove`). Bug fixes to the existing features are fine. This
covers how YAML files are read, included and combined, not a component's own configuration options.
## Memory and embedded constraints (highest value)
Heap allocation after `setup()` is treated as a reliability bug, not a
@@ -28,8 +58,8 @@ performance nit, because it fragments a small shared heap. Flag:
`StaticVector<T, N>` when a `push_back` API is needed) or fixed at runtime
init (use `FixedVector<T>`).
- Listener / child-entity registration lists stored as `std::vector`; these have
a compile-time-known count and should use `cg.slot_counter()` plus
`StaticVector`.
a compile-time-known count and should use `cg.slot_list()` plus
`SLOT_LIST` (or `cg.slot_counter()` plus `StaticVector`).
- `std::vector<uint8_t>` for a byte buffer that never grows: prefer
`std::unique_ptr<uint8_t[]>` or `std::array`.
- `std::map` / `std::set` / `std::unordered_map` for small datasets (1-16
@@ -39,6 +69,12 @@ performance nit, because it fragments a small shared heap. Flag:
literal already lives in flash).
- `std::string` / `std::to_string` / string-returning helpers on hot paths where
a buffer or view API exists.
- A `set_timeout` / `set_interval` / `defer` lambda whose captures exceed 8 bytes
on 32-bit or are not trivially copyable (`this` plus two 4-byte values, `this`
plus a member function pointer, a `std::string`, a `std::function`):
`std::function` then heap-allocates each time it is scheduled. Flag it on
retries, re-armed timeouts and hot-path `defer`; a `set_interval` set up once
in `setup()` is a single allocation. `[this]` plus one 4-byte value fits.
## C++ conventions
@@ -67,6 +103,29 @@ performance nit, because it fragments a small shared heap. Flag:
typed as `std::function`, so lightweight forwarders avoid a heap allocation.
- Two-space indent, `using` over `typedef`, wrap at 120 columns.
## Use the existing helper
New components often hand-write code a helper already covers. Ask for the
helper; `AGENTS.md` section 4 and the `component-codegen` skill have the detail.
| Seen in the PR | Ask for |
| --- | --- |
| `if (this->x_sensor_ != nullptr) this->x_sensor_->publish_state(v);` | `PUBLISH_STATE(this->x_sensor_, v);` (`esphome/core/entity_base.h`) |
| set warning / `return` / clear warning around a bus result | `if (!this->status_comm_result(ok)) return;` |
| set warning / `return` around an earlier step of a multi-step poll | `if (!this->status_comm_check(ok)) return;` |
| warning set, but no `status_comm_result` or `status_clear_warning` on the success path | the last step uses `status_comm_result(ok)`, or the warning never clears |
| hand-written `set_x_sensor()` plus `x_sensor_` member | `SUB_SENSOR(x)`, `SUB_BINARY_SENSOR`, `SUB_TEXT_SENSOR`, `SUB_SWITCH`, ... |
| `if CONF_X in config: s = await sensor.new_sensor(config[CONF_X])` plus setter | `sensors = sensor.sub_sensors(config)` then `await sensors(CONF_X, hub.set_x_sensor)` |
| hand-written `{"a": MyEnum.MY_ENUM_A, ...}` dict | `MyEnum.options("a", "b", prefix="MY_ENUM_")` |
| loops on `available()` / `read_byte()` | UART `read_available(on_byte)`, `discard_available()`, `LineReader<N>`, `FrameReader<N, HEADER...>` |
| hand-rolled CRC, checksum or bit loop | `crc8`, `crc16`, `xor_bytes`, `sum16_bytes`, `encode_uint16`, ... (`esphome/core/helpers.h`) |
| integer split into buffer bytes by hand (`buf[0] = v >> 8; buf[1] = v;`) | `put_be16`/`24`/`32`, `put_le16`, `put_le32` (`esphome/core/helpers.h`) |
| bare `mark_failed()`, or `ESP_LOGE` then `mark_failed()` | `this->mark_failed(LOG_STR("reason"))` or `LOG_STR(ESP_LOG_MSG_COMM_FAIL)` |
| button/switch/number/select C++ class that only calls the hub | `ApplyButton`, `ApplySwitch`, `ApplyNumber`, `ApplySelect` with `apply=` |
| action or condition class that only forwards to the parent | `register_apply_action` / `register_apply_condition` |
| enum-to-string `switch` returning literals | `PROGMEM_STRING_TABLE` with `get_log_str` / `get_progmem_str` |
| `if (x := config.get(CONF_X)) is not None: cg.add(var.set_x(x))` | `cg.add_optional(var.set_x, config.get(CONF_X))`, when the setter takes the value unchanged |
## Python conventions
- Type-annotate every new function signature (params and return), new dataclass
@@ -76,6 +135,10 @@ performance nit, because it fragments a small shared heap. Flag:
`if (blah := config.get(CONF_BLAH)) is not None:`.
- Reuse existing validators from `config_validation.py` (`cv.rename_key`,
`cv.has_exactly_one_key`, etc.) via `cv.All(...)` instead of hand-rolling.
- A new or changed validator function that wraps, chooses or builds a schema
must be decorated with `@schema_extractor(...)` and handle `SCHEMA_EXTRACT`,
or `script/build_language_schema.py` silently drops the keys behind it (see
the `config-validation` skill).
- `esphome/const.py` is frozen: no new `CONF_` constants there. Define them in
the component's own `.py`, or in `esphome/components/const/__init__.py` when
shared. The same constant defined in three or more component files fails CI.
@@ -96,6 +159,18 @@ performance nit, because it fragments a small shared heap. Flag:
- Config-only checks use the `validate.*.yaml` prefix; compiled checks use
`test.*.yaml`.
## Naming user-facing functions
Documented lambda methods, actions and config keys are public API, so a bad name
is hard to fix once released. Flag:
- A boolean where `true` means off (`display_off(true)`): name the positive
state, e.g. `set_power(bool on)`.
- A name that differs from a sibling component's name for the same operation.
Before a release, rename outright; after, follow the breaking-change policy
below.
## Breaking changes and public API
- Base classes under `esphome/core/` and documented config options are public
+278
View File
@@ -0,0 +1,278 @@
---
name: component-codegen
description: Write the Python side of an ESPHome component - __init__.py and platform files (sensor.py, switch.py, ...), DOMAIN and other metadata, to_code, hub child entities with sub_sensors and friends, pins and referenced components, hub-forwarding entities (ApplyButton, ApplySwitch), uart/i2c/spi constructor helpers, and CORE.data state. Use when creating a component or changing its code generation.
---
# Component code generation
Read https://developers.esphome.io/architecture/components/ for the component lifecycle and how a
component is registered. Schema rules and schema extraction are in the `config-validation` skill;
automations (`on_*`, actions, conditions) in the `automations` skill.
## Files
```
components/[component_name]/
├── __init__.py # Component configuration schema and code generation
├── [component].h # C++ header file (if needed)
├── [component].cpp # C++ implementation (if needed)
└── [platform]/ # Platform-specific implementations
├── __init__.py # Platform-specific configuration
├── [platform].h # Platform C++ header
└── [platform].cpp # Platform C++ implementation
```
## Metadata
- `DOMAIN`: required in every component's `__init__.py`, set to the component's own name as a plain string
(e.g. `DOMAIN = "my_component"`). Other code refers to the component through it, e.g.
`from esphome.components.my_component import DOMAIN`. Keep it in alphabetical order with the other
metadata constants. CI (`lint_component_domain`) fails without it.
- `DEPENDENCIES`: list of required components.
- `AUTO_LOAD`: components to automatically load. Not a way to put entities under the component's own key;
see "Entities of a hub" below.
- `CONFLICTS_WITH`: incompatible components.
- `CODEOWNERS`: GitHub usernames responsible for maintenance.
- `MULTI_CONF`: whether multiple instances are allowed.
## Basic pattern
```python
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_KEY, CONF_ID
DOMAIN = "my_component"
CONF_PARAM = "param" # A constant that does not yet exist in esphome/const.py
my_component_ns = cg.esphome_ns.namespace("my_component")
MyComponent = my_component_ns.class_("MyComponent", cg.Component)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(MyComponent),
cv.Required(CONF_KEY): cv.string,
cv.Optional(CONF_PARAM, default=42): cv.int_,
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_key(config[CONF_KEY]))
cg.add(var.set_param(config[CONF_PARAM]))
```
```cpp
namespace esphome::my_component {
class MyComponent : public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
void set_key(const char *key) { this->key_ = StringRef(key); }
void set_param(int param) { this->param_ = param; }
protected:
StringRef key_;
int param_{0};
};
} // namespace esphome::my_component
```
When the setter takes the value unchanged, use `cg.add_optional(var.set_blah, config.get(CONF_BLAH))`; it
is not awaited, and it skips only `None`, so `0` and `False` are still set. Required, invariant
dependencies are constructor arguments to `cg.new_Pvariable()`, not setters.
## Entity platforms
**Sensor:**
```python
from esphome.components import sensor
CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend(
cv.polling_component_schema("60s")
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
```
**Binary sensor:**
```python
from esphome.components import binary_sensor
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({...})
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
```
**Switch:**
```python
from esphome.components import switch
CONFIG_SCHEMA = switch.switch_schema().extend({...})
async def to_code(config):
var = await switch.new_switch(config)
```
## Entities of a hub
A new component's entities go in platform files (`sensor.py`, `binary_sensor.py`, ...) that take the hub's
id, never under the hub's own key with their platforms added to `AUTO_LOAD` to make that work. Auto loading
a platform because the C++ uses its type is fine. See
https://developers.esphome.io/architecture/components/#exposing-multiple-entities.
```yaml
# Bad - entities under the hub's key
my_hub:
connected:
name: Connected
# Good - the hub, then one platform entry that holds its entities
my_hub:
id: hub1
binary_sensor:
- platform: my_hub
my_hub_id: hub1 # optional with a single hub
connected:
name: Connected
```
**Optional child entities of a hub:** bind the config once with `sensor.sub_sensors(config)` (or
`sub_binary_sensors`, `sub_text_sensors`, `sub_buttons`, `sub_switches`, `sub_numbers`, `sub_selects` in
their domains), adding `parent=hub` for entities that derive from `Parented<T>`, then make one call per
key, even when there is only one. A call creates the entity only when its key is configured, passes it to
the setter and returns it (or `None`); extra arguments such as `min_value` or `options` go on the call.
Always name the setter explicitly on the object that owns it, never with `getattr` and an f-string, and
keep that variable short (`var` for the component itself, `hub` for one fetched with `cg.get_variable`) so
the calls fit on one line. Loops whose setter also takes an index, such as `set_gate_threshold(x, n)`, stay
as they are.
```python
# my_hub/sensor.py
async def to_code(config):
hub = await cg.get_variable(config[CONF_MY_HUB_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, hub.set_temperature_sensor)
await sensors(CONF_HUMIDITY, hub.set_humidity_sensor)
# my_hub/button.py
async def to_code(config):
hub = await cg.get_variable(config[CONF_MY_HUB_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_RESTART, hub.set_restart_button)
```
**Hub forwarding entities need no C++ class:** a button, switch, number or select that only calls its hub
uses `button.ApplyButton` (`switch.ApplySwitch`, `number.ApplyNumber`, `select.ApplySelect`) in its schema
and `apply=` on the `sub_*` call. The body is a call on the hub with `entity` and `state`, `value` or
`index`/`option` in scope; on a switch, number or select `publish="before"` or `"after"` publishes the
state, and `publish="on_success"` publishes only when the hub call returns true (a write the device can
refuse). `publish="momentary"` is only for legacy action switches (restart, factory reset): it publishes off
and runs the body only when turned on. Never use it for a new entity; an action is a button.
```python
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_BLUETOOTH, None, apply="set_bluetooth(state)", publish="after")
```
Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `restore(default)` to still
drive a state when the restore mode is disabled. A hub that applies its switches' restored states itself
reads `sw->get_initial_write_state()`, which already applies `inverted`.
**A hub that forwards data to its children:** when what each child does with the data is known at build time
(decode a register field, match a key or tag, publish a value), declare a `cg.HubFunction` once in the hub's
`__init__.py` and have each platform add one item per entity, built from codegen expressions. Code generation writes
one function per hub. Pass `define=` and put the hub's `HUB_PUBLISHER(<name>, args...)` (in a public section; it
ends on `public:`) and its call under that `#ifdef`; call the pointer only when it is not null. The children stay
plain entities, with no listener class or list. A child that lost a `Component` base keeps its dump lines through
a second hub function (`"dump"`, a `const char *` tag parameter, no define of its own) that the hub's
`dump_config()` calls inside the value function's `#ifdef`; every child adds to both.
**Entities that each watch a register field:** when every field (register, shift, mask, match) is known at
build time, use the rule above (see `bq25186`). A binary sensor tests its field value against an on-values
bitmap (`match`); a one-bit field reads the bit (simplified from bq25186's `_state_expression`):
```python
# hub __init__.py
STATUS_PUBLISHER = cg.HubFunction(
"status", # esphome__<hub>__status_fn, passed to HUB_PUBLISHER(status, ...)'s setter
cg.void,
[(BQ25186StatusRegisters.const_ref(), "status")],
define="USE_BQ25186_STATUS_PUBLISHER",
)
STATUS = STATUS_PUBLISHER.param("status")
# platform to_code, per configured entity
value = (STATUS[reg] >> shift) & mask
if mask != 0x01 or match != 0b10: # 0b10: a one-bit field that is on when set
value = (cg.MockObj(f"{match:#06b}") >> value) & 1
STATUS_PUBLISHER.add(hub, parent_ref(var).publish_state(value))
```
Store a field description per entity (a struct in a `SlotList` sized by `cg.slot_list`) only when the
field is chosen at run time, as in `sen6x`, where the status bit depends on the variant read in `setup()`.
Never a template per bit or a virtual listener base (a second vptr per entity).
## Other helpers
- **Pins and referenced components:** set them with `await cg.add_pin(var.set_cs_pin, config[CONF_CS_PIN])`
and `await cg.add_variable(var.set_bus, config[CONF_BUS_ID])`; both skip a `None` value, so pass
`config.get(KEY)` for an optional key, and return what they created. Use `cg.register_parented` for
`set_parent`.
- `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`, `spi.new_spi_component`,
`modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`) constructs, registers and
attaches in one call.
- A fixed buffer sized from the config is a file-scope static (BSS, not heap):
`buf = cg.static_buffer(config[CONF_ID], cg.uint8, size)`, named after its owner so `esphome analyze-memory`
counts it there; pass `buf` to the constructor.
A buffer that may live in PSRAM takes `psram=True`: PSRAM builds get a zeroed `psram_buffer<T>(size)` allocation
instead, so pass `buf` once and mark the component failed in `setup()` if it is nullptr. `align=4` emits
`alignas(4)`, so a word-aligned byte buffer stays `uint8`. `size` may be an expression such as `MyClass.static_("buffer_size")(n)`, which renders
`ns::MyClass::buffer_size(n)`.
- `cg.static_storage(config[CONF_ID], T, count)` is raw aligned storage for `count` objects as a `std::span<T>`,
for `StaticFixedVector::init()` when codegen knows the count.
- `display.add_writer(var, config)` sets the lambda.
- `MyEnum.options("on", prefix="MY_ENUM_")` builds an enum dict.
- `button.restart_button_schema(cls)` (and `factory_reset_`, `query_`) are button presets.
- `fv.get_declaring_config(id)` finds a hub's config in final validation.
## State during code generation
Use `CORE.data` for component state that needs to persist during configuration generation, namespaced
under the component's `DOMAIN` and held in a `@dataclass`. Avoid module-level mutable globals: they
persist between compilation runs if the host process (e.g. device-builder) doesn't fork/exec, whereas
`CORE.data` is cleared between runs.
```python
from dataclasses import dataclass, field
from esphome.core import CORE
DOMAIN = "my_component"
@dataclass
class MyComponentData:
feature_enabled: bool = False
item_count: int = 0
items: list[str] = field(default_factory=list)
def _get_data() -> MyComponentData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = MyComponentData()
return CORE.data[DOMAIN]
def request_feature() -> None:
_get_data().feature_enabled = True
```
Don't use flat keys such as `CORE.data["my_component_feature"] = True`. Some components use `TypedDict`
instead of a dataclass; both are acceptable.
+104
View File
@@ -0,0 +1,104 @@
---
name: component-tests
description: Write and run tests for ESPHome components - YAML build tests (test.*.yaml, validate.*.yaml, common.yaml) under tests/components, shared bus packages and CI test grouping, C++ gtest unit tests, and host integration tests including the uart_mock fixture. Use when adding or changing files under tests/, or when a code change needs test coverage.
---
# Component tests
## C++ unit tests
gtests live in `tests/components/<component>/*.cpp` and run with `script/cpp_unit_test.py <component>`; see
`tests/components/README.md`. Set the loop clock in a test with `LoopBlockingGuard{nullptr, nullptr,
now_ms}`; the runner resets it before every test.
## Integration tests
`tests/integration/` runs real firmware on the host. Serial components prove timeouts and recovery with the
`uart_mock` fixture (`tests/integration/test_uart_mock_*.py`). Its `responses` match on content every time
they see it; to answer a repeated request differently, use an `on_tx` lambda with a static counter and
`inject_to_rx_buffer` (see `uart_mock_ld2412_requery_after_giveup.yaml`).
## YAML build tests
```
tests/
├── test_build_components/
│ └── common/ # Shared bus packages (uart, i2c, spi, etc.)
│ ├── uart/ # UART at default baud rate
│ ├── uart_115200/ # UART at 115200 baud
│ ├── i2c/ # I2C bus
│ └── spi/ # SPI bus
└── components/[component]/
├── common.yaml # Component-only config (no bus definitions)
├── test.esp32-idf.yaml # config + compile
├── test.esp8266-ard.yaml # config + compile
├── test-variant.esp32-idf.yaml # variant test, config + compile
├── validate.esp32-idf.yaml # config-only (never compiled)
└── validate-legacy.esp32-idf.yaml # config-only variant
```
Run them with `script/test_build_components`. Use `-c <component>` to test specific components and
`-t <target>` for specific platforms.
- **Keep build-test variants few:** one `test.<platform>.yaml` per platform the code actually differs on.
A variant per board for platform-identical code only adds CI time.
- **Config-only test files (`validate.*.yaml`):** use this prefix when a YAML file only needs to exercise
schema/validation paths and does not need to be compiled. CI runs `validate.*.yaml` files with
`esphome config` only and skips them during compile. The grammar mirrors `test.*.yaml`:
- `validate.<platform>.yaml`: base config-only test
- `validate-<variant>.<platform>.yaml`: config-only variant
Use this for deprecated-syntax migration tests, schema edge cases, or platform-specific validation
branches where building firmware adds no signal. A component may have any mix of `test.*.yaml` and
`validate.*.yaml` files. Validate files never participate in bus grouping; each one runs as its own
`esphome config` invocation.
When a PR's only edits to a component are `validate.*.yaml` files (no source changes, no `test.*.yaml`
changes, and the component isn't pulled in as a dependency of another changed component), CI skips the
compile stage for that component and only runs config validation. This is decided in
`script/determine-jobs.py` via `_component_change_is_validate_only` and surfaced as the
`validate_only_components` output that the `test-build-components-split` job consumes.
## Grouping with packages
Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define
buses (uart, i2c, spi, modbus) directly in test YAML files**; always use packages from
`test_build_components/common/`.
All includes in test files must go through dict-style `packages:` so that batch grouping works; the
grouping scripts only understand dict-style packages. Never use list-style packages
(`packages: [- !include ...]`) or top-level merge keys (`<<: !include common.yaml`). Bus packages are keyed
by the bus name; the component's `common.yaml` is keyed by the component name (e.g.
`cst328: !include common.yaml`):
```yaml
# test.esp32-idf.yaml - everything included via named packages
packages:
uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
my_component: !include common.yaml
```
```yaml
# common.yaml - component config only, NO bus definitions
my_component:
id: my_instance
sensor:
- platform: my_component
name: My Sensor
```
Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing
CI build time. `!extend` and `!remove` in a `test.*.yaml` also make the component ungroupable; add a
standalone entity with its own id instead.
In a grouped test, set `i2c_id: i2c_bus`, `one_wire_id` and `motion_id` even with one candidate: a merged
batch can add a second one ("Too many candidates"). `script/ci-custom.py` checks this.
A test that needs a shared component in a particular state (`esp32_ble` security settings, an option that
PSRAM rejects) cannot be grouped: the merged block wins and the whole batch fails. Add the component to
`ISOLATED_COMPONENTS` in `script/analyze_component_buses.py`, or move the option to a `test-<variant>` file.
To verify that all components can be tested together without ID conflicts or configuration issues:
```bash
./script/test_component_grouping.py -e config --all
```
This tests all components in a single build to catch conflicts that might not appear when testing
components individually. Use `-e config` for fast configuration validation, or `-e compile` for full
compilation testing.
+124
View File
@@ -0,0 +1,124 @@
---
name: config-validation
description: Write or change ESPHome configuration validation - CONFIG_SCHEMA, cv.Schema, cv.typed_schema, custom validator functions, config keys and CONF_ constants, renamed or deprecated keys, and the language schema that script/build_language_schema.py extracts from them. Use whenever a change touches a component's schema or a validator in config_validation.py, automation.py or a component's .py files.
---
# Configuration validation
ESPHome validates YAML with `voluptuous` schemas built from `esphome/config_validation.py` (`cv`). The
same schemas are also walked by `script/build_language_schema.py`, which produces the JSON language schema
that editors and the dashboard use for completion and checking. A validation change is only finished when
both still work.
## Writing schemas
- **Type hints:** validators are typed like any other function: `def validate_x(config: ConfigType) ->
ConfigType:`, with `ConfigType` imported from `esphome.types`.
- **One schema per model:** when a component's options depend on a `model` key, use
`cv.typed_schema({...}, key=CONF_MODEL, default_type=...)` with each model's own keys and defaults, not
one schema plus a validator that rejects keys and fills defaults afterwards.
- **Reuse existing validators:** before writing a custom validator, check for an existing one in
`config_validation.py` and compose it in `cv.All(...)` rather than duplicating logic across components.
For example, rename a config key with `cv.rename_key(CONF_OLD, CONF_NEW, removed_in="2026.6.0")`, and
reject mutually-exclusive keys with `cv.has_at_most_one_key(...)` / `cv.has_exactly_one_key(...)`. See
how `api` composes `cv.has_exactly_one_key` + `cv.rename_key`.
- **Common validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0,
max=100)`, `cv.positive_int`, `cv.percentage`.
- **Complex validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
- **Platform-specific:** `cv.only_on(["esp32", "esp8266"])`, `esp32.only_on_variant(...)`,
`cv.only_on_esp32`, `cv.only_on_esp8266`, `cv.only_on_rp2040`.
- **Framework-specific:** `cv.only_with_framework(...)`, `cv.only_with_arduino`. On the ESP32, Arduino is
built as a component of ESP-IDF, so an ESP-IDF API, sdkconfig option or version floor is there on both
frameworks: gate on `esp32.require_idf_version(cv.Version(5, 5, 3), "psram_dma")`, which checks the
platform and the ESP-IDF under either framework, as `spi`, `usb_host` and `wifi` do. Reserve
`cv.only_with_framework("esp-idf")` for a build that has been tried on Arduino and fails there, and write
the actual failure next to it (`sendspin`: arduino-esp32 below ESP-IDF 6.0 bundles a second libsodium,
and from 6.0 sendspin-cpp's enumerators collide with `Arduino.h` macros such as `DISPLAY`).
- **Schema extensions:**
```python
CONFIG_SCHEMA = cv.Schema({ ... })
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(i2c.i2c_device_schema(0x48))
.extend(spi.spi_device_schema(cs_pin_required=True))
```
- **Constants:** `esphome/const.py` is frozen; do not add new `CONF_` constants there. Define a
component-local constant in the component's own `.py`; for a constant shared by multiple components, add
it to `esphome/components/const/__init__.py`. CI (`lint_constants_usage`) fails if the same constant is
defined in three or more component files. Constants used in core files (those not under
`esphome/components`) may be added to `esphome/const.py` but will require adjustment to the CI check.
- **Renamed or removed keys:** see the `breaking-changes` skill for `cv.rename_key` and manual
deprecation warnings.
- **Config-only tests:** schema edge cases and deprecated-syntax migrations go in `validate.*.yaml` test
files (see the `component-tests` skill).
## Schema extraction
`script/build_language_schema.py` sets `esphome.schema_extractors.EnableSchemaExtraction = True` before it
imports any component, then walks every `CONFIG_SCHEMA` (and platform schema) recursively. It understands
`cv.Schema`, `cv.All`, `cv.Any`, dicts of `cv.Required` / `cv.Optional` keys, `.extend()`, `cv.ensure_list`,
`cv.typed_schema`, registries and the known `cv` validators. **A plain function it cannot see into**: a
custom validator that builds or picks a schema at validation time hides every key behind it, and the
dumper silently emits nothing for them. CI only runs the script with `--check`, which does not compare the
output, so a lost schema is not caught.
When a validator function wraps, chooses or builds a schema (rather than checking a single value), make it
visible to the dumper:
1. Decorate it with `@schema_extractor("<type>")` from `esphome.schema_extractors`.
2. At the top of the function, return the data for that type when it is called with `SCHEMA_EXTRACT`
instead of a config value. Compare with `is` (`value is SCHEMA_EXTRACT`); older code uses `==`.
```python
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@schema_extractor("schema")
def _outgoing_connection_schema(config: ConfigType | None) -> ConfigType:
if config is SCHEMA_EXTRACT:
return _OUTGOING_CONNECTION_SCHEMA
if config is None:
config = {}
return _OUTGOING_CONNECTION_SCHEMA(config)
```
The type names the dumper accepts, and what `SCHEMA_EXTRACT` must return for each, are in the
`hidden_schemas` branch of `convert()` in `script/build_language_schema.py`. The common ones:
| Type | Return for `SCHEMA_EXTRACT` | Example |
|------|-----------------------------|---------|
| `"schema"` | the schema (or a representative one) to walk | `api` `_outgoing_connection_schema`, `restore_state_schema` in `esphome/core/entity_helpers.py`, `model_schema_extractor` in `esphome/components/mipi/__init__.py` |
| `"enum"` | the dict of options | `sensor_entity_category` in `esphome/components/sensor/__init__.py` |
| `"one_of"` | the list of options | `cv.one_of` |
| `"maybe"` | `(validator, key)` for a scalar shorthand | `cv.maybe_simple_value`, `maybe_conf` in `esphome/automation.py` |
| `"templatable"` | the inner validator | `cv.templatable` |
| `"automation"` | the automation schema | `automation.validate_automation` |
Points to watch:
- The dumper matches the function object by `repr`, so the decorated function must be the exact object
placed in the schema. In a factory that returns a new closure per call, decorate the inner function (as
`restore_state_schema` and `cv.maybe_simple_value` do), not the factory.
- The decorator only records anything when extraction is enabled, and that flag is set before imports, so
the decorator must run at import time (module level or inside a factory called at module level).
- A schema chosen per config value (for example by `model`) has no single answer: return a representative
schema, as `model_schema_extractor` does.
- Plain value checks (`cv.All(cv.string, my_check)`) need nothing: the dumper still sees the surrounding
types and keys.
- `cv.Schema`, `cv.typed_schema`, `cv.ensure_list` and the `cv` helpers are already handled; prefer
composing them over a custom function, which also avoids the extractor work.
### Checking the output
Build the language schema into a scratch directory and look at the component's JSON (it takes about ten
seconds):
```bash
python3 script/build_language_schema.py --output-path /tmp/schema
python3 -c "import json; print(json.dumps(json.load(open('/tmp/schema/my_component.json')), indent=1))" | less
```
Every key a user can write should appear under `config_vars` with a sensible `type`; compare against the
same output from `dev` when changing an existing schema. If the script prints `Unknown extracted schema
type` or raises, the decorator type or the value returned for `SCHEMA_EXTRACT` is wrong. Unit tests for
the dumper live in `tests/script/test_build_language_schema.py`.
+177
View File
@@ -0,0 +1,177 @@
---
name: embedded-memory
description: Choose ESPHome C++ containers and storage that avoid heap use and fragmentation on microcontrollers - std::array, StaticVector with cg.slot_list/SLOT_LIST, FixedVector, std::unique_ptr byte buffers, StringRef, RAMAllocator, and what to avoid (std::vector growth, std::map, std::deque, std::string, new (std::nothrow)). Use when adding or changing C++ members, containers, buffers, strings or registration lists, or when reducing RAM or flash use.
---
# Memory on embedded targets
ESP devices run for months with small heaps shared between Wi-Fi, BLE, LWIP, and application code. Over
time, repeated allocations of different sizes fragment the heap. Failures happen when the largest
contiguous block shrinks, even if total free heap is still large. We have seen field crashes caused by
this.
**Heap allocation after `setup()` should be avoided unless absolutely unavoidable.** Every
allocation/deallocation cycle contributes to fragmentation. ESPHome treats runtime heap allocation as a
long-term reliability bug, not a performance issue. Helpers that hide allocation (`std::string`,
`std::to_string`, string-returning helpers) are being deprecated and replaced with buffer and view based
APIs.
## Container guidelines
1. **Compile-time-known sizes:** use `std::array` instead of `std::vector` when size is known at compile
time.
```cpp
// Bad - generates STL realloc code
std::vector<int> values;
// Good - no dynamic allocation
std::array<int, MAX_VALUES> values;
```
Use `cg.add_define("MAX_VALUES", count)` to set the size from Python configuration.
**For byte buffers:** avoid `std::vector<uint8_t>` unless the buffer needs to grow. Use
`std::unique_ptr<uint8_t[]>` instead. It does **not** provide bounds checking or iterator support like
`std::vector<uint8_t>`; use it only when you do not need these features.
```cpp
// Bad - STL overhead for simple byte buffer
std::vector<uint8_t> buffer;
buffer.resize(256);
// Good - minimal overhead, single allocation
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(256);
// Or if size is constant:
std::array<uint8_t, 256> buffer;
```
When the size comes from the config, declare it with `cg.static_buffer()` in `to_code` and pass the pointer
to the constructor, so it lands in BSS (see the `component-codegen` skill).
2. **Compile-time-known fixed sizes with vector-like API:** use `StaticVector` from
`esphome/core/helpers.h` for compile-time fixed size with a `push_back()` interface (no dynamic
allocation).
```cpp
// Bad - generates STL realloc code (_M_realloc_insert)
std::vector<ServiceRecord> services;
services.reserve(5); // Still includes reallocation machinery
// Good - compile-time fixed size, no dynamic allocation
StaticVector<ServiceRecord, MAX_SERVICES> services;
services.push_back(record1);
```
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
**Listener and child-entity registration lists are the most common case, and the most commonly
missed.** When the hub only forwards data to its children and each child's handling is known at build
time, keep no list at all: use `cg.HubFunction` (see the component-codegen skill). When the hub must
store the children (it iterates them for other reasons, or picks fields at run time), a `register_*()`
method called once per child at code generation time has a count that is known at compile time, so it
should never be a `std::vector`. Declare the list with `SLOT_LIST` and register each child through the
call that `cg.slot_list()` returns, passing the hub's `DOMAIN`.
```python
# hub component's __init__.py
register_listener = cg.slot_list(
"MY_COMPONENT_LISTENER_COUNT", "register_listener", DOMAIN
)
# a platform of the hub
async def to_code(config: ConfigType) -> None:
...
hub = await cg.get_variable(config[CONF_MY_COMPONENT_ID])
cg.add(register_listener(hub, var))
```
```cpp
class MyComponent : public Component {
public:
SLOT_LIST(MyComponentListener, listeners_, register_listener, MY_COMPONENT_LISTENER_COUNT)
```
Do not put `#ifdef` around the macro or the loops over the list. Put `#if MY_COMPONENT_LISTENER_COUNT`
only around code that needs a child's own component (its header, its entity type).
If registering does more than append, write the method under `SLOT_LIST_REQUIRES(count)` and store into a
`[[no_unique_address]] SlotList<Entry, count>` member.
If the C++ side adds itself to the list, or the define gates more than the list, use `cg.slot_counter()`
keyed by hub (`_request_slot(str(hub))`) and put the storage behind `#ifdef`.
Request slots from `to_code`, not from a job that runs after `CoroPriority.FINAL`; a late request
raises rather than silently undercounting.
3. **Runtime-known sizes:** use `FixedVector` from `esphome/core/helpers.h` when the size is only known at
runtime initialization.
```cpp
// Bad - generates STL realloc code (_M_realloc_insert)
std::vector<TxtRecord> txt_records;
txt_records.reserve(5); // Still includes reallocation machinery
// Good - runtime size, single allocation, no reallocation machinery
FixedVector<TxtRecord> txt_records;
txt_records.init(record_count); // Initialize with exact size at runtime
```
This eliminates `_M_realloc_insert` and `_M_default_append` template instantiations (200-500 bytes per
instance), needs no upper bound, and is compatible with protobuf code generation when using
`[(fixed_vector) = true]`.
When codegen knows the count, use `StaticFixedVector<T>` and pass `cg.static_storage(config[CONF_ID], T, count)`
to its `init()`, so the elements live in BSS instead of the heap.
4. **Small datasets (1-16 elements):** use `std::vector` or `std::array` with simple structs instead of
`std::map`/`std::set`/`std::unordered_map`.
```cpp
// Bad - 2KB+ overhead for red-black tree/hash table
std::map<std::string, int> small_lookup;
std::unordered_map<int, std::string> tiny_map;
// Good - simple struct with linear search (std::vector is fine)
struct LookupEntry {
const char *key;
int value;
};
std::vector<LookupEntry> small_lookup = {
{"key1", 10},
{"key2", 20},
{"key3", 30},
};
// Or std::array if size is compile-time constant:
// std::array<LookupEntry, 3> small_lookup = {{ ... }};
```
Linear search on small datasets is often faster than hashing or tree overhead, but this depends on
lookup frequency; in hot paths the O(1) vs O(n) difference may still matter. It is the heavy containers
(`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows
otherwise.
5. **Strings set once from configuration:** use `StringRef` (`esphome/core/string_ref.h`) rather than
`std::string`. Code generation passes a string literal that lives in flash for the life of the program,
so storing a `std::string` copies it onto the heap for nothing. `StringRef` is a non-owning pointer plus
length; it does not copy, and it must only ever refer to storage that outlives it (a string literal, or
a buffer owned elsewhere).
```cpp
// Bad - heap copy of a literal that is already in flash
void set_keys(std::string keys) { this->keys_ = std::move(keys); }
std::string keys_;
```
```cpp
// Good - no allocation
void set_keys(const char *keys) { this->keys_ = StringRef(keys); }
StringRef keys_;
```
6. **Avoid `std::deque`:** it allocates in 512-byte blocks regardless of element size, guaranteeing at
least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained
devices.
7. **Never use `new (std::nothrow)`:** on ESP-IDF exceptions are disabled, so a failed nothrow allocation
aborts instead of returning `nullptr`. Use `RAMAllocator` from `esphome/core/helpers.h`; CI rejects
`std::nothrow`.
8. **Detection:** look for these patterns in compiler output:
- Large code sections with STL symbols (vector, map, set)
- `alloc`, `realloc`, `dealloc` in symbol names
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
- Red-black tree code (`rb_tree`, `_Rb_tree`)
- Hash table infrastructure (`unordered_map`, `hash`)
Callback storage (`CallbackManager` vs `LazyCallbackManager`) and templated registration methods are
covered in the `automations` skill.
## Where to spend effort
Prioritize container optimization for core components (API, network, logger), widely used components
(mdns, wifi, ble), and components causing flash size complaints. Avoiding heap allocation after `setup()`
is always required regardless of component type; the priority list is about effort spent on container
migration (e.g. `std::vector` to `StaticVector`).
+53 -9
View File
@@ -38,11 +38,15 @@ driven, what the logs show, and the same flow on a `dev` build when a quirk coul
5. **Flash and RAM deltas are measured against the PR's merge base** (`git merge-base origin/dev HEAD`
checked out in a throwaway worktree), with the baseline commit named in the table. When a delta
looks odd, diff symbol sizes (`nm -S -C --size-sort` from the matching toolchain) before quoting it.
When a change moves storage between the heap and static RAM, the static number alone misleads: also log
the free heap at the end of setup (`on_boot` at priority -100) over three boots of each build and report
the total in use (static RAM plus heap used).
6. **Prove a quirk is pre-existing before blaming the PR:** run the identical flow on a `dev` build in
the same session and say so in the results.
7. **Pipe safely.** Set `PYTHONUNBUFFERED=1` on any `esphome run` or `esphome logs` whose output is
piped, or lines land minutes late. Bound every long command with `timeout` (coreutils) or
`perl -e 'alarm shift; exec @ARGV' <seconds> <command>` on macOS without it.
`perl -e 'alarm shift; exec @ARGV' <seconds> <command>` on macOS without it. Save an exit code with `rc=$?`
right after the command: in `echo "$(date +%T) rc=$?"` the command substitution runs first and `$?` reads 0.
## Workflow
@@ -52,6 +56,9 @@ driven, what the logs show, and the same flow on a `dev` build when a quirk coul
them apart with a 10 s capture using `serial_capture.py --reset` (ESP8266 prints its ROM banner at
74880 baud, LibreTiny boards print `[LT]` and ESPHome lines, BK7238 prints its bootloader version).
`ports.py --wait 2e8a` blocks until a Pico re-enumerates.
A change in validation or platform independent code needs no board: a `host:` build serves the API on
port 6053 on every interface (reach it at 127.0.0.1, and add `api: encryption:` on a shared network); see
recipes/homeassistant.md for one against a throwaway Home Assistant.
2. **Make a scratch directory** outside the checkout, with `secrets.yaml` and the templates you need.
One yaml per board per variant, file name equal to `name:`.
3. **Compile** in the right tree:
@@ -67,9 +74,17 @@ driven, what the logs show, and the same flow on a `dev` build when a quirk coul
automations in the config itself.
7. **Analyze** with `grep -a -c` counts over the capture and keep an `-events.txt` extract of the lines
that matter. Decode crashes with the platform's `addr2line` (see `reference/boards.md`).
8. **Report** on the PR as a table: build SHA, board and framework, what was driven, counts, result,
plus the `dev` control row and the exact flow in a `<details>` block. No credentials, no port names,
no IP addresses.
8. **Report** on the PR. Every result comment starts with this two-line GitHub alert, then a blank line:
```
> [!NOTE]
> Automated testing results
```
Then one short sentence saying what was tested (builds and SHAs, board, what was driven), a results table
(build SHA, board and framework, what was driven, counts, result, plus the `dev` control row), short bullets
for the findings, and the setup and exact flow in a collapsed `<details>` block. Answer any question asked on
the PR directly in the comment. No credentials, SSIDs, IP addresses, port names or other bench details.
## Flashing by chip family
@@ -80,6 +95,7 @@ driven, what the logs show, and the same flow on a `dev` build when a quirk coul
| ESP32-S3, C3, C6, H2 native USB | `esphome upload --device /dev/cu.usbmodem*` | `logger: hardware_uart: USB_SERIAL_JTAG`. The USB console can wedge when the app takes over the console (0 bytes, esptool `No serial data received`); replug, or watch through `esphome logs --device <name>.local` |
| Pico W / Pico 2 W | `esphome upload --device <port>` (1200 baud touch); first flash ever via BOOTSEL and `--device /Volumes/RPI-RP2` or `RP2350` | port re-enumerates after upload: `ports.py --wait 2e8a` then capture |
| BW15 kit (RTL8720CF, rtl87xx) | `~/.platformio/penv/.libretiny/bin/python $SKILL/scripts/flash_rtl87xx_kit.py flash write -d <port> <build>/.pioenvs/<name>/*.uf2`. Plain `esphome upload` and plain ltchiptool time out because opening the port asserts DTR/RTS and holds the chip in reset | `serial_capture.py --released`, never `esphome logs` |
| BW12 and other AmebaZ modules (RTL8710BX, rtl87xx) on a plain USB-UART | `~/.platformio/penv/.libretiny/bin/python -m ltchiptool flash write -B 115200 -b 115200 -t 120 -d <port> <uf2>`, started first, then strap by hand: CEN to GND, TX2 to GND, release CEN, release TX2. Without `-B 115200` the system data read at 1.5 Mbaud fails on CH340s (`Data read error at 0x9000`) and nothing is written. A 1 MB image takes about 80 s. A development board with DTR and RTS wired to CEN and the download pin needs no strap | `serial_capture.py`; a reset pulse does not reach CEN on a plain adapter, so a capture starts mid-run |
| BK7238 / CB3S (bk72xx, RTS on CEN) | `~/.platformio/penv/.libretiny/bin/python $SKILL/scripts/flash_bk72xx.py flash write -d <port> <uf2>` (about 75 s) | logs are on UART2 by default; `logger: hardware_uart: UART1` puts them on the flashing adapter. `serial_capture.py --reset` gives a chip-enable reset; a plain open may or may not reset it, check for the bootloader banner |
| LN882H | ltchiptool forces 1 Mbaud for writes; weak adapters need `LN882H_YM_BAUDRATE` patched to 115200 in the penv's `ln882htool.py`; short BOOT to GND while the tool connects | OTA after the first flash |
| nRF52840 Dongle (PCA10059) | Nordic `nrfutil nrf5sdk-tools dfu usb-serial` on the bootloader port, or `dfu: true` in the config so the 1200 baud touch pin-resets into the bootloader; adafruit-nrfutil is refused | app has two CDC ports, ESPHome log on the second |
@@ -102,6 +118,8 @@ ESP-IDF OTA image: `.esphome/build/<name>/build/firmware.ota.bin`. LibreTiny OTA
and then; retry up to three times.
- `api_button.py HOST KEY list|press NAME [...]`: list a device's template buttons or press them over the API; the
recipes drive their test configs with it.
- `apiconn.py`: `connect(host, tries)` (API connect and login with retries, `API_KEY` from the env) and `log_text()`
(an API log line without ANSI codes), imported by the newer API drivers.
- `wifisave_hammer.sh KIND DUT_PORT DUT_YAML DUT_TREE CLIENT_PORT CLIENT_YAML CLIENT_TREE [SETTLE]`:
the fallback AP plus captive portal hammer end to end (compile, flash, capture, counts) with the
templates below. KIND is `esp`, `pico`, `bw15` or `bk`. It ends with `RESULT PASS` (exit 0),
@@ -110,12 +128,13 @@ ESP-IDF OTA image: `.esphome/build/<name>/build/firmware.ota.bin`. LibreTiny OTA
(exit 2: a precondition was missed, the stimulus was insufficient, or a capture did not last the run)
or exits 1 when a build, flash or port step failed.
Per-topic scripts, each described in the matching `reference/recipes/<component>.md`: `online_image/`
(server, image generator, API driver), `zwave_proxy/` (checksum verifier, device info), `usb_cdc_acm/`
Per-topic scripts, each described in the matching `reference/recipes/<component>.md`: `heap_alloc/` (operator new caller
recorder, dump saver and decoder, cover cycle driver), `online_image/`
(server with prefixed misbehaviour cases, PNG, JPEG, BMP and QOI generators, API drivers, first pass timer), `zwave_proxy/` (checksum verifier, device info), `usb_cdc_acm/`
(host driver, Zephyr driver probe), `rtsp_server/` (protocol client, raw rate reader, measurement set,
API image grab), `cse7766/` (API observer with ping), `zephyr_coredump/` (reader flow, decode with the
Zephyr tools), `libretiny_prefs/` (rtc preference harness header), `libretiny_crash/` (BK72xx crash probe header and
capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `ld2450/` (zone number driver, loopback harness driver with number and binary sensor states and TX frame summarizer), `api/` (raw plaintext native API client for user defined actions), `dfrobot_sen0395/` (API driver and case runner), `homeassistant_light/` (direct bulb driver, held-report case driver), `usb_audio/` (URL player), `speaker_media_player/` (API player driver, pipeline task case runner), `web_server/` (SSE client, request mix, SSE and HTTPD case runners), `mqtt/` (broker config with TLS, client-certificate and WebSocket listeners, test CA with server and client certificates, transport case runner, flash-and-drive runner, CLI TLS check without a board), `udp/` (datagram recorder, `udp.write` payload checker and case driver), `entity_icons/` (icon and device class readback over the API, MQTT discovery, web_server and dump_config, with a comparer).
Zephyr tools), `libretiny_prefs/` (rtc preference harness header), `libretiny_crash/` (BK72xx crash probe header,
capture decoder, RTL8720C ROM dump versus report matcher), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `ld2450/` (zone number driver, loopback harness driver with number and binary sensor states and TX frame summarizer, frame stream harness with presence hold and link loss drivers, BT MAC driver), `api/` (raw plaintext native API client for user defined actions, store_yaml recovery client over Noise or plaintext), `http_request/` (update manifest server, update state driver, request test server, action case driver, concurrency, reboot, update check and OTA drivers, `ota_auth/` OTA request cases with basic auth), `modem/` (button and sensor driver over the plaintext API, also used for the BLE MIDI boards, AT commands for the UART passthrough), `dfrobot_sen0395/` (API driver and case runner), `homeassistant_light/` (direct bulb driver, held-report case driver), `usb_audio/` (URL player), `speaker_media_player/` (API player driver, pipeline task case runner), `web_server/` (SSE client, request mix, SSE and HTTPD case runners), `mqtt/` (`ha_discovery/`, `disable/`, `retained_burst/` and `reconnect/` runners; broker config with TLS, client-certificate and WebSocket listeners, test CA with server and client certificates, transport case runner, flash-and-drive runner, CLI TLS check without a board, MAC suffix runner with per-variant configs for two boards, `shutdown/` shutdown message runner and matcher), `udp/` (datagram recorder, `udp.write` payload checker and case driver), `entity_icons/` (icon and device class readback over the API, MQTT discovery, web_server and dump_config, with a comparer), `uart_sim/` (API driver and flash, capture and drive runner for the simulated UART sensors), `remote_base/` (loopback runner and API log decoder per sent frame), `xiaomi_ble/` (frame and header generator with the expected counts, injecting, advertising and receiving configs, counter and runner), `ble_terminate/` (proxy and peer configs, connect and disconnect driver, peer disconnect matcher, runner), `i2c_slave/` (D1 mini slave and BK7238 master configs), `wpa3_ap/` (PMF AP with join and leave logging, BK7238 and CB3S DUTs, runner and summary), `wisp_ap/` (WISP upstream with AP buttons, WISP and ALWAYS_ON DUTs, client, runner and per phase summary), `wifi_restart/` (WPA3 mixed WISP upstream, static IP DUTs with syslog, runner and summary), `ota_timing/` (OTA upload timer with deflate, raw and TCP modes, API build check, A/B runner and median summary, `inflate_bench/` on-device inflate benchmark with stream generator, configs and syslog runner), `gpio_hold/` (ISR pin hold self test with ESP32 and S3 configs), `ble_event_inject/` (GAP and GATTC event injector header and proxy package), `light_sleep_wake/` (LEDC wave and edge timer for light sleep wakeup on a C3, external wave generator and per-call edge detail probe for peripheral power-down on the P4), `manual_ip/` (station and AP manual IP configs, DNS probe and runners), `web_handlers/` (late handler, web_server, captive_portal and prometheus variants, web OTA runner), `bk72xx_flash/` (flash readback with retries, per partition diff of two dumps), `ble_server_security/` (secure GATT server and pairing client configs, driver, summary and runner), `prometheus/` (datetime metrics configs and driver), `zephyr_ble_server/` (nRF52 server, looping ESP32 client and UF2 flash and count runner), `ble_adv_triggers/` (advertiser with six raw frames, ESP32 and BK72xx receivers with eleven triggers, runner and checker), `sun/` (longitude sweep package, runner and checker), `sensor_filters/` (every filter shape with a fixed input sequence, per-board packages and runner), `led_driver_sniff/` (SPI slave capture header with selftest, config generator, window comparer and runner), `hub_input_sim/` (keypad, wiegand and rdm6300 simulation on a D1 mini, API log runner), `value_range/` (on_value_range sequence with a restart, runner), `ezo_fake_bus/` (fake EZO I2C bus and step config, runner), `twai_self/` (TWAI self-reception header, on_frame config and counter runner), `ota_automations/` (OTA automation package for both OTA platforms, four case runner, repeated web upload runner and per case summary), `output_button/` (pad edge timer header, button package and runner), `homeassistant_button/` (raw plaintext API client for action calls and wizard inputs, package and runner), `uart_button/` (UART0 loopback package, API driver with TX and RX counts, runner), `interval/` (plain and delayed interval package, timing summary and runner), `smp_udp_ota/` (SMP reset fault injector, OTA runner, per mode result check, MCUboot swap limit, running image check and slot blanking UF2, Thread device config), `ld2420_sim/` (fake LD2420 answering the hub's commands on a UART0 loopback, API state recorder, two-build comparer and runner), `nrf52_tcp_probe/` (TCP and UDP socket probe config, build and log check, API check over Thread), `rs485_frame/` (simulated DLE/STX/ETX bus device, frame and Line truncated
## Templates in `$SKILL/templates`
@@ -132,10 +151,31 @@ capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `l
`globals-storage-dut.yaml`, `usb-audio-waveshare-s3-eth-dut.yaml`, `usb-audio-sendspin-s3-eth-dut.yaml`,
`cast-pro-1-sendspin-control.yaml`, `aux-2-preprod-stock-dut.yaml`, `aux-2-preprod-usb-audio-dut.yaml`,
`dfrobot-sen0395-dut.yaml`, `aux-2-preprod-speaker-player-dut.yaml`, `web-server-sse-dut.yaml`,
`mqtt-transport-dut.yaml`, `mqtt-tls-dut.yaml`, `udp-write-dut.yaml`, `entity-icon-dut.yaml`.
`mqtt-transport-dut.yaml`, `mqtt-tls-dut.yaml`, `udp-write-dut.yaml`, `entity-icon-dut.yaml`,
`online-image-s3-eth-dut.yaml`,
and the directories `store_yaml/` (package with secrets one directory up, per-board files),
`http_request_update/` (manifest URL setter shapes, per-board files), `modem/` (modem only and modem plus
Wi-Fi boot checks), `ble_midi/` (fake BLE MIDI instrument and the client), `http_request_action/` (request
action package with per-board files and an OTA flash variant), `led_wire/` (DUT config and header that reads the
LED strip pads back with RMT RX and an SPI slave), `uart_sim/` (simulator header, common package, per-build parts and run files) and `remote_base/` (common
package, transmit script, per-board templates and the ESP8266 software sender), `i2c_fake/` (fake I2C device
headers with their test configs, see `fake_i2c.md`).
- `microphone_http_stream/`: base config, fake counter and USB microphone variants and the multi-reader client,
described in `reference/recipes/microphone_http_stream.md`.
## When the PR keeps moving
Record every head in the stack (`gh pr view N --json headRefOid`), build, and recheck before flashing; if anything
moved, wait five minutes and check again so the test runs on a settled head. When a head moved only by merging dev,
compare the PR's own diff (`git diff $(git merge-base OLD origin/dev) OLD` against the same for NEW); if it is
identical and the dev commits do not touch the code under test, earlier results still describe it, but rebuild so the
comment quotes the exact commits. Fetch `origin dev` before computing a merge base.
## When something looks wrong
- A harness header fails to compile on Arduino or LibreTiny: those frameworks define `millis()` and `micros()` as
macros, so call `(esphome::millis)()`, which builds everywhere. Realtek SDK headers also define `OFF`, `ON`
and `SUCCESS`.
- A capture stopped early or a line is missing: check the timestamps before trusting it.
- A script that pipes into `tee` never ends although its work is done: a background command written as
`(cd dir && server & echo $!)` backgrounds the whole `cd && server` list, so a shell stays behind holding the
@@ -152,6 +192,10 @@ capture decoder), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `l
- The merge base comes from a freshly fetched `origin/dev`; a stale local `dev` puts dependency bumps into the
PR diff and builds the control from the wrong commit.
- A `.local` name can stop resolving in the middle of a run; keep the address from the boot log and drive by it.
- Never point `framework: source:` at a local `file://` copy of LibreTiny to try a patch: PlatformIO installs it over
the shared `~/.platformio/platforms/libretiny`, so every later build on the machine uses the patched copy. Patch
through the ESPHome side (a linker `-Wl,-wrap,<symbol>` or a define) instead, and if it happened, restore the
directory from a clean copy of the same release.
- `wc -l` pads its count on macOS, so `tail -n +$n0` needs `$((n0+0))` or an arithmetic expansion.
- `api:` reboots a board after 15 min without a client: `reboot_timeout: 0s` on `api:` and `mqtt:` for
soak tests.
@@ -24,6 +24,11 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
row in the PR.
- AsyncMqttClient drops inbound topics over 128 characters; topics of 255 or more wrap a counter and
stall the link until the broker drops TCP.
- `Arduino.h` already declares `_heap_start`; a lambda can use `&_heap_start` directly, and a second `extern` fails.
- `Hardware Watchdog` with `wDev_ProcessFiq` in the stack means interrupts were held off too long for the Wi-Fi
interrupt handler; `Software Watchdog` with `pm_send_nullfunc` means the loop was busy too long. Code that takes
interrupt locks needs a load run to show either (`scripts/esp8266_lock_load/`, see recipes/one_wire.md).
- `GPI` bits 16 to 31 are strapping bits (mask with 0xFFFF). GPIO16 has no interrupt; read `GP16O` for an output.
## ESP32 classic devkit
@@ -40,7 +45,11 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
- Signed OTA testing: build with `esp32.framework.advanced.signed_ota_verification.verification_keys`
under a different name, sign with `python -m espsecure sign_data --version 2 --keyfile
tests/components/esp32/dummy_signing_key.pem`, upload with `esphome upload --file`. The verifier
refuses unsigned images afterwards, so restore with a signed plain build.
refuses unsigned images afterwards, so restore with a signed plain build. The full accept and reject case set
(several keys, CRC-only edits) is in `recipes/ota.md`.
- An ESP32 whose bootloader has no app rollback is protected only by `safe_mode` after a bad OTA. Before sending a
change to such a device that nobody can reach by hand, boot the same code on a devkit first and keep a fallback
image of the merge base built.
## ESP32-S3, C3, C6, H2 with native USB
@@ -49,7 +58,20 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
`Disabling RNG early entropy source`, then 0 bytes, esptool reports `No serial data received`). Only a
USB replug fixes it. Fallback: `esphome logs --device <name>.local` (API log stream, which drops lines
while Wi-Fi is off).
- The ESP32-P4 before silicon revision 3 is bricked by rev3-only firmware (boot loop at bootloader entry).
- The USB console drops a line now and then even when it stays up; do not count events by log lines alone.
- The ESP32-P4 before silicon revision 3 is bricked by rev3-only firmware (boot loop at bootloader entry). The
Waveshare ESP32-P4-WIFI6 rev1.3 board is such pre-rev3 silicon.
- S3, C3 and C6 all enumerate as `303a:1001`; confirm the chip with `esptool --port <port> chip_id` before flashing.
- ESP32-C6 test configs: set `wifi: power_save_mode: none`. With the default light sleep, API commands and state
updates arrived seconds late and a 1.2 s step check failed at random on base and PR alike (esphome#22339).
- An S3 on Arduino with `logger: hardware_uart: USB_CDC` enumerates as TinyUSB CDC (`303a:0009`). If that build
crash loops (a FastLED plus NeoPixelBus build aborts in `check_rmt_legacy_driver_conflict`), esptool cannot catch
the port: hold BOOT, tap RST, keep BOOT held about 2 s, then flash with `--before no-reset`. A plain RTS hard reset
leaves it in the loader on USB JTAG; `--after watchdog-reset` boots the app. Keep Arduino LED tests on the classic
ESP32.
- A board left in the ROM bootloader after an upload (BOOT held, or the USB JTAG reset landing in download mode
again) starts its app without a replug through `esptool --port <port> --after watchdog-reset chip-id`; flash a
minimal Wi-Fi plus API image first when recovering from a crash-looping one.
## RP2040 / RP2350 (Pico W, Pico 2 W)
@@ -61,6 +83,22 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
joins must wait for `network::is_connected()`.
- Parallel Pico builds race on package extraction after a PlatformIO package wipe; one at a time.
- An RTS pulse from pyserial does not reset the board; reboot with `App.safe_reboot()` or the touch.
- LEAmDNS re-enters `update()` from the receive handler on the Pico too (measured), so an ESP8266-only guard does
not cover it.
- An image with only `ota: platform: web_server`, built from a tree without the fix for uploads of unknown size
(before 2026-10-19), cannot be updated again over the network; see `recipes/ota.md` before flashing one.
- RP2350 `panic()` goes through `_exit()` and `__breakpoint()` to a HardFault, and ESPHome's rp2 crash handler
reboots within about a second (USB back 1 to 2 s after the panic). A panic during component setup therefore
boot-loops before USB CDC ever enumerates, which looks like a dead board: no port and no BOOTSEL drive. Recover
with BOOTSEL held on replug, then flash to the `RP2350` volume. To see the crash, trigger it after boot.
- A Pico 2 W can sample any of its own GPIOs from a spare PIO block (PIO2) at 25 MHz while another peripheral
drives the pin, since PIO reads the pad without taking it. That decodes WS2812-style pulses with no wires or
LEDs. Turn a light on at full brightness before probing, or color correction writes zeros. The sample FIFO
overruns now and then; count only full captures.
- A Pico 2 W also makes a byte-exact fake I2C sensor: a raw RP2350 I2C0 interrupt handler (GP4/GP5) that answers
each RD_REQ with one byte from a 256-byte register file, so any read length works (used for BME280/BMP280 in
esphome#22236). The ESP32-S3 I2C slave v2 driver is not usable for register emulation: answering from a task or
straight from the interrupt both gave shifted multi-byte reads.
## BW15 kit (RTL8720CF, LibreTiny rtl87xx)
@@ -83,6 +121,25 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
from `~/.platformio/packages/toolchain-gccarmnoneeabi/bin/` (`raw_firmware.elf`, not `firmware.elf`).
Fault dumps print PC, LR and R0 to R3.
- The AmebaZ2 SDK sets `LWIP_TCPIP_CORE_LOCKING 0`.
- The `rtw_is_adapter_up` bus fault after `Disabling AP` happens on LibreTiny 1.13.0 and 1.14.0 alike
(2 of 4 and 3 of 3 runs in #21531); run at least four rounds per build before blaming a PR for it.
- On a fault the ROM handler (ram_vector_table entries 3 to 7) prints a dump of about 4 KB in 0.4 s and
then spins; only the watchdog resets the chip (10 s after `arch_init`, 1 s once the ESPHome crash
handler re-arms it). `.ram.bss.noinit*` (`.system_restore_data`) survives that reset. The core has no
FPU. For a crash that fires once, guard an `interval` lambda with
`!libretiny::crash_handler_has_data()`. For a handler mode (MSP) fault, point vector 16+11 (SDIOD,
unused) at a trapping function and set bit 11 in NVIC ISER and ISPR (0xE000E100, 0xE000E200).
- OTA slots (esphome#22012): a serial `ltchiptool flash write` fills both OTA1 and OTA2, and the slot flag decides
which one boots (`OTA1 USE` or `OTA2 USE` in the ROM banner). Each OTA writes the other slot, so a test that needs
the OTA1 to OTA2 direction runs two OTAs in a row. On dev before #22111 a deflate OTA into OTA2 corrupted the
image, and the next restart hung at `System_Init1` until a serial reflash.
- If the chip still waits in the ROM after a serial flash, pulse CEN to GND once (TX2 floating) to boot the app.
`expected ACK` failures at random blocks mid-transfer can come from one USB hub port; another port fixed them.
- Its Wi-Fi is slow and drops (esphome#21742): a 300 KB deflate OTA takes 3 to 10 minutes, so give the uploader 30
minutes, not 10.
- Wrong-MD5 OTA test without changing firmware: monkeypatch `espota2.hashlib.md5` so only the image checksum (data
over 64 KB) returns a wrong hexdigest. Give each build its own `name:` and read `device_info().name` after a
restart to see which image booted.
## BK7238, WB3S, CB3S (BK7231N) (LibreTiny bk72xx, CH340, RTS on CEN)
@@ -97,6 +154,8 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
assuming either.
- `flash_bk72xx.py` misses the link now and then (`Timed out attempting to link with chip`); retry up to 3
times before suspecting the board.
- Both flash wrappers pass ltchiptool `-L` with the newest installed LibreTiny platform (`scripts/lt_platform.py`):
its default `~/.platformio/platforms/libretiny` can be an older copy that rejects BK7238 UF2s as unsupported.
- Crashes: `__builtin_trap()` and a call into unmapped memory both end as an undefined-instruction trap
(`bk_misc_get_start_type()` 6, `lt_get_reboot_reason()` Crash); reads, unaligned loads and stores to
unmapped addresses never fault on the ARM968. Without Wi-Fi the SDK trap handler spins with interrupts off
@@ -113,8 +172,72 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
SDK's saved register banks are usable. Code is compiled Thumb, the SDK assembly is ARM; in an exception
handler only r0 to r3 are shared by all modes. BK7231N ITCM is `0x3FEE00..0x400000` (4.5 KB); a `.noinit`
placed there after `.itcm.code` works for a crash record.
- Pick the board that matches the module's flash layout. A NiceMCU XH-WB3S (BK7238) needs `board: xh-wb3s` (plain
Beken layout, RF calibration at 0x1E0000). Built as `generic-bk7238-tuya` (calibration at 0x1E3000, download
partition over 0x1E0000) the firmware boots and may even join a known network, but scans come back empty and its
AP is invisible to clients, with no warning in the log. If scans are empty, check the layout first.
- The BK7238 bootloader prints `App verify failed! Need to recovery factory firmware.` on every boot with any
build; it is harmless. Even with the right layout an ESP32 client did not see the BK7238 fallback AP on
LibreTiny 1.13.0 or 1.14.0, so the wifisave hammer cannot run on it.
- Moving a BK7231N device (CB3S) that sits on its fallback AP to another network: join the host's Wi-Fi to
`<name>-fallback`, OTA to 192.168.4.1, then rejoin the host's own network. Moving it with `/wifisave` instead can
leave it in the captive portal's BLIND_RETRY state (no scan, `Beacon Timeout` every 46 s) until it reboots, and a
log-only serial adapter cannot reset it. Rejoining may change the host's address; update a syslog target that
points at the old one.
- Callbacks run on the `async_tcp` task and log lines can lose a byte. Wi-Fi connect takes tens of ms;
radio enable 160 to 278 ms, disable about 200 ms.
- OTA install: the BK7238 bootloader installs any complete download whose own CRC passes at the next restart, even
when ESPHome refused the update; an incomplete download fails `rt_ota_part_fw_verify` (`Verify firmware CRC32
... !=`) and the old image stays.
- Power save: on the BK7238, `power_save_mode: light` with LibreTiny 1.14.0 gives ping times of 3 to 6 s because
the BDK sets a listen interval of 30 beacons when `CFG_LOW_VOLTAGE_PS` is 1 (esphome#19491).
## BW12 and other AmebaZ boards (RTL8710BX, LibreTiny rtl87xx)
- On a bare module wired to a plain USB-UART (the bench's BW12), download mode is a hand strap: CEN to
GND, TX2 to GND, release CEN, release TX2. Such an adapter does not drive CEN, so neither ltchiptool nor
`serial_capture.py --reset` can reset the chip. A development board that wires DTR and RTS to CEN and the
download pin enters download mode from the tools, like the BW15 kit; the notes below on baud rates still apply.
- ltchiptool links at 1.5 Mbaud and reads the system data at 0x9000 at that rate before it writes at
115200. A CH340 drops bytes there (`Data read error at 0x9000`, `Timed out waiting for Quiet-Handshake`)
and the run gives up without writing. `-B 115200 -b 115200` moves the link to 115200 first and works.
Start the command with `-t 120` so it is already polling while the board is strapped.
- After a failed run the ROM may sit at another baud rate and no longer link; strap again.
- The device builder frontend's AmebaZ flasher follows ltchiptool (link and system read at 1.5 Mbaud),
so it can hit the same read failure on the same adapters.
- Power the module from the adapter's 5 V pin into its own regulator; the adapter's 3.3 V pin cannot
carry the Wi-Fi transmit peaks.
- Capture with `serial_capture.py --released`. With DTR and RTS asserted on the open port, a software
reboot (`App.safe_reboot()`, the restart button) lands in the ROM downloader (`UARTIMG_Download 2`)
and stays there until CEN is pulsed. After a flash the chip also waits in the ROM until CEN is pulsed.
- A full flash takes 50 to 220 s at 115200. While the board is on Wi-Fi, OTA (port 8892) saves the strap,
but the board's Wi-Fi latency (one to two seconds a ping) can make the upload time out.
- `sdk_silent: none` under `framework:` keeps the Wi-Fi driver's join log (`start auth to`, `auth success,
start assoc`, `association success`, `sta recv deauth reason code(N)`); LibreTiny mutes it during the
connect otherwise. `wifi_get_last_error()` after a failed `WiFi.begin()` gives the SDK's reason
(1 no network, 2 connect failed, 3 handshake failed, 6 auth failed); LibreTiny only logs `ret=-1`.
- Some modules ship with no RF calibration in efuse, only the chip ID and MAC. Dump the logical map with
the ROM's `EFUSE_LogicalMap_Read(buf)` (512 bytes): TX power indexes are at 0x10 to 0x1A, crystal trim
at 0xC9, MAC at 0x11A; 0xFF means the driver uses its default (trim 0x20). Linking with
`-Wl,-wrap,EFUSE_LogicalMap_Read_ACUT` lets a wrapper rewrite blank bytes before the driver parses
the map on each radio start. See `recipes/libretiny.md` for the per-boot trial harness used to A/B them.
- Scan results report WPA2-AES access points as `WPA PSK`: LibreTiny truncates `rtw_security_t` to
`uint8_t` before mapping it. Cosmetic. Open networks come out as `WIFI_AUTH_INVALID` (255) on both
AmebaZ and AmebaZ2, so `auth != WIFI_AUTH_OPEN` marks them encrypted.
- After a BSSID plus channel join, LibreTiny's `set_pscan_channel(ch, false)` leaves the driver with no
channel to scan, so the next SSID only join fails at once (no `start auth`), and the first scan after a BSSID
join comes back empty (no results in about 62 ms on the BW15). A `wifi_set_pscan_chan` list of channels 1 to 13
returns 0 but leaves only the last channel enabled, and re-applying the channel plan does not help either
(AmebaZ2 reports plan 0xFF; AmebaZ's `rtw_set_chplan_cmd` rejects plans above 0x58 except 0x7F). What works
(esphome#21749, BW15): set the one-channel pscan list yourself and pass channel 0, so the join's own scan
rebuilds the full list; setting a placeholder SSID `-` before narrowing (esphome#21754) kept joins as fast as
dev with the first scan full on every boot. Not yet confirmed on AmebaZ.
- A 4-way handshake that never finishes after association is reported as `RTW_WRONG_PASSWORD` on AmebaZ
even with the right password; AmebaZ2 reports a real wrong password as a 4-way handshake timeout.
- The driver also posts a SCAN_DONE (number 0) inside every join, on AmebaZ2 too.
- RTL8710B GPIO interrupts fire on one edge only, and LibreTiny maps CHANGE to rising. The Arduino `digitalRead` called
from an ISR switches the pin back to GPIO and frees its interrupt (esphome#22400, #22403).
- The FreeRTOS tick is 1/1024 s, so `millis()` on the tick ran 2.4 % fast (esphome#22404).
## BW16 kit (RTL8720DN, AmebaD)
@@ -133,6 +256,15 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
to 115200 (lost on reprovision). Weak 3.3 V from the adapter: feed the module's 5 V pad instead. Short
BOOT to GND while the tool connects. OTA afterwards.
- `USE_WIFI_AP_EXCLUSIVE` (one radio mode at a time) exists only on LN882x; compile-check changes there.
- The ESPHome OTA port on LN882x is 8820, not LibreTiny's 8892 (BK72xx, RTL87xx); helper scripts that hardcode
8892 get `Connection refused`.
- WL2H-U modules with the Tuya flash layout: app at 0x007000 (0x113000 long), OTA at 0x11A000 (0xA8000 long). A
full OTA upload over a slow link takes 2 to 14 minutes.
- UF2 block order differs per family: LN882H puts the device image first and the flasher image after it, BK72xx
puts the flasher image first and the device image last. The LN882H bootloader installs a complete, valid OTA
partition even when the upgrade state is still `UPG_STATE_DOWNLOAD_ING` (esphome#22111).
- Without serial access to recover, never send a corrupted image; wrong-MD5 and cut-upload tests with valid
images are safe.
## nRF52840
@@ -144,9 +276,11 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
`dfu: true` wires up on this board. A pulsing red LED means bootloader; the side RESET button
re-enters it.
- Makerdiary MDK USB dongle: UF2 bootloader without SoftDevice, app at 0x1000, `UF2BOOT` volume,
adafruit-nrfutil, GPREGRET 0x57 works. RGB LED active low (red P0.23, green P0.22, blue P0.24),
button P0.18.
- Adafruit ItsyBitsy nRF52840 Express (UF2 bootloader with S140, bootloader port `239a:0051` plus an
adafruit-nrfutil, GPREGRET 0x57 works. The running app enumerates as `1915:CAFE` or `239A:0029`. RGB LED
active low (red P0.23, green P0.22, blue P0.24), button P0.18.
- Both boards reflash without a button once they run a `dfu: true` build: `esphome upload --device <any app CDC
port>` on the PCA10059, a 1200 bps open of the log port plus a copy of `zephyr.uf2` to the volume on the ItsyBitsy.
- Adafruit ItsyBitsy nRF52840 Express (UF2 bootloader 0.9.0 with S140 6.1.1, bootloader port `239a:0051` plus an
`ITSY840BOOT` volume): the board DTS already has `cdc_acm_uart0`, so the logger port is the first CDC
port and a `usb_cdc_acm` interface 1 the second. A `dfu: true` build enters the bootloader on the
1200 bps touch (an app without it needs a double tap on RESET); `adafruit-nrfutil` over the bootloader
@@ -160,14 +294,25 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
the port open, and the logger warns `logger took a long time`. Print one line per `loop()` instead.
- Zephyr settings survive a reflash: key a `restore_value` global by a `run` substitution when a test needs
a never-saved value.
- Thread: nRF52 `openthread:` accepts only `tlv:`; the individual keys (network_key, channel, ...) are rejected at
validation. To list nearby networks with channel and PAN ID, call
`otThreadDiscover(InstanceLock::acquire().get_instance(), 0, 0xFFFF, false, false, cb, nullptr)` from an
`interval:` lambda in an included header; results arrive on the OpenThread thread and log fine.
- A wrong network key on a real network's channel and PAN ID makes the device form its own partition as leader on
that PAN within about 30 s; take it off Thread at once. Never try guessed datasets against a network in use.
## Waveshare ESP32-S3-ETH with the OV5640 camera module
- Native USB console (`303a:1001`), ESP32-S3R8, 16 MB flash, 8 MB octal PSRAM, W5500 on SPI (MOSI 11,
MISO 12, SCLK 13, CS 14, INT 10, RST 9), TF card on 4..7, WS2812 on 21.
- Camera header (vendor schematic): XCLK 3, SIOD 48, SIOC 47, VSYNC 1, HREF 2, PCLK 39, D0..D7 = 41, 45,
46, 42, 40, 38, 15, 18. PWDN is tied to GND and RESET is an RC to 3.3 V on the board, so no reset or
power-down GPIO exists.
- Camera header (vendor schematic): XCLK 3, SIOD 48, SIOC 47, VSYNC 1 (a 0 ohm link to GPIO1, alternative
GPIO16), HREF 2, PCLK 39, D0..D7 = 41, 45, 46, 42, 40, 38, 15, 18. PWDN is tied to GND and RESET is an RC to
3.3 V on the board, so no reset or power-down GPIO exists.
- It can run from PoE and USB-C at once, so the USB-C can move from the host to a USB device for `usb_uart` or USB
host tests while the board stays up; flash and read logs over Ethernet while it is unplugged. In USB host mode
the USB-C is the host port after the first flash: log over the API with `logger: hardware_uart: UART0`.
- A crash-looping image can wedge its USB console until a BOOT replug. Never force allocation failures on it by
hogging RAM: Wi-Fi then aborts in `operator new` or the loop task trips the task watchdog.
- GPIO8 switches the camera 3.3 V rail through a P-channel MOSFET: LOW = camera powered, gate pulled up so
the camera is off by default. `power_down_pin: GPIO8` only pulls it low 10 ms before the probe, which is
not enough; use `switch: gpio GPIO8 restore_mode: ALWAYS_OFF setup_priority: 800` plus `on_boot:
@@ -191,3 +336,93 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
- `dns-sd -L <name> _meshcop._udp local.` shows the `omr` prefix; it disappears when routing is dead.
Keep one capture per chip; never reset the H2 over USB mid-test unless that is the test (the host
logs `Unexpected RCP reset: RESET_OTHER` and recovers).
## Building against a patched LibreTiny
- Clone LibreTiny at the tag ESPHome pins, commit the patch, and set `framework: version: <tag version>` plus
`source: file://<clone>` under `bk72xx:` or `rtl87xx:`. PlatformIO installs it as
`~/.platformio/platforms/libretiny` (the stock copy is `libretiny@src-<hash>` and is not touched).
- A later commit in the clone is not picked up by the next build; patch the installed copy or remove it. Remove
`~/.platformio/platforms/libretiny` when done.
- BDK headers such as `power_save_pub.h` have no `extern "C"`; wrap them when including them from the Arduino
core's C++ files.
## LibreTiny crash and abort() facts (esphome#21596, #22288)
- `abort()` on dev: the BK7238 hangs for good (the main loop stops and the watchdog never fires once Wi-Fi is up;
recover with an RTS pulse on CEN, `serial_capture.py --reset`). The BW15 (RTL8720CF) restarts with no crash
record. The LN882H reboots only after about 18 s through the watchdog (`WDT Reset`), with no record: the SDK's
`abort()` and `_exit()` (`components/libc/newlib_stub.c`) and its FreeRTOS `configASSERT` are spin loops. With
#22288 all three reboot in 1 to 2 s and log `Reason: Abort`, with the PC at the call site and no `LR:` line.
- `assert()`, `std::terminate` and a failed `new` all reach `abort()` (nano libsupc++). `raise(SIGABRT)` returns,
because `_kill` is a nosys stub.
- `__builtin_trap()` faults on every board. A store to `nullptr` or 0xDEADBEE0 does not fault on the BK7238. On
the BW15 a `nullptr` store does not fault, and loads and stores at 0xDEADBEE0 give only imprecise bus faults
(CFSR 0x400, no BFARVALID), so testing the fault address path needs another Cortex-M chip.
- BK7238 safe mode: each quick crash reboot counts toward it (`Last reset too quick; invoke in N restarts`), so
back-to-back crash tests can drop the board into safe mode.
- LN882H: the previous crash is replayed to every new API log client until the next crash; that is expected, not a
double record. Once, the first `__builtin_trap()` after a fresh OTA took about 12 s to reboot; retries took 2 to
3 s and it did not reproduce.
- `~/.platformio/packages/toolchain-gccarmnoneeabi` can vanish between a build and the decode (it comes back with
the next LibreTiny build). Until then the `arm-none-eabi-addr2line` and objdump of
`toolchain-rp2040-earlephilhower` read any ARM ELF.
- RTL8720C: a noreturn `bl abort` can be the last instruction of a function, so the raw return address points at
padding addr2line cannot place. Decode `(addr & ~1) - 2` to get the call line; #22288 records it that way.
- Abort reports (#21596): early heads put LR 0x00000000 first in the address list; later heads drop a zero LR, so
an abort report starts with the backtrace.
## Waveshare ESP32-C6-GEEK
- Native USB JTAG serial (`303a:1001`), 16 MB flash, 1.14" ST7789 LCD and TF card; the vendor docs give no GPIO
numbers. Pins that read a fixed level against the internal pulls: 1, 2, 7, 20, 23 high, 6 low. Free for pad
tests: 0, 3, 4, 5, 18, 19, 21, 22 (see `recipes/gpio_hold.md`).
## Waveshare ESP32-P4-Module-DEV-KIT (USB host)
- ESP32-P4 rev v1.3, 16 MB flash, console on a CH343 (`1a86:55d3`). Other boards use the same chip for their
console, so check with `esptool chip_id`.
- USB host for `usb_host`: set the Host/Device jumper to HOST and use the bottom USB-A port nearest the Ethernet
jack (the high-speed OTG controller); the other USB-A ports and the USB-C marked "USB" do not reach it. The ports
are unlabelled. The port powers the device.
- ESP-IDF programs the FIFO split only when a device connects: read `GRXFSIZ`, `GNPTXFSIZ` and `HPTXFSIZ` at
`DR_REG_USB2_BASE` + 0x24, 0x28 and 0x100 after enumeration. GHWCFG3 (0x4C, high 16 bits) reads 896 lines. The
default split is RX 512, NPTX 256, PTX 128.
- Not every P4 board has a USB host port; check before planning a `usb_host` test on one.
## Gelidus Research PS1 v2.3 (LD2420 presence sensor)
- ESP32-D0WD-V3 rev 3.1 on a CH340 (`1a86:7523`); `esphome upload --device <port>` works directly.
- LD2420 UART: TX GPIO17, RX GPIO16, 115200 (radar firmware v1.6.1). Status LED GPIO23, night light PWM on GPIO25.
- The vendor package is at github.com/GelidusResearch/grps (`packages/grps-ld2420.yaml`); copy it locally for tests.
- The radar needs a person in its field. Energy frames are `F4 F3 F2 F1 23 00 <presence> <range cm LE16> ...`; a
UART `debug:` block on RX shows them.
## ESP32-C3 Super Mini (native USB, 4 MB XMC flash)
- Shows up as `303a:1001`; confirm with `esptool --port <port> chip_id`. Board `esp32-c3-devkitm-1`,
`logger: hardware_uart: USB_SERIAL_JTAG`. Avoid GPIO2, 8 and 9 (strapping; 8 is the LED, 9 is BOOT).
- A 6-pin BME680 breakout (CS, ADDR/MISO, SCL, SDA, GND, VCC) on SDA GPIO6 and SCL GPIO7 with CS and ADDR left
open answers at 0x77 through the breakout's pull-ups, and the `bme680` platform reads fine.
- BME688 parallel mode never reads in `bme68x_bsec2` on dev: the read timer is re-armed by each 30 ms run before
its 41.6 ms delay ends. Entering parallel mode at all needs gas estimates subscribed at `BSEC_SAMPLE_RATE_SCAN`
and no IAQ outputs.
## mDNS responder tests (#22267)
- When the host's multicast does not reach the device, query its port 5353 directly as legacy unicast (a
python-zeroconf packet sent from a normal UDP socket) to dump records or hammer the responder.
- On a crowded Wi-Fi channel, back-to-back query loss swung from 0 to 14% between builds for no code reason. Judge
responder timing on an Ethernet board.
## nRF54LM20 DK (PCA10184, on-board J-Link)
- The power switch must be ON or nothing enumerates. The ESPHome log (uart20) is on the second J-Link VCOM port;
the first is unused.
- Flash with `esphome upload <yaml> --device JLINK` (sdk-nrf toolchain); the LM20 needs no west `--device` override.
The J-Link re-enumerates after the flash, so start the capture after the port comes back.
- Reset without halting: JLinkExe `-device NRF54LM20A_M33 -if SWD -autoconnect 1` with `w4 0xE000ED0C 0x05FA0004`
(plain `r` and `g` halt and resume, no reboot).
- LED0 P1.22, LED1 P1.25; BUTTON0 P1.26, BUTTON1 P1.09, BUTTON2 P1.08, BUTTON3 P0.05 (active low, pull-up). GPIO
P1 registers at 0x500D8200 (IN +0x00C, PIN_CNF +0x080), P0 at 0x5010A200.
- Binary sensor state changes are not logged at DEBUG; log the states or count `on_press` to see button presses.
@@ -4,30 +4,82 @@ Each flow names the pull requests it was used on, so their result tables show wh
One file per component, named after it:
- [`api.md`](api.md): noise handshake and OTA encryption on the ESP8266, outgoing connection wake timing on deep sleep and after a Wi-Fi drop, user defined action listing and execution with a raw plaintext client
- [`api.md`](api.md): noise handshake and OTA encryption on the ESP8266, outgoing connection wake timing on deep sleep and after a Wi-Fi drop, user defined action listing and execution with a raw plaintext client, store_yaml recovery over Noise or plaintext
- [`ble_midi.md`](ble_midi.md): BLE MIDI client against a fake ESP32 instrument, every packet shape and the send actions
- [`captive_portal.md`](captive_portal.md): fallback AP and /wifisave hammer
- [`cse7766.md`](cse7766.md): Sonoff S31 parser check over the API
- [`espnow.md`](espnow.md): ESP8266 broadcast receive
- [`hdmi_cec.md`](hdmi_cec.md): two-board wire
- [`ld2450.md`](ld2450.md): polygon zones, zone number round trip through the radar's readback, ESP8266 loopback harness without a radar
- [`http_request.md`](http_request.md): update manifest URL changed at run time, every setter shape, a swap under a running check, non-blocking request actions with loop gap, queue, cancel and reboot checks, OTA request cases with basic auth and MD5 sources
- [`ld2420.md`](ld2420.md): hub setup and sensor states with a simulated module on a UART0 loopback (energy and simple mode)
- [`ld2450.md`](ld2450.md): polygon zones, zone number round trip through the radar's readback, ESP8266 loopback harness without a radar, presence hold and link loss on a frame stream, BT MAC readback
- [`ld2412.md`](ld2412.md): output pin level through the basic configuration command, held back writes
- [`libretiny.md`](libretiny.md): rtc preference emulation, BK72xx crash report on the next boot
- [`mqtt.md`](mqtt.md): broker bench, QoS, reconnect, crash log over MQTT, WebSocket transports with a local and a public broker, TLS trust sources (own CA, client certificate, certificate bundle, no verification) with the CLI check
- [`nrf52.md`](nrf52.md): dongle board support
- [`online_image.md`](online_image.md): stalled and malformed downloads
- [`libretiny.md`](libretiny.md): rtc preference emulation, BK72xx and RTL8720C crash report on the next boot with a ROM dump matcher, BK7238 BLE bond store safety with a full flash backup and per partition diff, BK7238 OTA versus the board layout and bootloader, pulse counting, RTL8710B edge interrupts and millis timing
- [`modem.md`](modem.md): PPPoS modem component boot check with no modem attached, loop blocking and disable timing, SIM7080G behaviour with no SIM and with no data plan through a raw UART passthrough
- [`mqtt.md`](mqtt.md): broker bench, QoS, reconnect, crash log over MQTT, WebSocket transports with a local and a public broker, TLS trust sources (own CA, client certificate, certificate bundle, no verification) with the CLI check, MAC suffix topics and will messages on two boards, shutdown message delivery on restart, disable and deep sleep
- [`nrf52.md`](nrf52.md): dongle board support, zephyr_ble_server connection slot under reordered callbacks, SMP over UDP OTA over Thread with reset faults, MCUboot swap limit and stuck slot recovery, TCP stack probe, sdk-nrf 3.4.1 build and USB notes
- [`online_image.md`](online_image.md): stalled and malformed downloads, PNG, JPEG, BMP and QOI decode time per loop pass with large images and small buffers
- [`openthread.md`](openthread.md): border router on the S3 host plus H2 RCP
- [`ota.md`](ota.md): overlap and lock, timeouts, deflate
- [`ota.md`](ota.md): upload without a configuration and the refusal checks, overlap and lock, timeouts, deflate, upload timing A/B per build and board with deflate and raw, on-device inflate benchmark, OTA automations on two platforms
- [`rtsp_server.md`](rtsp_server.md): camera streaming on the Waveshare ESP32-S3-ETH
- [`scheduler.md`](scheduler.md): loop wake A/B
- [`scheduler.md`](scheduler.md): loop wake A/B, interval startup delay and control from lambdas
- [`udp.md`](udp.md): datagrams, multicast, sockets, `udp.write` payload shapes
- [`usb_cdc_acm.md`](usb_cdc_acm.md): nRF52840 dongle host driver
- [`wifi.md`](wifi.md): reboot_timeout with a fallback AP, DHCP lease cache, Easy Connect (DPP), WISP bridge with an ethernet upstream
- [`wifi.md`](wifi.md): reboot_timeout with a fallback AP, DHCP lease cache, Easy Connect (DPP), WISP bridge with an ethernet upstream, reboots against a WPA2/WPA3 PMF access point, WISP and ALWAYS_ON AP switching, AP restarts with WPA3 mixed and LIGHT power save, soft AP clients with the BLE tracker scanning, ESP8266 power save after captive portal provisioning
- [`zephyr.md`](zephyr.md): preferences on the nRF52840 dongle
- [`zephyr_coredump.md`](zephyr_coredump.md): crash and decode on nRF52 boards
- [`zwave_proxy.md`](zwave_proxy.md): frame checksums on a live proxy
- [`one_wire.md`](one_wire.md): ESP8266 interrupt locks under Wi-Fi load with a DS18B20 search and a busy-wait control
- [`cc1101.md`](cc1101.md): sleep and wake checks read directly from the chip (CHIP_RDYn, PATABLE loss, registers per setter)
- [`globals.md`](globals.md): restoring globals with storage rtc and flash across every reset kind
- [`homeassistant.md`](homeassistant.md): light platform mirrored from a local Home Assistant, held reports versus later calls
- [`homeassistant.md`](homeassistant.md): light platform mirrored from a local Home Assistant, held reports versus later calls, wizard-linked entities on every platform, homeassistant switch on a script with a host build
- [`usb_audio.md`](usb_audio.md): USB audio speaker and microphone, the usb_host FIFO split, and Music Assistant playback through sendspin
- [`dfrobot_sen0395.md`](dfrobot_sen0395.md): SEN0395 mmWave command flow, module quirks and overlong lines through its error reply
- [`speaker.md`](speaker.md): media player pipeline tasks on the Connect AUX-2 with the `speaker` platform player
- [`web_server.md`](web_server.md): state SSE stress and the HTTPD task stack on web_server_idf, entity icon and device class readback
- [`uart_sim.md`](uart_sim.md): UART sensors simulated on a classic ESP32 with loopback and cross-wired UARTs
- [`gpio_expander.md`](gpio_expander.md): interrupt driven expander inputs, loop idle checks, testing a device out of reach
- [`led_strip.md`](led_strip.md): forcing a failed strip allocation, RMT symbols per chip, loop cost per strip
- [`remote_base.md`](remote_base.md): remote_receiver protocol decoding through a one-pin loopback on ESP32 and ESP8266, no IR hardware
- [`xiaomi_ble.md`](xiaomi_ble.md): Xiaomi, ATC and PVVX parsing by injecting scan results into the ESP32 tracker or advertising them from a second ESP32, real AES-CCM frames, duplicate filter model
- [`ble_terminate.md`](ble_terminate.md): proxy disconnect against a peer with peripheral latency
- [`i2c_slave.md`](i2c_slave.md): fake I2C slave on a D1 mini, ESP32 hardware master as the control
- [`fake_i2c.md`](fake_i2c.md): fake I2C devices with no chip: in-firmware buses for many drivers, a Pico 2 W BME280/BMP280 target, a DS2482-800 simulator, NACK injection for vl53l0x
- [`gpio_hold.md`](gpio_hold.md): GPIO hold through the ISR pin API, pad read back with no wiring, per chip pin notes
- [`ble_event_inject.md`](ble_event_inject.md): made up GAP and GATTC events fed into the ESP32 BLE handlers from a button
- [`light_sleep_gpio_wake.md`](light_sleep_gpio_wake.md): GPIO wakeup from auto light sleep with an LEDC wave that keeps running in sleep, and with an external wave and peripheral power-down on the ESP32-P4
- [`manual_ip_and_web_handlers.md`](manual_ip_and_web_handlers.md): station, AP and Ethernet manual IP, web handlers added late and web OTA with web_server, captive_portal or prometheus
- [`ble_server_security.md`](ble_server_security.md): esp32_ble_server pairing, encrypted and MITM characteristics, subscription before and after pairing, bond reuse per ESP-IDF version
- [`prometheus.md`](prometheus.md): date, time and datetime metrics in UTC and another time zone
- [`ble_adv_triggers.md`](ble_adv_triggers.md): BLE service and manufacturer data advertise triggers over the air on the ESP32 and BK72xx trackers, with a second ESP32 advertising known frames
- [`sun.md`](sun.md): on_sunrise and on_sunset by sweeping the observer's longitude, one full sweep in about 17 minutes
- [`sensor_filters.md`](sensor_filters.md): every sensor, binary_sensor and text_sensor filter shape fed a fixed input sequence, outputs compared bit for bit between builds
- [`led_driver_sniff.md`](led_driver_sniff.md): bit-banged LED drivers captured on the ESP32's own pads with SPI slaves through the GPIO matrix, no chips
- [`hub_input_sim.md`](hub_input_sim.md): matrix_keypad, wiegand, rdm6300 and key_collector driven from a D1 mini's own pins and UART loopback
- [`value_range.md`](value_range.md): sensor and number on_value_range with a fixed sequence and a restart
- [`ezo_fake_bus.md`](ezo_fake_bus.md): the ezo component against a fake I2C bus that answers like an EZO circuit
- [`twai_self.md`](twai_self.md): esp32_can on_frame with the controller hearing its own transmit pad, no transceiver
- [`buttons.md`](buttons.md): output button pad timing, homeassistant button through a raw API client with the wizard input, uart button on the UART0 loopback
- [`rs485_frame.md`](rs485_frame.md): DLE/STX/ETX bus on an ESP32-C3 with a cross-wired simulated bus device (frame cases, gate timing)
- [`hx711.md`](hx711.md): fake HX711 driven from the other core, every published value matched to the served conversion
- [`automation_trace.md`](automation_trace.md): automation codegen A/B, a marker trace of chains, conditions, loops and script modes over the API
- [`bluetooth_proxy.md`](bluetooth_proxy.md): held GATT link after a half-open API session, frozen Home Assistant stand-in, out-of-order GATT discovery responses injected on the proxy
- [`crash_report.md`](crash_report.md): crash buttons, fetching the previous boot's crash report over the API, per-chip abort facts
- [`https_idf.md`](https_idf.md): web_server_idf HTTPS test component, listener switch, stalled TLS clients, bad key
- [`mdns.md`](mdns.md): legacy unicast mDNS dump, hammer and latency straight to the device
- [`sim_buses.md`](sim_buses.md): PCF8574/HD44780, MAX7219, SPI spy, packet_interface and SN74HC165 simulators, status LED probe, fake Home Assistant, cg.static_buffer probes
- [`sigma_delta.md`](sigma_delta.md): pulse density of the sigma_delta speaker pin read inside the ESP32-P4, rtttl note onset and release
- [`microphone_http_stream.md`](microphone_http_stream.md): raw PCM over HTTP with a counter fake mic or a USB mic, multi-reader client
- [`i2s_audio.md`](i2s_audio.md): I2S microphone fed by an I2S TX channel on the same S3 through three jumpers
- [`voice_assistant.md`](voice_assistant.md): late TTS start on the Voice PE with a slow announcement URL
- [`mcp251xfd.md`](mcp251xfd.md): MCP2518FD CAN FD over SPI in loopback, forced modes, chip reset and bus-off recovery, TX objects read back from chip RAM, idle SPI cost from a PCNT on CS
- [`crc32.md`](crc32.md): crc32 vectors on every platform path and the signed OTA signature block CRC on an ESP32-C3
- [`i2c_isr_fault.md`](i2c_isr_fault.md): software INA219 on the master's own pads with seeded bus faults, interrupt storm and stuck bus
- [`bmp390.md`](bmp390.md): BMP390/BMP388 register readback, INT data ready rate, start write faults by software NACK or held SDA, and the IDF I2C hang after a held SDA
- [`ipv6.md`](ipv6.md): IPv6 link-local and SLAAC boot and reconnect timing on ESP32 Wi-Fi, with an injected esp-netif delay that forces the race
- [`mcp23017.md`](mcp23017.md): MCP23017 IOCON aliasing and INT line behavior (open drain, push-pull, polling) on an ESP8266 with PA to PB jumpers
- [`pwm_capture.md`](pwm_capture.md): every LEDC PWM period captured on the same ESP32-C6 through RMT receive and PCNT, with and without power save
- [`spi.md`](spi.md): SPI bus capture on the same board with no chips (ESP32 SPI3 slave, edge interrupt snapshots on C3, ESP8266 and Pico 2 W), checked and diffed between builds
- [`heap_alloc.md`](heap_alloc.md): operator new counting, and a ring of allocation callers decoded with addr2line to tell the first run after boot from repeats
- [`wled.md`](wled.md): WLED realtime sender on a board's own access point
- [`combination.md`](combination.md): combination sensor NaN behaviour from the last source
@@ -40,3 +40,25 @@ length delimited strings and `int[]` as repeated sint32 (field 7); `ALLOW_MISSIN
defaults) and `templates/api-actions-strings-dut.yaml` (required strings) log what arrived. String arguments are
`StringRef`s that are not null terminated: copy them with `snprintf("%.*s", size, c_str())` into a buffer before
logging, and do not pass `%.*s` to `logger.log`, whose validator counts it as one pattern.
## store_yaml recovery over the native API (#16445)
`scripts/api/syrecover.py HOST KEY OUTDIR [ORIGDIR]` (KEY `-` for plaintext) builds the client library's
`APINoiseFrameHelper` around a stub `APIConnection` subclass (the compiled helper type checks the connection, and
`ConnectionParams` needs every field), sends Hello, Authentication (never answered without a password) and
`DeviceCapabilitiesRequest`, then `GetYamlRequest` (161), collects `GetYamlResponse` (162) frames by field number
(1 data, 2 done, 3 total_size, 4 encoding) until done, requests again, decompresses with `compression.zstd` (Python 3.14, or the `zstandard` package) and
unpacks the envelope with the tree's `unpack_envelope` (run with `PYTHONPATH` on that tree). Configs in
`templates/store_yaml/`: entry files in `boards/`, a package one directory above them with `!secret` and an inline
OTA password,
so the redaction and the `..` paths are exercised. Repetitive YAML compresses into one chunk even at 22 KB; a 6 KB
include of random base64 lines with `include_secrets: true` gives four chunks (1390 byte packets). Rough edge at
0bd3738430: `include_secrets: true` fails the build when a package in another directory uses `!secret` and has no
`secrets.yaml` beside it; a copy next to the package works around it.
Results: https://github.com/esphome/esphome/pull/16445#issuecomment-6335435397
## Allocations of a connecting client (ratgdo/esphome-ratgdo#705)
An API client that connects allocates about 15 times on ESP32: the connection object, then the list entities and
initial state batches growing the `DeferredBatch` vector. A log subscription adds more per line. Connect before a
heap measurement and subscribe to logs only afterwards (`heap_alloc.md`).
@@ -0,0 +1,24 @@
# Automation code generation A/B (#21978 to #21980)
A change to how actions, conditions or scripts are generated must not change what runs or in which order.
`scripts/automation_trace/automation-trace-common.yaml` (a package) logs a marker `M <case> <values>` at every step.
It covers:
- merged action chains, delays inside chains, nested `if`/`else` with and/or/not/xor, `choose` with `else`;
- `while` with and without a delay, nested and dynamic `repeat`, `wait_until` and `for:`;
- `single`, `restart`, `queued` and `parallel` scripts with parameters, a self `script.stop` mid-chain;
- `on_value_range`, alarm panel triggers, and parallel scripts with `max_runs` 2, 3 and 4 that execute themselves
from an `if` branch.
`run.sh LABEL` writes the per-run file (board, Wi-Fi from `secrets.yaml`, the package), builds and flashes over
`PORT`, takes the device address from the serial log and runs `drive.py HOST OUT`. The driver records the boot run,
a "Run All" press and API-driven triggers (Chain Button twice, Num 70 and 20, Sw on and off, Sel c and a). The
runner keeps the part from `DRIVER press Run All` on in `out-at<LABEL>.trace`; diff dev against each PR head. The
boot run is partly missed while the API connects, so it is left out of the diff.
- Pass: the Run All pass reaches `M done`, no `SHOULD_NOT_RUN` or `NOT_REACHED` marker appears, and the trace is the
same on both builds.
- The interleaving of the `queued` and `parallel` scripts with later steps is timing dependent, but it was identical
across builds and runs.
- To check a real device config as well, OTA the PR build and toggle one light and one switch from an API client,
returning each to its original state, and compare states and logs with the base build. Get the owner's approval
before flashing a device that is in use.
@@ -0,0 +1,28 @@
# BLE service and manufacturer data advertise triggers (#21822, #21823, #21827)
`scripts/ble_adv_triggers` checks `on_ble_service_data_advertise` and `on_ble_manufacturer_data_advertise` over the
air on the ESP32 and BK72xx trackers.
- Advertiser: `advtrig.yaml` on any second ESP32 with BLE. Every 250 ms it switches to the next of six raw
non-connectable advertisements (`adv_frames.hpp`, random static addresses `C4:C1:38:00:00:21` to `26`):
| Address | Contents |
|---|---|
| `...:21` | service data 181C `AA01` and manufacturer data 02E5 `BB01` |
| `...:22` | service data 32-bit 12345678 `AA02` |
| `...:23` | service data 128-bit 12345678-9ABC-DEF0-1122-334455667788 (wire order) `AA03` |
| `...:24` | manufacturer data 181C `BB04` (the service UUID of `...:21`, other data kind) |
| `...:25` | two service data 181C entries, `AA05` then `AA06` (only the first may fire) |
| `...:26` | the 128-bit UUID in printed byte order, `AA07` (must not match) |
- Receivers: `rx-esp32.yaml` (classic ESP32, passive scan) and `rx-bk72xx.yaml` include the same eleven triggers
(`triggers.yaml`). `rx.hpp` counts each trigger's hits by payload (`AAnn`, `BBnn`, anything else as `other`) and
logs the table every 20 s, plus all adverts and those from the advertiser.
- `run.sh esp32|bk72xx LABEL` builds, flashes, captures 150 s and runs `check.py`, which prints PASS or FAIL. Run it
once per build (dev merge base and each PR head). About 550 hits per trigger on ESP32 and 170 on a BK7238 in that
time.
- `mac_address: 00:00:00:00:00:00` meant no filter on the ESP32 tracker before #21823 and filters on that address
after it (BK72xx always filtered). Pass `ZERO_MAC=any` for an ESP32 build from before #21823.
- BK7238: with no `wifi:` in the config the tracker logs `Scan started` but receives nothing at all, on dev too. The
BK config therefore joins Wi-Fi.
- Results: https://github.com/esphome/esphome/pull/21827#issuecomment-6489703755
@@ -0,0 +1,30 @@
# Injecting BLE GAP and GATTC events (#21698)
Some BLE paths (repeated connection parameter update failures, a disconnect in the middle of them) are hard to get
from a real peer on demand. `scripts/ble_event_inject` feeds made up events straight into the ESP32 BLE handlers.
- `ESP32BLE::gap_event_handler` and `gattc_event_handler` are protected statics and the class is `final`.
`cp_inject.hpp` reaches them through explicit template instantiation, where access checks do not apply. Its
`cpinject::run()` sends six labelled steps (`STEP1` to `STEP6`): four peers failing five times each, a success
followed by more failures, and disconnects between failures.
- `cp-inject.yaml` is a package for a copy of a proxy config: it adds the include, a VERBOSE logger with the busy
tags back at DEBUG, and an `Inject conn param events` button. Press it with `scripts/api_button.py` while a
log capture (`esphome logs ... --device <ip>` under `timeout`) runs, then count the warning and verbose lines per
step on the PR and on dev.
- The handlers push into a queue and event pool that take a single writer, normally the Bluetooth task. Injecting
from the main task while real events arrive can corrupt them, so the button stops the scan, waits for it to end,
injects and starts it again. Run it with no active proxy connections (no Home Assistant BLE client holding one).
- Fake disconnects use `gattc_if` 0xFE: every client and proxy connection filters on its own `gattc_if_`, so the
queued event reaches only the shared handler. Do not use 0xFF (`ESP_GATT_IF_NONE`), which every client accepts.
- The fake peers (`C0:FF:EE:00:00:0n`) are never connected, so a live proxy keeps serving its real connections;
put it back on its own config afterwards.
## ThermoPro advertisements as scan results (#22047, #22054)
`scripts/ble_event_inject/tpinject.hpp` holds the advertisement records from the `thermopro_ble` unit tests (TP357,
TP357S, TP393, TP960R, TP970R, TP972S, TP25, plus invalid and 13 byte probe frames) and `tpinject::inject_all()`
feeds each one to `global_esp32_ble_tracker->gap_scan_event_handler()` with the device name and the manufacturer
data, the way `scripts/xiaomi_ble` does. Frame N comes from `A4:C1:38:00:10:0N` (up to `:10`), so give each
`thermopro_ble` sensor that MAC, include the header and call `tpinject::inject_all();` from an `interval:` or a
template button. Compare the published states (or the sensor DEBUG lines) on the PR and on dev; no ThermoPro
device is needed.
@@ -0,0 +1,19 @@
# ble_midi
## BLE MIDI client against a fake instrument (#18508)
No MIDI hardware needed: `templates/ble_midi/midiperi.yaml` turns a second ESP32 (an S3 with ethernet, so the
API stays reachable while BLE is busy) into a BLE MIDI peripheral with `esp32_ble_server`: the MIDI service
advertised, the MIDI characteristic with read, write, write without response and notify, an `on_write` that logs
the bytes, buttons that notify hand built packets (channel messages, two messages in one packet, running status,
SysEx in one packet, split over two, oversize, cut short, clock, aftertouch, orphan data bytes, a 50 packet burst)
and a restart button for the disconnect test. `midicli.yaml` is the component test config on a classic devkit with
`wifi:`, `api:`, the peripheral's BLE address in the `peri_mac` substitution, given on the command line with
`-s peri_mac <mac>` (the base MAC plus 2, printed by `esp32_ble` in `dump_config`), `on_sysex` and `on_message` lambdas that print the bytes, debug sensors and out of range send
buttons. Drive both over the plaintext API with `scripts/modem/mdrive.py HOST LOGFILE press:NAME wait:S` 3 s
apart and compare the peripheral's `TX` and `RX` lines with the client's decoded lines. Gotcha:
`ble_server.characteristic.set_value` latches the value of its first play and re-applies it before every notify,
so the peripheral script sets the value from a lambda (`id(midi_io).set_value(std::vector<uint8_t>(pkt));
id(midi_io).notify();`). At VERBOSE the client logs four lines per message, so a burst shows an
`esp32_ble took a long time` of a couple of seconds that is the serial port, not the parser.
Results: https://github.com/esphome/esphome/pull/18508#issuecomment-6348272717
@@ -0,0 +1,68 @@
# esp32_ble_server pairing and encrypted characteristics (#17562)
`scripts/ble_server_security`: a classic ESP32 devkit runs the secure GATT server (`server.yaml`) and a second ESP32
(an S3 here) runs `ble_client` as the central (`client.yaml`). The server has a `read_encrypt` battery level
(`99.9`), a plain characteristic, a `write_encrypt` characteristic that logs what it receives, a `read_encrypt_mitm`
characteristic and a `read_encrypt` characteristic that notifies every 2 s. `run.sh TAG IO AUTH` builds both with the
pairing mode as substitutions, pairs once, reconnects twice, and `summary.py` prints PASS or FAIL. The server needs
`esp32_ble_server` security from #17562 or later: point `SERVER_TREE` at that checkout (the client builds from
`TREE`, dev is fine). `summary.py` needs each refused read to show an `Error reading char` reply, so a run where the
client never connected fails instead of passing.
- `ble_client` never starts pairing on its own (a phone does, after an `insufficient authentication` reply); the
client's `Pair` button calls `BLEClientBase::pair()`. Read errors show as `Error reading char at handle N, status=5`
(not authenticated: no bond) or `status=15` (not encrypted: bond known, link not encrypted yet).
- The `ble_client` text sensor subscribes once, at connect, before any pairing, so a protected characteristic refuses
it. The `Subscribe` button writes the CCCD again after pairing; `ble_client` drops its service list after discovery,
so it writes the characteristic handle + 1 (where ESP-IDF's GATT server puts the CCCD).
- Numeric comparison (`display_yes_no` with `sc_mitm_bond`) tells a fresh pairing from a bond reuse: the
`numeric comparison` line only appears on a fresh pairing. Just Works cannot show it.
- ESP-IDF 5.5.6 drops the bond on the second disconnect on both roles (`Deleting unpaired device`), so that reconnect
pairs from scratch; 5.5.5 keeps it (`-s idf 5.5.5`).
- The server keeps its bonds in NVS. One holding an authenticated bond for the client refuses a Just Works re-pairing
silently; erase the devkit's flash before switching from nc to jw.
- `summary.py` failed both modes at 58636914ab: `read_encrypt_mitm` was readable with no pairing, because
`BLEService::create_characteristic()` took an 8 bit `esp_gatt_char_prop_t` and dropped the MITM bits (1 << 8 and
1 << 9). Check the generated `create_characteristic` call and the property type when a flag above bit 7 is added.
## ESP32 central against the secure server (#17562, #22081)
`scripts/ble_server_security/esp32central-*` swaps the roles: an ESP32-S3 runs the secure server
(`esp32central-srv.yaml`: plain, encrypted, MITM read and write, and an encrypted notify characteristic, numeric
comparison accepted automatically) and a classic ESP32 devkit runs `esp32central-cli.yaml` (`ble_client` with
`display_yes_no` and `sc_mitm_bond`, text sensors that log every value, a MITM write and a link cycle every 40 s).
`bonds.hpp` logs the bond list Bluedroid loaded from NVS on both boards.
- The server never starts pairing (a peripheral only answers a security request), and `ble_client` does not start
it on an insufficient authentication reply (status 5) the way Android does. The client's `on_connect` calls
`esp_ble_set_encryption(remote_bda, ESP_BLE_SEC_ENCRYPT_MITM)` instead.
- `esp32central-flow.sh TAG` erases both boards, flashes them, captures a fresh pairing with three link cycles
(phase A), then resets both together so each loads its bond (phase B), and prints per phase the connects, numeric
comparisons (a fresh pairing), MITM reads, server writes and `Deleting unpaired device` lines. It passes when
phase B reads the MITM characteristic with no numeric comparison and no `Deleting unpaired device` line.
- On ESP-IDF 5.5.6 every other reconnect pairs again (`Deleting unpaired device` on the disconnect after a bonded
reconnect), the regression #22081 describes.
#22559 fixes it: the bond survived 15 reconnects and a reset of both boards with no new pairing.
## Bluedroid hex parsing bench (#22081)
`scripts/bluedroid_hex_bench/`: `hexbench.hpp` times Bluedroid's `sscanf` parsing (`string_to_bdaddr` and the
`btc_config_get_bin` loop) against the parsers in #22081's `esp32_ble/bluedroid_hex.h` on the same inputs, valid and
invalid, and checks that the outputs match. `run.sh LABEL` (env `TREE`, `PORT`, `SC`, `PY`, `BOARD`) builds
`bench.yaml`, flashes, captures 25 s and fails on any mismatch or a missing `DONE`. Classic ESP32: about 7 to 9 times
faster for addresses, 6 to 7 times for 16 and 28 byte keys.
## ble_client on_notify with a real server (#21821)
`scripts/ble_notify/`: `server.yaml` (ESP32-S3 GATT server; a numeric and a text characteristic notify every 2 s,
with uint16 frame and empty notification rounds, set through `id(c)->set_value(...)` and `notify()` in a lambda) and
`client.yaml` (classic ESP32 client with two `on_notify` triggers per sensor, one of them delayed, and a link cycle
every 60 s). `run.sh LABEL` (env `TREE`, `SERVER_PORT`, `CLIENT_PORT`, `SERVER_BLE_MAC`, `SC`, `PY`) flashes both,
captures the client 150 s and counts `on_notify` against `on_value` per sensor.
- `run.sh` also requires `All clients established, services released`: before #21821 it never appears with a text
sensor `on_notify`. The delayed txt `on_notify` may miss only the last notify of the capture.
- The server takes one connection. Park any other client that would connect to it with
`esptool --after no-reset chip_id` (the board stays in the bootloader).
- An ESP32-C6 devkit whose native USB port refuses esptool can be flashed and logged through its UART bridge port
with `logger: hardware_uart: UART0`.
@@ -0,0 +1,27 @@
# BLE terminate with peripheral latency (#20232)
`scripts/ble_terminate`: an ESP32-S3 `bluetooth_proxy` (active) over Ethernet as the central (`btproxy.yaml`) and a
classic devkit `esp32_ble_server` peer (`btpeer.yaml`). The peer asks for 50 ms / latency 10 / 6 s with
`esp_ble_gap_update_conn_params` 4 s after connect; earlier requests are overwritten by the proxy's own "medium
conn params" after service discovery. Read the proxy's `Conn params updated` line (VERBOSE, API logs) to confirm;
the peer side never logs the event. The proxy only connects to a device its scanner has seen, so `btdrive.py`
subscribes to raw advertisements first.
Measure the peer's own disconnect (`serial_capture.py --ms`) after the client's disconnect: about 0.2 to 0.8 s when
the terminate arrived, about 6 s (the supervision timeout) when it was lost. The client side disconnect time is the
API reply and says nothing. On dev the S3 lost 6 of 12; the PR lost 0 of 16.
`run.sh LABEL ROUNDS` writes `central_bda.hpp` from `PROXY_BT_MAC`, builds and flashes both boards, runs the rounds
and `an.py`, which prints PASS only when every round completed, every peer disconnect came within 3 s and the last
parameter update the proxy logged before each disconnect had latency=10 (a round without it did not test peripheral
latency and counts as inconclusive).
Controller build check: ESP-IDF 5.5.6 ships `libbtdm_app.a` byte for byte the same as 5.5.5 for esp32c3 and esp32s3
(build 51d9dfd). Without hardware:
```bash
strings -a <idf>/components/bt/controller/lib_esp32c3_family/<chip>/libbtdm_app.a | grep -E '^[0-9a-f]{7}$'
```
On a board: a button lambda logging `btdm_controller_get_compile_version()` (declared `extern "C"`) over the API,
since the ESP-IDF init line only reaches the UART.
@@ -0,0 +1,57 @@
# bluetooth_proxy
## Held GATT link after a half-open API session (#21256, #22077)
When Home Assistant stops answering without closing its socket, the proxy must not keep the BLE link it opened for
that session. `scripts/bluetooth_proxy_half_open/` reproduces it with:
- `proxy.yaml`: the proxy under test on a classic ESP32 devkit (ESP-IDF, VERBOSE logger with the busy tags at DEBUG,
encrypted API). Build it from the tree under test with a `secrets.yaml` (`wifi_ssid`, `wifi_password`, `api_key`).
- `peripheral.yaml`: a second ESP32 (an S3 here) as a peripheral that only advertises while nobody is connected
(`esp32_ble_server` takes one client by default), with manufacturer data `FF FF 21 56` so the client can find it.
- `ha.py`: the Home Assistant stand-in (aioesphomeapi, `client_info` "Home Assistant test"). Roles: `owner` connects
to the peripheral through the proxy and idles, `observe` counts the peripheral's adverts and the slot report and can
ask for a connect at the end, `swap` connects at once and retries on the first advert. Every role stays subscribed
to advertisements: with no subscriber the proxy's loop disconnects every link at once, which hides the bug (an
early run failed with `disconnect while backend idle` for that reason).
`cases.sh PROXY_HOST LABEL` (env `PROXY_PORT`, `PERI_PORT`, `SC`, `PY`, `API_KEY`) captures both boards and runs
case C (old client killed, new client after 4 s), case H (old client frozen with `SIGSTOP`, so the socket stays open
with no FIN; new client after 4 s, watched 300 s, then it asks to connect and releases) and case N (frozen and nobody
reconnects for 200 s). `cases.sh PROXY_HOST LABEL swap` instead repeats H three times with the new client
connecting at once (`ha.py ... swap ADDR`) and retrying on the first advert. Both end with PASS or FAIL: the cases
fail when an observer exits non-zero, never sees the peripheral advertise (the held link) or the proxy resets, and
swap fails when a round never connects. An owner that never got the link also fails (a rig problem, not a result).
- The proxy drops a frozen session as unresponsive after about 144 s.
- Before #22077, case H holds the link for good: 0 adverts, and the address stays in the slot report. With #22077 the
link is released in the same millisecond as the `replaces` warning. In case 4 the first request logs `Connection
request ignored, state: DISCONNECTING` (connected=false at 0.2 s) and the retry connects at 0.5 to 0.7 s.
## Out-of-order GATT discovery responses (#22518)
Some peripherals answer a characteristic discovery (Read By Type) with records out of handle order. ESP-IDF 5.5.6
rejects that (`gatt_process_read_by_type_rsp invalid handle ...`) and ends the whole discovery; 5.5.5, Android and
BlueZ accept it. `scripts/att_reorder/` reproduces it with no such device:
- `server.yaml` (ESP32-S3) serves `FFF0` with six characteristics, then `1826`.
- `proxy.yaml` (classic ESP32, `idf` substitution; keep per-build files such as `example-proxy.yaml` in the same
directory) links `components/attswap`, which wraps Bluedroid's
`l2c_rcv_acl_data` (`-Wl,--wrap`); `attswap.hpp` swaps the first two records of the response whose first record
declares `FFF1`, and logs a swap count.
- `gattget.py PROXY PEER_MAC ROUNDS` reads the services through the proxy like Home Assistant (no cache);
`run.sh PROXY_YAML [ROUNDS]` flashes both boards and prints the services per round and the discovery errors.
On 5.5.6 Home Assistant gets `fff0` with no characteristics and no `1826`, while the proxy still logs
`Service discovery complete` (the proxy ignores the error status); on 5.5.5 every service arrives. To find the order a
real device uses, decode an Android HCI snoop: a Read By Type response lists its records in the order sent.
- Handles, reads and notifies after a reordered response: flash `server-notify.yaml` (`SERVER_YAML` on `run.sh`) and run
`gattcheck.py PROXY PEER_MAC`. It prints every service, characteristic and descriptor handle, reads FFF1, FFF2 and
FFF3 by the received handles (each holds its own value), and enables notify on FFF1, FFF2 and 2ACD. Compare the table
with a run of a proxy built without `attswap`. On ESP-IDF 5.5.5, which accepts the unsorted response, FFF1's
descriptor range runs over FFF2's declaration, value and CCCD, so FFF2 has no CCCD (#22576).
- `bluetooth_gatt_start_notify` only registers the notify on the proxy; the client writes the CCCD itself, as Home
Assistant does. Without that write no notification arrives.
- Give the proxy an `api` encryption key (`API_KEY` for the scripts). A Home Assistant on the same network connected to
a plaintext test proxy and took over its Bluetooth subscription (`Subscription from Home Assistant ... replaces
aioesphomeapi`), dropping the test client's connection mid-run.
@@ -0,0 +1,35 @@
# BMP390 / BMP388 (bmp3xx_i2c): register readback, data ready rate, start write faults (#21632, #21633)
Bench: ESP32-S3 devkit (native USB console, ESP-IDF), BMP390 breakout on 3V3, SDA GPIO1, SCL GPIO2, CS and SDO open
(address 0x77), INT to GPIO42. Some breakouts answer chip ID 0x50 (the BMP388 value); the register map is the same.
`scripts/bmp390/`:
- `gen.py NAME [--rate 50Hz] [--iir 128x] [--interval 1s] [--fault --kinds ... --probe-delay MS] [--reset-at 40,80]
[--open-line] [--bus-reset]` writes `NAME.yaml` (no Wi-Fi).
- `bmp390probe.hpp` wraps the sensor's I2C bus (`set_i2c_bus()` from an `on_boot` priority 800 lambda), dumps
CHIP_ID, ERR, INT_CTRL, PWR_CTRL, OSR, ODR and CONFIG before and after setup and every 10 s (`REGS` lines), turns on
data ready on INT after setup (`INT_CTRL = 0x42`, push-pull, active high: the driver never writes INT_CTRL and its
soft reset in setup clears it), counts rising edges (`INT edges=N in M ms`) and logs every pressure value with four
decimals (`READ <ms> <hPa>`).
- `run.sh NAME SECONDS [TAG]` (env `TREE`, `PORT`, `SC`, `PY`) builds, flashes, captures from a fresh boot and runs
`stats.py CAPTURE [SKIP_S]`: read count, cadence, INT rate, detrended noise (21 sample window), repeated values.
Flows:
- Register A/B: the same config on both builds; compare `REGS` after setup and between updates. Forced mode leaves
PWR_CTRL at 0x03 (sleep, both sensors enabled), normal mode reads 0x33. ODR codes: 0x00 200 Hz, 0x02 50 Hz, 0x07
1.5 Hz, 0x0B 0.1 Hz (period 5 ms << code).
- Real output rate: INT edges per 10 s window. Forced mode gives one edge per update.
- Settling: the bench cannot make a pressure step, but the soft reset in setup restarts the IIR filter with an
offset, so the first readings after boot show its step response (50 Hz with 128x settled in about 8 s).
- Reset behind the driver: `--reset-at` writes CMD 0x7E = 0xB6 at those seconds; a driver that handles it logs that
the sensor was reset and restores the registers.
- Start write faults (`--fault --kinds`, one letter per event at 30 s and then every 20 s): `N` the next start write
NACKs in software, `M` every start write NACKs for 5 s, `T` SDA held low through the GPIO matrix for the next start
write, `W` the same for every transfer for 5 s, `L` a continuous 5 s hold, `S` a 300 ms hold, `H` and `D` held
transfers of the harness only. `PROBE` lines check the bus `--probe-delay` ms later.
Pitfall: on the S3 with ESP-IDF 5.5.6, any transfer that fails while SDA is routed to a GPIO held low (or routed away
with the line floating high) leaves the `i2c_master` driver hung on every build, with or without the sensor: every
later transfer logs `I2C software timeout` while the lines read idle high and the saved pad registers are back, and
`i2c_master_bus_reset()` does not help. Use the software NACK kinds (`N`, `M`) to test what a component does after a
failure and the recovery; use the wire kinds only to show the first failure.
@@ -0,0 +1,46 @@
# buttons
Button platforms converted to `button::ApplyButton` change no behaviour on the wire, so each test compares what a
press does on dev and on the PR: the pad, the action call or the bytes sent. All three run on a D1 mini with no
wiring and no other device.
## output button pad timing (#21907)
`scripts/output_button`: two `gpio` outputs on GPIO5 and GPIO4 and three output buttons (500 ms and 1000 ms on
GPIO5, 200 ms on GPIO4). `edges.hpp` attaches a CHANGE interrupt to each output pad and stamps every edge with
`micros()`; on ESP8266 a pad reads back its own output level. A script runs four cases three times: one press, a second
press 250 ms in (the reset restarts, high 750 ms), both outputs at once, and the 1000 ms button then the 500 ms
button on the same output 300 ms later (low at 800 ms). `run.sh LABEL` flashes, captures 45 s and prints the edge
lines; diff dev against the PR. Each button keeps its own reset timer, so the 1000 ms button's reset fires later on a
pad that is already low and shows no edge.
Results: https://github.com/esphome/esphome/pull/21907#issuecomment-6516597178
## homeassistant button through a raw API client (#21908)
`scripts/homeassistant_button/ha-button-common.yaml` has three `homeassistant` buttons: `button.doorbell`,
`input_button.chime`, and one with no `entity_id` that is an `api: wizard:` input. An `on_boot` press runs before
any client connects. `habdrive.py HOST` is a plaintext API client that subscribes to Home Assistant actions, presses
each button and prints every action call (service and data). It sets the wizard input with `WizardInputSetRequest`
(message 158, key = FNV-1 hash of the input's id), first to an `input_button`, then to a `button`, then to an invalid
value the device must reject. `run.sh LABEL` flashes, captures and drives it. Expect `button.press` or
`input_button.press` per entity, `No entity ID set yet` and no call before the wizard input is set, and `No clients
connected` from the boot press.
- `homeassistant` entities are internal by default, so the client sees no buttons unless they set `internal: false`.
- Wait for each reply type instead of a fixed time: the hello reply can take more than a second, and a fixed wait
shifts every later reply by one step.
- Results: https://github.com/esphome/esphome/pull/21908#issuecomment-6516693220
## uart button on the UART0 loopback (#21906)
`scripts/uart_button/uart-button-common.yaml` puts four uart buttons on UART0 of a D1 mini: a 10 byte string, five
raw bytes with `0x00` and `0xFF`, the same string again (shares one generated function on the PR), and 64 bytes. It
sets the UART0 loopback bit in `on_boot`, so every press comes back on RX, and `uart: debug: direction: BOTH` logs
both sides. `run.sh LABEL HOST` flashes by USB and runs `ubdrive.py`, which presses each button 3 times over the API
and saves the log. Every `>>>` line must equal the configured payload and its `<<<` line.
- Set the loopback bit at `on_boot` priority -100. At 800 the UART setup runs afterwards and clears it.
- Nothing else reads the bus, so the debugger needs `dummy_receiver: true` to log RX.
- UART0 is the bus, so `logger: baud_rate: 0` and logs go over the API. The device address comes from DHCP or mDNS.
- An RX line sometimes lands after the next press marker when the log stream lags. That is not a byte difference.
- Results: https://github.com/esphome/esphome/pull/21906#issuecomment-6516762337
@@ -0,0 +1,17 @@
# cc1101
## Sleep and wake checks read from the chip (#22519)
`scripts/cc1101_sleep/`: `cc1101-sleep.yaml` runs an ESP32-S3 (ESP-IDF) with an E07-M1101D module on SPI and runs
tests T0 to T6 4 s after boot; `cctest.hpp` reads every config register and the PATABLE directly over SPI. The log
ends with `DONE fails=N`, and T6 prints whether each setter woke a sleeping chip.
- Proof that the chip sleeps: with CSn low, SO (MISO) stays high until the crystal runs, so it is high after
`power_down()` and low at once when awake. PATABLE entries 1 to 7 are also lost in SLEEP, so write marks to them
before `power_down()` and check that they are gone.
- Selecting the chip to check it wakes it, so after the check the chip is awake while the component still thinks it
sleeps. Call `wake_up()` and `begin_rx()` before the next case.
- Leave FSCAL3 to FSCAL1 (0x23 to 0x25), FSTEST (0x29) and AGCTEST (0x2B) out of register comparisons: calibration
and the chip itself change them.
- Results: on 7e6b90d08b, `set_frequency`, `set_channel` and `set_modulation_type` woke a sleeping chip and left it
in RX. 9e65d0c327 fixed that; every other check passed on both, and 200 sleep and wake cycles took about 1 s.
@@ -0,0 +1,7 @@
# combination
## NaN from the last source (#22510)
A combination sensor defers each update and uses the value of the source that published last. A NaN from that
source drops the whole update, so when testing NaN handling keep the last-publishing source finite, and compare
rows one update late. Heap churn per update: see `heap_alloc.md`.
@@ -0,0 +1,26 @@
# Crash report over the API (#21596, #22288)
#21596 lets an API client fetch the crash recorded on the previous boot; #22288 records `abort()` as a crash on
LibreTiny. `scripts/crash_report/crash-common.yaml` (a package: Wi-Fi from `secrets.yaml`, plaintext API, OTA,
DEBUG logger) adds three buttons: `crash abort`, `crash null store` and `crash div zero`. The per-run file supplies
the board and the name. Flash it, then run the drivers (they import `scripts/apiconn.py`, so run them in place or
put `scripts/` on `PYTHONPATH`):
- `drive.py HOST BUTTON_OBJECT_ID` (for example `crash_abort`) reads the capability, presses the button, reconnects
after the reboot and fetches the report twice (reason, PC, cause, fault address, build time, per-core backtrace),
subscribes to logs with `dump_config` to see what is replayed, and checks the capability from a second client.
It reads an earlier report first, so a stale one cannot pass. It exits 1 when the device still answers after the
press, does not offer a report after the crash, or the report has no reason or a PC of 0.
- `peek.py HOST` prints only the capability and how many replayed `CRASH DETECTED` lines a new log client gets. To
only press a button, use `scripts/api_button.py HOST - press "crash abort"`.
- Decode the PC and the backtrace with the build's ELF (`addr2line -e firmware.elf`). On RTL8720C a noreturn
`bl abort` can be the last instruction of a function, so the return address points at padding: decode
`(addr & ~1) - 2` to get the call line.
- `__builtin_trap()` faults on every board. A store to `nullptr` does not fault on every chip (BK7238 and RTL8720C
ignore it), so the `crash null store` button proves nothing there.
- With #22288, `abort()` reboots BK7238, RTL8720C and LN882H boards in 1 to 2 s and logs `Reason: Abort` with the PC
at the call site. Before it, the BK7238 hangs until reset, the RTL8720C restarts with no record and the LN882H
waits about 18 s for its watchdog.
- Quick crash reboots count toward BK72xx safe mode, so space out back-to-back crash tests on those boards.
- A device that keeps the previous crash replays it to every new log client until the next crash; that is one
record, not two.
@@ -0,0 +1,13 @@
# crc32 helper and the signed OTA block check (esphome#22398, 2026-11-07)
`scripts/crc32/crctest.hpp` checks `esphome::crc32()` against Python `zlib.crc32` values: `"123456789"` = `0xCBF43926`,
empty input returns the seed, chained calls (4 + 5, 300 one-byte calls, 97-byte chunks from an odd offset), bytes
0..255, 32 × 0xFF, 4093 bytes from offsets 0 to 3, and 8 KiB timed with `(esphome::micros)()` (best of five).
`common-crc.yaml` runs it at boot and every 20 s and also makes an unqualified `crc32(...)` call from a lambda, which
catches a clash with anything the generated `main.cpp` pulls in. Paths and timings seen (8 KiB): ESP32 ROM 512 µs,
ESP8266 PROGMEM nybble table 3072 µs, RP2350 932 µs, BW15 (FlashDB) 656 µs, BW12 2000 µs, BK7238 1026 µs.
The signed OTA signature block check (`block_is_valid()`), the PR's only converted caller, ran the accept and reject
cases in `ota.md` (signed OTA with several trusted keys).
Results: https://github.com/esphome/esphome/pull/22398#issuecomment-6716330334 and #issuecomment-6716545831.
@@ -0,0 +1,35 @@
# EZO circuits without hardware: a fake I2C bus (#21840)
`scripts/ezo_fake_bus`: the real `ezo` component runs on a classic ESP32, but an `on_boot` priority 900 lambda calls
`id(ph).set_i2c_bus(&fakeezo::bus)`. `fake_ezo.hpp` is an `i2c::I2CBus` subclass that answers like an EZO pH circuit
and logs every write and read with the ms since the command: `R`, `i`, `Slope,?`, `Cal,?`, `T,?`, `L,?`, `Name,?`,
and test replies `pend` (254 twice), `bad` (2), `nodata` (255), `nak` (read NACK) and `full31` (32 bytes with no
terminator), and `sNN` (an NN-character reply, terminated when the read has room). `ezo.yaml` runs the steps:
command bursts larger than the queue, custom commands, calibration, LED, sleep and address changes, and the error
replies.
- The same trick works for any I2C device whose driver only needs write and read: swap the bus pointer, no wiring.
A real `i2c:` block is still needed so the config validates.
- `run.sh LABEL` builds, flashes, captures 90 s and writes `out-ezo<LABEL>.events`. Compare dev and the PR: the
replies, callback texts, read values and the wait before each read (912 ms for reads and calibration, 304 ms for
the rest) must match; differences in queue limits are the PR's intended change.
- Found with it: dev read past its 32-byte buffer on an unterminated reply (a custom callback got 56 bytes), and
two queued address changes both went to the first new address.
- Results: https://github.com/esphome/esphome/pull/21840#issuecomment-6493565259
## Trigger argument lifetime (StringRef or an owning copy, #22204)
Give each text trigger two automations (`- then:` list items): a synchronous one that logs `x`, `x.size()` and
`strlen(x.c_str())` and hands `x` to a parallel `script` (`text: !lambda return x;`, the script delays 2.5 s), and
one with `delay: 3s` or `wait_until` that logs `x` afterwards. Fire bursts so newer replies reuse the reply buffer
during the wait, then diff the synchronous texts, in order, against the delayed and script texts. Check the generated
`main.cpp` for `StringRef` or `TriggerForwarder<std::string>`. The fake's `sNN` replies give texts of any length.
## A real EZO circuit
An EZO-HUM ships in UART mode (blinking green, streams `33.91\r` every second at 9600 baud). To switch it to I2C
without bridging PGND, flash a one-shot software UART build (TX to the EZO RX, RX from the EZO TX) that writes
`I2C,111\r`; the LED turns blue and the circuit answers at 0x6F. Replies: `i` gives `HUM,1.01`, `Status`
`?STATUS,S,3.31`, `O,?` `?O,HUM,T,DEW`, and `R` with all outputs `34.82,22.03,Dew,5.85`; `Name,x` returns no text.
EZO-HUM rejects `T,?`, `Slope,?` and `Cal,?` (syntax error), so `on_t`, `on_slope` and `on_calibration` need another
circuit or the fake bus. Log every callback with an `EV` prefix and count them per step.
@@ -0,0 +1,153 @@
# Fake I2C devices without a chip
## Fake devices on an in-firmware I2C bus
When there is no chip and no wiring, subclass `i2c::I2CBus` in an include, implement `write_readv()` only, answer
the registers the driver reads and check each write. Point the component at it from an `on_boot` lambda that runs
before the component's `setup()`: `id(x)->set_i2c_bus(&fake::bus)` at priority 950, which is ahead of every I2C
component (hubs set up at IO, 900, sensors at DATA, 600). The configs here that use 700 to 1000 work for their
components; use 950 for a new one. Keep a real `i2c:` block in the config so validation passes. Bulk
loops in a button lambda must call `App.feed_wdt()` and keep their per-iteration logs below INFO: 2000 DEBUG lines at
115200 baud trip the task watchdog, which is a fault of the harness and not of the PR.
`templates/i2c_fake/` holds the fakes; each `*-common.yaml` or `*-test.yaml` is a classic ESP32 or ESP32-S3 config
that includes its `.hpp` (add `esphome: name:` for the `-common` ones):
- `ccsfake.hpp`, `ccs-common.yaml`: CCS811 at 0x5A (#21927). Answers setup and update reads and checks every
ENV_DATA write against the humidity and temperature the case set; "CCS Cases" runs edge values, "CCS Sweep" 2000
random ones and logs `SWEEP checked N bad M bad_reads R bad_writes W`. A read without a register, of a register
the fake does not model or of the wrong length gets a NACK and counts in `bad_reads`; a write other than the
driver's SW_RESET, APP_START, MEAS_MODE and BASELINE writes (each with its own length) gets a NACK and counts in
`bad_writes`. The CASES and SWEEP lines end in PASS only when something was checked and `bad`, `bad_reads` and
`bad_writes` stayed 0, and in FAIL otherwise.
- `eefake.hpp`, `ee-common.yaml`: HTE501 (0x40) and TEE501 (0x48) with CRC words; "Next" steps through raw
temperature words and logs the value a signed hundredths decode should give (#21930). A command other than
identification and measure, and a read longer than the reply, gets a NACK and counts as `bad`. The DONE line
200 ms later, after both reads, shows the running `bad` total, the measure replies each driver read and the
published values; it ends in PASS only while `bad` is 0, both drivers read a reply this case and both published
the expected temperature (and HTE501 the fixed humidity).
- `scdfake.hpp`, `scd-common.yaml`: SCD4x at 0x62 and 0x63 with Sensirion CRC; logs every `0xE000` ambient pressure
write, and the CASE and SHOW lines count writes with a bad CRC apart from the good ones. A command the scd4x driver
does not send, one sent with the wrong length (2 bytes bare, 5 bytes with one CRC word), or any other command with a
bad CRC, gets a NACK and counts as `bad writes`. A read after a command the fake does not answer, or one longer than the command's
reply, gets a NACK and counts as `unknown reads`. The second instance has a fixed `ambient_pressure_compensation`
to cover the setup path (#21935). Each CASE line ends in PASS only when bad CRC, bad writes and unknown reads did
not move, the `0xE000` writes match `expect_e000()` (none, or one with the expected value) and, for a source case,
CO2 was published. `RANGE_CHECKED` in `scdfake.hpp` picks the rule: false truncates as dev does, true skips values
outside 700 to 1200 hPa and rounds as #21935 did. The SHOW line ends in PASS when nothing bad was counted, neither
instance failed and 0x63 got exactly one write of its fixed value.
- `sxfake.hpp`, `sx-common.yaml`: two SX1509 register files with reset values and the `7D 12/34` reset, recording
every write. Only a byte that lands on RegReset (0x7D) drives the reset, so a burst through it keeps the bytes for
0x7E and 0x7F. A read without a register, or a transfer past 0x7F, gets a NACK and `dump_log()` warns with the
count. Compare per address: hub setup order can legitimately move between hubs (#21897). Outputs of an Apply
platform are distinct template types, so a lambda list of them needs
`std::initializer_list<output::FloatOutput *>{...}`. Each case ends with an END line, PASS only while no write was
dropped and no transfer was bad; the writes themselves are judged by the diff.
- `msfake.hpp`: eight MS8607 pressure/temperature fakes at 0x76 and up and humidity fakes at 0x40 and up, with faults
per device (reset NACKs, bad PROM CRC4, zero D1 every Nth read, humidity CRC8 errors, out of range D2, full scale
humidity). It counts every command, so skipped conversions show in `report()`, and an ADC read with no conversion
pending answers 0 like the real chip and counts as `adc_no_conv`, and a humidity read with no measure pending gets
a NACK like the real chip in no-hold mode and counts as `read_no_measure` (a reset aborts either); each injected
fault counts when it is delivered (`inj_reset_nack` with `reset_nack_left`, `inj_zero_d1`, `inj_bad_crc`), so a
run that never reached a fault path shows 0 there; a PROM read, conversion or humidity measure
before that device's first reset counts as `no_reset`; a humidity read of any shape but a bare
3 byte read, and a pressure/temperature read that is not a 2 byte read of a PROM word (even addresses 0xA0 to 0xAC) or
a 3 byte ADC read, gets a NACK and counts as `bad_reads`; a bare read answers the last PROM or ADC pointer and never
repeats a reset or conversion; a command write the fake does not know, or one longer than a
byte, gets a NACK and counts as `bad_cmds`. Point both the component and its `humidity_i2c_id` device at the bus. That
driver logs no values, so give each sensor an INFO `on_value` log (#16487).
- `htfake.hpp`, `ht16k33-char-test.yaml`: HT16K33 chips at 0x70 to 0x77 (0x74 and 0x76 NACK) keeping the 16 byte
display RAM and a command trace, rendered back as text for the Adafruit 0.56" 7-segment backpack ("12:34",
decimal points, "!" for stray bytes); a read gets a NACK and counts as malformed. The test runs 64 checks (setup order, font, colon and dots, chained chips,
printf/strftime/clock, dimming, blink, standby, NACK) and ends with `ALL DONE pass=N fail=M` and PASS or FAIL (#8520). It found a
crash (null glyph table) when one display had `add_characters` and another did not.
- `ssfake.hpp`, `seesaw-test.yaml`: Adafruit seesaw at 0x36 (ATtiny817), 0x49 (SAMD09) and 0x4A (NACK): status,
GPIO bulk with the pull latch, ADC, touch, encoder and NeoPixel with chunk offsets, and the write to read gap per
module. A read it does not model, a write without a register and a write to a module or register it does not model
(or with the wrong data length) get a NACK and count in `bad`. A 100 ms interval step machine drives it and ends with `ALL DONE` and PASS or FAIL (#13538).
- `bldcfake.hpp` (three M5Unit-BLDC units at 0x65 to 0x67 with simple motor physics, one that never spins down),
`gtfake.hpp` (GT911 at 0x5D: switches, max values, touch points and key byte) and `sdpfake.hpp` (SDP3x/SDP8xx at
0x21 to 0x26: product IDs, an unknown product, a failing read every third time, a failing ID read, stop refused
while idle) have no config yet; wire them the same way. Each fake sets up its own faults in its constructor, so
there is no init step to call. Each NACKs a read it does not model or one of the wrong
length, and a write or command it does not know, and counts them as `bad_reads` and `bad_writes` (`bad_cmds` for
the SDP) in `report()`; check those are 0. The SDP report also counts each injected ID read NACK at 0x24 as
`id_fail`, so 0 there means the driver never reached that fault.
Check the PR head again just before posting results: a head that moves during a run means a retest.
## Pico 2 W as an I2C target (#22236)
`scripts/pico_i2c_target/`: `picofake.hpp` is a raw RP2350 I2C0 interrupt handler on GP4 (SDA) and GP5 (SCL) at 0x76 that
answers one byte per read request from a 256 byte register file, so any read length is byte exact. `picofake.yaml`
plays a BME280 (`chip_id` 0x60) or BMP280 (0x58) with the datasheet calibration; every 15 s it switches between the
datasheet example readings and the 0x80000 "no measurement" values, and logs `STAT` counters every 5 s. `bad`
counts reads and writes of registers the drivers do not use (soft reset only with 0xB6, read pointer wrapping past
0xFF). The DUT is
an ESP32-C3 (`dut-common.yaml`, GPIO6/GPIO7) with two sensors on the one fake (`dut-bme280.yaml`,
`dut-bmp280.yaml`), the second with oversampling NONE. `run.sh bme280|bmp280 LABEL` flashes the DUT, starts the
fake capture and waits for a `STAT` line as the baseline, then resets and captures the DUT and prints the distinct
values per sensor. The fake capture runs on until a `STAT` line arrives after the DUT capture ends, so that line
covers the whole run. It prints PASS when every sensor gave a numeric state, that `STAT` line came, and the fake's
`abrt` (read replies the controller aborted) and `bad` counts did not move from the baseline; diff the output of dev
and the PR.
The ESP32-S3 I2C slave v2 driver cannot emulate a register device: answering from a task and straight from the ISR
both gave shifted multi-byte reads. Use the Pico.
## DS2482-800 simulator (ds248x)
`scripts/ds248x_sim/`: `dssim.yaml` with `sim.hpp` turns a Pico 2 W into a DS2482-800 at 0x18 (GP4 SDA, GP5 SCL)
with four DS18B20s: channel 0 at 21.5 °C, channel 1 at 33.0 °C, channel 2 at -5.25 and 40.0 °C. The DUT is an
ESP32-C3 (`dspd.yaml`, GPIO4/GPIO5 plus GND); the internal pull-ups are enough at 50 kHz. Faults are one character
on the Pico's USB console: `c` NACKs the read pointer write after the next channel select, `z` NACKs the 9 reads
after the next 0xBE, `b` and `k` stick busy with a failed reset or config write, `x` and `X` make it dead and alive.
`SC=<serial_capture.py> python3 drive.py TAG SIM_PORT DUT_PORT [c,cd,z,b,k,dead]` (default `c,z,b,k,dead`; `cd`
runs the `c` fault densely, 28 times about 3 s apart) resets the DUT per phase and prints
readings, wrong values, log counts and the simulator's counters (`wrong_ch` counts ROM matches on the wrong
channel). The DUT is captured with `--ms` so the log can be split at the last fault: the recovery window starts
`SETTLE` (3) seconds after it. A phase fails when any of the four sensors got no correct reading in the recovery
window, the DUT logged a CRC, held low, busy timeout or no response line in it, a temperature is wrong, a phase other
than `dead` logged `recovery failed`, `DS248x hub failed` or a component marked failed, a ds248x warning is still
set at the end of the phase, the DUT capture
failed (stderr in `TAG-<phase>.err`), a fault moved its injection counter (counted when the DUT meets the
fault, not when it is armed) by other than the number of faults sent (`arm_lost` counts arms that landed while one
of the same fault was still pending and merged into it), a NACK fault never moved `nacked`, a write delivered data while a NACK was armed
(`nack_miss`, also counted when the write is followed by a repeated start into a read; the NACK never reached the wire and stays pending), the DUT sent an address-only write (`empty_wr`), a write was cut off by a repeated start into another write and
never processed (`dropped`),
a fault is still pending (`pend=1`) at the end of the
phase, the simulator counted a config write with a wrong complement (`cfg_bad`) or an unknown, short or overlong
command (`bad_cmd`), a counter is missing from the simulator's CNT line, the simulator aborted a read reply
(`tx_abrt`, not gated in the `dead` phase, which switches the controller off), a phase other than `b` and `k` (which
reset the chip under the driver) counted `wrong_ch` or a 1-Wire command without the active pull-up bit (`ops_no_apu`),
the simulator never logged `DEAD` for the `dead`
phase, or the DUT log of the `dead` phase has no sign of the outage (`did not respond`, a ds248x warning,
`DS248x hub failed` or a component marked failed). It ends with PASS or `FAIL:` and the failed phases. The whole
phase CRC, held low, busy timeout and no response counts are printed for information: the faults cause them.
- Sensors polled at the same interval go through the channels in blocks, so a stale channel cache never shows.
Poll them at different intervals, as `dspd.yaml` does (1, 1.3, 1.7 and 2.3 s).
- The NACKs use the RP2350 `slv_data_nack_only` register, set at STOP.
- A NACKed write and an address-only write both end with no data, so the simulator cannot tell them apart.
The ds248x driver never sends one, `dspd.yaml` sets `scan: false` and the simulator only sees STOPs after its
own address, so `empty_wr` (address-only writes outside a NACK window) stays 0; a nonzero count means `nacked`
may include writes that never carried a NACKed byte.
## I2C fault injection in software (vl53l0x)
`scripts/i2c_fault/`: `faultbus.h` wraps the real `I2CBus` and returns NACK for chosen transfers to one address. An
`on_boot` priority 800 lambda swaps the sensor's bus (`id(d)->set_i2c_bus(&fb)`) before its setup, and an interval
arms the faults. `vlpf.yaml` runs a VL53L0X on an ESP32-S3 (GPIO8/GPIO9) through quiet, fault A (the next
measurement-in-progress read fails), quiet, fault B (the next measurement start fails) and quiet phases over 150 s,
with a model ID probe every 10 s to tell a driver stall from a dead chip. `run.sh LABEL` builds, flashes, captures
and runs `an.py`, which counts readings per `PHASE` line. Each `PHASE` line clears a fault that was armed and has
not fired yet and logs the fired totals so far, so `an.py` counts the fires of each phase. It fails when the phases are not quiet, faultA, quiet,
faultB, quiet (a short capture or a restart), the first quiet phase has no readings, a later quiet phase reads less
than half as often as the first, a later quiet phase reads NaN (`nan` or `-nan`) or logs the driver's "update called before prior
reading" warning when the first did not (a target out of range reads NaN in every phase), a quiet phase has a failed
model ID probe, one that did not return 0xEE or no successful probe at all,
anything crashed or rebooted, or a fault did not fire in its own phase or fired in another one.
Prefer this to glitching the wire. Holding SDA low by rerouting the pin (`esp_rom_gpio_connect_out_signal` to
`SIG_GPIO_OUT_IDX` and back to `I2CEXT0_SDA_OUT_IDX`) wedges the ESP-IDF controller, which then needs
`i2c_master_bus_reset()` after each glitch, and glitches timed onto writes left the VL53L0X answering 0x00 until a
power cycle.
@@ -0,0 +1,13 @@
# GPIO expanders
## Interrupt driven base class (#21469, #21468)
Any device with a PCF8574 input expander whose INT line goes to an ESP32 GPIO works; the inputs should change
during the test (end switches on a gate or cover are ideal). Before flashing, check with `nm` that
`gpio_expander::interrupt_pin_isr` lands in IRAM (0x4008xxxx on a classic ESP32). Then confirm `dump_config` lists
the interrupt pin and the input states match reality, and that the `pcf8574` entry is absent from the runtime stats
while idle (its loop is disabled). Drive the inputs (for a cover, `cover_command(position=1.0)` and then `0.0` over
the API, waiting for each end state): every input edge should log, `pcf8574` should show a small count in that
minute's stats and vanish in the next. For a device that cannot be reached physically, boot the same code on a
bench board first and keep a merge base image built as the fallback; an ESP32 bootloader without rollback support
leaves only `safe_mode` to recover a bad image.
@@ -0,0 +1,17 @@
# ESP32 GPIO hold through the ISR pin API (#21576)
`scripts/gpio_hold/hold_test.hpp` builds `ESP32InternalGPIOPin`s by hand, calls `to_isr()` and reads each pad back
with `gpio_get_level`: 20 ISR writes, a raw `gpio_set_level` control (a held pad must not change, an unheld one must)
and ISR `pin_mode` pulls (input must drop to `NONE` between pull changes, because the ISR path only applies pulls when
`FLAG_INPUT` toggles). It needs no wiring: unconnected pins read the pulls. Each pin runs held and unheld, then the
round logs `ROUND n PASS failures=0` or `FAIL`.
- `USE_GPIO_HOLD` comes from any pin with `hold_state: true`, so the configs add a `gpio` output on a spare pin.
- `gpio-hold-esp32.yaml` (classic devkit) runs three rounds 5 s after boot and has a `rerun` button;
`gpio-hold-esp32s3.yaml` (Waveshare ESP32-S3-ETH, where most pins go to the W5500, TF card, camera, WS2812 or
USB) runs a round every 15 s.
- C3: ESP-IDF 5.5 has `SOC_RTCIO_PIN_COUNT` 0, so `rtc_gpio_is_valid_gpio()` is false on every pin and only the
digital hold path runs.
- C6 (Waveshare ESP32-C6-GEEK, native USB): LP pads GPIO0 to 7 are RTC IO with hold. GPIO1, 2, 7, 20, 23 read high
and GPIO6 low whatever the pull (LCD, TF card or resistors); clean pins are 0, 3, 4, 5, 18, 19, 21, 22. Dev
already holds LP pads through `gpio_hal_hold_en`, so the PR and dev look the same there. H2 is untested.
@@ -0,0 +1,41 @@
# Heap allocation counts and callers (#22509, #22510, ratgdo/esphome-ratgdo#705)
## Counting
Replace the global `operator new` in an `esphome: includes:` header on ESP32 (Arduino or ESP-IDF) and log the
delta from an `interval`. Compare a PR build with its base on the same flow; the rate per second shows churn.
- The ESP8266 core defines `operator new` non-weak. `-Wl,--wrap=_Znwj` worked in a sensor-only config but
boot-looped a neopixelbus config (alignment fault in the wrapper), so report free heap only there.
- `vec = {}` keeps a std::vector's capacity (it is an initializer_list assignment); only
`std::vector<T>().swap(vec)` or `clear()` plus `shrink_to_fit()` frees it. Seen as heap that did not come back
after a WLED effect stopped.
## Callers
`scripts/heap_alloc/`:
- `allocring.hpp` counts every `operator new` and, once armed, keeps the first 1024 with their size, time and
five caller PCs, about 28 KB of RAM. It disarms itself when full. The frame walker
(`esp_backtrace_get_next_frame`) exists only on Xtensa (ESP32, S2, S3); on RISC-V chips entries keep size and
time without callers. Aligned `operator new` (`std::align_val_t`) is not replaced, so it is not counted;
nothrow `new` goes through the replaced one.
- `allocring.yaml` (package, substitution `alloc_arm_s`) arms it that many seconds after boot and adds a
"Dump allocations" button that logs what was kept. The press stops recording until the next reboot, so
reboot (or reflash) before each run; a second press returns the same entries.
- `alloc_marker(a, b)` from a lambda writes a marker entry, for example on each cover operation change, so the
dump splits into phases.
- `dump.py HOST OUT` presses the button and writes the dump to a fresh OUT, pressing again until the header
and every entry arrived (a late log subscription or dropped log lines). `decode.py LOG ELF [ADDR2LINE]` prints each entry with its callers as function and file:line, and exits 2
when the recorder never armed, an entry was still being written, entries are missing, repeated or out of
range, no Xtensa entry has a caller, the ELF is from another build (the buffer address in the header differs), or the buffer
filled. A PR build and its base can share that address, so copy each build's `firmware.elf` next to its dump. On RISC-V it prints sizes and times only. It reads only the newest dump in the file. The ESP-IDF ELF is `.esphome/build/<name>/build/firmware.elf`.
- `cycles_cover.py HOST N OUT` waits for the "Allocation recorder armed" binary sensor, then moves the first
cover to the far end and back N times (`cover_command(position=...)`, waiting for idle each way) with a states-only API client, then saves the dump the same way. It exits 2 when a move
never reached its position.
Findings:
- Arm well after boot: the scheduler regrows its storage once after the freelist trim (see `scheduler.md`).
- Repeat the action several times in one session and count only what comes back. On a ratgdo cover (an external
component, ESP32 ESP-IDF) the first door open after boot allocated 18 times: four 36 B SchedulerItems,
`to_add_` and `items_` growth, and the API `DeferredBatch` vector. Cycles 2 and 3 allocated nothing.
- Keep the API client out of the count (see `api.md`): connect before the run, subscribe to states only, and
subscribe to logs only for the dump.
@@ -45,3 +45,44 @@ without the fix logs `Applying the report that arrived during the transition` an
ends at 100% on both builds because the last call's target differs from the held report, so it is a regression
check only, not a reproduction. Separate button presses are too slow (about 1.5 s each) to land inside the fade.
The mirrors are not API entities, so read their state from the "Log state" button's line. Results: https://github.com/esphome/esphome/pull/21144#issuecomment-6278257695
## Wizard-linked homeassistant entities on every platform (#21908)
`scripts/homeassistant_wizard/`: `ha-all-common.yaml` adds to the buttons of `ha-button-common.yaml` one entity with a
fixed `entity_id` and one linked to an `api: wizard:` input for each of sensor (plus one with `attribute:`),
binary_sensor, text_sensor, number, switch, select, text and light (the wizard light is in `ha-wiz-light.yaml`), and
logs the ESP8266 heap every 5 s (`ramchk`). `hadrive.py HOST` plays Home Assistant over a plaintext API connection: it
records the state subscriptions the device asks for, sends states (including number and text min, max, step and mode,
select options and light supported_color_modes), commands every writer, sets each wizard input with
`WizardInputSetRequest` (key = FNV-1 hash of the input id), changes it to a second ID in another domain, sends an
invalid one and reconnects. It prints SUB, STATE and ACTION lines that diff cleanly between two builds, and PASS or
FAIL. `run.sh LABEL` (env `TREE`, `PORT`, `SC`, `PY`; `NO_LIGHT=1` for a build without #21908) flashes a D1 mini, takes
three idle boots of heap readings and drives one boot. `lightcheck.py HOST` checks again that the wizard light follows
only its current ID.
- Builds before #21908 cannot compile a wizard-linked homeassistant light (the wizard called `set_entity_id()` on the
LightState); `NO_LIGHT=1` leaves `ha-wiz-light.yaml` out.
- An unset wizard number or select rejects a command in the core (`> max 0`, `has no options`) before the
`No entity ID set yet` check; nothing is sent either way.
- Field 3 of LightStateResponse is brightness, not `missing_state`; SwitchStateResponse has no missing flag.
- A light publishes only changes, so a report of the state it already has publishes nothing.
## homeassistant switch on a script, host build against a throwaway Home Assistant (#22548)
No board is needed when the change is in validation or platform independent code: an ESPHome `host:` build serves the
API on 127.0.0.1:6053. `scripts/homeassistant_script_switch/`:
- `ha-configuration.yaml` is the Home Assistant config (port 8129, a 20 s `long_job` script that sets and clears
`input_boolean.script_ran`, a `quick_job` toggle script). Onboard it with `scripts/mqtt/ha_discovery/onboard.py DIR`.
- `hsw.yaml` mirrors both scripts and the flag with homeassistant switches, with template buttons that call
`switch.turn_on` and `switch.turn_off`.
- `haflow.py TOKENFILE` adds the device with the esphome config flow and turns on `allow_service_calls` in the options
flow; without it the device's action calls are refused.
- `drive.py TOKENFILE NAME` presses the buttons through the REST API and prints the script and flag states after each
step.
Notes:
- homeassistant import entities default to `internal: true`, so the mirrors are not Home Assistant entities; read
them from the device's `Got state` log lines.
- Turning a script off stops it mid run (the flag stays on). A script that finishes at once reports on, then off, in
the same second.
@@ -0,0 +1,48 @@
# http_request
## Update manifest URL changed at run time (#21373)
`scripts/http_request/manifest_server.py PORT [DELAY]` serves `stable.json`, `beta.json` and `slow.json` (answered
after DELAY s) with both chip families and logs every request. `templates/http_request_update/upd-common.yaml`
(secret `manifest_base` is the server URL) has the update entity on the stable manifest, buttons for every
`set_source_url` shape (`std::string` local, literal, temporary, `.c_str()` of a local, the current URL back in),
`Check Then Swap` (ESP32 task path: check the slow manifest and replace the URL in the same lambda), `Arm Swap In
Check` plus a `logger: on_message` lambda that replaces the URL from inside the inline check (ESP8266; the log line
from inside `on_message` is swallowed, judge by the next check's result), and a `Heap` button. `upd-base-common.yaml`
keeps only the shapes that compile on a tree with the bare pointer setter (for the base build, copy a board file
and point its `common` include at `../upd-base-common.yaml` with a different `name:`); `boards/upd8266.yaml` and `boards/upd32.yaml` are the
per-board files, one directory below the package their `../` include points at. Drive with `scripts/http_request/updrive.py HOST KEY LOGFILE press:NAME wait:S`, which prints every
update state (latest version, summary). The ESP8266 Arduino client gets `Error reading manifest: -1` on roughly a
quarter of the fetches from this Python server on every build, so count checks against the base before blaming a
PR. `http_request` on the ESP8266 needs `verify_ssl: false` even for plain http URLs.
Results: https://github.com/esphome/esphome/pull/21373#issuecomment-6337544933
## Non-blocking request actions (#21486)
`scripts/http_request/hsrv.py PORT` serves `/fast`, `/slow?d=S`, `/big` and `/chunked` (`n` bytes at `rate` B/s,
deterministic bytes `(i*7+3)&0xFF`), `/post` (echoes length and byte sum), `/redirect` and `/hang`.
`templates/http_request_action/hr-common.yaml` (substitutions `name`, `bufsize`; per-board files `hr32.yaml` and
`hr66.yaml`, `-s name hr32base` on the command line for the base build; `hrota32.yaml` adds the `otaflash` action
for `otadrive.py`) has API actions `req_get`, `req_post`, `burst` (N runs of a `mode: parallel` script on one action),
`cancel` (`mode: single` script run twice, then `script.stop`) and `reboot_during`, and logs
`RESULT n status len sum took maxgap`, where maxgap is the longest main loop gap during the request (an
`interval: 1ms` lambda plus the time since its last pass, so a fully blocking request counts too). Drive with
`scripts/http_request/hrdrive.py IP http://HOST:PORT LABEL [https]` (`ONLY=case,case` and `ROUNDS=n` env; the
`cross` case runs two actions on one component). `burstonly.py IP URL N` checks concurrency against the server's
completion times, `reboottest.py IP URL` times `App.safe_reboot()` with requests in flight, `updcheck.py IP LABEL N`
runs N update checks over the API (for real device configs that use `update: platform: http_request`) and
`otadrive.py` triggers `ota.http_request.flash` (the API variable is `sum`; `md5` is a reserved variable name).
`execute_service` must be awaited on current aioesphomeapi or nothing is sent. Keep the ESP32 `bufsize` at 32 KB or
less: the action copies the body into a `std::string` while the buffer is still held, and 80 KB aborts with out of
memory on dev and on the PR alike. `http_request` refuses BK72xx and RTL87xx at validation.
## OTA request cases with basic auth (#21556)
`scripts/http_request/ota_auth/`: `ho-common.yaml` (API actions for `ota.http_request.flash` with `md5_url`, `md5`
and basic auth, `flash()` with no URL, an invalid URL, the `md5_computed()`/`md5_expected()` getters), `fwsrv.py`
(serves a www dir, `/auth/` needs `otauser:otapass`), `hodrive.py` (11 cases, reconnects after each good flash) and
`run.sh BUILD BOARD` (compiles from `TREE`, serial flash, serves that exact image; `TREE`, `P32` or `P66`, `SC` and `HOST_IP` in
the environment). On the D1 mini the authenticated MD5 read fails
with `Error reading MD5: -1` on dev too, and stored credentials carry over to the next flash on both builds.
Results: https://github.com/esphome/esphome/pull/21556#issuecomment-6381067561
@@ -0,0 +1,26 @@
# HTTPS on web_server_idf (#22185)
#22185 adds an HTTPS developer API to `web_server_idf` with no YAML option, so `scripts/https_idf/` brings a test
component: `ext/httpstest` (`httpstest.hpp`) serves `/hw` (a status line with mode, request count and free heap),
`/hw/big` (16 KB) and `/hw/switch/{http,https,https8443,bad}`. A switch runs on the main loop through `defer()`, or
3 s later with a `/later` suffix (a stalled TLS client holds the server task, so its switch is armed first), and
calls `set_ssl_credentials(cert, key, port)`: plain HTTP on 80, HTTPS on 443 or 8443, or a key that does not match
the certificate. Its schema uses `web_server_idf.consume_ssl_sockets`. The run-time switch is the follow-up API; on a
tree that only has #22185, drop the switch paths and set the credentials once before `init()`.
Copy the directory into a scratch directory with a `secrets.yaml`, then `run.sh LABEL` (env `TREE`, `PORT`, `SC`,
`PY`): it runs `make_certs.sh` (a self-signed P-256 certificate, its key and an unrelated key in
`ext/httpstest/certs.h`; test material, never commit it), builds `https-dut.yaml` (ESP32-C3, logs on USB), flashes,
takes the address from the serial log and runs:
- `t1.sh HOST`: one GET with the TLS time, 20 sequential, 6 parallel three times and the 16 KB body. The fixture has
no event source, so the 501 that #22185 sends for an event stream on HTTPS is not covered here.
- `stall.py HOST`: arms a switch to HTTP and to 8443, opens two idle TCP clients (no ClientHello) on 443 before it
runs, then switches back; every switch must be accepted and work, and the idle clients must be closed.
- `t2.sh HOST`: HTTP and back, an armed switch to 8443 with a client open that sends a handshake record header and
never the rest, 60 requests that must all get a 200 for a heap check (compare the `/hw` heap before and after),
then the bad key. The server must log the error and stay off, without falling back to plain HTTP; reset the board
afterwards.
The runner fails on any unexpected status, a missing key error for the bad key, a reset or a task watchdog. `https-soak.yaml` adds a switch between 443
and 8443 every 20 s for a long run with clients attached. Each HTTPS connection takes about 26 KB of internal RAM
(no PSRAM on the C3), so watch the `ALIVE heap=... min=...` lines.
@@ -0,0 +1,21 @@
# matrix_keypad, wiegand, rdm6300 and key_collector without hardware (#21787, #21788, #21790)
All on a D1 mini with no wiring: `scripts/hub_input_sim/hub.yaml` and `sim.hpp`.
- Keypad: one row (GPIO5) and columns GPIO4, 14 and 12 with pull-ups. `sim::press(pin)` switches the column to
`OUTPUT_OPEN_DRAIN` driven low, so the single row reads that key pressed on every scan; `sim::release` goes back
to `INPUT_PULLUP`.
- Wiegand: D0 on GPIO13, D1 on GPIO0. Call `sim::wiegand_arm()` once, then `sim::wiegand(value, bits)` sends the
pulses (80 us low, 1.5 ms apart, most significant bit first). `pinMode(OUTPUT_OPEN_DRAIN)` clears the pull-up and,
with the output latch low, makes a falling edge that adds a bit: write `GPOS` high before the mode switch and set
`GPF |= 1 << GPFPU` after. The interrupt type survives `pinMode`.
- rdm6300 reads UART0 with its loopback bit set (`USC0(0) |= 1 << UCLBE`), so the logger runs with `baud_rate: 0`
and the log goes over the API. Frames are 0x02, 12 ASCII hex characters (version, 4 id bytes, xor) and 0x03. The
uid binary sensor pulses on and off per read.
- key_collector takes both the keypad and wiegand as `source_id`.
- `run.sh LABEL` flashes over serial, reads the API log until `DONE` and writes `out-hubsim<LABEL>.ev`; diff the
`[ev` lines of dev and the PR.
- Wiegand frames with valid parity: 26 bit 0x3579bdf (11259375), 34 bit 0x3bd5b7dde (3735928559), 37 bit
0x1b4b4b4b4a (24253932965). 0x3579bdd fails parity, so only `on_raw` fires for it.
- rc522 and sx1509 need their chips; compile them only.
- Results: https://github.com/esphome/esphome/pull/21790#issuecomment-6491675405
@@ -0,0 +1,28 @@
# hx711 without a chip: fake HX711 on the same ESP32 (#8899)
`scripts/hx711`: classic ESP32, no wiring. SCK is GPIO32, DOUT GPIO33 (configured as an input with pull-up, so DOUT
reads busy until the fake runs). `hxsim.hpp` starts from `on_boot` priority -100 (after the component has set up its
pins), switches both pins to `GPIO_MODE_INPUT_OUTPUT` (it reads the clock and drives the DOUT level the component
reads back) and runs a task pinned to core 0 (the ESPHome loop runs on core 1; core 0's idle task is removed from
the task watchdog).
- Ready means DOUT low; the task waits for the clock with interrupts off in 2 ms windows. Each rising edge puts the
next bit out and the 25th sets DOUT high; extra pulses pick the next conversion (25 A/128, 26 B/32, 27 A/64), and
each conversion takes 100 ms. Clock high for more than 60 us is power down (back to A/128), clock low powers up.
- Values encode the conversion: A/128 is 128000+n, A/64 64000+n, B/32 -(32000+n), so a published value shows which
conversion it came from. `check.py CAP...` matches every `Got value` against the served values and exits non-zero
on an unmatched value, no values or a crash.
- `mode`: 0 normal, 1 DOUT stuck high, 2 DOUT left low after the read.
- The waiting and shifting code must run from IRAM. `IRAM_ATTR` on inline class members either gets inlined into
flash code or fails to link ("dangerous relocation: l32r: literal placed after use"); use static free functions
with `IRAM_ATTR __attribute__((noinline))` and check with `xtensa-esp32-elf-nm` that they sit at 0x4008xxxx. Run
from flash, the first transfer after boot lost one clock pulse (the value shifted right by one bit). Log from the
main loop (an interval drains an event ring), never inside the critical section.
- Runs: `hxbase` (65 s), `hxpr1` channel B plus set_gain and power actions (115 s), `hxpr2`
power_down_after_reading (45 s). `hx-common.yaml` starts each run's `run_all` script and logs the `DONE` line.
`build.sh NAME TREE LABEL` writes `NAME<LABEL>.yaml` with its own name (so dev and the PR builds of the same config
do not share a build directory) and builds it; then `flashcap.sh DIR NAME<LABEL> TREE SECONDS` (`PORT`, `SC` in
the environment), which fails without a `DONE` line and runs `check.py`.
- The configs have no Wi-Fi on purpose: the fake keeps core 0 busy with interrupts off in 2 ms windows, which would
starve the Wi-Fi task. Keep the harness without Wi-Fi, API or OTA.
- Result on #8899: no missed pulses over 141 transfers, and every published value matched.
@@ -0,0 +1,20 @@
# I2C master interrupt storm and stuck bus faults (esphome#22270 / #22396, 2026-11-06 to 11-07)
`scripts/i2c_isr_fault/inasim.hpp` is a software INA219 at 0x40 answered on the ESP32 master's own pads (SDA GPIO32,
SCL GPIO33, nothing wired): the slave pulls a line low by switching the pad's output mux from the I2C0 signal to plain
GPIO output 0 (a wired AND with the master), from a GPIO ISR on core 0 that spins through one transaction. A core 0
task re-asserts long stuck lines every tick. From 20 s after boot it injects seeded faults into `p_fault` per mille of
transactions (garbage bits, NACK, clock stretch, SCL/SDA held low up to `hold_max_ms`, glitches, vanish).
`i2cbase.yaml` (package): `i2c:` at `freq` with `scan: true`, an `ina219` polled every `ina_interval`, a bus voltage
register read every `hammer`, a boot self test, and 30 s `MASTER`/`SLAVE` counter lines (ok, timeouts by kind,
garbage, longest failed run, loop stalls). `example.yaml` shows the substitutions: framework and version (`fw`,
`fwver`), `seed` (seed 1 at 5 % hangs dev with an interrupt watchdog on CPU0 at about 245 s, the #22270 report),
`p_fault`, `fault_mask`. `run.sh LABEL [SECONDS]` builds, flashes and captures (640 s default); `summary.sh` prints
crashes, watchdogs, timeout kinds and the minimum ok reads per window.
Findings: ESP-IDF 5.4.2 has a different failure (the driver waits for bus idle with no timeout after a NACK, so the task
watchdog reboots in a loop with or without the guard); 5.5.5, 5.5.6 and 6.0.1 hang on the storm without the guard and
run clean with it. ESPHome never uses the driver's async mode, so there is no asynchronous path to test.
Results: https://github.com/esphome/esphome/pull/22396#issuecomment-6713396173
@@ -0,0 +1,16 @@
# Fake I2C slave (#21586)
`scripts/i2c_slave`: a D1 mini running an Arduino `Wire` slave at 0x40 (`i2cslave.yaml` with `i2cslave.hpp`): a fake
HDC1080 plus a 256 byte register file, and reads of 0x7E and 0x7F stretch the clock for 0.5 and 5 ms. It needs
`libraries: [Wire]` and 160 MHz. With jumper wires only and the internal pull ups it works to about 50 kHz; it cannot
keep up with a true 100 kHz hardware master.
The master (`i2cbk.yaml`, a BK7238 on P26/P24, needs the software I2C bus from that PR) writes a counter, reads it
back with a repeated start, probes an absent address and reads both stretch registers every 2 s, logging `RW OK`
(the read back matches, the absent address NACKs and both stretched reads succeed) or `RW BAD` with running totals.
Control: the same blocks on a classic ESP32 devkit (SDA GPIO21, SCL GPIO22) with hardware I2C at 25 and 50 kHz got
every repeated start read right, so failures on a bit banged master at a similar real clock point at the master.
On the NiceMCU WB3S V1 board (BK7238), P26 and P24 are on the right-hand header.
Fake devices that need no real chip (in-firmware buses, the Pico 2 W target, the DS2482-800 simulator, NACK
injection) are in [fake_i2c.md](fake_i2c.md).
@@ -0,0 +1,14 @@
# i2s_audio
## I2S microphone loopback on one S3 (#22130)
With no I2S microphone at hand, `scripts/i2s_loopback/` makes one. `dcgen.hpp` runs an ESP-IDF std TX channel on
I2S1 as master (GPIO4 BCLK, 5 WS, 6 DOUT, 16-bit stereo) in its own task, switching patterns (DC step, opposite DC
per channel, DC plus a 1 kHz tone, full and half scale 50 Hz squares) through `dcgen::set_mode()`. Jumper 4 to 15,
5 to 16 and 6 to 17 to an `i2s_audio` microphone on I2S0 with `i2s_mode: secondary`. `dcgen::on_data` (registered
with `add_data_callback`) keeps per-segment and last 500 ms stats plus a 1 ms trace of channel 0, and `report()`
logs `SEG` and `SETTLE` lines from an interval. `run.sh LABEL [SAMPLE_RATE] [CHANNEL]` flashes and captures 60 s,
and `sum.py` tabulates. The segment min and max include the tail of the previous pattern (capture latency), so judge
with the tail values. The runner fails on a missing segment, a full-scale square under rms 8000 (no signal through the
jumpers), on either channel for a stereo run, or a crash after the first segment.
Results: https://github.com/esphome/esphome/pull/22130#issuecomment-6615554179
@@ -0,0 +1,28 @@
# IPv6 link-local and SLAAC on ESP32 Wi-Fi: boot timing and race injection (#22364)
`scripts/ipv6_boot/` (ESP32-S3 and ESP32-C3, native USB console, ESP-IDF). One YAML per build
(`example-s3.yaml`, `example-c3.yaml` show the shape) with the substitutions `min_count` (network
`min_ipv6_addr_count`) and `cap_ms`.
- `v6probe.hpp` and `v6common.yaml`: `network: enable_ipv6`, VERBOSE logs, and a 50 ms interval that logs milestones
from boot (`MS assoc|ipv4|linklocal|slaac|connected <ms>`; addresses come from `esp_netif_get_all_ip6`, so they
count once they are preferred, after duplicate address detection), then `RESULT boot ...` with `heap_used`, then
disables Wi-Fi for 3 s and logs `RESULT reconnect ...` the same way, then reboots. A phase ends when SLAAC and the
connection are both seen, or after `cap_ms` (`timeout=1`).
- `v6hf.yaml` adds a `HighFrequencyLoopRequester` (a busy main loop, as with a display or audio).
- `v6inject.yaml` links with `-Wl,--wrap=esp_netif_action_connected` and defines `V6_INJECT`; the wrapper in
`v6probe.hpp` sleeps 300 ms in the event task before esp-netif brings the station interface up, after the wifi
component already queued STA_CONNECTED. That makes the "interface not up yet" race happen on every connect.
- `run.sh NAME SECONDS` (env `TREE`, `PORT`, `SC`, `PY`) builds `NAME.yaml` as device `NAME`, flashes, captures the
self-rebooting boots and runs `an.py CAPTURE...`: per phase the `esp_netif_create_ip6_linklocal failed` count,
boots without link-local, SLAAC or a connection, connection timeouts, and median and worst times.
- Check the network first: a computer on the same network shows an `autoconf` IPv6 address when a router advertises
a prefix. SLAAC arrived about 2 s after link-local on the test network.
- The natural race did not show: no failure in about 160 boots on the S3 (normal and busy loop) and the C3 without
the fix. esp_event runs handlers in registration order and the wifi component registers before esp-netif, but the
event task reaches esp-netif's handler within microseconds. Only the injected delay reproduced the failure.
- An extra event handler registered from `on_boot` cannot inject the race: it runs after esp-netif's handler, when
the interface is already up. Wrap the esp-netif call instead.
- `wifi.disable` removes the addresses, so the reconnect phase creates link-local again on the same path.
- Count failures by the probe's `ll=0` too, not only by the warning: the USB console drops lines.
@@ -0,0 +1,21 @@
# ld2420 without a module (#21914)
`scripts/ld2420_sim/` runs the hub on a D1 mini UART0 with the loopback bit set and no radar. A `uart: debug` TX hook
(delimiter `04 03 02 01`, the command footer) hands each command frame the hub sends to `ldsim::on_tx()`, which writes
a success ack right behind it: the hub reads its own echo and then the ack. The acks carry the version (the `fw`
substitution), the min and max gate and timeout registers and the gate parameters, so `setup()` completes (`Sim stats`
logs `cmds 21 acks 21`). Template buttons then inject energy frames (`Energy steps`) or simple mode `ON`, `OFF` and
`Range N` lines (`Simple steps`). Firmware v1.5.3 or older keeps the hub in Simple Mode.
- `run.sh LABEL energy|simple HOST` builds, flashes and records two `lddrive.py` runs over the API; `compare.py` takes
the same run of two builds and prints PASS when the state sequences after the steps button match, every setup
command was answered, a Distance and a Presence state arrived and no ERROR line was logged.
- The loopback bit is set at `on_boot` priority 900: after the UART is set up and before the hub's blocking `setup()`.
- `fire_trigger_()` hands the debug lambda the debugger's own buffer, which writing the ack changes; `on_tx()` copies
it first, and ignores its own acks (command high byte 0x01) when the nested call sees them.
- The hub also reads its own echoed command frames. A read register command carries `00 00` where an ack has its error
word, so the echo counts as that command's ack and every real ack lands one command late. The Firmware text sensor
published during `setup()` therefore shows `v0.0.0` in this harness; the config dump and the Simple Mode check see
the simulated version. A real module does not echo, so this harness cannot check that text sensor value.
- Energy mode and simple mode publish at most once a second; a burst of 5 frames 100 ms apart publishes only the first.
- Results: https://github.com/esphome/esphome/pull/21914#issuecomment-6517294344
@@ -40,3 +40,29 @@ least 24 selects per build and read the raw bytes with `uart: debug: direction:
id) before blaming the parser; the API log stream drops lines under that dump volume, so some misses stay
unclassified.
Results: https://github.com/esphome/esphome/pull/21357#issuecomment-6333244886
## Frame stream harness: presence hold and link loss (#21733, #21734)
`scripts/ld2450/stream-harness.yaml` is a loopback harness that feeds the hub one periodic frame every 100 ms from a
mode global set by API buttons: no target, target 1 approaching, silent, or 12 garbage bytes per tick (what a radar
at another baud rate looks like). `has_target` has `filters: []` so the hub's own hold is what gets timed, and every
change logs `presence raw ON|OFF at <millis>`. Add `stream-connected.yaml` as a second package on builds whose ld2450
has the link watchdog (`connected` binary sensor).
- `holddrive.py HOST OUT`: for `presence_timeout` 5 and 0, three seconds of target frames, then empty frames; prints
the ms from the press to the off line, then sets 0 and 20 and reads the number back after a restart. On dev 0
still held 5 s and snapped back to 5; with #21733 it clears on the next empty frame (under 200 ms) and stays 0.
- `linkdrive.py HOST OUT`: steps through target frames, 10 s of silence, empty frames, 10 s of garbage, target
frames, then the hub's radar restart followed by 3.5 s and by 15 s of silence; prints every state change and link
log line with its time. Dev freezes every entity through silence and garbage. Run against a real radar too: a
radar restart and a Bluetooth toggle must not report a loss (the Apollo R PRO-1 ETH did not).
- A freshly booted ESP8266 delays API commands sent in its first seconds and then runs them back to back; the
drivers wait 8 s after connecting before timed presses.
## BT MAC sensors (#21731)
`scripts/ld2450/macdrive.py HOST OUT` reads every LD24xx BT MAC, Bluetooth and firmware entity, turns each Bluetooth
switch off, then on, and prints the values after each step. The MAC sensors are often `disabled_by_default` in
vendor configs but still publish over the API. On the Apollo R PRO-1 ETH both radars (LD2450 fw 2.04, LD2412 fw
1.26) have the multicast bit set in the first MAC byte, which dev showed as `unknown`. A radar MAC identifies a
device: quote only its first byte in PR text.
@@ -0,0 +1,29 @@
# Bit-banged LED drivers without the chips (#21841 to #21847)
The GPIO LED drivers (bp5758d, bp1658cj, sm2235, sm2335, tlc5971, tlc5947, my9231, sm16716) never read the chip
back, so a classic ESP32 devkit can capture its own pads with no wiring. `scripts/led_driver_sniff`.
- `sniff.hpp`: SPI2 and SPI3 run as DMA slaves fed from the driver's clock and data pads through the GPIO matrix
(`esp_rom_gpio_connect_in_signal`, pad input enabled with `gpio_input_enable`). Mode 1 samples on the rising edge
of its clock input (measured, not what the mode table suggests): SPI2 takes the clock straight (rising-edge bits,
`R` lines) and SPI3 inverted (falling-edge bits, `F` lines; my9231 clocks a bit on both edges). CS is GPIO2,
driven by the test. PCNT counts clock and data edges and latch rises per window.
- ESP32 slave DMA drops a partial last word, so `close()` pads the clock with whole pulses to a 32-bit boundary plus
32, and the bit count comes from PCNT. `selftest()` sends a known 57-bit pattern with the clock idling low and
high; `selftest.py` checks it, and `run.sh` runs it on every capture.
- bp5758d, bp1658cj and sm10bit_base (sm2235, sm2335) release the data line for the ack with `pin_mode(INPUT)`, and
`pin_mode(OUTPUT)` disables the pad input. The `sniff` local component (`ext/`) links with
`-Wl,--wrap=gpio_set_direction` and the header re-enables input on the sniffed pins. The ack bit reads the
floating line, the same on every build.
- `gen.py` writes `led.yaml`: all eight drivers on separate pins (bp5758d 25/26, bp1658cj 23/4, sm2235 18/19, sm2335
21/22, tlc5971 27/32, tlc5947 33/13 latch 14, my9231 16/17, sm16716 5/12, selftest 15/0) and a script per driver:
one window per step (open, set levels, 200 ms of main loop, close), a repeated step that must send nothing, two
direct `loop()` calls timed with `esp_timer`, and an idle window.
- `run.sh LABEL` builds, flashes, captures until `DONE` and checks the selftest. `compare.py BASE PR DRIVER` diffs
every window of one driver between two runs and decodes it (9-bit groups for the BP and SM frames, 12-bit words
for tlc5947, 16-bit for tlc5971, the two-edge interleave for my9231, 50 zero bits plus 25-bit groups for
sm16716). Loop timing varies about 3 % between builds even for drivers a PR does not touch.
- my9231 toggles DCKI with `!digital_read()`. ESP32 output pins have the pad input disabled, so without the
sniffer's input enable the read is always 0 and the clock never toggles (on dev too).
- Out-of-range channels: run `esphome config` with channel = last and last + 1 on dev and the PR.
- Results: https://github.com/esphome/esphome/pull/21844#issuecomment-6491228132
@@ -0,0 +1,63 @@
# Addressable LED strips (#21447, #21445)
- To exercise a failed strip allocation, use a classic ESP32 without PSRAM with `num_leds: 65535` and
`channel_colors: GRBW` (262 kB, bigger than any internal block). `num_leds` is uint16 in the drivers, larger
values wrap. On dev such a strip boot-loops the board.
- Never force allocation failures by hogging RAM instead: Wi-Fi then aborts in `operator new` or the loop task
trips the task watchdog.
- On the ESP32-S3 give each RMT strip `rmt_symbols: 48`, or the first channel takes all 192 symbols.
- A board with a few WS2812 on a pin already defined as a light can be extended with `id: !extend <light id>` to
add effects; restore its own config afterwards.
- Loop cost of a strip (#19337): give each strip an `addressable_lambda` effect with `update_interval: 0ms` that
changes every pixel, and add `runtime_stats: log_interval: 30s`; compare the main loop and light times per pass,
leaving out the first period after boot. The strips need no LEDs attached. A classic ESP32 has 512 RMT symbols in
all, so three strips need `rmt_symbols: 128` each.
## RMT channel and memory budget (#22172)
`scripts/rmt_budget/`: each line of `cases.txt` is a mix of strips, transmitters and receivers with their
`rmt_symbols` (`s:96`, `t:def`, `r:48`, ...) on an ESP32-S3 or ESP32-C6, and `gen_case.py CASE` turns it into a
config whose strips have an `output_id` and which logs `STATUS so0=<is_failed> tx1=...` every 3 s. `run.sh CASE...`
builds each case under one name per board (so builds stay incremental), flashes, captures 15 s and prints the STATUS
line and the RMT driver errors; it exits 1 when a case printed no STATUS. `verdict.sh CASE...` runs only
`--only-generate` and prints the config errors, the RMT memory warnings and the components registered, to compare
with what the board then does. No LEDs are needed: allocation failures show at boot.
- On the S3 USB console the ESP-IDF `no free tx channels` lines are printed before the port reopens; the C6 on
UART0 shows them.
- S3 TX channels can use RX memory (384 symbols at most) but only start on blocks 0 to 3, so two 192 symbol strips
do not fit.
- 17 of 17 predictions of the PR matched the driver. Results:
https://github.com/esphome/esphome/pull/22172#issuecomment-6615083139
## Capture the wire, not the buffer (#21577)
`templates/led_wire/wire.hpp` reads what the strips send, with no wiring:
- RMT RX channels on the `esp32_rmt_led_strip` pads at 10 MHz, decoded with a high-time threshold per chipset
(ws2812 0.4/0.8 µs: threshold 7 ticks; sk6812 0.3/0.6 µs: threshold 4);
- an SPI2 DMA slave fed from the `spi_led_strip` pads, with `spi: interface: spi3` for the strip.
`led-wire-dut.yaml` (classic ESP32) drives a GRB ws2812 strip, a GRBW sk6812 strip, an SPI strip, an RGB light and a
partition through steps S0 to S8 (on, `addressable_set`, lambda writes, partition, effect, power limit, off), logging
the decoded `RMT`, `SPI` and buffer `GET` lines and `DONE` per step, then runs `stress_step()` over 300 shows and
logs bad frames per strip. Diff the step lines of dev and the PR.
- A comparison of the buffers alone missed a PR bug in which the start frame was not sent.
- Give the captured strip `rmt_symbols` that hold a whole frame (192) and capture one RX channel at a time: with 64
symbols the TX refill interrupt stretches bits on dev and the PR alike while the capture runs. The substitutions
at the top of the config pick the strip.
## rp2040_pio_led_strip: a self-sampling PIO probe (Pico 2 W)
`scripts/pio_probe/pioscope.hpp` samples any GPIO from PIO2 at 25 MHz while another peripheral drives it (PIO reads
the pad without taking the pin) and decodes WS2812-style pulses into bit timings and bytes; no wires or LEDs.
`pa.yaml` probes five strips on PIO0 and PIO1 (WS2812 and two custom timings), `pc.yaml` one strip after claiming
DMA channels 0 to 11. `run.sh pa|pc LABEL` builds, flashes, captures 40 s and prints the `RESULT` lines; a correct
frame of two LEDs set to `Color(0xA5, 0x3C, 0x0F)` reads `3ca50f3ca50f`.
- Turn the light on at full brightness before probing, or color correction writes zeros.
- The probe FIFO overruns once in a while: count only full captures. `run.sh` fails unless every strip gave at least
one correct frame.
- An RP2350 `panic()` escalates to a HardFault and the rp2 crash handler reboots within about a second, so a panic
during component setup boot-loops before USB CDC enumerates and the board looks dead (no port, no BOOTSEL drive).
Hold BOOTSEL while plugging it in and flash the UF2 drive. To see such a fault, trigger it after boot.
@@ -56,3 +56,125 @@ OTA-only board: copy the device's own config, add the probe include and the step
next to that config. A LibreTiny OTA is a 1.5 MB UF2 and takes minutes on a slow link, so do not wrap the
upload in a short timeout. Read the replayed report with `esphome logs ... --device <name>.local` (an API
client gets it on subscribe) or on the board's serial log port if it has one.
## RTL8720C crash report on the next boot (#21648)
`templates/rtl8720c-crash-buttons-dut.yaml` has one API button per case: `trap` (`__builtin_trap()`, usage fault),
`bus fault` (store to 0x70000000, imprecise, CFSR 0x400), `wild call` (call to 0xFFFFFFF1, MemManage fault with PC
0xFFFFFFF0 and LR in the caller), `isr trap` (a trap from a spare interrupt pended by software, hard fault), `hang`
(interrupts off, the watchdog resets it and nothing must be reported) and `restart`. Every boot logs
`boot reason N, crash record yes|no|n/a`; `n/a` means the handler is not built, as on a dev control build.
Flow per build: flash with `flash_rtl87xx_kit.py`, start one `serial_capture.py PORT 600 OUT --released --ms`, then
`scripts/libretiny_crash/rtl_drive.py HOST OUTDIR TREE YAML trap restart "bus fault" "wild call" "isr trap" hang restart`.
The `restart` after the first crash checks the report shows only once. Time each reset from the serial capture
(the ROM fault dump to the next `ROM Version` line), not from the API port: the board is back before an API client
notices it went away. Compare the reported PC with the `PC =` line of the ROM dump, and check the decoded lines in the
`caseN-*.txt` files. Pass the UF2 by name (`firmware.uf2`): the build directory holds a second identically sized copy.
Regression rerun of the same flow (#21728): `scripts/libretiny_crash/rtl_rom_vs_report.py CAPTURE` lists, for each ROM
fault dump in a `--ms` capture, the ROM's CFSR and PC, the ms to the next `ROM Version` banner and the Reason, CFSR, PC
and SP the next boot reported, so two builds compare line by line. The ROM lines start with a bare CR; split the
capture on newlines only. The imprecise bus fault's PC moves between builds (it is wherever the core was when the
fault was raised); compare it with that run's ROM dump, not with another build. SP stays the same per fault (8 word
frame, no FPU). On the BK7238 time the reboot from the button press to the next `BK7238_1.0.14` banner (0.6 to 0.7 s).
## BK7238 BLE and flash sector safety (#21735)
The Beken BLE bond store reads its partition at boot and erases that sector when the CRC does not match; before
LibreTiny 1.14 the partition was unmapped and pointed at sector 0, erasing the bootloader (#18646). To prove a change
like this safe on a board that cannot be recovered without a dump:
1. Take a full backup first: `scripts/bk72xx_flash/readback.sh full-before` (ltchiptool family `beken-7238`, about
190 s for 2 MB). Keep it until the board is back on a known build.
2. Serial flash a build without the feature, then deliver the feature build by OTA (the path users take). Use
`bk72xx: framework: sdk_silent: none` with `logger: hardware_uart: UART1`, so the SDK's own lines (bootloader
banner, `CRC fail, remove bonded info`) show up in the capture.
3. Soak with Wi-Fi up, OTA again from the new build, pulse CEN a few times (`serial_capture.py --reset`), and check
each boot comes back through the bootloader.
4. Read the whole chip again and run `scripts/bk72xx_flash/partdiff.py full-before.bin full-after.bin
<libretiny>/boards/_base/beken-72xx.json`: bootloader, calibration and userdata must be unchanged; app, download
and one KVS sector change normally.
Check what already sits in the target partition before the first boot: on an XH-WB3S that shipped with Tuya firmware,
0x1E1000 (the `ble_bonding` partition of the generic Beken layout) held the Tuya `diff2ya` record (magic `ba dc cd
ab`), which the bond store erased on its first boot. Nothing depended on it there, but say so in the results.
## BK7238 OTA and the board layout (issue #18222)
A BK7238 has one of two bootloaders, and the board in the YAML must match it: Beken OEM (`BK7238_1.0.14` in the
boot banner, `generic-bk7238`, OTA as RBL to `download` at 0x132000) or Tuya T1 2.0.0 (`generic-bk7238-tuya`,
`t1-*`, `download` at 0x12C000 plus a `diff2ya` record at 0x1E1000). `ltchiptool uf2 info` on `firmware.uf2` shows
the layout an image targets (`FAL_PTABLE`, `OTA_PART_INFO`). Measured on the OEM bootloader (2026-10-27):
| Running | Uploaded | Result |
|---|---|---|
| Tuya layout | Tuya layout | upload succeeds, bootloader prints `App verify failed! Need to recovery factory firmware.`, old build stays |
| Tuya layout | OEM layout | applied |
| OEM layout | Tuya layout | `End error: 9` (`UPDATE_ERROR_ACTIVATE`: no `RBL` at the running download offset), old build stays |
| OEM layout | OEM layout | applied |
Run it with a full backup first (`scripts/bk72xx_flash/readback.sh`), `sdk_silent: none` so the bootloader's lines
show, and each result checked against `build_info.json` over the API. A Tuya-layout build on an OEM-layout module
reads the RF calibration from 0x1E3000: copy the 0x1E0000 sector there for the test and erase it afterwards. The
Tuya OTA writes up to about 0x1EF000 and its own settings at 0x1E4000, so write those sectors back from the backup
and check with `partdiff.py`.
## RTL8710B Wi-Fi join diagnosis (issue #21742)
For a board that joins rarely: build `dev` with `sdk_silent: none`, log `wifi_get_last_error()` and
`rtw_join_status` after each failed `WiFi.begin()`, and dump the efuse map once (see the AmebaZ section in
`boards.md`). The driver log tells the stages apart: `start auth to` with nothing after it is the AP not answering,
`auth success, start assoc` then `association success` then error 3 is a failed 4-way handshake, and
`sta recv deauth reason code(6)` is the AP dropping the session mid-handshake. Tally per BSSID: if the share of
auth frames answered falls with distance while receive stays strong, the board's transmit path is weak.
A/B a value one boot at a time so time of day and AP state hit both sides equally. Keep the trial counter in a
`restore_value` global, set the value in an `on_boot` lambda at priority 600 (before Wi-Fi starts), and reboot
after a fixed window:
```yaml
esphome:
on_boot:
priority: 600
then:
- lambda: |-
// value under test, read by the code being measured
wifi::diag_value = (id(trial_idx) % 2) ? 0x3A : 0;
ESP_LOGW("trial", "TRIAL %d start value=0x%02X", id(trial_idx), wifi::diag_value);
- delay: 75s
- lambda: |-
bool ok = wifi::global_wifi_component->is_connected();
ESP_LOGW("trial", "TRIAL %d value=0x%02X result=%s", id(trial_idx), wifi::diag_value,
ok ? "JOINED" : "FAILED");
id(trial_idx)++;
App.safe_reboot();
```
Turn off `api:` and `wifi:` `reboot_timeout`, and leave out `ap:` unless the AP is the variable under test.
Do not cycle trials with `wifi.disable` and `wifi.enable`: doing that while connected reset the RTL8710B on the next
scan. Results from 2026-10-27, on one BW12 with blank efuse calibration: crystal trim `0x20`, `0x30` and `0x38`
all joined 20 of 20 trials, and boosted TX power indexes changed nothing against an AP that answered 0 of 43
auth frames.
Known LibreTiny behaviour seen here: after a join with a BSSID and channel, LibreTiny's partial-scan reset leaves
the next SSID-only join with nothing to scan (`RTW_NONE_NETWORK` in about 18 ms). That is a firmware bug, not
the board; passing channel 0 avoids it.
## Pulse counting and edge interrupts on LibreTiny (esphome#22391, #22400, #22403, #22404, 2026-11-06 to 11-07)
`scripts/libretiny_edges/`:
- `pulse-bk.yaml` + `pulsegen.hpp` (BK7238, generator pins `GEN_PIN_A`/`GEN_PIN_B` jumpered to the counted pins) and
`pulse-rtl.yaml` + the same header (BW15, mbed `pwmout` on `GEN_PIN_NAME`): one pulse shape per 35 s stage, from
BL0937-like 38 µs pulses at 1 kHz up to 50 ms, counted by `pulse_counter` with `inverted` and filters as
substitutions; `meas` lines give count per stage. Found: Beken counted half of BL0937 pulses (readings halve, so a
calibration note was needed), RTL8720C counted none, RTL8710B none with inverted pins, LN882H counted correctly.
- `bw12-base.yaml` with two pin layouts (`common-bw12.yaml`, `common-bw12-b.yaml`) + `bwdbg.hpp` (BW12, RTL8710B): edges
without wiring by toggling the pad pull on floating pins (`PAD_PullCtrl`), `-Wl,--wrap=digitalRead` to count ISR
versus task reads, and a pin/IRQ register dump; `summ.py` summarizes. A jumper IO12 to IO13 on the jig gave a real
driven edge.
- `common-bw12-time.yaml`: `millis()`/`micros()` every 5 s against host time, a DWT cycle bench per call (also under
`InterruptLock`), and a mask ROM dump of `SYSTIMER_TickGet`.
Results: #22400 https://github.com/esphome/esphome/pull/22400#issuecomment-6712520146, #22403
https://github.com/esphome/esphome/pull/22403#issuecomment-6712901242, #22404
https://github.com/esphome/esphome/pull/22404#issuecomment-6713095035.
@@ -0,0 +1,36 @@
# GPIO wakeup from auto light sleep (#21074)
`scripts/light_sleep_wake` needs no wiring. LEDC on `LEDC_USE_RC_FAST_CLK` with
`sleep_mode = LEDC_SLEEP_MODE_KEEP_ALIVE` keeps toggling GPIO10 at 2 Hz through light sleep, and an
`ESP32InternalGPIOPin` interrupt (`ANY_EDGE`) on the same pad timestamps every edge in an IRAM handler.
`wakeprobe::report()` logs every 30 s: edges seen and expected, the shortest and longest gap, how many edges came
more than 20 ms off the half period, and the SLEEP share from `esp_pm_dump_locks()` (needs `profiling: true`).
- Native ESP-IDF builds need `include_builtin_idf_components: [esp_driver_ledc]`.
- After `ledc_channel_config` call `gpio_input_enable(pin)`, never `gpio_set_direction(INPUT_OUTPUT)`: that routes the
pad back to a plain GPIO output, the wave stops and the pad reads a constant 1.
- The C3 shares one LEDC clock across timers, so a board whose own config drives LEDs through `ledc` needs a test
config without them. Logging over the API works with `logger: baud_rate: 0`.
- Read the reports over the API with `esphome logs ... --device <ip>` under `timeout`. Run PR, dev, then the PR
again. On a C3 bulb dev caught every edge but handled 8 of 845 more than 20 ms late
(up to 45 ms), the PR none; both slept 57 to 58% of the time.
## Peripheral power-down needs an external signal (ESP32-P4)
LEDC in `LEDC_SLEEP_MODE_KEEP_ALIVE` keeps the TOP power domain on, so the self-generated wave above never tests
`power_down_peripherals`. With that option the PR's ext1 path also hands an LP pin to the LP IO block, and an LEDC
wave on the same pad no longer reaches the input (the pad reads 0).
- `wake-stim.yaml` puts a 2 Hz square wave on GPIO25 of a classic ESP32 (LEDC on REF_TICK). Wire it to GPIO4 of the
board under test; boards on the same USB host share ground.
- `wake_detail.hpp` with `wake-detail-esp32p4.yaml` (substitution `power_down`): GPIO4 is the any-edge interrupt pin
and GPIO22, jumpered to it, is read as the true level. The first 40 s hold a `NO_LIGHT_SLEEP` lock, then sleep is
allowed. Each report counts interrupt calls, real edges (the GPIO22 level changed) and calls with no change, per
phase, and two windows (from 9.5 s and from 69.5 s) log every call with its gap and both levels. A light-sleep
exit callback counts wakes when `CONFIG_PM_LIGHT_SLEEP_CALLBACKS` is set.
- Waveshare ESP32-P4-WIFI6 (pre-rev3 silicon, CH343 console): `engineering_sample: true`, `flash_size: 32MB` and
`logger: hardware_uart: UART0`, or the app logs nothing on that port. No Wi-Fi needed. GPIO4 is the left header,
12th pin from the USB end; GPIO22 the right header, 9th.
- Result on #21074 at 2ef417bd81 with peripherals powered down: the PR caught all 644 real edges (dev 66) but added
4642 calls while the line was high, one per sleep cycle (about 16 ms), with the level read correctly. Peripherals
on: both builds caught every edge. https://github.com/esphome/esphome/pull/21074#issuecomment-6503939850
@@ -0,0 +1,31 @@
# Manual IP and web handler flows (#21708, #21709, #21711, #21712)
## Station manual IP (`scripts/manual_ip/run-sta.sh`)
Set `static_ip` to the board's own DHCP lease so nothing collides, and DNS2 to a server DHCP does not hand out
(9.9.9.9), so the dump and `dns_getserver()` prove the manual values are in use. A missing SSID with its own
`manual_ip` comes first, so the real network is a later table row and inherits the top level `manual_ip`. The probe
logs both DNS servers and resolves a name every 20 s; `ipaddr_ntoa` uses one static buffer, so it uses
`ipaddr_ntoa_r` to log two addresses in one line. PASS needs the address, both DNS servers, a lookup and a ping.
## AP manual IP (`scripts/manual_ip/run-ap.sh`)
The DUT has only a missing SSID, `ap_timeout: 10s` and `captive_portal:`, so its AP comes up on 10.42.7.1; the other
board joins as a client and fetches `http://10.42.7.1/` every 10 s. A D1 mini AP drops some client requests on dev
too, so compare failure counts with a dev run and do not expect zero.
## Ethernet manual IP
Same idea on an Ethernet board: its lease as `manual_ip` plus an outside DNS2 and the DNS probe from
`scripts/manual_ip/dnsprobe.hpp`; restart it over the API and check again.
## Web handlers (`scripts/web_handlers/run.sh`)
`webbase.yaml` adds a GET /probe handler from `on_boot` at priority -100 (the shape of an external component adding
a handler late) and takes a variant: `ws` (web_server), `cp` (captive_portal only, started on the station network by
a button calling `global_captive_portal->start()`) or `prom` (captive_portal and prometheus). The runner fetches
/probe and /, then uploads the same image through web OTA (`curl -F update=@<bin> http://ip/update`; ESP-IDF
`build/<name>.bin`, not `firmware.ota.bin`; ESP8266 `.pioenvs/<name>/firmware.bin`) and checks over the API that the
board is back on that build. On dev too, prometheus GET / gets no answer on ESP32 and 404 on ESP8266.
A background `(cd dir && cmd) &` does not change the parent shell's directory: give capture scripts absolute paths.
@@ -0,0 +1,17 @@
# MCP23017 IOCON and interrupt line (#22232)
Bench: an ESP8266 (Feather HUZZAH) with an MCP23017 breakout at 0x27, SDA GPIO4, SCL GPIO5, INTA to GPIO14, INTB to
GPIO13, PA0 jumpered to PB0 and PA1 to PB1, no extra pull-ups (the breakout's were enough at 100 kHz). The esptool
reset keeps the expander powered, so a reflash shows whether register bits from the previous build survive.
`scripts/mcp23017/`: the `mcptest` external component (`ext/mcptest`) reads IOCON at 0x0A and 0x0B 6 s after boot,
toggles PA0 and PA1 20 times, samples the INT levels right after and 200 ms after each write, counts INTB edges in
an interrupt, checks the PB binary sensors (`SUMMARY` line), then checks whether IOCONB aliases IOCONA
(`IOCONB_TEST`). `int.yaml` wires INTA to the hub's `interrupt_pin`, open drain or push-pull by the `open_drain`
substitution (variants `int-od` and `int-pp`); `no-int.yaml` has no interrupt pin and counts INTA edges in
`mcptest`. `run.sh VARIANT LABEL` (env `TREE`, `PORT`, `SC`, `PY`) builds, flashes and captures 14 s.
- In BANK=0, 0x0A and 0x0B are the same register: a write to one reads back from the other.
- A push-pull INT line works without a pull-up. Open drain with no pull-up stays low after the first interrupt
(ESP8266 INPUT), so the hub loop never stops and the inputs still update by polling.
- Builds before #22232 leave ODR and MIRROR set across reflashes.
@@ -0,0 +1,33 @@
# MCP2518FD CAN FD over SPI (esphome#21951, 2026-11-07)
Bench: ESP32-PICO-D4 (`board: pico32`, ESP-IDF) wired to an MCP2518FD module with an ATA6563 transceiver: SCK GPIO25,
SDI GPIO26, SDO GPIO27, nCS GPIO32, INT GPIO33, module 3V3/GND, CLK pin to GPIO34. No second CAN node, so everything
runs in `LOOPBACK`, or with the controller forced into internal loopback behind the driver.
- **Read the crystal.** The module's can is marked `H20.00` (20 MHz) even though listings say "8 Mbps"; the PR default
is `40MHZ`. Loopback never leaves the chip, so a wrong `clock:` still passes at half the configured rates. Measure it:
`pulse_counter` on the CLK pin with `internal_filter: 0us` reads SYSCLK/10 (2 MHz) at reset. Older heads wrote 0 to
`OSC` byte 0, which set `CLKODIV` to /1 (CLKO = 20 MHz, still countable by PCNT).
- `scripts/mcp251xfd/canlb.hpp`: stream checker (one frame in flight, burst of 8 every 64th; standard/extended IDs, FD
with and without BRS, every 4th classic, DLC 0 to 15, sequence in the payload; starts 5 s after boot so the
`dump_config` stall cannot trip the 50 ms in-flight timeout), a queue checker for bursts, RTR and TX objects, raw SPI
register access on the driver's own `SPIDevice` (`rd32`, `wr8`, `chip_reset` = `0x00 0x00`), forced modes
(`force_mode` writes `CON+3`; `force_via_config` goes through Configuration so the chip accepts it), `watch_start`
(polls `CON` every 2 ms until `OPMOD` and `REQOP` equal the target, logs the time), `txcheck` (reads each TX object
back from chip RAM after a frame full of 0xAA, padding included), `burst` (sends N frames in one call so the loop
cannot drain RX: FD depth 20, CLASSIC depth 32 + one overrun) and a PCNT unit on the CS pin, which counts SPI
transactions while the pin stays an output (`cs_mark`/`cs_report`: idle cost per second).
- `common-can.yaml` is the package; each config adds its `canbus` block and a `phases` script: `can-loopback.yaml`
(stream, RTR, TX objects, idle cost, chip reset, two refused `REQOP` writes), `can-classic.yaml` (CLASSIC with a
forced internal-loopback window: force once to trigger the driver's recovery, force again right after it restores,
then burst 48 and RTR inside the next 5 s check interval), `can-normal-busoff.yaml` (no node acknowledges, so the
controller cycles error passive/bus-off; each forced fault and reset waits for TREC `TXBO`).
`run.sh CONFIG LABEL [SECONDS]` builds, flashes, captures and prints the summary lines.
- Gotchas found: a refused `REQOP` (LISTENONLY or Restricted from LOOPBACK) leaves `OPMOD` unchanged and stalls TX;
the chip refuses LOOPBACK to CLASSIC directly (go through Configuration); after a chip reset TREC reads `0x00200000`
(`TXBO` set in Configuration mode), so a driver that skips recovery on bus-off never recovers a reset during an error
state. Neither a reset nor a refused request sets an interrupt flag, so detection waits for the periodic check.
Results: https://github.com/esphome/esphome/pull/21951#issuecomment-6715452893 (first run),
#issuecomment-6716105991 (TX objects, idle cost), #issuecomment-6716741859 (reset during bus-off bug),
#issuecomment-6716957543 (fixed).
@@ -0,0 +1,18 @@
# mDNS responder (#22267)
`scripts/mdns/mdns-common.yaml` (a package) adds a `_hwtest._tcp` service with a short and a 62 character TXT
value and logs free, minimum and largest free heap every 10 s; `mdns-wifi.yaml` adds Wi-Fi from `secrets.yaml` and
switches it off and on every 120 s so the responder restarts. The host's multicast often does not reach the boards
(a host with several network interfaces, or a mesh Wi-Fi network), so the scripts send legacy unicast queries (source port not 5353)
straight to the device's port 5353 with python-zeroconf packets:
- `mq.py IP HOSTNAME dump` asks for the `_esphomelib` and `_hwtest` PTR records, the A record and the SRV and TXT
records of both services, prints the answers and exits 1 when one is missing. Diff the dump between builds.
- `mq.py IP HOSTNAME hammer N [TIMEOUT]` sends N questions back to back (0.5 s timeout each) and counts misses per
question. It exits 1 when nothing was answered, or when more than `MAX_MISS_PCT` percent (env) were missed.
- `mq.py IP HOSTNAME lat N` sends A queries 300 ms apart and prints the median, p90, p99 and maximum latency and how
many answers came after 1.5 s or never.
Judge responder timing on an Ethernet board: on a crowded Wi-Fi network back-to-back query loss swung between 0 and
14% between builds for no code reason. Run the hammer across a Wi-Fi off and on cycle too, and compare the heap
lines before and after many cycles.
@@ -0,0 +1,26 @@
# microphone_http_stream (#21593)
`microphone_http_stream` serves raw microphone PCM at `/audio/microphones/<stream_id>` (and a listing at
`/audio/microphones`) on the web server. `templates/microphone_http_stream/`:
- `mh-base.yaml`: Waveshare ESP32-S3-ETH on PoE (logs over the API on Ethernet, the native USB port is the USB host
port), encrypted API, `web_server` on 80 with a dummy sensor and switch, mic start and stop buttons and a line
every 5 s with the mic state and internal and PSRAM heap. Any ESP32-S3 with PSRAM works for `mh-fake.yaml`.
- `mh-fake.yaml`: the stream fed by `ext/fakemic`, a test microphone that publishes 32 ms chunks of a 16-bit counter
(one count per sample) from its own task, so a reader can check every sample. Its "Mic stall" button stops the
chunks for 6 s.
- `mh-usb.yaml`: the stream fed by a USB microphone on `usb_audio` (48 kHz, 16-bit stereo, channel 0 streamed).
Copy the directory next to a `secrets.yaml` with `api_key`, flash, then run
`mhclient.py HOST STREAM_ID [--clients N] [--seconds S] [--pattern] [--stall K] [--stagger S] [--expect-reject R]`.
Each reader prints its HTTP status, format headers, sample count and rate, the longest gap between reads and, with
`--pattern`, every break in the counter. `--stall K` makes reader K stop reading after 2 s while keeping its socket
open, then checks that the device closed it. `--expect-reject R` marks the last R readers as past `max_clients`;
they must get a non-200 status or a close or reset before any samples, and a timeout or a served reader fails.
The script exits 1 when the listing is not a 200 naming the stream, the stalled reader was not dropped, a reader past
the limit was not refused, or any other reader got no 200, no samples, an early EOF, a read error or a broken counter.
Cases worth running on each head: one reader with `--pattern` (rate must match the sample rate, 0 breaks), the
`max_clients` limit plus one with `--expect-reject 1` (note what the extra one gets; the others must be unaffected), a stalled reader among
three (the device should drop the stalled one only), the "Mic stall" button during a read (a gap of about 6 s, then
the counter continues), mic start and stop while readers are attached, and the heap line after many connect and
disconnect cycles. On the USB microphone, check the rate and `rms` instead of the pattern with `--rate 16000 --min-rms 1`, and add `--max-gap-ms` to fail on long gaps; these set the exit code.
@@ -0,0 +1,33 @@
# modem
## PPPoS modem component with no modem attached (#6721)
A boot check on a classic ESP32 devkit with the modem UART pins left open. `templates/modem/modem-common.yaml` is
the package (`api:`, debug `loop_time` and `free` sensors, uptime, buttons for `modem.enable`, `modem.disable` and
`modem.send_at`, a 20 s alive log); `modemo.yaml` is the modem only build with `reboot_timeout: 2min` and the
component test config's `on_boot` block at `priority: -100` (earlier, `modem.send_at` runs before the modem's
`setup()` and logs `Modem DCE not ready`); `modemw.yaml` adds `wifi:` with `reboot_timeout: 0s` so the API stays
reachable. Expect `Modem not responding at 115200 baud` once per pass with each pass blocking the loop about 1 s,
the reboot at the configured timeout (`SW_CPU_RESET`), and a `modem.disable` with no modem answering to block
the loop about 37 s (`Failed to leave data mode`, then the radio off timeout) without tripping the 30 s watchdog
floor because the component feeds it in between. Drive the Wi-Fi build with
`scripts/modem/mdrive.py HOST LOGFILE list press:NAME wait:S` (plaintext API; prints sensor and text sensor
states, saves the device log). Before 8cc8df4a1f a modem only config with `api:` failed validation because a
config dependent `AUTO_LOAD` added `network` after the dependency check; an explicit `network:` block was the
workaround. Results: https://github.com/esphome/esphome/pull/6721#issuecomment-6347395159 and
https://github.com/esphome/esphome/pull/6721#issuecomment-6348520416
## SIM7080G on an M5Stack Atom Lite (#6721)
The M5 Unit CatM (SIM7080G) on the Atom Lite's Grove port: RX GPIO32, TX GPIO26, `model: SIM7080`.
`templates/modem/modem-passthrough.yaml` is a raw UART passthrough with both directions logged, and
`scripts/modem/mdrive.py HOST LOGFILE text:AT+CPIN? wait:5 ...` sends AT commands through it and saves the
replies, to check what the module does with no component in the way.
- With no SIM, the component sets the `SIM not ready` warning and polls `AT+CPIN?` once a second.
- With a SIM that has no data plan, `AT+CGATT=1` blocks the AT channel for about 141 s and then answers `ERROR`.
The module answers nothing meanwhile, so the component logs `Network attachment request refused` and then `Modem not
responding`, and loops every 2 minutes 22 seconds.
- `AT+CFUN=1,1` leaves the module silent for about 2.5 minutes. It comes back without a power cycle.
- A module left in CMUX (`AT+CMUX=0`, then an ESP restart) recovered 10 of 10 times (`Autodetect mode: 5`).
- PPP and a warm reboot during PPP need a SIM with a working data plan.
@@ -51,3 +51,91 @@ context rejects a CA that lacks a `keyUsage` extension (`CA cert does not includ
on the device accepts it, which is why the test CA carries `basicConstraints` and `keyUsage`. A broker kill shows as one
`Last esp-tls error: 0x8008` on every build, TLS or not. Results:
https://github.com/esphome/esphome/pull/14318#issuecomment-6311863827
## MAC suffix topics (#9315)
`scripts/mqtt/mac_suffix/run.sh X BUILD` compiles (from `TREE`) and flashes variant X (a defaults, b custom `topic_prefix`, c explicit topics,
d no suffix) of BUILD (`pr` or `base`) on an ESP32 and a D1 mini, records the broker with
`mosquitto_sub -F '@H:@M:@S r=%r %t %p'`, presses Restart through the `cmd_t` found in the discovery payloads, then
holds both chips in the bootloader for 40 s (`esptool --after no-reset chip-id`) so the broker sends the will; a
plain reset only gives "session taken over" and no will. The two boards' configs live in separate folders because
variants a to c share a `name` (the MAC suffix tells them apart), and the same name in one folder shares a build
directory. A D1 mini CH340 can garble the serial capture after an RTS reset, so read its topics from the broker.
`esphome logs --device MQTT` on a current tree needs paho-mqtt 2.x; an older venv with 1.6.1 fails, so use a venv
made from the tree's requirements.txt. A Restart press never delivered the shutdown message on any build tested.
## Shutdown message delivery (#21566)
`scripts/mqtt/shutdown/`: `sd-common.yaml` sets birth/will/shutdown payloads `hello`/`dead`/`bye` so the shutdown
message and the will cannot be confused, and takes `restart`, `disable` and `sleep` on `<name>/cmd`.
`sdrun.sh V N` (`TREE`, `P32` and `P66` in the environment) compiles and flashes the ESP32 and D1 mini, restarts each N times, disables MQTT once and deep sleeps the ESP32 three
times (never the D1 mini: without the GPIO16 to RST wire it never wakes); `match.py OUT` counts a `bye` within 5 s of
each command. Dev: ESP32 0/10 restarts, D1 mini 1 to 3/10. Put the broker address (`broker_ip`) in the secrets.yaml next to
the included package, and quote `"${name}/status"` inside flow mappings.
Results: https://github.com/esphome/esphome/pull/21566#issuecomment-6381448061
Retest at c9cf3c2813 (shutdown message sent at QoS 1, teardown waits for the broker's ack): 10/10 restarts on both
boards and 3/3 deep sleeps, against 1/10, 3/10 and 0/3 on base. An earlier 100 ms hold gave 3/10, 6/10 and 2/3, so
judge a delivery fix by counts over 10 or more restarts, never one or two presses.
Results: https://github.com/esphome/esphome/pull/21566#issuecomment-6382076613
## MQTT discovery against a throwaway Home Assistant (#21956)
Run a Home Assistant dev install with its own config directory (`hass -c <scratch>/ha`) holding `http:
server_port: 8129`, `api:`, `config:`, `onboarding:` and MQTT debug logging, plus a local mosquitto on 1883. In
`scripts/mqtt/ha_discovery/`, `onboard.py DIR` creates a generated test user (the credentials stay in DIR) and a
token, `flow.py TOKENFILE` adds the MQTT entry (the `protocol` and `other_settings: {set_client_cert: false,
set_ca_cert: "off", transport: tcp}` fields are required), and `hadrive.py TOKENFILE NAME` reads the device's time,
date and datetime entities, calls `set_value` on each and reads again; it exits 1 on a missing entity, a failed
call or a state that does not read back the value that was set. The three share the REST calls in `ha_rest.py`. `dt-common.yaml` is the device side (classic ESP32, three
template datetime entities, `${broker}`).
- The HA dev branch keeps a YAML port change as a pending HTTP config and reverts it after 5 minutes. Before
starting HA again, set `data.stable.server_port` to 8129 and `data.pending` to null in `.storage/http`.
- ESPHome MQTT unique ids are name based: clear the previous build's retained discovery (`mosquitto_pub -r -n` on
each config topic) and expect the entity ids of the first build.
## mqtt.disable and broker drops on ESP-IDF (#22230)
`scripts/mqtt/disable/disable-common.yaml` disables MQTT for 30 s every 60 s (every third cycle starts with fast
toggles, a disable while connecting), publishes a tick every 2 s and logs `EV CONNECT`, `EV DISCONNECT` and
`EV MSG`. `run.sh LABEL outage|drop SECONDS` (env `TREE`, `PORT`, `SC`, `PY`, `BROKER`, `BROKER_PORT`) starts a
private mosquitto so it can stop and start it, flashes and captures:
- `outage` publishes to `md<LABEL>/in` every second and stops the broker inside a disabled window; `analyze.py`
prints per window the device connects, inbound messages, broker connects, ticks that reached the broker and the
time from enable to connect, and fails the run on a device connect or an inbound message inside a window.
- `drop` stops the broker 6 s after every enable for 15 s. This is the case that found a watchdog reset: calling
`esp_mqtt_client_stop()` while the esp-mqtt task sleeps in WAIT_RECONNECT blocks the caller for up to
reconnect_timeout/2 (5 s), 6 of 6 task watchdog resets on an S3 and a C6.
Both modes also fail when the device never connects or the broker was never stopped, and `outage` when no inbound
message arrived.
Use the host's current address for `BROKER`: a stale one gives `esp-tls select() timeout`. Before #22230 the client
reconnects on its own during disable (14 of 16 windows). Results:
https://github.com/esphome/esphome/pull/22230#issuecomment-6614854671
## Retained message burst at connect (#22200)
`scripts/mqtt/retained_burst/`: `burst-topics.yaml` (a package) subscribes 25 `on_message` topics plus 15 switch
command topics, `burst-common.yaml` logs the counts 15 s after the connect, and `seed.sh BROKER_PORT` leaves all 40 retained on a local mosquitto (`seed.sh BROKER_PORT clear` removes them).
`run.sh LABEL BOOTS` (env `TREE`, `PORT`, `SC`, `PY`, `BROKER`, `BROKER_PORT`, default board an ESP32-C3) resets the
board BOOTS times and prints `RESULT got=x/25 switches_on=y/15` and any `Dropped` lines per boot; it passes only with
40 of 40 on every boot. Before #22200 the C3 dropped exactly 49 events (80 events minus 31 slots): the main loop
drains nothing during the burst.
## Forced reconnect mid-burst (#22248)
`scripts/mqtt/reconnect/`: `kick-common.yaml` includes the same `burst-topics.yaml` and counts per connection with
a `mode: restart` script on `on_connect`, logging `RESULT` 6 s after each connect. It and `run.sh` use files from
`../retained_burst/`, so copy the whole `mqtt/` directory to the scratch directory. `kick.py BROKER_LOG BROKER_PORT
PREFIX CYCLES` tails the broker log and takes over the device's client id 0 to 400 ms after every other connect
(mosquitto drops it with "session taken over"), then leaves the next connection alone for 8 s and ends it. It
starts by kicking the connection that is already up and skips its own connections (from 127.0.0.1 with the same id).
`run.sh LABEL CYCLES` (env as above plus `BROKER_LOG`) seeds, flashes, waits for the first connect and runs the
kicker. The device counts its first connection as CONN 1, so the kicked ones are the even CONN numbers and the
left-alone ones the odd numbers from 3 up; kick.py prints each left-alone connection with its time and CONN number,
and `judge.py` pairs those with the RESULT lines by time and passes when every one has `got=25/25`. A reset after
the first connect fails the run. Retained switch commands fire `on_turn_on` only on the first connection, so
`switches_on` is not judged.
@@ -4,3 +4,111 @@
Test config: the two LEDs as inverted GPIO outputs, the button as an inverted pull-up binary sensor, a
10 s alive interval. To retest the nrfutil download path, move the cached `sdk-nrf/nrfutil` aside first.
## ItsyBitsy over UF2 when serial DFU fails (#20481)
Serial DFU to the ItsyBitsy nRF52840 fails about half the time ("Target is not in DFU mode"). Reliable path: a
1200 baud touch on the app port, wait for the `ITSY840BOOT` volume, convert `zephyr/zephyr.hex` with the SDK's
`zephyr/scripts/build/uf2conv.py -f 0xADA52840 -c` and copy the .uf2 to the volume (the copy ends in an I/O error
when the board reboots, which is normal). macOS BLE scans give UUIDs, not MAC addresses.
## zephyr_ble_server connection slot under reordered callbacks (#21723)
`scripts/zephyr_ble_server`: the ItsyBitsy runs `server.yaml` (one connection slot) and an ESP32 runs `client.yaml`,
which connects, drops the link 300 ms later and reconnects as soon as the server advertises again. `run.sh NAME 90`
flashes the UF2, counts the server's `Connected`/`Disconnected`/`Advertising` lines and the client's connects,
and ends with PASS (more than one server connection) or FAIL; an empty capture or a silent client fails the setup.
- The connect and disconnect callbacks each `defer()` to the main loop and the bug needs the disconnect handled
first, which cannot be forced over the air. Force it with a test-only patch on both trees: replace the connect
callback's `defer(...)` with `set_timeout(5000, ...)`. A leaked reference then shows as one connection and nothing
after it (the dump still lists the closed link); a fixed build keeps cycling (17 in 90 s on #21723).
- `dfu: true` makes the next flash a 1200 baud touch; an app built without it needs a double tap on RESET. The
Zephyr app enumerates as `2fe3:0100`, the bootloader as `239a:0051`, so find the board by its USB serial number.
- macOS is a poor central here: it keeps old bonds with a board (`Peer removed pairing information` on connect)
and clearing them is a system setting. Use an ESP32 `ble_client`; the server's random static address comes from
its own log.
## SMP over UDP OTA over Thread, with reset faults (#17245, #21910)
Setup: a Thread border router (see openthread.md) whose OMR prefix the laptop routes to, and an ItsyBitsy nRF52840
(the Adafruit bootloader carries the MCUboot layout; the nRF52840 Dongle's Open DFU bootloader is rejected for
`zephyr_mcumgr` OTA). `scripts/smp_udp_ota/smp-udp-ota-dut.yaml` joins the network with `force_dataset: true` and
logs its build label every 10 s. The first image goes in over UF2, every later one with
`ota.sh MODE YAML VER ADDR` to the device's OMR address. A full image (about 380 KB) takes about 130 s.
- An ESP Thread Border Router board running ESPHome `openthread: border_router: rcp:` works as the border router
(pins in `boards.md`). Give both it and the device a fixed dataset with `force_dataset: true`; each keeps an old
stored dataset otherwise. The host learns the route to the OMR /64 from the border router's RA; ping the
device's OMR address before the first upload.
- Install the smpclient version pinned in requirements. An older one fails with `unexpected keyword argument
'timeout_s'` and a missing `get_max_cbor_and_data_size`, which looks like a PR bug. Check the version with the
Python of the venv that runs ESPHome, not a system pip.
- The build holds two `zephyr.uf2` files. Copy `.pioenvs/<name>/zephyr/zephyr.uf2` (the app, from 0x26000 up,
about 827 KB). `mcuboot/zephyr/zephyr.uf2` (about 70 KB) lands on the app slot and the board stops enumerating;
double tap RESET and copy the right one.
- To log the device's addresses, format `otIp6GetUnicastAddresses()` by hand: `network::IPAddress::str_to()`
does not link on nRF52 (no `inet_ntop`).
- A new image refuses the next upload (`No free slot. Testing mode but not confirmed yet.`) until it confirms itself.
Wait about 4 minutes between uploads.
- An image that is already in the secondary slot is only marked and reset (a few seconds). Change `ver` for every
upload that has to transfer, and expect a host-only change to rebuild byte-identical firmware.
- `inject.py` faults the reset on the host: `lostreply` drops the reply of a reset the device got, `lostreq` drops the
first request, `lostreq2` drops every reset. The device resets after a short delay, so even after a lost reply the
client usually still finds the old image running with the new one pending and resends the reset to the old image,
which is safe. `lostreq2` must end in an error with the old image still running. `ota.sh` ends with `check.sh`,
which prints PASS or FAIL for the mode, and `inject.py` fails the run when no reset was faulted.
- Without `libc_nano` the config overflows the 396,800 B slot by about 4 KB with Zephyr's TCP stack linked (about
11 KB of it, unused by SMP over UDP). With TCP left out (#21910) it fits with the full libc. A config with `api:`
keeps TCP.
- **The real OTA size limit is lower than the link limit.** The nRF Connect SDK partition manager overrides the device
tree, so read `partitions.yml` in the build directory, not `zephyr.dts`. On the ItsyBitsy layout `mcuboot_primary` is
0x61000 (97 sectors of 4 KB, 0x2f000 to 0x90000) and the app partition 0x60e00 (396,800 B); `mcuboot_secondary`
is 0x90000 to 0xf1000 and `settings_storage` 0xf2000 to 0xf4000. MCUboot swap using move keeps one sector for
its trailer and one for the move, so it only swaps in an image up to (97 - 1 - 1) × 4 KB = 389,120 B
(`app_max_sectors()` and `app_max_size()` in bootutil `swap_move.c`). An image between 389,120 and 396,800 B links,
uploads and the CLI reports `Successfully uploaded program`, but MCUboot never swaps it in: it stays pending in the
secondary slot, and every later upload and the SMP image erase fail with `NO_FREE_SLOT`. OTA stays stuck until
the slot is cleared over USB. Measured: 388,787 B swapped in, 392,155 B did not. `ota.sh` checks `app_update.bin`
against this limit with `mcuboot_slot.py limit` before it uploads, and afterwards asks the device which image runs
(`mcuboot_slot.py active`, by the image's SHA-256), since the CLI's exit code alone is not proof. That check passes once the
new image runs and says whether it has confirmed itself; add `--expect confirmed --wait 300` to prove it stays.
- With #17245 at 03d45d1aaa the build itself fails above the limit (`The application image is N bytes, but MCUboot
can only swap in L bytes on this board`), and after a confirmed reset the client waits for the device and logs
`runs the new image` or fails with `rebooted but did not swap in the new image`. After a dropped reset reply it still
ends with `The reboot of ... is not confirmed` without that wait, so `ota.sh`'s own image check is what proves those
runs.
- Clearing a stuck pending image: the SMP image erase is refused while the image is pending, and resets do not help.
Over USB, touch the app's CDC port at 1200 baud to enter the UF2 bootloader, then copy the 16-block UF2 from
`mcuboot_slot.py blank-uf2 BUILD OUT`, which writes 0xFF over the secondary slot's header page (`mcuboot_secondary`
in `partitions.yml`, 0x90000 on the ItsyBitsy). The bootloader resets on its own once the last block is written
(`cp` may report an I/O error then, which is normal), MCUboot finds no image header and clears the slot, and the
image states show slot 0 only.
- Over USB, the Adafruit UF2 bootloader only accepts blocks inside its app area (its `CURRENT.UF2` maps 0x1000 to
0xE9000). A UF2 with a block outside that area is never completed, and the bootloader then ignores every
later file, even a good app UF2, until a physical reset (double tap RESET or replug). Keep hand-made UF2 writes inside the app area.
- Results: https://github.com/esphome/esphome/pull/17245#issuecomment-6516152182,
https://github.com/esphome/esphome/pull/17245#issuecomment-6518292041 (swap limit),
https://github.com/esphome/esphome/pull/17245#issuecomment-6519010038 (retest)
## TCP stack only when a component uses TCP (#21910)
`scripts/nrf52_tcp_probe/tcp-probe.yaml` joins the test Thread network with no TCP user and opens a TCP and a UDP
socket twice from an interval. Build it per tree with `-s name NAME`, flash over UF2, capture the USB log, then
`check.sh BUILD CAPTURE`: it reads `CONFIG_NET_TCP` and the packet and buffer counts from
`.pioenvs/<name>/zephyr/zephyr/.config` and wants `tcp null udp ok` twice plus the `TCP stack not built` error exactly
once on a build without TCP, or `tcp ok udp ok` twice and no error with it. Add `api:` to see TCP and the larger pools
come back, and connect with `apicheck.py ADDR` over Thread. Measured on the ItsyBitsy: 12.9 KB less flash and 17.1 KB
less RAM without TCP (Zephyr's default pools, 16/16 instead of 24/48), and SMP over UDP OTA still works with them.
Results: https://github.com/esphome/esphome/pull/21910#issuecomment-6518287139
## sdk-nrf 3.4.1 on the ItsyBitsy (#22233, #22558)
- Set `framework: version: 3.4.1` and `board: adafruit_itsybitsy`.
- Trees before #22233 include `<nrf.h>` in `logger_zephyr.cpp`, which 3.4.1 no longer has on the include path, so
Adafruit bootloader boards fail to build (`nrf.h: No such file or directory`). To test such a tree on 3.4.1, change
that include to `<nrfx.h>` locally.
- On the new USB stack, `usb_cdc_acm` bulk echo ran at about half the legacy rate (67 against 121 KiB/s for 64 KiB),
and `flush()` returned `3`. The logger, DTR, line coding and the 1200 baud touch worked.
- The zephyr_ble_server flow above, on 3.4.1: before #22558 advertising failed to restart after the first disconnect
(`Advertising failed to start (rc -12)`); with it the server took 39 connections in 120 s.
@@ -0,0 +1,32 @@
# one_wire
## ESP8266 interrupt locks under Wi-Fi load (#22452, #22453)
Code that holds interrupts off for long on the ESP8266 starves the Wi-Fi interrupt handler (reset signatures in
`reference/boards.md`). `scripts/esp8266_lock_load/` is the shared harness for any component that takes such locks:
- `base.yaml` is the shared DUT base: reset reason and info logged at boot, `power_save_mode: none`, `web_server` for
the HTTP load, `api: reboot_timeout: 0s`.
- `one-wire.yaml` reads a DS18B20 on GPIO4 every 2 s and runs a bus search every `search_interval`; `busy_control:
"true"` replaces the search with the same busy time (13 ms) and no 1-Wire traffic.
- `ir-one-pin.yaml` is the remote_transmitter case (see [remote_base.md](remote_base.md)).
- `wifi_load.py HOST SECONDS PPS` sends 512 byte UDP datagrams to port 9 at a fixed rate plus back to back GETs of
the web_server page.
- `run.sh` builds and flashes a DUT, captures the serial log, runs the load and prints every reset reason (usage in its
header). A clean run has one BOOT line.
Make one file per build with `substitutions: {name: ...}` and `packages: {t: !include one-wire.yaml}`, with
`secrets.yaml` next to it, and give dev and the PR different names.
Notes:
- Run each build for 45 minutes at 500 packets per second. Shorter or lighter runs often show no reset on either
build: one search every 5 s survived on both.
- The busy-wait control separates the lock from the CPU time. On #22452 it had no Hardware Watchdog in 45 minutes,
while the PR still had 2 (dev had 6), so the remaining resets came from the slot locks. All three builds had one
Software Watchdog, which is the busy time plus the load.
- A search with no sensor answering is much shorter than one with a sensor (about 190 slot locks per search with
one DS18B20), so test with a real sensor on the bus.
- A stale `secrets.yaml` gives `No networks found` and the run has no IP to load.
- The 1-Wire bus code is shared by every platform, so check a lock change on an ESP32 too: the same DS18B20 search
config on ESP-IDF with `bluetooth_proxy` scanning and the same load gave 0 resets and no missed searches on dev and
on #22452.
@@ -18,3 +18,47 @@ ends with `Error downloading image: -28679` (EAGAIN) after the transport timeout
and cut chunked cases and every later update logs `Image already being updated.`, so a baseline run needs
a reboot per group, the driver's 45 s cap per case, and an outer `timeout` that covers the whole list.
A case expected as `any` passes on `finished` or `error`, never on no outcome.
## PNG decode time limit (#21364, #21391)
The same server also serves any plain file in its www directory by name (404 for a missing file or a path
outside it). `scripts/online_image/make_big_pngs.py <www>` (after `make_images.py`) writes a 320x240 gradient (618 B, also as `ok.png` so the misbehaviour cases carry a
real multi-row PNG), a flat 640x480 PNG (1.6 KB for 307200 pixels), 231 KB of noise and a JPEG. The device is
`templates/online-image-dut.yaml` with `type: BINARY` on the main image (a 640x480 RGB565 does not fit a classic
ESP32 without PSRAM) and `debug` loop time and heap sensors at 2 s; the 256 byte buffer image stays RGB565, so do
not point big images at it (out of memory on every build). `scripts/online_image/pngdrive.py HOST KEY LOGFILE
BASEURL LABEL` (default case set `png`; `CASES=short` for the six case list) calls the actions, waits for the
finished or error line and reports the case time, the highest loop time and the lowest free heap. The per pass `online_image.image took a long time` values come from
the serial capture and only print on a new maximum, so read the first fetch of each image. The pass that decodes a
whole buffer is the decoder cost (before the change 510 ms gradient and 1980 ms flat, after it 77 and 140 ms); for
the noisy PNG the longest pass is the blocking 64 KB read on both builds (0.7 to 3.7 s). Alternate PR, base, PR
for a timing claim.
For a replacement PR, compare against the parent PR head, not dev. `CASES=noise` fetches the noisy PNG three times
(run it right after a boot, alternating builds); `CASES=end` runs flat and gradient as plain, `noclen_`, `chunked_`,
`half_` and `stall_` through the prefixed server cases below.
## JPEG decoded as it downloads (#21342)
`oi_server.py` serves prefixed cases on any file: `half_<file>` (first half, matching Content-Length),
`noclen_<file>` (close delimited), `chunked_<file>` (1 KB chunks), `stall_<file>` (a third, then the socket held
40 s). `scripts/online_image/make_jpegs.py <www>` writes noise, gradient, 12 KB EXIF and progressive JPEGs. Device:
`templates/online-image-dut.yaml` with `type: BINARY` on both entities and `buffer_size: 4096` on the second;
`CASES=jpeg scripts/online_image/pngdrive.py HOST KEY LOGFILE BASEURL LABEL` runs the JPEG list; every case set
prints case time, highest loop time and lowest free heap. Before the change a JPEG needs a contiguous block of its file size (233 KB never
allocates on a classic ESP32, and after a few failures 69 KB does not either until a reboot); after it the 64 KB
buffer stays 64 KB and 4 KB is enough for a baseline JPEG. The first pass still reads up to `buffer_size` in one
blocking read, so the 64 KB entity blocks 1.7 to 4 s and the 4 KB one 272 ms at most; judge the decode budget on
the small buffer entity. Already on dev: a JPEG without Content-Length (plain or chunked) ends with `Could not open
image for decoding: 4` then `Invalid type`.
## BMP and QOI decode time limit (#21392)
Use a wired board (`templates/online-image-s3-eth-dut.yaml`: Waveshare ESP32-S3-ETH, W5500, PSRAM, `type: RGB565`)
rather than a classic ESP32 on Wi-Fi: with a weak link the reads block up to the 4.5 s timeout and the per case loop
numbers are noise. `scripts/online_image/make_bmp_qoi.py <www>` writes 1 bit BMP 640x480, 8 and 24 bit BMP 320x240,
QOI 640x480 one colour (all runs) and checkerboard, QOI 320x240 gradient and noise. `pngdrive.py` with
`CASES=bmpqoi` runs each as plain, `noclen_`, `chunked_`, `half_` plus stalls. The `loop_time` sensor sits at 75
to 160 ms even for a 16x16 image, so for decode passes use `scripts/online_image/firstpass.py HOST KEY BASEURL
LABEL IMAGE...`: it reboots the device before each fetch and reports that fetch's `took a long time` maximum. A
`half_` file with matching Content-Length reports `download finished` with "not caching it" on dev too.
@@ -6,3 +6,101 @@ All overlap orders (a web upload during a native OTA and the reverse) must be re
after an interrupted web upload waits about 14 s (ESP8266) or 7 s (ESP-IDF) for disconnect detection. A
refused web client on `web_server_idf` never gets the busy reply; the connection drops at reboot.
`pkill -f "a\|b"` does not alternate; use two pkills.
## OTA upload timing A/B (#21639, #21641, #21642)
`scripts/ota_timing/run.sh N PHASE...` times native OTA uploads per build (phase) and per board. It builds every
board from `TREE_<PHASE>`, does one warm-up upload per board (it installs the phase's build and is left out of the
results), checks over the API that each board reports the compile time from that build's `build_info.json`
(`apiinfo.py`), then runs N rounds of every mode on every board. Boards are interleaved per round so they share the
same air; put a nearby ESP32 on the same Wi-Fi in `BOARDS` as a network control. `summ.py` prints medians: never
judge from single runs, slow Wi-Fi gave 33 to 235 s for the same upload in one night.
- `otatime.py` drives `espota2.run_ota()` from the tree on `PYTHONPATH` and prints one `RESULT` line with the
`Update took` split, the compressed size, whether UDP was used and the time at each tenth of the upload.
An upload that leaves no `RESULT` line (board gone, script crash, timeout) is recorded as `rc: -1`, and `summ.py`
fails when a board and mode has fewer than N rows or an upload time cannot be read.
- Boards with an `api: encryption: key` also encrypt OTA: set `API_KEY` for `otatime.py` and `apiinfo.py`. LibreTiny
LN882H listens for OTA on 8820, not 8892; BK72xx and RTL87xx use 8892.
- Raw mode leaves `CLIENT_FEATURE_SUPPORTS_DEFLATE` out of the client's offer. Never switch deflate off after the
offer (`if deflate and 0:`): once the device accepted deflate it expects a deflate stream.
- `-tcp` modes make `espota2._loss_reason` return None, so the client asks to stay on TCP. Clearing
`CLIENT_FEATURE_SUPPORTS_UDP` instead desyncs the handshake (`receiving UDP token response: timed out`).
- LibreTiny trimmed UF2 (#21642): the CLI sends what `esphome.components.libretiny.uf2.device_ota_uf2()` returns for
`firmware.uf2`; that module only exists with #21642, so on older trees the full UF2 goes out. Compare the sizes
in the `Compressed to` lines, and rely on the API compile time check to show that the trimmed image booted.
`receiving chunk result: Unknown error from ESP` is error 0xFF sent by the device, not a dropped connection. The
device sends it for a decode or flash write error, and also when no data arrives for 105 s, so on a weak link it
usually means a stall. A failure about 105 s after the last progress, or the same error on a raw upload, points to
the link; to be sure, capture the device log (`Inflate err`, `Inflate overrun`, `No data received`).
Results: https://github.com/esphome/esphome/issues/21639#issuecomment-6409628227 (dev and LibreTiny 1.14.0 on a
CB3S), https://github.com/esphome/esphome/pull/21642#issuecomment-6412972562 (trimmed UF2 on CB3S, BK7238 and BW15),
https://github.com/esphome/esphome/pull/21641#issuecomment-6412976859 (new decoder on the same three).
## On-device inflate benchmark (#21639, #21641)
`scripts/ota_timing/inflate_bench`: `gen_streams.py NAME=IMAGE...` cuts a 128 KB slice from each image and writes
`bench_streams.hpp` with raw deflate streams (level 9, 4 KB and 32 KB windows). `bench.hpp` inflates every stream with
each ring size at least the encoder window, three times, and logs the best time plus input and output rates, then
`BENCHDONE`. `bench.sh CONFIG DEVICE` builds and uploads `bench-esp32s3.yaml` or `bench-bk72xx.yaml`, collects one pass
over syslog and fails when a case did not reach the end of its stream.
- Keep the streams near 130 KB on a BK7231N: an image with about 330 KB of extra incompressible data failed its
OTA with "Writing OTA data to flash memory failed". Use one image and `--windows 12` there.
- `bench.hpp` targets the decoder interface with `dest_limit`. #21641 drops it (the output runs into the window up
to `dest`); after that lands, remove the `dest_limit` line.
Results: https://github.com/esphome/esphome/issues/21639#issuecomment-6409786224
## Upload without a configuration (#21738)
`esphome upload --file IMAGE --device IP` with no YAML picks the OTA port from the image header and takes the password
from `--ota-password` or `ESPHOME_OTA_PASSWORD` and the key from `ESPHOME_OTA_KEY`. Test each chip family with its own
current image (`build/firmware.ota.bin` for ESP-IDF, `.pioenvs/<name>/firmware.bin` for ESP8266, `firmware.uf2` for
LibreTiny) and check the build over the API; switch one board between two builds to show a new image lands.
Point every case that should be refused before connecting at an unroutable address (192.0.2.1), never at a board: a
refusal prints no `Connecting to` line, and an image that slips through cannot reach a device. An ESP32-S3
`firmware.factory.bin` slipped through on 2026-10-27 (its bootloader header at offset 0 looks like an app), the device
reported `OTA successful` and its bootloader rejected the slot at boot; send the board its proper app image afterwards.
A factory image has the partition table magic `AA 50` at 0x8000 and the app at 0x10000.
## OTA automations on two OTA platforms (#21903)
`scripts/ota_automations/ota-automations-common.yaml` (a package) puts `esphome` and `web_server` OTA in one config,
each with all six automations (`on_begin`, `on_progress`, `on_end`, `on_abort`, `on_error`, `on_state_change`)
logging a platform tag and the argument. `run.sh esp32|esp8266 LABEL` builds, flashes by serial, and runs four cases
under one capture: a native upload, a native upload whose uploader is SIGKILLed after the first progress fire, a
web_server upload of a random 64 KB file and a web_server upload of the real image. `summ.py OUT_DIR` prints each
case's fires in order with progress collapsed to a count; diff dev against the PR.
- `web_server: ota: false` removes `/update` (404), so leave it out when the web platform is under test.
- A killed native uploader ends in `state 4` and `error 255`, not `abort`. `on_abort` only comes from a web upload
cut on the network. On ESP8266 those happen by themselves (1 in 4 uploads on dev), so count them on both builds with
`webrep.sh IP IMAGE COUNT` before blaming the PR.
- A random file through web_server gives `error 128` on the ESP32 and `error 132` on the ESP8266.
- A SIGTERM to the uploader lets it finish the transfer; use SIGKILL for the native error path.
- Results: https://github.com/esphome/esphome/pull/21903#issuecomment-6516529409
## Signed OTA with several trusted keys (`verification_keys`, #22398)
`scripts/signed_ota/` on an ESP32-C3 (`signed-ota-c3.yaml`, `signed_ota_verification: signing_scheme: rsa3072,
verification_keys: [keyA.pem]`):
1. `python -m espsecure generate-signing-key --version 2 --scheme rsa3072 keyA.pem` (and keyB).
2. Build four tagged images with `esphome -s tag <name> compile`, serial-flash the first factory image (no on-boot
signature check without secure boot), copy each `firmware.ota.bin`.
3. `sign_variants.py <image> keyA.pem keyB.pem <prefix>` writes key A, key B, B then A, stored CRC flipped, byte 2
changed (covered only by the CRC), the same with the CRC recomputed by zlib, and B then A with block 1's CRC flipped.
4. `otaseq.sh <ip> FILE=reject... FILE=<tag>...` sends them with `esphome upload --file` and checks the first verdict
in the device log and the upload result against the expectation (a rejection, or the `BUILDTAG` of that build);
send the rejects first so every check runs in the same build, then the accepted ones.
Expected: unsigned, B only and every CRC edit rejected; A, B+A and the zlib-fixed CRC accepted. The byte 2 pair isolates
the CRC from the signature and key digest.
Gotchas: an ESP-IDF image can contain `E7 02` at a 4 KiB boundary by chance; find the signature sector by the CRC, as
`sign_variants.py` does. An unsigned image sent after a signed one can be rejected with "trusted key failed to verify"
instead of "no signature block": the partition still holds the old signature sector past the new image's end.
Results: https://github.com/esphome/esphome/pull/22398#issuecomment-6716330334
@@ -0,0 +1,10 @@
# prometheus
## Date, time and datetime metrics (#9941)
`scripts/prometheus/prom-datetime.yaml` is a D1 mini with `web_server`, `prometheus` and three optimistic template
datetime entities; `prom-datetime-tz.yaml` adds an sntp time component with `HST10` (the zone applies without a sync).
`datetime_metrics.py HOST` prints the rows of `/metrics`, sets the entities over the API to 2040 values (past 2038 on
purpose) and prints them again with their UTC reading. Dates are local midnight in ms, times ms since midnight,
datetimes ms since the epoch, so a datetime set as an instant reads the same in both zones. It exits 1 when a time
or datetime row does not hold the value it set (dates depend on the zone and are only printed).
@@ -0,0 +1,15 @@
# Per period PWM capture on the same chip (#15438)
`scripts/pwm_capture/` (ESP32-C6): `pwmcap.hpp` routes the `ledc` pad through the GPIO matrix into RMT receive at
80 MHz (ping pong buffers, `en_partial_rx`, `mem_block_symbols: 96`) and a PCNT unit, checks every period in the IRAM
safe RMT interrupt and logs a `WIN` line every 10 s (high time and period histograms, minimum and maximum, level
changes, spikes, reversals). `common.yaml` needs `include_builtin_idf_components: [esp_driver_rmt, esp_driver_pcnt]`
and `CONFIG_RMT_ISR_IRAM_SAFE` and `CONFIG_PCNT_ISR_IRAM_SAFE`; build it with `-s name NAME -s ps light` (or `none`).
`run.sh NAME SECONDS [--every 5 --transition 1]` (env `TREE`, `PORT`, `SC`, `PY`) resets and captures while
`apiclient.py` holds an API connection and optionally sends light transitions; `summ.py` totals the windows.
- LEDC and RMT share PLL_F80M, so a 20 kHz 11 bit output reads exactly 4000 ticks per period with no jitter.
- Result: no glitch in about 10 million periods with power save `light` or `none`; #15718 (LEDC in IRAM) changed
nothing. Positive control: a lambda that changes the duty for a few periods is caught every time.
- Window boundaries follow RMT chunks (about 1000 periods), so `n` alternates between 199584 and 200592; sum over
the run.
@@ -0,0 +1,46 @@
# remote_base / remote_receiver
## Loopback on one pin, no IR hardware (#21560 to #21563)
`templates/remote_base/`: `rb-common.yaml` (API action `tx_all`, a dumper registered from `on_boot` through
`register_dumper()` standing in for an external component), `rb-txscript.yaml` (14 `remote_transmitter.transmit_*`
frames with `tx <proto>` log markers), `rb32.tmpl` and `rb66.tmpl` (`@V@` is the build label).
`scripts/remote_base/run.sh LABEL B N` (`TREE`, `P32` or `P66`, `SC` in the environment) writes the run file from the
template, builds and flashes it from `TREE` (`esphome run --no-logs`), finds the IP and runs `rbdrive.py IP N`, which segments the API log by the
markers and prints what each sent frame decoded as.
- ESP32: `remote_transmitter` and two `remote_receiver`s on the same GPIO (`allow_other_uses`), carrier 100%, no
inversion. RMT reads its own output, every frame decodes. Second receiver `dump: [nec, raw]` shows raw runs only
where no primary matched.
- ESP8266 before #22453: the transmitter sends the whole frame with interrupts off, so a receiver on the same pin
sees nothing (from #22453 on it decodes, see below). For older trees, `softtx.hpp`
bit-bangs frames built with each protocol's `encode()` with interrupts on, and an `interval` sets the pin back to
OUTPUT after the receiver's setup. Bit-banged NEC decodes as NEC about 2 of 3 times (timing), on every build.
- Dish never decodes in loopback on dev either. Pronto matches every frame, so raw on `dump: all` never prints.
- API log lines can drop under bursts; rerun with more rounds before calling a shortfall.
- `register_dumper()` returns nothing, so the config logs `ext dumper register called`, not whether it stuck. On dev
the list has a fixed size and a dumper beyond it is dropped with `No dumper slot` (ERROR); judge by that line and by
`ext dumper saw` counts in the `rbdrive.py` table, which only appear when the dumper is really registered.
`rbdrive.py` exits 1 when the external dumper saw no frame, so a build that rejects the registration fails the
drive; set `EXPECT_REGISTER=1` on `run.sh` to also fail the run on any `No dumper slot` line.
Results: https://github.com/esphome/esphome/pull/21563#issuecomment-6381304387
Retest of the moved stack with 5 rounds per build: https://github.com/esphome/esphome/pull/21563#issuecomment-6382075726.
A D1 mini CH340 sometimes fails the 460800 baud upload; esphome retries at 115200 by itself, but a runner that reads
only the last line sees "Failed to upload" first, so rerun before blaming the build.
## ESP8266 transmitter to receiver on the same board (#22453)
- Wire the `remote_transmitter` pin to a `remote_receiver` pin (GPIO12 to GPIO14 on a D1 mini), `inverted: false`.
- Keep the receiver's default `idle`. `idle: 4ms` is shorter than the NEC leader space (4.5 ms), so every frame is
split and nothing decodes.
- Dev before #22453 sends the whole frame inside one `InterruptLock`, so a receiver on the same chip decodes 0
frames. That is expected on old builds, not a wiring fault. With locks per edge, 3,692 of 3,695 frames decoded
under Wi-Fi load.
- Count with `on_nec` matching a counter in the command, and log the counts on an `interval`.
- Without a jumper, put both on GPIO12 with `allow_other_uses: true` and call `pinMode(12, OUTPUT)` before each frame
(the receiver's setup leaves the shared pin as an input); the receiver's edge interrupt sees the transmitter's own
output. `scripts/esp8266_lock_load/ir-one-pin.yaml` does this, and its `run.sh` adds Wi-Fi load (see
[one_wire.md](one_wire.md)). Under load, the build that sends the whole frame in one lock hit a Hardware Watchdog in
`wDev_ProcessFiq` after 11 minutes.
@@ -0,0 +1,22 @@
# rs485_frame DLE/STX/ETX bus without a bus (#16758)
`scripts/rs485_frame`: an ESP32-C3 (logger on USB_SERIAL_JTAG) with the hub's UART on tx GPIO3 rx GPIO4 and a
simulated bus device on tx GPIO4 rx GPIO3, cross-wired through the GPIO matrix (`allow_other_uses: true` on all
four pins), 9600 baud, no wires. The simulator is polled from an `interval` and logs with the `sim` tag; the test
script logs with `hwt`.
- `simrs.hpp` builds sum16 big endian, header-inclusive frames (`10 00` stuffing, `10 10` with
`simrs::double_mode`) and logs each frame the hub sends (`RXF`) with start and end poll times, the silence since
the previous frame and the time since the simulator's own last byte. `rs-base.yaml` (board, cross-wired UARTs,
poll interval, the hub's CRC and a log line per received frame) and `buttons.yaml` are shared packages.
- Case files: `common.yaml` runs good / bad CRC / garbage / split at 25 and 150 ms / overlong / max length / two
frames in one write / a cut frame followed by a new DLE STX / a bad escape, then button presses that the hub must
hold until a `01 01` gate frame arrives (`replace_latest` keeps only the last press). `rsi2.yaml` (idle_gap gate),
`rsx.yaml` (fixed_delay gate), `rsd.yaml` (sniffer_only, double escape).
- `run.sh CASES LABEL [SECONDS]` (`CASES` is `common`, `rsi2`, `rsx` or `rsd`; `TREE`, `PORT`, `SC` in the
environment) writes a run file with its own name, builds and flashes with `esphome run --no-logs`, captures and
writes `out-rs<CASES><LABEL>.log` with time stamps and line numbers stripped. It fails without a `DONE` line or on
a crash. Diff the logs of dev and the PR; for the gate timing cases read the `RXF` lines (silence before each frame
the hub sent).
- On ESP32 at 9600 baud the default `rx_full_threshold` is 8, so a component does not see bus bytes for about 9 ms.
Queue a test frame at least 20 ms after the bus starts talking, or it collides on any idle-gap design.
@@ -6,3 +6,24 @@ An external component whose template button arms an SDK `os_timer` (50 ms, one s
the callback runs in SYS context, records `millis()` and calls `set_timeout` or `defer`; the timeout
logs the elapsed time and loop count delta. Drive with an aioesphomeapi script (`subscribe_logs` plus
`button_command`). Build the same yaml twice with different names, one per tree.
## interval startup_delay and control from lambdas (#21799)
`scripts/interval/interval-common.yaml` has plain 1 s and 2.5 s intervals, a 1 s interval with `startup_delay: 5s`, a
3 s one with `startup_delay: 500ms`, and a 15 s control interval (`startup_delay: 0s`). The control interval
changes the plain 1 s interval to 500 ms with `id(i_plain).set_update_interval(500)` and `start_poller()`, then
stops it and the 5 s delayed one with `stop_poller()`. Every run logs `millis()`. `run.sh LABEL` flashes a D1 mini,
captures 40 s from boot and runs `ivsum.py`, which prints the run count, first run after boot and gaps per interval.
- Check that each delayed interval's first run comes at or after its delay, that the gaps equal the interval, that
the 500 ms change takes effect, and that nothing runs after a stop.
- The first run of an interval without a delay falls at a different time on every boot (the scheduler spreads first
runs), so do not compare those offsets between builds.
- `main.cpp` shows the class each interval gets (`IntervalTrigger` or `DelayedIntervalTrigger` on #21799).
- Results: https://github.com/esphome/esphome/pull/21799#issuecomment-6516798398
## Freelist trim after boot (ratgdo/esphome-ratgdo#705)
10 s after setup the scheduler frees its item pool and shrinks its `to_add_` and `items_` vectors
(`SCHEDULER_FREELIST_TRIM_DELAY_MS` in `application.cpp`). The first burst of timers after that allocates new
items and regrows the vectors once, so heap or timing measurements should skip the first run after boot.
@@ -0,0 +1,20 @@
# Sensor, binary_sensor and text_sensor filter A/B (#21831, #21813, #21830)
`scripts/sensor_filters` needs only a board. `sensors.yaml` holds 29 template sensors with every offset, multiply,
clamp, NTC, calibrate_linear, delta and lambda filter shape, plus a sensor with id `x` (a value lambda that reads an
id named `x`). `sf.yaml` adds binary and text sensor lambda filters and an interval that feeds the 50 inputs of
`sf.hpp` (NaN, ±inf, -0.0, values on and across the bounds) 200 ms apart, logging one line per input with every
filter output as float bits (`-` means dropped), then `DONE`.
- `run.sh esp8266|bk72xx LABEL` writes the per-run file, builds, flashes, captures and writes `out-sf<kind><LABEL>.out`
(100 lines). Compare a PR build with dev using `cmp`: identical files mean identical filter output, including
which values were dropped. ESP8266 matters because its float math is in software.
- The template sensor ids are `flt1`, `flt2` ...: `s8` and `s16` clash with ESP8266 SDK typedefs.
- Grep the build's `main.cpp` for the new code (for example `clamp_filter_value`, `delta_passes`, generated filter
functions) to prove the build came from the PR tree.
- `BASELINE` sets the delta baseline: a lambda works everywhere, a plain number crashed before #21811.
- Between boards the only difference is the sign of the NaN that `to_ntc_temperature` returns for 0 Ω (libm), on dev
too, so compare builds on the same board.
- The ESP8266 esptool stub can hit serial noise on a CH340 and the BK7238 link sometimes times out once; the runner
retries.
- Results: https://github.com/esphome/esphome/pull/21831#issuecomment-6490999545
@@ -0,0 +1,36 @@
# sigma_delta speaker and rtttl sample taps
## Pulse density of the speaker pin, measured inside the chip (ESP32-P4, #18702)
Nothing is wired to the pin. `scripts/sigma_delta/sdprobe.hpp` calls `gpio_input_enable()` on the SDM output pin,
and a core 0 task reads it with `gpio_get_level()` 2000 times per window, with pseudo-random gaps of 0 to 255 cycles
(xorshift) that decorrelate the samples from the 1 MHz modulator. One window is about 2 ms and the density noise is
about 0.5% sd. The expected duty for an int8 density d is (d + 128) / 256, so 16-bit PCM at +50% reads 75%, 0 reads
50%, and full scale reads 99.6% or 0%.
The same header has:
- a core 0 feeder task (DC, sine, stereo L/R, a burst of N frames, any 8/16/24/32 bit mono or stereo format) that
calls `speaker->play(..., 20 ms)`;
- a played-frame counter on `add_audio_output_callback()`: frames per second over 4 s matches the rounding of the
10 MHz timer exactly;
- a CPU probe: a priority 10 task pinned to the loop core spins for 200 ms, and the iterations while playing against
idle give the interrupt share (44.1 kHz cost 20% of core 1).
`gen.py tpl.yaml > NAME.yaml` builds the whole sequence (the device name comes from `esphome -s name NAME`): DC levels,
volume, mute, formats, 2 Hz and 50 Hz sines with DUMP lines, sample rates, underrun, stop mid-play, finish, rtttl and a
play/stop hammer. `gen2.py` (both share the step helpers in `steps.py`) builds the inverted pin run (set the speaker
pin to `{number: GPIO21, inverted: true}` in a copy of `tpl.yaml`) plus the CPU cost per rate. `run.sh LABEL
[gen|gen2]` generates, builds, flashes, captures with `--ms` until `ALLDONE` and prints the `RES` lines; for `gen` it
then runs `an.py`, which fits the sines and prints the level edges of the underrun, stop and finish dumps. GPIO21 is
free on the Waveshare ESP32-P4 Module DEV KIT. Long operation warnings in the log come from the probe's own lambdas.
## rtttl click measurement (#22075)
`scripts/rtttl_edge_tap/edgetap.hpp`: `EdgeTap`, a pass-through `speaker::Speaker` that forwards to the real speaker
and reads the int16 samples rtttl writes. `rtttl.set_speaker()` it from a lambda, and call `sync()` from an
`interval: 10ms`, because `is_running()` and `is_stopped()` read the base class state. Use tunes with a rest between
every note, so a note is a run of samples between runs of 8 or more zeros. It logs each note's onset and release
level (the largest of the first or last 4 samples as a share of the note's peak): dev about 95%, the 32-sample fade
at most 12.5%. `rtttl-edge-tap-dut.yaml` is the AUX-2 config with a USB speaker as the mixer output (it needs the
AUX-2 package next to it, see [usb_audio.md](usb_audio.md)); `run.sh LABEL` plays eight tunes and prints the note
reports. Any speaker works the same way: wrap the one rtttl writes to.
@@ -0,0 +1,75 @@
# Simulated chips on the bus, fake Home Assistant, static buffers
`scripts/sim_buses/` and `scripts/static_buffer/`. Each header is added with `esphome: includes:` and wired in an
`on_boot` lambda, so no chip is needed. The wrappers take the real bus or SPI delegate and pass through every
address they do not simulate.
- `lcdsim.hpp`: `LcdSimBus`, an `i2c::I2CBus` wrapper that plays PCF8574 backpacks (P0 RS, P2 EN, P4 to P7 D4 to D7)
driving HD44780 controllers. It decodes the EN-strobed nibbles and rebuilds the rows written after each 0x80 and
0xC0 address command. `add()` up to three addresses, hand it to each `lcd_pcf8574` with `set_i2c_bus()` at
`on_boot` priority 800, and call `report()` from an interval. `lcd_an.py SERIAL` checks the rows against the
patterns its docstring lists (0x27 20x4, 0x26 16x2, 0x25 64x4), and fails on a display whose frame count does not
grow between its first and last report, on a segment longer than its rows or on
data past the 128 byte segment buffer, which the report counts as `overflow`, on a report from an address it does
not expect, or on any read (`reads`; the driver never reads, so the simulator NACKs them). A 64x4 display hangs the
code before #22331 in
setup.
- HT16K33: use `templates/i2c_fake/htfake.hpp` with its test runner (see [fake_i2c.md](fake_i2c.md)).
- `mxsim.hpp`: `MxSniff`, an `spi::SPIDelegate` in front of the real one that keeps what each MAX7219 in a chain
latches. Swap it in at `on_boot` priority 399 through `DelegatePeek::of(id(x))`:
`auto &d = DelegatePeek::of(id(x)); d = new MxSniff(d, chips);`. `mx_an.py SERIAL CHAIN...`
summarizes the reports, CHAIN being each name passed to `report()`, and fails on a named chain with no report line,
on malformed transactions (byte count not two per chip, or a partial byte from `write()` or
`write_cmd_addr_data()`, which are recorded and forwarded too), on a named chain with fewer than two report lines,
or a chain whose transaction count stays 0 or stops growing, and prints PASS otherwise.
- `spysim.hpp`: the same delegate trick (both headers carry `DelegatePeek`, so one config can include both), logging length and FNV-1a hash of every transaction over 1000 bytes (whole
frames, used for `pixoo`). `write()` and `write_cmd_addr_data()` are hashed and forwarded like `write_array()`;
a partial byte or a bus width other than 1 is hashed as whole bytes, so the SPY line carries `odd`, the count of
such transactions, and a dev vs PR hash match only holds when `odd` is 0 on both.
- `pisim.hpp`: drives two `packet_interface` stacks on cross-wired UARTs through `top()`, with payloads whose bytes
are a function of their index so every byte is checked, and the index and length each side sent (last 256) so a
truncated or split packet, or one whose slot a later index reused, counts as `rx_bad`. Send indices 0, 1, 2 and so
on from each side: a skipped index counts as `rx_lost` and an old or repeated one as `rx_bad`. `report(tag, true)` once both links are idle ends each
line in PASS only when that side received every packet the peer sent intact and the peer had no send refused;
`missing` is the rest, packets still in flight or lost at the tail. `packet_transport` drops packets carrying its own device
name, so a one-board test must use the layers directly.
- `hcsim.hpp`: a software SN74HC165 chain on ESP-IDF (data GPIO6, clock GPIO7, load GPIO10). A falling load edge
latches the pattern and each rising clock edge drives the next bit onto the data pad, which is in
`GPIO_MODE_INPUT_OUTPUT` so the component reads it from the same pin. Call `hcsim::report(label, loads0, clocks0)`
after each window with `hcsim::loads` and `hcsim::clocks` from its start; it logs `INVALID` instead of a result
when the GPIO or interrupt setup failed or the window saw no load or no clock edge, so a dead simulator cannot pass
as a wrong but steady pattern.
- `ledprobe.hpp`: samples a status LED (or any output) pin every 2 ms from an `esp_timer`, off the loop task, and
prints blink count, on time, period and the longest gap per window. It measures `status_led` while the loop is
blocked (#16407). Pass `ledprobe::n` from the window start as the mark; a window with more than 400 edges logs
`INVALID` and no result line instead of passing for a stalled LED. So does every window when the pin or timer
setup failed or the timer has not sampled for 100 ms, so a dead probe cannot read as a quiet LED.
- `fakeha.py HOST OUT_PREFIX`: an aioesphomeapi client standing in for Home Assistant for `homeassistant` select
mirrors. It answers state subscriptions from a table, pushes option and state changes (full, overfull, quoted,
unparseable), checks each step through a "Dump" button, captures the action calls, churns 600 option updates
while watching the heap, and checks the entity options after a reconnect. A step fails when either DUMP line is
missing, and a rejected update also needs the device's rejection log line, so an update that never arrived cannot
pass. Exits 1 on a failed step and prints PASS otherwise.
- `wizard_drive.py HOST`: finishes the device wizard through aioesphomeapi 46.28 (`device_wizard`,
`wizard_input_set`), answers the subscriptions the inputs create and checks the values reached the entities.
## Static buffers (`cg.static_buffer`)
`scripts/static_buffer/` holds two local external components and `static-buffer-dut.yaml`, which loads them:
- `sbtest` allocates three buffers with `cg.static_buffer` (37 `uint8_t`, 5 `uint32_t`, and `const char *` sized by
a C++ expression) and checks at setup that they are zeroed and aligned, then fills each completely; at
`dump_config()` it checks no other instance's fill reached them. Two instances prove the buffers are distinct.
Both `SBTEST` lines end in `PASS` or `FAIL`; the `dump_config()` one passes only when the setup checks passed too.
- `sbpsram` asks for `psram=True` and uses the pointer codegen hands it: the static buffer without PSRAM, or a
zeroed `psram_buffer<T>()` with a `psram:` block, which logs `external=1` (nullptr if the allocation failed).
It also gets a plain 8 word `uint32_t` static buffer, which must be non-null, aligned and zeroed at start and read
back its fill (`plain=1`). Its `dump_config()` line ends in `PASS` only when both readbacks match, both buffers
were zeroed at start and, with PSRAM, the PSRAM one is in external RAM.
## Gotchas
- Some "classic" devkits with a CP2102N carry an ESP32-PICO-D4. GPIO16 and GPIO17 are its flash pins, and an
ESP-IDF build that drives them hits the interrupt watchdog. Use `board: pico32` and GPIO9/GPIO10.
- An `addressable_light` display writes nothing until its light is turned on.
- Before calling a rebased PR's change trivial, diff the PR's own diff (head against its parent) old against new: a
move that looks like a merge only can carry real commits from below.
@@ -17,3 +17,46 @@ only, announcement all four, after stop none, every cycle. API commands land a s
the probe against the `State changed to` lines rather than the driver's timestamps. Internal free heap after a stop
moved by 2.5 kB between two boots of the same build, so a heap delta needs more than one boot per build. Results:
https://github.com/esphome/esphome/pull/21203#issuecomment-6299581943
## Streamed TTS with a pause (#21090, #21091)
Home Assistant streams TTS while it is still being generated, so the player must start on the first part instead of
waiting for the whole file. `scripts/speaker_tts_pause/`:
- `mk.py INTRO_AUDIO CONT_AUDIO www/s1.flac` builds one 48 kHz mono FLAC stream (the intro padded to a 4096-sample
frame boundary, then the continuation) and writes the byte offset of the split to `s1.flac.cut`. Make the intro
with any TTS, e.g. `say -o intro.aiff "..."` on macOS; build a short one (`s1`) and a long one (`s2`).
- `srv.py PORT www` serves the file chunked: the header and intro at once, then it holds the connection open for
`PAUSE` seconds (8), then the rest.
- `tts-common.yaml` swaps the AUX-2's `speaker_source` player for the `speaker` platform player and counts played
frames with `add_audio_output_callback` on both resampler speakers into the counters in `frames.h` (one `[frames`
log line per change, read over serial with `--ms`). Its `on_boot` must be a list entry: a dict `on_boot` replaces
the package's list and the AUX-2 never starts Wi-Fi or Ethernet.
- `run.sh LABEL=TREE...` flashes each build and drives two short-intro and two long-intro announcements and one
media play through `scripts/speaker_media_player/drive.py` (set `DRIVE` when running from a scratch copy); `an.py`
prints first audio, the server's send times and how much of the intro played before the rest arrived, per request.
A request fails when it never played the whole intro or stopped short of 95 % of the stream.
Before the two PRs the player waited for the rest of the stream (8.2 s) for both a 22.5 KB and a 50 KB intro; with
both it starts at about 1 s.
## Voice PE wake word audio (#21885)
`scripts/voice_wake_word_audio/va.py HOST KEY OUTDIR accept,reject,timeout` stands in for Home Assistant
(aioesphomeapi 46.27.0 or later, `wake_word_audio=True`), answers each run by mode and saves both channels as WAV;
`locate.py OUTDIR` finds the played phrase in them by envelope cross-correlation against `wake16k.wav`.
`run.sh HOST TAG [ROUNDS]` runs reject and timeout rounds while playing the phrase every 9 s, starting once `va.py`
is READY. `va.py` fails a START before READY or during a run, and `run.sh`, `stale.py` and `mww.py` fail at once
when the player fails or hangs for 30 s.
- Give the test build its own API key so Home Assistant cannot hold the voice assistant subscription.
- Make the phrase with any TTS (`say -o wake.aiff "Okay Nabu"`, then `ffmpeg -i wake.aiff -ac 1 -ar 16000
wake16k.wav`) and play it from the host near the device (`PLAY`, default `afplay`). On macOS the terminal needs
audio permission first; before that `afplay` hangs.
- The PE mute switch only mutes samples. To stop microWakeWord, add `mww-buttons.yaml` (template buttons calling
`micro_wake_word.stop` and `start`); `mww.py` checks that no run starts while it is stopped.
- Stale clip check: `stale.py` says a spoken marker word (`marker.aiff`), stops microWakeWord for 3 s, starts it and
says the wake word; `stalean.py` measures loud audio before the phrase in each clip and exits 1 above
`MAX_LOUD_MS` (default 100). The loud level has no default: calibrate `LOUD_LEVEL` on each setup from the phrase
and pre-phrase peaks it prints (a run without it prints them and fails), somewhere between room noise and the
marker's peak. A trial whose phrase peak is under twice `LOUD_LEVEL` fails as too quiet to judge. A steady 1 kHz
beep never reaches the PE stream (the XMOS processing removes it), so the marker has to be speech.
@@ -0,0 +1,32 @@
# SPI bus capture on the same board, no chips or wiring (#22376)
`scripts/spi_self/`: `gen.py` writes `spi<board>.yaml` (e32 classic ESP32, c3, e66 ESP8266, rp Pico 2 W) with a
hardware bus, a software bus, several `spi_device` instances (all four modes, MSB and LSB first, different rates and
CS pins) and a `max7219`. `spitest.hpp` drives every device from a 20 ms interval (an 8 byte transfer, write16, a 12
bit write, read_array, write_array, back to back transfers, teardown plus setup, a second setup, a max7219 update),
three passes, then reboots. `run.sh BOARD LABEL SECONDS` (env `TREE`, `PORT`, `SC`, `PY`, and `PORTS` for the Pico)
builds, flashes, captures and checks; `analyze.py spi<board>.yaml BASE.txt PR.txt` checks each record against the
expected wire data and diffs the two builds by the majority payload per record, so interrupt jitter does not count.
- Classic ESP32: the ESPHome bus is on SPI2 through the matrix and SPI3 runs as a slave without DMA (64 bytes at
most), set up again in the device's mode for each operation. Its nominal CS must be a spare pad held high
(GPIO14); the device CS is routed into `spics_in` only after `spi_slave_queue_trans()`, because the slave driver
reconnects the nominal pad when a transaction loads. Never give the slave the device's CS pad:
`spicommon_cs_initialize` takes over its output and `spi_slave_free` resets it. `gpio_set_direction(..., OUTPUT)`
turns a pad back into a plain GPIO output, so only call `gpio_input_enable` on pads the master drives.
- With that slave attached, full duplex reads on the master lose their last bytes after the first read of a boot,
even through a MOSI to MISO loopback; a config without the slave reads fine. Do not judge reads on that layout.
- The slave sees bit-banged mode 1 data one bit late; the interrupt captures decode it correctly.
- C3, ESP8266 and Pico 2 W: GPIO edge interrupts on SCK and CS take snapshots of the input register. C3 SPI2 cannot
clock below about 78 kHz; at 80 to 100 kHz the capture works with
`gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3 | ESP_INTR_FLAG_IRAM)`, but one build lost an SCK pair at the start
of a frame in every run and a rebuild did not: compare two builds before calling a missing edge real. Let the log
output drain (20 ms) before a capture that follows `spi_setup()`.
- ESP8266: the HSPI pads (12, 13, 14) do not show in `GPI` while in SPI function; the script logs `SPI1CLK`,
`SPI1C`, `SPI1U` and `SPI1P` after each hardware bus operation instead.
- Pico 2 W: `IO_BANK0` INOVER on the MISO pad forces the SPI receive input low or high (reads 00 and ff).
- Software bus loopback: MOSI and MISO on one pad with `allow_other_uses`, then set the pad back to output.
- Seen on dev before #22376: the software bus ignores `data_rate` (`cycle_clock_()` never waits), and SCK moves to
the new idle level only after CS is low when devices with different clock polarity share the software bus.
- LibreTiny (BK72xx, RTL87xx, LN882x) has no `spi:` (config validation allows esp32, esp8266, nrf52 and rp2 only),
so SPI changes cannot be run there.
@@ -0,0 +1,19 @@
# sun on_sunrise and on_sunset without waiting for the sun (#21803)
`scripts/sun/sun-common.yaml` (a package) puts the observer on the equator and, once SNTP has synced, searches the
longitudes for one where the sun stands near -5° and rises as the observer moves east. It holds that longitude for
one 60 s elevation poll first: the poll before saw the sun at longitude 0, and the jump could read as a crossing.
Crossings until then are logged as `crossing before the sweep ignored` and not counted. An interval then moves the
longitude by 0.125° every 5 s, which sweeps the elevation from -5° to +6° and back at about 1.5° a minute, reversing
above +6° and below -5°. Five triggers (sunrise at the default, 2° and 4°, sunset at the default and -2°) fire from
the real 60 s elevation poll; each logs the elevation it fired at and counts in `fires`, logged every 30 s.
- The sweep stops after one full pass and logs `sweep done`. `run.sh esp32|esp8266 LABEL` builds, flashes and
captures up to 20 minutes (a pass takes about 17), then `check.py` wants `sweep done` and the counter at 5 with no
trigger firing twice. Run dev and the PR on the same board; two boards can run in parallel.
- At 1.5° a minute a crossing fires up to about 1.7° past its elevation, one poll late at most.
- The seconds past the minute of the fire lines show the poll phase: separate pollers fire at different seconds,
one shared poll fires every trigger in the same second.
- `id(sun_id)` is a pointer in lambdas (`->`). Judge by the `fires` counter: the classic devkit's serial log can
garble single lines.
- Results: https://github.com/esphome/esphome/pull/21803#issuecomment-6490173072
@@ -0,0 +1,24 @@
# CAN (esp32_can) without a transceiver (#21792, #21795)
`scripts/twai_self`: `esp32_can` with `mode: NO_ACK`, TX on GPIO25 and RX on GPIO26. After boot `canself.hpp` enables
the input on GPIO25 and routes it to the controller's receive signal (`esp_rom_gpio_connect_in_signal(25,
TWAI_RX_IDX, false)`), so the controller hears its own transmit pad. Frames go out with `twai_transmit_v2` and
`TWAI_MSG_FLAG_SELF` (the driver handle is reached through explicit template instantiation), so every frame runs
through the normal receive path and `on_frame`.
- `can.yaml` has nine `on_frame` entries (standard and extended ids, masks, remote-only and data-only filters, two
entries with the same filter, an id with bits outside its mask that must never fire) and sends 20 rounds of 7
frames. `run.sh LABEL` writes `out-can<LABEL>.counts` (fires per trigger, id, flags and data); diff dev and the PR.
- Set `rx_queue_len: 32`: with the default 5, a burst of 7 frames loses the last two most of the time, on every
build.
- ESPHome's own `canbus.send` does not set the self flag, so its frames are not received in this setup.
- Results: https://github.com/esphome/esphome/pull/21795#issuecomment-6491777381
## ESP32-C3 and extra frame shapes (#21950)
The C3 works the same way: `variant: esp32c3`, TX GPIO5 and RX GPIO6, `TX_PAD = GPIO_NUM_5` in `canself.hpp`, and
the logger on USB_SERIAL_JTAG. `canx.hpp` adds a classic frame with DLC 12 (`TWAI_MSG_FLAG_DLC_NON_COMP`), the
`send_data` overloads (standard, extended, remote, empty, a 10-byte payload that must be clamped, a vector) and a
`twai_get_status_info_v2` dump before and after (`canx::tx_status(id(can0), "before")`). The `get()` that reads the
private handle is a friend found only by argument-dependent lookup: call `get(canself::Handle{})`, never
`canself::get(...)`.
@@ -0,0 +1,74 @@
# Simulated UART sensors on a classic ESP32 (#21449, #21446)
No sensor needed. `templates/uart_sim/sim.hpp` (included with `esphome: includes:`) has frame builders for wts01,
ds1603l, sm300d2 and ufm01, `sim::loopback(id(uart))` (ESP-IDF `uart_set_loop_back` on the component's own UART),
`sim::send()` that logs the bytes, and `sim::probe`, a SEN0465 responder polled from a 10 ms interval. The run files
`fra.yaml`, `frb.yaml` and `frc.yaml` set the name and include `fr-common.yaml` plus their `fr*-parts.yaml` as
packages; copy them with a different name (`frabase` and so on) for the base build.
- Push-style sensors: put the component on a UART, switch it to loopback in `on_boot`, and inject frames from
template buttons (`fra-parts.yaml`, `frb-parts.yaml`). ufm01 enters its active stream by itself when a 1 s
interval streams frames during its 2 s startup wait.
- Request/response sensors: cross-wire two UARTs through the GPIO matrix (UART1 tx 25 rx 26, UART2 tx 26 rx 25,
`allow_other_uses: true` on all four pins); the second UART runs the simulated device (`frc-parts.yaml`).
A select sets the probe's misbehaviour (tail, FF tail, silent, split, wrong gas).
- Polled readers: run `update()` from a script and log `micros()` around it to measure blocking
(sm300d2 on dev blocks about 104 ms on an empty buffer).
- The logger stays on UART0, so three builds cover five components. Drive with `scripts/uart_sim/frdrive.py HOST
LOG press:NAME sel:NAME=OPT num:NAME=V wait:S @FILE` (`@FILE` reads one op per line), or
`scripts/uart_sim/run.sh NAME TREE SECONDS OPS...` (build and flash from TREE with `esphome run --no-logs`,
capture, drive; `PORT` and `SC` in the environment, `ERASE=1` erases the flash first, `DRIVER` picks another
API driver).
## Particulate sensors: gcja5, pm1006, sds011 (#21939)
`templates/uart_sim/pmsim.hpp` adds frame builders `sim::gcja5`, `sim::pm1006` and `sim::sds011`, each with a bad
checksum or bad end flag. `fp-parts.yaml` puts gcja5 on UART1 (9600 8E1, with a template binary sensor that shows
`status_has_warning()`) and pm1006 on UART2; `fs-parts.yaml` has sds011 alone. Write the run files the same way as
`fra.yaml` (`fp.yaml`, `fs.yaml` with their own name).
- Every button injects one case with its own PM value, so each published state names its case: good, two frames,
bad checksum, end or tail, garbage before a frame, a cut-off frame followed by a good one, a repeated first header
byte, and splits of 300 and 700 ms (either side of the 500 ms gap reset).
- `run.sh NAME TREE 150 @fp-ops.txt` runs the op list from the file; `fs-ops.txt`, `fd-ops.txt` and `fj-ops.txt`
are the other lists.
- The loopback also feeds each component its own writes (pm1006 request, sds011 setup and sleep/work commands),
which must not disturb the next frame.
- The API coalesces two states published in the same loop pass, so "two frames" shows only the second value.
- Found with it: on dev gcja5 published bad and misaligned frames as readings (268435456).
## 4-byte distance sensors: a01nyub, a02yyuw, jsn_sr04t (#21948)
`sim::ds1603l(mm, bad_ck)` builds the same `FF H L sum` frame; `fd-parts.yaml` (a01nyub on UART1, a02yyuw on UART2) and `fj-parts.yaml`
(jsn_sr04t, polled every 15 s), with the same case list as above plus a reading below the minimum distance. With
loopback the jsn_sr04t request byte `55` can land inside a split frame and break it; check the `Request read out`
timestamps before calling a difference between builds.
## LD6002B TinyFrame parser (#19836, #19837)
`templates/uart_sim/ld6002b/`: `ld6sim.hpp` is a fake module on a UART cross-wired to the component's UART (UART1
tx 25 rx 26, UART2 tx 26 rx 25), set up by the shared `ld6-base.yaml` package. It ACKs every command with an empty
frame of the same type and logs the bytes it received (`MOD RX`), so the bytes on the wire can be diffed between
builds. Target reports carry a distinct X per
case, and `T1 X` shows which frames got through.
- `ld6-parser-cases.yaml` (used by `ld6-common.yaml`, which turns the query answers off): good, byte by byte (one
byte per poll, so the component's loop runs in between), garbage before a frame, bad and lost header or data
checksums, an oversized payload, a burst of 20. The script runs by itself from `on_boot`; `ld6p.yaml` is the run file
(copy it with another name for the base build). `run.sh NAME TREE` builds, flashes over serial and captures,
and fails without a `CASE done` line.
- Garbage that contains `01` right before a frame loses that frame on dev and on the PR (a false header, no rescan).
- Full component A/B: `ld6sim.hpp` also answers every query with a fixed report (delay, sensitivity, trigger,
installation, Z range, low power, low power sleep, both area reports, version); `ld6x-common.yaml` has the full
entity set and runs the parser cases too (`ld6x.yaml` is its run file); `ld6drive.py HOST` presses every button,
sets every number and select and toggles every switch over the API. Run it with `ERASE=1 DRIVER=ld6drive.py
scripts/uart_sim/run.sh NAME TREE 100`: switch and select states and the area id are stored preferences, and a previous build's run changes the
next build's boot state.
- `m4-common.yaml` and `m5-common.yaml` run the same fake against `ld6004`: target and area reports, work status and
radar on/off reports, malformed reports, and the point cloud and target display switches across a reboot
(`m5` reboots once and checks the restored switch states). `m4.yaml` and `m5.yaml` are the run files, and like
`ld6p.yaml` each can be copied with another name for the base build. Run m5 with `ERASE=1 run.sh NAME TREE 75`: the boot
counter and switch states are stored preferences that survive a flash, and the second boot's `CASE done` comes
about 49 s after reset. Only the second boot logs `CASE done`, so a skipped reboot fails the run.
See `rs485_frame.md` for the same cross-wired UART trick on an ESP32-C3.
@@ -24,3 +24,8 @@ identical payloads share one table, a lambda is sent from its vector. An empty c
(`sendto() error 22` in the log, no datagram) until it accepted `nullptr` with a zero length; the old
`TemplatableBytes::visit` had passed a zero length stack buffer instead. Compare sizes against the stacked PR's own
head when the PR is stacked, not against dev. Results: https://github.com/esphome/esphome/pull/21263
## Two sockets on one port (#22509)
lwIP lets two sockets bind one UDP port when `SO_REUSEADDR` is set, and unicast then goes to the socket opened
last; without it the second bind fails with errno 112.
@@ -0,0 +1,15 @@
# sensor and number on_value_range A/B (#21814, #21817, #21818)
`scripts/value_range/value-range-common.yaml` (a package) has one template sensor with seven triggers (constant
bounds, one-sided, a lambda bound on `x`, a bound from another sensor's state that stays NaN until step 22, both
bounds NaN, and an identical twin) and one template number with three triggers. It feeds them 30 values 200 ms apart
(on the bounds, just past them, repeats, NaN, ±inf, a moving bound), then syncs preferences and restarts, then
sends 50, 95 and 50 to check the restored in-range flag. Every fire logs its step and `x`.
- `run.sh bk72xx|esp8266|esp32 LABEL` writes the per-build file with a fresh `tag`, builds, flashes, captures 90 s
(through the restart) and writes `out-rng<kind><LABEL>.seq`; diff dev against each PR head.
- The sequence primes every trigger first (bound 0, value -500, bound NaN): the in-range flags are stored in flash,
so without priming a fresh build starts from whatever the previous run left, and the first fires differ.
- The phase global resets when `tag` changes, so each reflash runs the whole sequence.
- Log line numbers differ between trees; the runner strips them before writing the `.seq` file.
- Results: https://github.com/esphome/esphome/pull/21818#issuecomment-6491846503
@@ -0,0 +1,15 @@
# voice_assistant
## Voice PE late TTS start (#19408)
Only the subscribed client (Home Assistant) may send `VoiceAssistantAnnounceRequest`, so both builds carry
`scripts/voice_announce/slow-announce.yaml`, a template button that calls `id(va).on_announce(msg)` with
`start_conversation = true` and a URL on the host. `drive.py HOST KEY DELAY OUT` serves a FLAC (WAV as `audio/wav`
is rejected by micro_decoder) only after DELAY seconds, presses the button and timestamps the device logs. It
holds the voice assistant subscription throughout, since without a subscriber the device drops the announcement
state at once. It imports `va_common.py` from `scripts/voice_wake_word_audio/`, so keep the two directories side
by side.
`run.sh DEVICE LABEL=CONFIG=TREE...` builds, OTAs and runs both builds twice. The player gives up after 6 header
attempts of 5 s.
The wake word audio flow is in [`speaker.md`](speaker.md).
@@ -12,13 +12,14 @@ on ESP32. Keep `power_save_mode: none`, and drive the device by address: mDNS na
`scripts/web_server/sse.py HOST SECONDS [--clients N] [--stall] [--churn CYCLES]` opens raw TCP connections to
`/events`, decodes the chunked body and parses every `state` event as JSON; `--stall` adds a connection with a 2 kB
receive buffer that never reads (the device must log `Closing stuck EventSource connection` after the 20 s stall
timeout), `--churn` opens and resets a connection CYCLES times. `sse_cases.sh HOST KEY LABEL CAPTURE` runs the
set: 1 client 60 s, 4 clients 60 s, a reader plus a stalled client 45 s, a reader plus 10 abrupt closes, then two
clients with the "Burst" button (50 sensor and 50 text publishes in one loop pass). Pass: 0 invalid JSON on every
client, the oversize sensor dropped with its warning on every publish, the stalled session closed after about
20 s, no reset. The loop task watermark is a minimum since boot, so compare the value after the first client, after
four clients and after the burst between the builds (the SSE split in #21109 left 1280 B more on the ESP32 classic).
receive buffer that never reads (the device must log `Closing stuck stream` on ESP-IDF and `Closing stuck
EventSource connection` elsewhere, after the 20 s stall timeout), `--churn` opens and resets a connection CYCLES
times. `sse_cases.sh HOST KEY LABEL CAPTURE` runs the set: 1 client 60 s, 4 clients 60 s, a reader plus a stalled
client 45 s, a reader plus 10 abrupt closes, then two clients with the "Burst" button (50 sensor and 50 text
publishes in one loop pass). Pass: 0 invalid JSON on every client, the oversize sensor dropped with its warning on
every publish, the stalled session closed after about 20 s, no reset. The loop task watermark is a minimum since
boot, so compare the value after the first client, after four clients and after the burst between the builds (the
SSE split in #21109 left 1280 B more on the ESP32 classic).
For the HTTPD task, `web_server: auth: type: digest` (the default `auth` is Basic) and
`scripts/web_server/httpd_cases.sh HOST USER PASS LABEL CAPTURE`: `httpmix.sh` sends a `curl --anyauth` request
@@ -37,3 +37,83 @@ AP start (the fix in that PR), the first AP start failed on every boot with `esp
(DHCP server already started by the AP start event) and `Failed to enable NAT`, and the bridge only came up on the
retry 90 s later; a client that cannot find the SSID for the first minute on an older build is that. With the fix
the bridge is up 6 s after boot. Any change to the ESP-IDF AP start should also rerun the captive portal hammer.
## Reboot against a WPA2/WPA3 PMF access point (#21590)
- AP: `scripts/wpa3_ap/wdap.yaml` on a classic devkit. ESPHome `wifi: ap:` only, then an on_boot lambda switches it
to `WIFI_AUTH_WPA2_WPA3_PSK`, PMF capable, and registers a WIFI_EVENT handler that logs `STA JOIN` and
`STA LEAVE ... reason N` (the include header pulls in `esp_wifi.h`). It disables Wi-Fi after 420 s.
- AP leave reason 3 means the station sent a deauth; reason 2 means the AP found a stale association when the
station came back; several reason 2 before one join are rejected reconnects.
- DUTs (`wdbk.yaml` BK7238, `wdcb.yaml` CB3S, both on `wd-common.yaml`) list the AP at priority 10 and the main
network at 0, and reboot 30 s after joining the AP and 150 s after joining the main network. So they move to the
AP while it is up and fall back to the main network (where they can be flashed over the air) once its window
ends. `run.sh BUILD` flashes the AP, does OTA for the CB3S and a serial flash for the BK7238, then captures one AP
window; `an.py` prints each station's join and leave sequence and fails on a rejected reconnect, an AP that did
not switch to WPA2/WPA3 (`AP CONFIG FAILED`), a DUT missing from the AP events (`DUT_MACS`) or a BK7238 that never
joined the AP.
- Gotchas: the LibreTiny build directory has two `.uf2` files, so flash `firmware.uf2`. The BK7238 serial flash
sometimes fails to link while the chip is in a reboot loop; retry it 3 or 4 times. OTA fails if the DUT reboots
mid-upload; retry.
- Finding: BDK 3.0.78 rejects about 2 reconnects per reboot when the station left without a deauth; BDK 3.0.33
does not.
## WISP and ALWAYS_ON AP rig (#20077)
`scripts/wisp_ap`: the upstream is an ESP32-S3 with Ethernet running a WISP bridge (`wisp-upstream.yaml`) with its AP on
10.40.0.1/24 and web buttons `ap off` and `ap on`. Its subnet must not be 192.168.4.0/24, the DUT AP's default, or
nothing routes. The DUT is a classic devkit (`dut-wisp.yaml` or `dut-always-on.yaml`); the client is a D1 mini on
the DUT's AP (`wispcli.yaml`) logging its DNS server, a name lookup, a GET by name, the AP itself and a LAN file by
IP. `run.sh DUT START SECONDS T:STATE...` switches the upstream AP at the given times and `sum.py` groups the client
results by phase. Capture the D1 mini without `--reset` (a CH340 floods garbage after an RTS reset). Clients keep
the captive portal's DNS answer for its TTL.
## AP restart with WPA3 mixed and LIGHT power save (#21531)
`scripts/wifi_restart`: the WISP upstream (`wisp-upstream-wpa3.yaml`) switched to WPA2/WPA3 mixed by a script that
runs again after every `ap on`, DUTs (`rebk.yaml`, `recb.yaml` on `re-common.yaml`) with a static IP on the bridge's
subnet, a fallback AP, `power_save_mode: LIGHT` and syslog to the host through the NAT. `run.sh BUILD` switches the
AP off and on three times; `an.py` fails on a reboot, a crash or a DUT that is not back on syslog within 120 s of an
AP start. After 19 minutes the DUTs run `wifi.configure` to the main network; that keeps the static IP, so the next
OTA needs the host to have an address on the bridge's subnet as well (an interface alias). The CB3S log is on UART1
(TX1), so its configs need `logger: hardware_uart: UART1`. Runners must `pkill` only their own ports; a global pkill
stops the other runs' captures.
## Channel and network switching with a bench AP (#22142)
`scripts/wifi_channel/`: `bench-ap.yaml` makes an ESP32-S3 an AP-only bench network on a fixed channel (11 by
default; pick one your normal network does not use), and `dut-common.yaml` gives the device under test that AP at
priority 10 plus your normal network, logs every `wifi_info` ssid and channel update (with the channel the component
reports and whether it is connected), and cycles `wifi.disable` and `wifi.enable` every 90 s. `run.sh LABEL` (env
`TREE`, `PORT`, `AP_PORT`, `SC`, `PY`, and `BENCH_CHANNEL` when the AP is not on 11) flashes the device and captures 420 s while it takes the AP down with
`esptool --port <AP port> --after no-reset chip_id` (the board stays in the bootloader) and back with
`--after hard-reset`. The channel sensor must follow every move between the two networks: the run passes when the
connected channel starts and ends on the bench channel with another channel in between.
- Dropping the AP under a connected device gives a `Beacon Timeout` link loss.
- The 90 s interval fires once right after boot.
- A `Disconnected ... Association Leave` right after `Connecting to` is the aborted attempt, not a missed connect.
## Soft AP with the BLE tracker scanning (#22534, #22566)
`scripts/ble_scan_soft_ap/`: `dut.yaml` runs only `wifi: ap:` with `captive_portal:` and `bluetooth_proxy:`, and
`client.yaml` (on a second ESP32) joins that AP and fetches the portal page every 5 s, logging `JOIN` and `STAT`
lines. `run.sh` erases and flashes the DUT, resets the client and prints the generated scan window, the first join, the
page counts and the client's disconnect reasons (usage in its header).
- A scan window equal to the scan interval (320 ms of 320 ms) leaves the radio no time for the soft AP: on dev no
client joined on the classic ESP32, S3, C3 or C6. With #22566 the tracker uses its 30 ms connection window while
the AP runs, and every chip took a client in about 4 s.
- Only one AP with the test SSID may be up at a time; list the other AP boards in `PARK` so they wait in their ROM
bootloader.
- To check that the full window comes back after provisioning, log `esp32_ble_tracker` at VERBOSE and provision
with `templates/client-wifisave.yaml` (set `ap_ssid` to the DUT's and `ap_password: ""`, the DUT's AP is open): `Connection or AP active, using 48 unit scan window` must stop once the AP stops.
- Some clients do not join an ESP8266 AP at all (the C6 failed on dev too with `Too Many Associations`); use a C3.
## ESP8266 power save after captive portal provisioning (#22542, #22564)
The DUT runs `ap:` plus `captive_portal:` with no station network and logs `wifi_get_sleep_type()` every 5 s (0 is
`NONE_SLEEP_T`, 2 is `MODEM_SLEEP_T`); an ESP32-C3 with `templates/client-wifisave.yaml` posts the real credentials.
Erase the flash before each run so no saved credentials skip the portal. On dev the sleep type stayed 2 after
provisioning although the config asked for none; with #22564 it read 0 from the moment the AP started, and the
`networks:` path still read 0.
@@ -0,0 +1,9 @@
# wled
## Realtime sender on the bench (#22509)
When the access point stops passing traffic between clients (lwIP `sendto` on the sender fails with errno 12
because ARP never resolves), run the sender on a board instead of the host: an ESP32-S3 runs `wifi: ap:` and a
25 ms interval lambda sends WARLS, DRGB or DNRGB frames with lwIP `sendto` to the devices under test, which join
that AP with `manual_ip`. Two lights on the same port: see `udp.md`. Heap after an effect stops: see
`heap_alloc.md`.
@@ -0,0 +1,38 @@
# Xiaomi BLE
## Xiaomi, ATC and PVVX parsing without the sensors (#21483)
No Xiaomi devices are needed, and one ESP32 cannot hear its own advertising, so the frames are injected into the
tracker as scan results: everything after the radio runs as normal.
- `scripts/xiaomi_ble/gen.py` writes `frames.hpp` and `expected.json`: plain MiBeacon (0x50), encrypted v5 23 byte
with MAC and 19 byte without, real AES-CCM (`cryptography` AESCCM, tag 4, AAD 0x11, nonce = MAC reversed + pid +
cnt + 3 byte ext counter), ATC (13 byte, big endian) and PVVX (15 byte, little endian) 0x181A frames, a corrupted
tag every other cycle and a counter 0 boot frame. It models the old shared duplicate counter (`base`) and the per
device one (`pr`) and prints the expected publish counts.
- `xbinject.yaml` (ESP-IDF devkit) includes `frames.hpp`; an `interval:` builds a `BLEScanResult` and calls
`global_esp32_ble_tracker->gap_scan_event_handler(r)`. `count.py` counts the `Got Xiaomi` blocks (sensor
publishes are not logged at DEBUG) and compares them with `expected.json`.
- On ESP32 the tracker's own triggers sit in `listeners_`, which runs before `neutral_listeners_` (the
ble_device_base devices), so an `on_ble_service_data_advertise` never sees what a Xiaomi listener did to the
shared data. Check that directly: `xb_rewrite_probe(id(xa))` builds an `ESPBTDevice` with `parse_scan_rst()`,
calls the component's `parse_device()` and logs the service data before and after (`[probe` lines).
- `run.sh inject pr|base` builds, flashes, captures and prints PASS when the counts match that build's model and,
on the PR build, the probe's service data is the same before and after `parse_device()`.
Base dropped a second device's frame with the same counter, the frame after a bad tag and a first frame with
counter 0, and rewrote the shared data in place; the PR fixed all of them.
## Over the air with a second ESP32
- Advertiser: an ESP32-S3 with Ethernet (OTA over Ethernet), `xbadv.yaml` with `adv_frames.hpp` from
`gen.py --ota`. Per frame: stop advertising, `esp_ble_gap_set_rand_addr`, `config_adv_data_raw`, start
non-connectable advertising with `own_addr_type` random, 20 ms interval, 250 ms dwell. Random static addresses
need the top two bits set, so these use C4:C1:38 instead of A4. `esp32_ble:` alone brings up the stack. The
web_server button starts a run: `curl -X POST -d "" http://<advertiser>/button/start/press` (no body gives 411).
Its `failed sends` sensor counts only GAP calls rejected when queued; a frame the controller drops later shows
up as missing publishes in `count.py`, the same as radio loss.
- Receiver: `xbrecv.yaml` on the devkit, passive scan with window = interval = 100 ms. `run.sh ota pr|base` (with
`ADV` set) flashes it, resets it with a capture and presses start 18 s later, so the counter 0 boot frame is the
first one it hears.
- Each frame arrives about 4 times; the counts matched the injection run exactly apart from one radio loss, so the
over the air comparison allows two missing publishes per tag.
@@ -0,0 +1,23 @@
# Run artifacts the runners write next to their scripts; none of these patterns match a tracked file.
out-*/
out-*.*
.esphome/
secrets.yaml
__pycache__/
ble_adv_triggers/rxbk72xx*.yaml
ble_adv_triggers/rxesp32*.yaml
ezo_fake_bus/ezo?*.yaml
homeassistant_button/habtn*.yaml
hub_input_sim/hubsim*.yaml
interval/ivl*.yaml
led_driver_sniff/led*.yaml
ota_automations/otaauto*.yaml
output_button/outbtn*.yaml
remote_base/rb*.yaml
sensor_filters/sf?*.yaml
sun/sunesp*.yaml
twai_self/can?*.yaml
uart_button/uartbtn*.yaml
value_range/rng*.yaml
ld2420_sim/ldenergy*.yaml
ld2420_sim/ldsimple*.yaml
@@ -0,0 +1,221 @@
"""Pull the store_yaml blob off a device over the native API (plaintext or Noise) and unpack it.
usage: syrecover.py HOST KEY OUTDIR [ORIGDIR] (KEY of - means a plaintext API)
Prints the capability flag, chunk count, advertised and received sizes, encoding, the envelope's files, and
when ORIGDIR is given compares each recovered file with ORIGDIR/<path>.
Run with PYTHONPATH pointing at the esphome tree that has store_yaml (for unpack_envelope). Needs Python 3.14
(compression.zstd) or the zstandard package.
"""
import asyncio
from pathlib import Path
import sys
import time
from aioesphomeapi import api_pb2
from aioesphomeapi._frame_helper.noise import APINoiseFrameHelper
from aioesphomeapi._frame_helper.plain_text import APIPlaintextFrameHelper
from aioesphomeapi.connection import APIConnection, ConnectionParams
from aioesphomeapi.zeroconf import ZeroconfManager
from esphome.components.store_yaml import unpack_envelope
try:
from compression import zstd
except ImportError:
try:
import zstandard as zstd
except ImportError:
sys.exit("needs Python 3.14 (compression.zstd) or the zstandard package")
HOST, KEY, OUT = sys.argv[1], sys.argv[2], Path(sys.argv[3])
ORIG = Path(sys.argv[4]) if len(sys.argv) > 4 else None
HELLO, HELLO_RESP, CONNECT, CONNECT_RESP = 1, 2, 3, 4
DISCONNECT_REQ = 5
FATAL = -1 # queued by report_fatal_error so a waiting expect() stops at once
CAPS_REQ, CAPS_RESP = 149, 150
GET_YAML_REQ, GET_YAML_RESP = 161, 162
def decode_varint(buf: bytes, pos: int) -> tuple[int, int]:
value = shift = 0
while True:
b = buf[pos]
pos += 1
value |= (b & 0x7F) << shift
if not b & 0x80:
return value, pos
shift += 7
def fields(payload: bytes) -> dict[int, list]:
out: dict[int, list] = {}
pos = 0
while pos < len(payload):
key, pos = decode_varint(payload, pos)
fnum, wt = key >> 3, key & 7
if wt == 0:
v, pos = decode_varint(payload, pos)
elif wt == 2:
ln, pos = decode_varint(payload, pos)
v = payload[pos : pos + ln]
pos += ln
elif wt == 5:
v = payload[pos : pos + 4]
pos += 4
else:
raise ValueError(f"wire type {wt}")
out.setdefault(fnum, []).append(v)
return out
async def main() -> None:
loop = asyncio.get_running_loop()
queue: asyncio.Queue[tuple[int, bytes]] = asyncio.Queue()
errors: list[Exception] = []
class Shim(APIConnection):
"""Only the two callbacks the frame helper uses; the real connection logic is bypassed."""
def process_packet(self, msg_type_proto, data) -> None:
queue.put_nowait((msg_type_proto, bytes(data)))
def report_fatal_error(self, err: Exception) -> None:
errors.append(err)
queue.put_nowait((FATAL, b""))
params = ConnectionParams(
addresses=[HOST],
port=6053,
password=None,
client_info="syrecover",
keepalive=15,
zeroconf_manager=ZeroconfManager(),
noise_psk=None if KEY == "-" else KEY,
expected_name=None,
expected_mac=None,
timezone=None,
provide_time=True,
)
shim = Shim(params, None, False, HOST)
if KEY == "-":
helper = APIPlaintextFrameHelper(
connection=shim, client_info="syrecover", log_name=HOST
)
else:
helper = APINoiseFrameHelper(
connection=shim,
noise_psk=KEY,
expected_name=None,
expected_mac=None,
client_info="syrecover",
log_name=HOST,
)
await loop.create_connection(lambda: helper, HOST, 6053)
try:
await asyncio.wait_for(helper.ready_future, 10)
except Exception as exc: # noqa: BLE001
sys.exit(f"handshake failed: {exc!r} {errors}")
print(f"transport {'plaintext' if KEY == '-' else 'noise'}: handshake done")
async def expect(msg_type: int, timeout: float = 10) -> bytes:
while True:
t, d = await asyncio.wait_for(queue.get(), timeout)
if t == msg_type:
return d
if t == FATAL:
sys.exit(
f"transport error while waiting for message {msg_type}: {errors}"
)
if t == DISCONNECT_REQ:
sys.exit(
f"device closed the connection while waiting for message {msg_type}"
)
print(f"skipping message type {t} while waiting for {msg_type}")
hello = api_pb2.HelloRequest(
client_info="syrecover", api_version_major=1, api_version_minor=10
)
helper.write_packets([(HELLO, hello.SerializeToString())], False)
await expect(HELLO_RESP)
# Sent like the client library does; a device without a password never answers it
helper.write_packets(
[(CONNECT, api_pb2.AuthenticationRequest().SerializeToString())], False
)
helper.write_packets(
[(CAPS_REQ, api_pb2.DeviceCapabilitiesRequest().SerializeToString())], False
)
caps = fields(await expect(CAPS_RESP))
supported = None
if 7 in caps:
supported = fields(caps[7][0]).get(1) == [1]
print(f"capabilities: store_yaml field present={7 in caps} supported={supported}")
t0 = time.monotonic()
helper.write_packets([(GET_YAML_REQ, b"")], False)
chunks: list[bytes] = []
total = encoding = None
done = False
while not done:
f = fields(await expect(GET_YAML_RESP, 20))
if 1 in f:
chunks.append(f[1][0])
done = f.get(2) == [1]
if 3 in f:
total = f[3][0]
if 4 in f:
encoding = f[4][0].decode()
dt = time.monotonic() - t0
compressed = b"".join(chunks)
print(f"chunks {len(chunks)} sizes {[len(c) for c in chunks]}")
print(
f"received {len(compressed)} bytes, advertised {total}, encoding {encoding!r}, {dt:.2f} s"
)
# a second request right after: the device must answer again, not stay stuck
helper.write_packets([(GET_YAML_REQ, b"")], False)
again = 0
done = False
while not done:
f = fields(await expect(GET_YAML_RESP, 20))
again += len(f.get(1, [b""])[0])
done = f.get(2) == [1]
print(f"second request: {again} bytes")
helper.close()
if errors:
sys.exit(f"transport errors: {errors}")
if encoding != "zstd":
sys.exit(f"unexpected encoding {encoding!r}, expected 'zstd'")
if total is not None and len(compressed) != total:
sys.exit(f"received {len(compressed)} bytes but the device advertised {total}")
if again != len(compressed):
sys.exit(
f"the second request returned {again} bytes, the first {len(compressed)}"
)
envelope = zstd.decompress(compressed)
files = unpack_envelope(envelope)
print(
f"envelope {len(envelope)} bytes, {len(files)} files, ratio {len(compressed) / len(envelope):.2f}"
)
OUT.mkdir(parents=True, exist_ok=True)
for rel, content in files.items():
target = OUT / rel.replace("../", "up_/")
if not target.resolve().is_relative_to(OUT.resolve()):
print("skipping a path outside the output directory:", rel)
continue
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(content)
note = ""
if ORIG is not None:
orig = (ORIG / rel).resolve()
if orig.exists():
note = (
"IDENTICAL to original"
if orig.read_bytes() == content
else f"DIFFERS from original ({orig.stat().st_size} bytes)"
)
else:
note = "no original"
print(f" {rel}: {len(content)} bytes {note}")
asyncio.run(main())
@@ -0,0 +1,60 @@
"""Shared helpers for the API drivers: connect with retries, and the text of an API log line.
Import it with this directory on sys.path (the drivers add their parent directory, runners set PYTHONPATH).
"""
import asyncio
import contextlib
import os
import re
import sys
from aioesphomeapi import (
APIClient,
EncryptionPlaintextAPIError,
InvalidAuthAPIError,
InvalidEncryptionKeyAPIError,
RequiresEncryptionAPIError,
)
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
# a wrong key or a plaintext/Noise mismatch does not go away with a retry
FATAL = (
EncryptionPlaintextAPIError,
InvalidAuthAPIError,
InvalidEncryptionKeyAPIError,
RequiresEncryptionAPIError,
)
async def connect(host: str, tries: int = 30, timeout: float = 10) -> APIClient:
"""Connect and log in, retrying once a second; exits after TRIES failures with the last error.
API_KEY in the env selects Noise; unset, empty or - is plaintext.
"""
key = os.environ.get("API_KEY")
last: BaseException | None = None
for _ in range(tries):
c = APIClient(
host, 6053, None, noise_psk=None if key in (None, "", "-") else key
)
try:
await asyncio.wait_for(c.connect(login=True), timeout)
except FATAL as e:
sys.exit(f"no API connection to {host}: {type(e).__name__}: {e}")
except Exception as e: # noqa: BLE001
last = e
# release the half-open socket so retries do not use up the device's API slots
with contextlib.suppress(Exception):
await c.disconnect(force=True)
await asyncio.sleep(1)
else:
return c
sys.exit(
f"no API connection to {host} after {tries} tries: {type(last).__name__}: {last}"
)
def log_text(msg) -> str:
"""The message of a subscribe_logs callback as text, without ANSI colour codes."""
return ANSI.sub("", msg.message.decode(errors="replace"))
@@ -0,0 +1,31 @@
#pragma once
// Test only: swaps the first two records of an ATT Read By Type Response whose first record declares characteristic
// 0xFFF1, the way some peripherals answer out of handle order (esphome#22518). Count of swaps in attswap_count.
#include <cstdint>
#include <cstring>
#include "esphome/core/log.h"
struct AttSwapHdr {
uint16_t event, len, offset, layer_specific;
};
extern "C" void __real_l2c_rcv_acl_data(void *p_msg);
static volatile uint32_t attswap_count = 0;
extern "C" void __wrap_l2c_rcv_acl_data(void *p_msg) {
auto *h = static_cast<AttSwapHdr *>(p_msg);
uint8_t *p = reinterpret_cast<uint8_t *>(h + 1) + h->offset;
// HCI ACL header (4), L2CAP header (4), then ATT: opcode, record length, records
if (h->len >= 4 + 4 + 2 + 14) {
uint16_t cid = p[6] | (p[7] << 8);
uint8_t *att = p + 8;
// record: handle(2) properties(1) value handle(2) uuid16(2)
if (cid == 0x0004 && att[0] == 0x09 && att[1] == 7 && att[2 + 5] == 0xF1 && att[2 + 6] == 0xFF) {
uint8_t tmp[7];
memcpy(tmp, att + 2, 7);
memcpy(att + 2, att + 9, 7);
memcpy(att + 9, tmp, 7);
attswap_count = attswap_count + 1;
}
}
__real_l2c_rcv_acl_data(p_msg);
}
@@ -0,0 +1,11 @@
"""Test only: wrap Bluedroid's l2c_rcv_acl_data so attswap.h can reorder a Read By Type response."""
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.types import ConfigType
CONFIG_SCHEMA = cv.Schema({})
async def to_code(config: ConfigType) -> None:
cg.add_build_flag("-Wl,--wrap=l2c_rcv_acl_data")
@@ -0,0 +1,5 @@
substitutions:
name: attprx6
idf: 5.5.6
packages:
c: !include proxy.yaml
@@ -0,0 +1,125 @@
"""Through the proxy (no cache): print the full GATT table, read FFF1/FFF2/FFF3 by handle, and enable notify on
FFF1, FFF2 and 2ACD (writing the CCCD the way Home Assistant does), counting what arrives per handle for 6 s.
usage: gattcheck.py PROXY PEER_MAC env: API_KEY (the proxy's api encryption key)
Exits 1 when a characteristic or CCCD is missing, a read or notification carries another characteristic's value,
a notify fails, or a notifying characteristic sends nothing."""
import asyncio
import os
import sys
from aioesphomeapi import APIClient
SFX = "-0000-1000-8000-00805f9b34fb"
def short(u: str) -> str:
return u[4:8] if u.endswith(SFX) else u
async def main(host: str, peer: str) -> None:
addr = int(peer.replace(":", ""), 16)
cli = APIClient(host, 6053, None, noise_psk=os.environ["API_KEY"])
await cli.connect(login=True)
seen = asyncio.Event()
cli.subscribe_bluetooth_le_raw_advertisements(
lambda m: (
seen.set() if any(a.address == addr for a in m.advertisements) else None
)
)
await asyncio.wait_for(seen.wait(), 30)
state: asyncio.Queue = asyncio.Queue()
cancel = await cli.bluetooth_device_connect(
addr,
lambda c, mtu, err: state.put_nowait((c, mtu, err)),
timeout=20,
address_type=0,
feature_flags=0xFF,
has_cache=False,
)
c, mtu, err = await asyncio.wait_for(state.get(), 25)
if not c:
sys.exit(f"connect failed err={err}")
svcs = await cli.bluetooth_gatt_get_services(addr)
chars = {}
for s in svcs.services:
print(f"SVC {short(s.uuid)} 0x{s.handle:04X}", flush=True)
for ch in s.characteristics:
chars[short(ch.uuid)] = ch
print(
f" CHR {short(ch.uuid)} 0x{ch.handle:04X} props=0x{ch.properties:02X}",
flush=True,
)
for d in ch.descriptors:
print(f" DSC {short(d.uuid)} 0x{d.handle:04X}", flush=True)
wanted = ("fff1", "fff2", "fff3", "2acc", "2acd")
problems = [f"{u} missing" for u in wanted if u not in chars]
for u in ("fff1", "fff2", "fff3", "2acc"):
if u in chars:
v = bytes(await cli.bluetooth_gatt_read(addr, chars[u].handle))
text = v.decode(errors="replace")
print(f"READ {u} 0x{chars[u].handle:04X} -> {text}", flush=True)
# every server value starts with its own UUID, so a mixed-up handle shows here
if not text.startswith(f"{u}-"):
problems.append(f"{u} read returned {text!r}")
got: dict[int, list[str]] = {}
def on_notify(handle: int, data: bytearray) -> None:
got.setdefault(handle, []).append(bytes(data).decode(errors="replace"))
for u in ("fff1", "fff2", "2acd"):
if u in chars:
try:
await cli.bluetooth_gatt_start_notify(addr, chars[u].handle, on_notify)
# like Home Assistant's client: write the first CCCD listed under the characteristic
cccd = next(
(d for d in chars[u].descriptors if short(d.uuid) == "2902"), None
)
if cccd is None:
print(
f"NOTIFY {u} 0x{chars[u].handle:04X} NO CCCD in its descriptor list",
flush=True,
)
problems.append(f"{u} has no CCCD")
continue
await cli.bluetooth_gatt_write_descriptor(
addr, cccd.handle, b"\x01\x00"
)
print(
f"NOTIFY {u} 0x{chars[u].handle:04X} enabled, CCCD 0x{cccd.handle:04X} written",
flush=True,
)
except Exception as exc: # noqa: BLE001
print(
f"NOTIFY {u} 0x{chars[u].handle:04X} FAILED {type(exc).__name__}: {exc}",
flush=True,
)
problems.append(f"{u} notify failed")
await asyncio.sleep(6)
if not got:
print("GOT nothing", flush=True)
for h, vals in sorted(got.items()):
name = next((u for u, ch in chars.items() if ch.handle == h), "?")
print(
f"GOT 0x{h:04X} ({name}) x{len(vals)}: {vals[0]} .. {vals[-1]}", flush=True
)
problems.extend(
f"0x{h:04X} ({name}) notified {v!r}"
for v in vals
if name == "?" or not v.startswith(f"{name}-")
)
# the server notifies FFF1 and 2ACD every second
problems.extend(
f"{u} sent nothing"
for u in ("fff1", "2acd")
if u in chars and chars[u].handle not in got
)
await cli.bluetooth_device_disconnect(addr)
cancel()
await cli.disconnect()
if problems:
sys.exit("FAIL: " + ", ".join(problems))
print("PASS", flush=True)
asyncio.run(main(sys.argv[1], sys.argv[2]))
@@ -0,0 +1,56 @@
"""Connect to PEER_MAC through the proxy like Home Assistant (v3 without cache) and print the services it returns.
usage: gattget.py PROXY PEER_MAC ROUNDS env: API_KEY (the proxy's api encryption key)"""
import asyncio
import os
import sys
from aioesphomeapi import APIClient
async def main(host: str, peer: str, rounds: int) -> None:
addr = int(peer.replace(":", ""), 16)
cli = APIClient(host, 6053, None, noise_psk=os.environ["API_KEY"])
await cli.connect(login=True)
seen = asyncio.Event()
def adv(msg) -> None:
if any(a.address == addr for a in msg.advertisements):
seen.set()
cli.subscribe_bluetooth_le_raw_advertisements(adv)
await asyncio.wait_for(seen.wait(), 30)
failed = 0
for r in range(1, rounds + 1):
state: asyncio.Queue = asyncio.Queue()
cancel = await cli.bluetooth_device_connect(
addr,
lambda c, mtu, err, q=state: q.put_nowait((c, mtu, err)),
timeout=20,
address_type=0,
feature_flags=0xFF,
has_cache=False,
)
c, mtu, err = await asyncio.wait_for(state.get(), 25)
if not c:
print(f"R{r} connect failed err={err}", flush=True)
failed += 1
cancel()
await asyncio.sleep(5)
continue
svcs = await cli.bluetooth_gatt_get_services(addr)
sfx = "-0000-1000-8000-00805f9b34fb"
out = [
f"{s.uuid[4:8] if s.uuid.endswith(sfx) else s.uuid}({len(s.characteristics)})"
for s in svcs.services
]
print(f"R{r} mtu={mtu} services: {' '.join(out)}", flush=True)
await cli.bluetooth_device_disconnect(addr)
cancel()
await asyncio.sleep(5)
await cli.disconnect()
if failed:
sys.exit(f"{failed} of {rounds} rounds did not connect")
asyncio.run(main(sys.argv[1], sys.argv[2], int(sys.argv[3])))
@@ -0,0 +1,37 @@
# Proxy (classic ESP32): bluetooth_proxy without service cache, encrypted API, the reorder wrapper.
# Substitutions: name, idf.
# Keep the per-build file in this directory: attswap.hpp and components/ are found relative to it.
esphome:
name: ${name}
includes: [attswap.hpp]
esp32:
board: esp32dev
framework:
type: esp-idf
version: ${idf}
logger:
level: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
reboot_timeout: 0s
# a plaintext proxy can be taken over by any Home Assistant on the network mid-test
encryption:
key: !secret api_key
ota:
- platform: esphome
external_components:
- source: {type: local, path: components}
components: [attswap]
attswap:
esp32_ble_tracker:
scan_parameters:
active: true
bluetooth_proxy:
active: true
cache_services: false
interval:
- interval: 5s
then:
- lambda: 'ESP_LOGI("swap", "attswap swaps=%u", (unsigned) attswap_count);'
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
# run.sh PROXY_YAML [ROUNDS]: build server.yaml and PROXY_YAML from TREE, flash both, capture the proxy and read the
# server's services through it ROUNDS times (default 3). Prints the services per round and the Bluedroid discovery
# errors. Exit 0: every round got all services; 1: a round got a partial table (the bug); 2: setup failed, a round
# did not connect, or the swap never fired (the result would be meaningless). PROXY_YAML must sit in this
# directory, next to attswap.hpp and components/.
# env: TREE, SERVER_YAML (default server.yaml), API_KEY (proxy api key), SERVER_PORT, PROXY_PORT, SERVER_BLE_MAC
# (the server's BLE address), SC (path to serial_capture.py), PY
D=$(cd "$(dirname "$0")" && pwd); Y=$(cd "$(dirname "${1:?proxy yaml}")" && pwd)/$(basename "$1"); R=${2:-3}
W=${TREE:?set TREE}; SP=${SERVER_PORT:?set SERVER_PORT}; PP=${PROXY_PORT:?set PROXY_PORT}; MAC=${SERVER_BLE_MAC:?set SERVER_BLE_MAC}
API_KEY=${API_KEY:?set API_KEY}; SC=${SC:?set SC}; PY=${PY:-$W/venv/bin/python}; N=$(basename "$Y" .yaml); O=$D/out-$N; mkdir -p "$O"
(cd "$W" && PYTHONPATH=$W $PY -m esphome -s name attsrv run --no-logs "$D/${SERVER_YAML:-server.yaml}" --device "$SP") > "$O/server.log" 2>&1 ||
{ echo "server build or upload failed"; exit 2; }
(cd "$W" && PYTHONPATH=$W $PY -m esphome run --no-logs "$Y" --device "$PP") > "$O/proxy-upload.log" 2>&1 ||
{ echo "$N build or upload failed"; exit 2; }
rm -f "$O/proxy.txt"
$PY "$SC" "$PP" $((R * 30 + 160)) "$O/proxy.txt" --reset > "$O/capture.log" 2>&1 &
CAP=$!
IP=""
for _ in $(seq 40); do
IP=$(LC_ALL=C grep -a -o -E "IP Address: [0-9.]+" "$O/proxy.txt" | tail -1 | cut -d' ' -f3)
[ -n "$IP" ] && break; sleep 2
done
[ -n "$IP" ] || { kill "$CAP"; echo "$N no IP"; exit 2; }
sleep 5
timeout $((R * 30 + 60)) $PY "$D/gattget.py" "$IP" "$MAC" "$R" | tee "$O/client.txt"
[ "${PIPESTATUS[0]}" = 0 ] || { kill "$CAP"; echo "$N client failed"; exit 2; }
wait "$CAP" || { echo "$N capture failed (see $O/capture.log)"; exit 2; }
echo "== $N"
LC_ALL=C grep -a -E "invalid handle|Service discovery complete|attswap swaps=[1-9]" "$O/proxy.txt" | sed 's/^\[[0-9:]*\]//' |
sort | uniq -c
grep -a -q -E "attswap swaps=[1-9]" "$O/proxy.txt" || { echo "$N INVALID: the reorder never fired"; exit 2; }
# every round must list the reordered service (fff0) and the one after it (1826) with all their characteristics
grep -a -q "^R[0-9]* mtu=" "$O/client.txt" || { echo "$N no client output"; exit 2; }
grep -a "^R[0-9]* mtu=" "$O/client.txt" | grep -v -q "fff0(6) 1826(2)" && exit 1
exit 0
@@ -0,0 +1,45 @@
# Peripheral (ESP32-S3) for gattcheck.py: FFF1 to FFF6 each hold their own value, FFF1, FFF2 and 2ACD can notify,
# and FFF1 and 2ACD send a changing value every second. Substitution: name.
esphome:
name: ${name}
esp32:
variant: esp32s3
framework:
type: esp-idf
logger:
hardware_uart: USB_SERIAL_JTAG
esp32_ble:
name: ${name}
esp32_ble_server:
manufacturer: ESPHome
model: attcheck
services:
- uuid: FFF0
advertise: true
characteristics:
- {id: c1, uuid: FFF1, read: true, notify: true, value: "fff1-v"}
- {uuid: FFF2, read: true, notify: true, value: "fff2-v"}
- {uuid: FFF3, read: true, value: "fff3-v"}
- {uuid: FFF4, read: true, value: "fff4-v"}
- {uuid: FFF5, read: true, value: "fff5-v"}
- {uuid: FFF6, read: true, value: "fff6-v"}
- uuid: "1826"
characteristics:
- {uuid: 2ACC, read: true, value: "2acc-v"}
- {id: cd, uuid: 2ACD, read: true, notify: true, value: "2acd-0"}
on_connect:
- logger.log: {format: "SRV connected", level: INFO}
globals:
- {id: n, type: int, initial_value: "0"}
interval:
- interval: 1s
then:
- lambda: id(n)++;
- ble_server.characteristic.set_value:
id: cd
value: !lambda 'std::string v = "2acd-" + to_string(id(n)); return std::vector<uint8_t>(v.begin(), v.end());'
- ble_server.characteristic.notify: {id: cd}
- ble_server.characteristic.set_value:
id: c1
value: !lambda 'std::string v = "fff1-n" + to_string(id(n)); return std::vector<uint8_t>(v.begin(), v.end());'
- ble_server.characteristic.notify: {id: c1}
@@ -0,0 +1,32 @@
# Peripheral (ESP32-S3): a 6-characteristic service 0xFFF0, then an FTMS-like 0x1826 service. Substitution: name.
esphome:
name: ${name}
esp32:
variant: esp32s3
framework:
type: esp-idf
logger:
hardware_uart: USB_SERIAL_JTAG
esp32_ble:
name: ${name}
esp32_ble_server:
manufacturer: ESPHome
model: g22518
services:
- uuid: FFF0
advertise: true
characteristics:
- {uuid: FFF1, read: true, value: [1]}
- {uuid: FFF2, read: true, value: [2]}
- {uuid: FFF3, read: true, value: [3]}
- {uuid: FFF4, read: true, value: [4]}
- {uuid: FFF5, read: true, value: [5]}
- {uuid: FFF6, read: true, value: [6]}
- uuid: "1826"
characteristics:
- {uuid: 2ACC, read: true, value: [0, 0, 0, 0]}
- {uuid: 2ACD, read: true, notify: true, value: [0, 0]}
on_connect:
- logger.log: {format: "SRV connected", level: INFO}
on_disconnect:
- logger.log: {format: "SRV disconnected", level: INFO}
@@ -0,0 +1,348 @@
# Automation code generation trace: every step logs a marker "M <case> <values>". A package: the per-run file from
# run.sh supplies the board, Wi-Fi and name. drive.py records the markers over the API.
logger:
level: DEBUG
api:
reboot_timeout: 0s
ota:
- platform: esphome
globals:
- {id: g_a, type: int, initial_value: "0"}
- {id: g_b, type: int, initial_value: "0"}
- {id: g_tick, type: int, initial_value: "0"}
- {id: g_ticking, type: bool, initial_value: "false"}
- {id: g_par, type: int, initial_value: "0"}
- {id: g_par3, type: int, initial_value: "0"}
- {id: g_par4, type: int, initial_value: "0"}
interval:
- interval: 100ms
then:
- if:
condition:
lambda: return id(g_ticking);
then:
- lambda: id(g_tick)++;
number:
- platform: template
id: num
name: Num
optimistic: true
min_value: 0
max_value: 100
step: 1
on_value:
- logger.log: {format: "M num_on_value %.0f", args: [x]}
- if:
condition:
lambda: return x > 50;
then:
- logger.log: {format: "M num_high %.0f", args: [x]}
else:
- logger.log: {format: "M num_low %.0f", args: [x]}
switch:
- platform: template
id: sw
name: Sw
optimistic: true
on_turn_on:
- logger.log: "M sw_on"
- globals.set: {id: g_b, value: !lambda "return id(g_b) + 1;"}
- logger.log: {format: "M sw_on_b %d", args: [id(g_b)]}
on_turn_off:
- logger.log: "M sw_off"
select:
- platform: template
id: sel
name: Sel
optimistic: true
options: [a, b, c]
initial_option: a
on_value:
- logger.log: {format: "M sel_on_value %s idx %d", args: [x.c_str(), (int) i]}
sensor:
- platform: template
id: sens
name: Sens
update_interval: never
on_value_range:
- above: 50
then:
- logger.log: {format: "M sens_above %.0f", args: [x]}
- below: 10
then:
- logger.log: {format: "M sens_below %.0f", args: [x]}
alarm_control_panel:
- platform: template
id: acp
name: Acp
codes: ["1234"]
requires_code_to_arm: false
arming_away_time: 0s
on_armed_away:
- logger.log: "M acp_armed_away"
on_disarmed:
- logger.log: "M acp_disarmed"
on_state:
- logger.log: "M acp_state"
button:
- platform: template
name: Run All
on_press:
- logger.log: "M press_run_all"
- script.execute: master
- platform: template
name: Chain Button
on_press:
- logger.log: "M cb_1"
- lambda: id(g_a) = 7;
- globals.set: {id: g_b, value: "3"}
- logger.log: {format: "M cb_2 %d %d", args: [id(g_a), id(g_b)]}
- switch.toggle: sw
- logger.log: "M cb_3"
script:
- id: s_single
mode: single
parameters: {x: int}
then:
- logger.log: {format: "M single_start %d", args: [x]}
- delay: 150ms
- logger.log: {format: "M single_end %d", args: [x]}
- id: s_restart
mode: restart
parameters: {x: int, s: string}
then:
- logger.log: {format: "M restart_start %d %s", args: [x, s.c_str()]}
- delay: 150ms
- logger.log: {format: "M restart_end %d %s", args: [x, s.c_str()]}
- id: s_queued
mode: queued
max_runs: 5
parameters: {x: int}
then:
- logger.log: {format: "M queued_start %d", args: [x]}
- delay: 60ms
- logger.log: {format: "M queued_end %d", args: [x]}
- id: s_parallel
mode: parallel
parameters: {x: int}
then:
- logger.log: {format: "M parallel_start %d", args: [x]}
- delay: !lambda "return 50 + x * 30;"
- logger.log: {format: "M parallel_end %d", args: [x]}
- id: s_selfstop
mode: single
then:
- logger.log: "M selfstop_1"
- lambda: id(g_a) = 100;
- script.stop: s_selfstop
- logger.log: "M selfstop_SHOULD_NOT_RUN"
- lambda: id(g_a) = 200;
- id: s_par_self
mode: parallel
max_runs: 2
then:
- lambda: id(g_par)++;
- logger.log: {format: "M parself_run %d running=%d", args: [id(g_par), (int) id(s_par_self).is_running()]}
- if:
condition:
lambda: return id(g_par) < 4;
then:
- script.execute: s_par_self
- delay: 200ms
- id: s_par3
mode: parallel
max_runs: 3
then:
- lambda: id(g_par3)++;
- logger.log: {format: "M par3_run %d", args: [id(g_par3)]}
- if:
condition:
lambda: return id(g_par3) < 8;
then:
- script.execute: s_par3
- delay: 200ms
- id: s_par4
mode: parallel
max_runs: 4
then:
- lambda: id(g_par4)++;
- logger.log: {format: "M par4_run %d", args: [id(g_par4)]}
- if:
condition:
lambda: return id(g_par4) < 8;
then:
- script.execute: s_par4
- delay: 200ms
- id: master
mode: single
then:
- logger.log: "M c1_chain_start"
- lambda: id(g_a) = 1;
- globals.set: {id: g_b, value: "2"}
- logger.log: {format: "M c1_vals %d %d", args: [id(g_a), id(g_b)]}
- switch.turn_on: sw
- number.set: {id: num, value: 42}
- select.set: {id: sel, option: b}
- lambda: |-
int local = id(g_a) + id(g_b);
if (local > 0) { ESP_LOGI("main", "M c1_lambda_local %d", local); return; }
ESP_LOGI("main", "M c1_lambda_NOT_REACHED");
- logger.log: "M c1_after_return"
- logger.log: "M c2_delay_chain_start"
- delay: 50ms
- logger.log: "M c2_after_delay"
- lambda: id(g_a) = 5;
- delay: 0ms
- logger.log: {format: "M c2_end %d", args: [id(g_a)]}
- logger.log: "M c3_if_start"
- repeat:
count: 4
then:
- if:
condition:
and:
- lambda: return iteration % 2 == 0;
- not:
lambda: return iteration == 2;
then:
- logger.log: {format: "M c3_even_not2 %d", args: [iteration]}
else:
- if:
condition:
or:
- lambda: return iteration == 1;
- xor:
- lambda: return iteration == 2;
- lambda: return false;
then:
- logger.log: {format: "M c3_or_xor %d", args: [iteration]}
else:
- logger.log: {format: "M c3_else %d", args: [iteration]}
- logger.log: "M c4_choose_start"
- repeat:
count: 3
then:
- choose:
- if:
lambda: return iteration == 0;
then:
- logger.log: {format: "M c4_choice0 %d", args: [iteration]}
- if:
lambda: return iteration == 1;
then:
- logger.log: {format: "M c4_choice1 %d", args: [iteration]}
- else:
- logger.log: {format: "M c4_default %d", args: [iteration]}
- logger.log: "M c5_while_start"
- globals.set: {id: g_a, value: "0"}
- while:
condition:
lambda: return id(g_a) < 4;
then:
- logger.log: {format: "M c5_while %d", args: [id(g_a)]}
- lambda: id(g_a)++;
- logger.log: {format: "M c5_while_done %d", args: [id(g_a)]}
- globals.set: {id: g_a, value: "0"}
- while:
condition:
lambda: return id(g_a) < 3;
then:
- logger.log: {format: "M c5_while_delay %d", args: [id(g_a)]}
- delay: 20ms
- lambda: id(g_a)++;
- logger.log: {format: "M c5_while_delay_done %d", args: [id(g_a)]}
- logger.log: "M c6_repeat_start"
- repeat:
count: 2
then:
- lambda: id(g_b) = iteration;
- repeat:
count: 3
then:
- logger.log: {format: "M c6_nested outer=%d inner=%d", args: [id(g_b), iteration]}
- repeat:
count: !lambda "return id(g_b) + 2;"
then:
- logger.log: {format: "M c6_dyn %d", args: [iteration]}
- delay: 10ms
- logger.log: "M c7_wait_until_start"
- lambda: |-
id(g_tick) = 0;
id(g_ticking) = true;
- wait_until:
condition:
lambda: return id(g_tick) >= 3;
timeout: 2s
- logger.log: {format: "M c7_waited %d", args: [(int) (id(g_tick) >= 3)]}
- wait_until:
condition:
for:
time: 300ms
condition:
lambda: return id(g_tick) >= 3;
timeout: 3s
- lambda: id(g_ticking) = false;
- logger.log: "M c7_for_done"
- logger.log: "M c8_scripts_start"
- script.execute: {id: s_single, x: 1}
- script.execute: {id: s_single, x: 2}
- script.wait: s_single
- script.execute: {id: s_restart, x: 1, s: one}
- delay: 40ms
- script.execute: {id: s_restart, x: 2, s: two}
- script.wait: s_restart
- script.execute: {id: s_queued, x: 1}
- script.execute: {id: s_queued, x: 2}
- script.execute: {id: s_queued, x: 3}
- script.wait: s_queued
- script.execute: {id: s_parallel, x: 2}
- script.execute: {id: s_parallel, x: 1}
- script.wait: s_parallel
- logger.log: "M c9_selfstop_start"
- script.execute: s_selfstop
- script.wait: s_selfstop
- logger.log: {format: "M c9_selfstop_g_a %d", args: [id(g_a)]}
- logger.log: "M c10_sensor_start"
- sensor.template.publish: {id: sens, state: 60}
- sensor.template.publish: {id: sens, state: 30}
- sensor.template.publish: {id: sens, state: 5}
- logger.log: "M c11_acp_start"
- alarm_control_panel.arm_away: acp
- delay: 100ms
- alarm_control_panel.disarm: {id: acp, code: "1234"}
- delay: 100ms
- logger.log: "M c12_parself_start"
- globals.set: {id: g_par, value: "0"}
- script.execute: s_par_self
- delay: 800ms
- logger.log: {format: "M c12_parself_total %d", args: [id(g_par)]}
- logger.log: "M c13_parmax_start"
- globals.set: {id: g_par3, value: "0"}
- script.execute: s_par3
- delay: 800ms
- logger.log: {format: "M c13_par3_total %d", args: [id(g_par3)]}
- globals.set: {id: g_par4, value: "0"}
- script.execute: s_par4
- delay: 800ms
- logger.log: {format: "M c13_par4_total %d", args: [id(g_par4)]}
- switch.turn_off: sw
- logger.log: "M done"
esphome:
on_boot:
priority: -100
then:
- logger.log: "M boot"
- delay: 8s
- script.execute: master
@@ -0,0 +1,72 @@
"""Record every 'M ...' marker from the device log while driving the automation trace config.
usage: drive.py HOST OUT env: API_KEY (unset for a plaintext API)
Order: wait for the boot run's 'M done', press Run All and wait for 'M done' again, press Chain Button twice, set Num
70 then 20, toggle Sw on and off, select c then a. Writes one marker per line to OUT (timestamps dropped). Exits 1
when the Run All pass never reached 'M done'.
"""
import asyncio
import pathlib
import re
import sys
from aioesphomeapi import LogLevel
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
from apiconn import connect, log_text # noqa: E402
HOST, OUT = sys.argv[1], sys.argv[2]
marks: list[str] = []
done = asyncio.Event()
async def main() -> bool:
cli = await connect(HOST)
def on_log(m) -> None:
if mm := re.search(r"\]: (M .*)$", log_text(m)):
marks.append(mm.group(1).strip())
if mm.group(1).strip() == "M done":
done.set()
cli.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_DEBUG)
infos, _ = await cli.list_entities_services()
by = {i.name: i for i in infos}
try:
await asyncio.wait_for(done.wait(), 40)
except TimeoutError:
marks.append("DRIVER boot run not seen")
marks.append("DRIVER press Run All")
done.clear()
cli.button_command(by["Run All"].key)
ok = True
try:
await asyncio.wait_for(done.wait(), 40)
except TimeoutError:
marks.append("DRIVER run all timed out")
ok = False
steps = (
("cb1", lambda: cli.button_command(by["Chain Button"].key)),
("cb2", lambda: cli.button_command(by["Chain Button"].key)),
("num70", lambda: cli.number_command(by["Num"].key, 70.0)),
("num20", lambda: cli.number_command(by["Num"].key, 20.0)),
("swon", lambda: cli.switch_command(by["Sw"].key, True)),
("swoff", lambda: cli.switch_command(by["Sw"].key, False)),
("selc", lambda: cli.select_command(by["Sel"].key, "c")),
("sela", lambda: cli.select_command(by["Sel"].key, "a")),
)
for label, send in steps:
marks.append(f"DRIVER {label}")
send()
await asyncio.sleep(0.8)
await asyncio.sleep(1)
await cli.disconnect()
return ok
OK = asyncio.run(main())
pathlib.Path(OUT).write_text("\n".join(marks) + "\n", encoding="utf-8")
print(f"{len(marks)} lines")
sys.exit(0 if OK else 1)
@@ -0,0 +1,40 @@
#!/bin/bash
# run.sh LABEL: build the automation trace config as at<LABEL> from TREE, flash it over PORT, read the device address
# from the serial log, record the marker trace with drive.py and keep the part from "DRIVER press Run All" on in
# out-at<LABEL>.trace. Diff the .trace files of dev and the PR; the boot run is partly missed while the API connects.
# Run it from a scratch copy of this directory with a secrets.yaml (wifi_ssid, wifi_password).
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (checkout python),
# BOARD (esp32 board, default esp32dev), VARIANT (default esp32), API_KEY (only if you add api encryption)
R=$(cd "$(dirname "$0")" && pwd); L=${1:?label}; T=${TREE:?set TREE}; P=${PORT:?set PORT}
PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}; NAME=at$L; OUT=$R/out-$NAME; rm -rf "$OUT"; mkdir -p "$OUT"
cat > "$R/$NAME.yaml" <<YAML
esphome:
name: $NAME
esp32:
board: ${BOARD:-esp32dev}
variant: ${VARIANT:-esp32}
framework:
type: esp-idf
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
packages:
trace: !include automation-trace-common.yaml
YAML
pkill -f "[s]erial_capture.py $P"
(cd "$T" && $PY -m esphome run --no-logs "$R/$NAME.yaml" --device "$P" > "$OUT/upload.log" 2>&1) || { echo "build or upload failed"; exit 1; }
grep -a -E '^(RAM|Flash):' "$OUT/upload.log"
$PY "$SC" "$P" 120 "$OUT/serial.txt" --reset > /dev/null 2>&1 &
CAP=$!; trap 'kill $CAP 2>/dev/null' EXIT
H=; for _ in $(seq 60); do
H=$(grep -a -o -E 'IP Address: [0-9.]+' "$OUT/serial.txt" 2>/dev/null | tail -1 | awk '{print $3}'); [ -n "$H" ] && break; sleep 1
done
[ -n "$H" ] || { echo "FAIL: no IP address in the serial log"; exit 1; }
PYTHONPATH=$(dirname "$SC") $PY "$R/drive.py" "$H" "$OUT/markers.txt"; RC=$?
sed -n '/^DRIVER press Run All/,$p' "$OUT/markers.txt" > "$R/out-$NAME.trace"
CR=$(grep -a -c -E 'Guru|abort\(\)|Backtrace|task_wdt' "$OUT/serial.txt")
echo "$NAME: $(wc -l < "$R/out-$NAME.trace") trace lines, crashes $CR"
[ "$CR" = 0 ] || { echo "FAIL: a crash or watchdog in the serial log"; exit 1; }
[ "$RC" = 0 ] || { echo "FAIL: Run All never reached M done"; exit 1; }
grep -q -E "SHOULD_NOT_RUN|NOT_REACHED" "$R/out-$NAME.trace" && { echo "FAIL: a step that must be skipped ran"; exit 1; }
echo PASS
@@ -0,0 +1,64 @@
"""Compare two full flash dumps of a BK72xx board per partition of its LibreTiny board layout.
usage: partdiff.py BEFORE.bin AFTER.bin LAYOUT.json
LAYOUT.json is a board base file from the LibreTiny platform (for example boards/_base/beken-72xx.json); its
"flash" map ("name": "0xSTART+0xLENGTH") names the partitions. Prints the 4 KB sectors that changed in each one,
plus any changed sector outside the map (a WARN, to check by hand). Exits 1 when the bootloader, calibration or
userdata partition changed.
"""
import json
import pathlib
import sys
before, after = (pathlib.Path(p).read_bytes() for p in sys.argv[1:3])
if len(before) != len(after):
sys.exit(
f"dump sizes differ: {len(before)} and {len(after)} bytes; compare two whole chip reads"
)
parts = []
for name, span in json.loads(pathlib.Path(sys.argv[3]).read_text(encoding="utf-8"))[
"flash"
].items():
start, length = (int(x, 16) for x in span.split("+"))
parts.append((start, start + length, name))
parts.sort()
PROTECTED = ("bootloader", "calibration", "userdata")
if missing := [n for n in PROTECTED if n not in {name for _, _, name in parts}]:
sys.exit(
f"the layout has no {', '.join(missing)} partition; use the Beken board base file"
)
if (end := max(e for _, e, _ in parts)) > len(before):
sys.exit(
f"the layout reaches {end:#x} but the dumps are {len(before):#x} bytes; read the whole chip"
)
SECTOR = 0x1000
covered = set()
protected = []
for start, end, name in parts:
changed = [
a
for a in range(start, end, SECTOR)
if before[a : a + SECTOR] != after[a : a + SECTOR]
]
covered.update(range(start, end, SECTOR))
if changed and name in PROTECTED:
protected.append(name)
listed = " ".join(hex(a) for a in changed[:4]) if 0 < len(changed) <= 4 else ""
print(
f"{name:12} {len(changed):4} of {(end - start) // SECTOR:4} sectors changed {listed}"
)
other = [
a
for a in range(0, len(before), SECTOR)
if a not in covered and before[a : a + SECTOR] != after[a : a + SECTOR]
]
print(
f"{'unmapped':12} {len(other):4} sectors changed {' '.join(hex(a) for a in other[:8])}"
)
if other:
print(
"WARN: sectors outside the layout changed; check what they held in the backup"
)
if protected:
sys.exit(f"FAIL: {', '.join(protected)} changed")
@@ -0,0 +1,19 @@
#!/bin/bash
# usage: readback.sh TAG [START LENGTH] read flash from a BK72xx over its adapter into TAG.bin (whole chip by default)
# env: PORT (adapter), FAMILY (ltchiptool family, default beken-7238), FLASH_BK (path to flash_bk72xx.py),
# LT (LibreTiny penv python, default ~/.platformio/penv/.libretiny/bin/python), SIZE (whole chip bytes, default
# 2097152). A whole 2 MB chip takes about 190 s. The read must come back at the expected size.
T=${1:?tag}; P=${PORT:?set PORT}; FB=${FLASH_BK:?set FLASH_BK}; LT=${LT:-$HOME/.platformio/penv/.libretiny/bin/python}
ARGS=(); [ -n "$2" ] && ARGS=(-s "$2" -l "${3:?length}")
WANT=$(( ${3:-${SIZE:-2097152}} ))
: > "$T.log"
ok=
for i in 1 2 3; do
echo "### try $i" >> "$T.log"
rm -f "$T.bin"
timeout 900 "$LT" "$FB" flash read -d "$P" "${ARGS[@]}" "${FAMILY:-beken-7238}" "$T.bin" >> "$T.log" 2>&1 &&
[ "$(wc -c < "$T.bin" | tr -d ' ')" = "$WANT" ] && ok=1 && break
sleep 3
done
[ -n "$ok" ] || { echo "FAIL: read $T (expected $WANT bytes, see $T.log)"; exit 1; }
shasum -a 256 "$T.bin"
@@ -0,0 +1,61 @@
#pragma once
// Rotating non-connectable advertisements with known service and manufacturer data (random static addresses).
#include <esp_gap_ble_api.h>
#include <cstring>
#include "esphome/core/log.h"
namespace af {
struct Frame {
uint8_t last; // address C4:C1:38:00:00:<last>
uint8_t len;
uint8_t adv[31];
};
// AD structures after the flags. 0x16 = service data 16-bit, 0x20 = 32-bit, 0x21 = 128-bit, 0xFF = manufacturer.
static const Frame FRAMES[] = {
// 21: service 181C AA01 + manufacturer 02E5 BB01
{0x21, 12, {5, 0x16, 0x1C, 0x18, 0xAA, 0x01, 5, 0xFF, 0xE5, 0x02, 0xBB, 0x01}},
// 22: service 32-bit 12345678 AA02
{0x22, 8, {7, 0x20, 0x78, 0x56, 0x34, 0x12, 0xAA, 0x02}},
// 23: service 128-bit 12345678-9ABC-DEF0-1122-334455667788 (wire order) AA03
{0x23, 20, {19, 0x21, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
0xF0, 0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12, 0xAA, 0x03}},
// 24: manufacturer 181C BB04 (same id as the service UUID of 21, other data kind)
{0x24, 6, {5, 0xFF, 0x1C, 0x18, 0xBB, 0x04}},
// 25: service 181C AA05 then a second 181C entry AA06 (only the first may fire)
{0x25, 12, {5, 0x16, 0x1C, 0x18, 0xAA, 0x05, 5, 0x16, 0x1C, 0x18, 0xAA, 0x06}},
// 26: the 128-bit UUID in printed byte order AA07 (must not match)
{0x26, 20, {19, 0x21, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0xAA, 0x07}},
};
static constexpr int COUNT = sizeof(FRAMES) / sizeof(FRAMES[0]);
static uint32_t failed = 0;
static uint32_t sent = 0;
inline void send(int i) {
const Frame &f = FRAMES[i];
uint8_t adv[31];
uint8_t n = 0;
adv[n++] = 2;
adv[n++] = 0x01;
adv[n++] = 0x06;
memcpy(adv + n, f.adv, f.len);
n += f.len;
esp_ble_gap_stop_advertising();
uint8_t addr[6] = {0xC4, 0xC1, 0x38, 0x00, 0x00, f.last};
esp_err_t e1 = esp_ble_gap_set_rand_addr(addr);
esp_err_t e2 = esp_ble_gap_config_adv_data_raw(adv, n);
esp_ble_adv_params_t p{};
p.adv_int_min = 0x20; // 20 ms
p.adv_int_max = 0x20;
p.adv_type = ADV_TYPE_NONCONN_IND;
p.own_addr_type = BLE_ADDR_TYPE_RANDOM;
p.channel_map = ADV_CHNL_ALL;
p.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY;
esp_err_t e3 = esp_ble_gap_start_advertising(&p);
sent++;
if (e1 != ESP_OK || e2 != ESP_OK || e3 != ESP_OK) {
failed++;
ESP_LOGW("af", "frame %d failed: %d %d %d", i, e1, e2, e3);
}
}
} // namespace af
@@ -0,0 +1,41 @@
# Advertiser for the service and manufacturer data trigger test: any ESP32 with BLE. Every 250 ms it switches to
# the next of six raw non-connectable advertisements in adv_frames.hpp (addresses C4:C1:38:00:00:21 to 26).
esphome:
name: advtrig
includes:
- adv_frames.hpp
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
framework:
type: esp-idf
logger:
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
reboot_timeout: 0s
ota:
- platform: esphome
esp32_ble:
globals:
- id: idx
type: int
initial_value: "0"
interval:
- interval: 250ms
then:
- lambda: |-
af::send(id(idx));
id(idx) = (id(idx) + 1) % af::COUNT;
- interval: 30s
then:
- lambda: ESP_LOGI("af", "sent %u, failed %u", (unsigned) af::sent, (unsigned) af::failed);
@@ -0,0 +1,72 @@
"""check.py CAPTURE [--zero-mac any|filter]: judge the last count report of a receiver capture.
Expected per trigger (see triggers.yaml): T1 AA01+AA05, T2 AA01, T3 nothing, T4 AA02, T5 AA03, T6 BB01, T7 BB04,
T8 the same count as T2, T9 (mac 00:00:00:00:00:00) AA01+AA05 with --zero-mac any or nothing with filter,
T10 AA03, T11 nothing. "other" on a trigger without a MAC filter is another device in range and is only reported.
Prints PASS or FAIL with the reason and exits 0 or 1.
"""
import argparse
import pathlib
import re
import sys
NO_MAC = {1, 7, 10}
def main() -> int:
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("capture")
parser.add_argument("--zero-mac", choices=["any", "filter"], default="filter")
args = parser.parse_args()
zero = args.zero_mac
text = pathlib.Path(args.capture).read_text(encoding="utf-8", errors="replace")
rows: dict[int, dict[str, int]] = {}
for m in re.finditer(r"count T(\d+):([^\n]*)", text):
counts = dict(re.findall(r"(AA\d\d|BB\d\d|other)=(\d+)", m.group(2)))
rows[int(m.group(1))] = {k: int(v) for k, v in counts.items()}
if len(rows) != 11:
print(
f"FAIL: found {len(rows)} trigger rows, want 11 (no report in the capture?)"
)
return 1
want = {
1: {"AA01", "AA05"},
2: {"AA01"},
3: set(),
4: {"AA02"},
5: {"AA03"},
6: {"BB01"},
7: {"BB04"},
8: {"AA01"},
9: {"AA01", "AA05"} if zero == "any" else set(),
10: {"AA03"},
11: set(),
}
# With --zero-mac any, the all-zero MAC means no filter, so T9 also hears other devices
no_mac = NO_MAC | {9} if zero == "any" else NO_MAC
bad = []
for t, keys in want.items():
got = {k for k in rows[t] if k != "other"}
if got != keys:
bad.append(f"T{t} fired on {sorted(got)}, want {sorted(keys)}")
if rows[t].get("other") and t not in no_mac:
bad.append(
f"T{t} has other={rows[t]['other']} although it filters on a MAC"
)
if keys and min(rows[t].get(k, 0) for k in keys) < 10:
bad.append(
f"T{t} has fewer than 10 hits on one of {sorted(keys)}: {rows[t]}"
)
if rows[8] != rows[2]:
bad.append(f"T8 {rows[8]} differs from T2 {rows[2]} (same filter)")
for t in sorted(rows):
print(f"T{t}: {rows[t]}")
print("FAIL: " + "; ".join(bad) if bad else "PASS")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,21 @@
#!/bin/bash
# run.sh esp32|bk72xx LABEL: build rx-<kind>.yaml as rx<kind><LABEL> from TREE, flash it, capture 150 s, run check.py.
# The advertiser (advtrig.yaml) must already be running on a second ESP32 in range.
# env: TREE (esphome checkout to build from), PORT (receiver serial port), SC (path to serial_capture.py),
# PY (checkout python), BOARD (bk72xx board, default xh-wb3s), BKPY + BKFLASH (python and flash_bk72xx.py for
# bk72xx), ZERO_MAC (any or filter, what mac_address 00:00:00:00:00:00 should do on this build; default filter)
R=$(cd "$(dirname "$0")" && pwd); K=${1:?esp32 or bk72xx}; L=${2:?label}; T=${TREE:?set TREE}; P=${PORT:?set PORT}
PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}; NAME=rx$K$L; OUT=$R/out-$NAME; rm -rf "$OUT"; mkdir -p "$OUT"
sed -e "s/\${name}/$NAME/" -e "s/\${board}/${BOARD:-xh-wb3s}/" "$R/rx-$K.yaml" > "$R/$NAME.yaml"
pkill -f "[s]erial_capture.py $P"
if [ "$K" = esp32 ]; then
(cd "$T" && $PY -m esphome run --no-logs "$R/$NAME.yaml" --device "$P" > "$OUT/upload.log" 2>&1) || { echo "build or upload failed"; exit 1; }
$PY "$SC" "$P" 150 "$OUT/serial.txt" --reset > /dev/null 2>&1
else
(cd "$T" && $PY -m esphome compile "$R/$NAME.yaml" > "$OUT/compile.log" 2>&1) || { echo "build failed"; exit 1; }
${BKPY:?set BKPY} "${BKFLASH:?set BKFLASH}" flash write -d "$P" "$R/.esphome/build/$NAME/.pioenvs/$NAME/firmware.uf2" > "$OUT/flash.log" 2>&1 || { echo "flash failed"; exit 1; }
$PY "$SC" "$P" 150 "$OUT/serial.txt" > /dev/null 2>&1
fi
[ -s "$OUT/serial.txt" ] || { echo "serial capture got nothing"; exit 1; }
grep -a "adverts" "$OUT/serial.txt" | tail -1
$PY "$R/check.py" "$OUT/serial.txt" --zero-mac "${ZERO_MAC:-filter}"
@@ -0,0 +1,39 @@
# Receiver on a BK72xx board (bk72xx_ble_tracker). substitutions: name, board
# Wi-Fi must be configured: without it the BK7238 tracker logs "Scan started" and never receives anything.
esphome:
name: ${name}
includes:
- rx.hpp
bk72xx:
board: ${board}
logger:
hardware_uart: UART1
level: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
bk72xx_ble:
enable_on_boot: true
bk72xx_ble_tracker:
scan_parameters:
interval: 100ms
window: 30ms
continuous: true
<<: !include triggers.yaml
on_ble_advertise:
- then:
- lambda: |-
rx::all++;
if ((x.address_uint64() >> 8) == 0xC4C1380000ULL) rx::ours++;
interval:
- interval: 20s
then:
- lambda: |-
rx::report("count");
ESP_LOGI("rx", "adverts %u, from the advertiser %u", (unsigned) rx::all, (unsigned) rx::ours);
@@ -0,0 +1,32 @@
# Receiver on a classic ESP32 (esp32_ble_tracker). substitutions: name
esphome:
name: ${name}
includes:
- rx.hpp
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: DEBUG
esp32_ble_tracker:
scan_parameters:
interval: 100ms
window: 100ms
active: false
<<: !include triggers.yaml
on_ble_advertise:
- then:
- lambda: |-
rx::all++;
if ((x.address_uint64() >> 8) == 0xC4C1380000ULL) rx::ours++;
interval:
- interval: 20s
then:
- lambda: |-
rx::report("count");
ESP_LOGI("rx", "adverts %u, from the advertiser %u", (unsigned) rx::all, (unsigned) rx::ours);
@@ -0,0 +1,37 @@
#pragma once
// Per trigger counts of the received payloads: AAnn (service data) and BBnn (manufacturer data) from the advertiser,
// anything else counted as "other".
#include <cstdint>
#include <cstdio>
#include <vector>
#include "esphome/core/log.h"
namespace rx {
static constexpr int TRIGGERS = 12;
static uint32_t cnt[TRIGGERS][16];
static uint32_t other[TRIGGERS];
static uint32_t all, ours;
inline void hit(int t, const std::vector<uint8_t> &x) {
if (x.size() == 2 && (x[0] == 0xAA || x[0] == 0xBB) && x[1] >= 1 && x[1] <= 7) {
cnt[t][(x[0] == 0xBB ? 8 : 0) + x[1]]++;
} else {
other[t]++;
}
}
inline void report(const char *label) {
for (int t = 1; t < TRIGGERS; t++) {
char buf[160];
int n = snprintf(buf, sizeof(buf), "T%d:", t);
for (int k = 0; k < 16; k++) {
if (cnt[t][k])
n += snprintf(buf + n, sizeof(buf) - n, " %s%02u=%u", k >= 8 ? "BB" : "AA", (unsigned) (k & 7),
(unsigned) cnt[t][k]);
}
if (other[t])
n += snprintf(buf + n, sizeof(buf) - n, " other=%u", (unsigned) other[t]);
ESP_LOGI("rx", "%s %s", label, buf);
}
}
} // namespace rx
@@ -0,0 +1,44 @@
# Shared trigger block, included under the tracker key.
on_ble_service_data_advertise:
- service_uuid: 181C
then:
- lambda: rx::hit(1, x);
- mac_address: C4:C1:38:00:00:21
service_uuid: 181C
then:
- lambda: rx::hit(2, x);
- mac_address: C4:C1:38:00:00:22
service_uuid: 181C
then:
- lambda: rx::hit(3, x);
- mac_address: C4:C1:38:00:00:22
service_uuid: "12345678"
then:
- lambda: rx::hit(4, x);
- mac_address: C4:C1:38:00:00:23
service_uuid: 12345678-9ABC-DEF0-1122-334455667788
then:
- lambda: rx::hit(5, x);
- mac_address: C4:C1:38:00:00:21
service_uuid: 181C
then:
- lambda: rx::hit(8, x);
- mac_address: 00:00:00:00:00:00
service_uuid: 181C
then:
- lambda: rx::hit(9, x);
- service_uuid: 12345678-9ABC-DEF0-1122-334455667788
then:
- lambda: rx::hit(10, x);
on_ble_manufacturer_data_advertise:
- mac_address: C4:C1:38:00:00:21
manufacturer_id: 02E5
then:
- lambda: rx::hit(6, x);
- manufacturer_id: 181C
then:
- lambda: rx::hit(7, x);
- mac_address: C4:C1:38:00:00:22
manufacturer_id: 02E5
then:
- lambda: rx::hit(11, x);
Loaded 100 of 4673 files, more files were not shown because too many files have changed in this diff. Show more