Compare commits

...
Author SHA1 Message Date
J. Nick Koston ad82e8c078 Merge remote-tracking branch 'origin/dev' into neutral-ble-client 2026-10-26 05:51:44 -10: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 8ce475ccef Merge remote-tracking branch 'origin/dev' into neutral-ble-client 2026-10-25 07:57:38 -10: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
rwrozelleandClaude Opus 5.5 69785f56f6 [ota] Fit UDP OTA datagrams in Thread's 1280-byte IPv6 MTU (#21429)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:10:31 -10:00
J. Nick Koston 61b52c0a34 [esp32_hosted] Accept a verified image another build cached when Windows refuses the rename (#21431) 2026-10-23 00:10:00 +00:00
esphome[bot] 54c4cc81c5 Bump aioesphomeapi from 46.21.0 to 46.22.0 (#21434) 2026-10-23 00:09:49 +00:00
02ea1b2696 [seeed_multi_channel_relay] Add the Seeed Studio Grove multi-channel relay boards (#7233)
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>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-22 14:03:45 -10:00
Bonne EgglestonandJ. Nick Koston 16023df4fd [cw2017] Add CW2017 battery fuel gauge (#21369)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-22 13:58:54 -10:00
0fd72f74f1 [islandaire] Add Islandaire PTAC climate IR component (#20153)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-22 13:43:32 -10:00
sebo-b 4254d76e5c [ln882x_spi_led_strip] Add SPI DMA addressable LED strip for LN882x (#21409) 2026-10-22 13:30:06 -10:00
7605958db5 [spi] Add nRF52 (Zephyr) hardware SPI support (#18214)
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>
Co-authored-by: tomaszduda23 <tomaszduda23@gmail.com>
2026-10-22 13:20:26 -10:00
puddlyandJ. Nick Koston 35ea89859b [usb_uart] Implement hardware flow control (#20823)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-22 22:51:54 +00:00
J. Nick Koston fd7a0d3679 [components] Restore switches with Switch::restore() (#21428) 2026-10-22 12:48:39 -10:00
backcountrymountainsandJ. Nick Koston dc37e06143 [light] Add `effect to initial_state` (#18795)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-22 12:44:36 -10:00
9226e03496 [m5stack_4relay] m5stack 4relay switch implementation (#7234)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
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>
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@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-22 12:40:37 -10:00
J. Nick Koston a4fff7738b [ethernet] Check the RGMII ESP-IDF floor with the shared validator (#21430) 2026-10-22 18:29:56 -04:00
4242bcb084 [vnc] Add the Hextile encoding and skip unchanged tiles (#20625)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-22 21:52:53 +00:00
J. Nick Koston 8a88e97497 [weikai] Share print_buffer between the SPI and I2C drivers (#20721) 2026-10-22 21:21:41 +00:00
Frédéric MetrichandClaude d586e20a31 [teleinfo] Fix default units of unitless, text and PMAX tags (#21367)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-22 17:21:09 -04:00
e6161e64b1 [rx8111] Add alarm and wake-up timer support (#21217)
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-22 21:14:37 +00:00
J. Nick Koston 431843c27e [tests] Use LineWaiter for the integration tests that parse or count log lines (#21306) 2026-10-22 11:14:34 -10:00
J. Nick Koston a1da3d6b90 [unicode] Stop reading past a truncated UTF-8 sequence (#21374) 2026-10-22 11:14:17 -10:00
J. Nick Koston 930429061a [tests] Use LineWaiter for the integration tests that only wait on log lines (#21305) 2026-10-22 11:14:01 -10:00
J. Nick Kostonandpre-commit-ci-lite[bot] afceb91d90 [tests] Use wait_or_fail for the remaining integration test timeouts (#21304)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-22 11:13:55 -10:00
J. Nick Koston 7d741361a3 [weikai] Keep the register name tables in flash (#20712) 2026-10-22 11:13:04 -10:00
miloit 9c70ffd826 [ds3231] Add time platform, alarms, binary sensors and output control (#21054) 2026-10-22 10:58:28 -10:00
J. Nick Koston dac6a9c8cc [cover] Pack the cover traits flags into one byte (#21339) 2026-10-22 16:53:20 -04:00
J. Nick Koston a3a4144e4f [valve] Pack the valve traits flags into one byte (#21343) 2026-10-22 16:52:35 -04:00
J. Nick Koston 29e9ae008d [lock] Pack the lock traits flags into one byte (#21344) 2026-10-22 16:52:05 -04:00
Bonne Eggleston c92c4d7f99 [deep_sleep] Add woke_from_deep_sleep() and build get_wakeup_cause() without on_wake (#21425) 2026-10-22 10:49:15 -10:00
5bcc3f42e9 [switch] Add initial_state and restore_state, with flash or RTC storage (#20978)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-22 10:35:23 -10:00
Jonathan Swoboda d6aa5ba14c [esp32] Share one validator for options that need a newer ESP-IDF (#21422) 2026-10-22 10:20:22 -10:00
Clyde StubbsandClaude Opus 5.5 59c7178910 [runtime_image] Decode JPEG a step at a time (#21341)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 06:52:41 +11:00
J. Nick Koston e55741a26b [usb_uart] Read the channel limit when the config is validated, not at import (#21331) 2026-10-22 09:34:55 -10:00
dependabot[bot] 0bdc14154b Bump prek from 0.5.5 to 0.5.6 (#21419)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 19:32:52 +00:00
dependabot[bot] 1d6f839ee7 Bump platformdirs from 4.13.4 to 4.13.5 (#21418)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 19:32:20 +00:00
dependabot[bot] 2544518aca Bump filelock from 4.2.2 to 4.2.4 (#21417)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 19:32:17 +00:00
dependabot[bot] 70e56df630 Bump j178/prek-action from 3.0.1 to 3.0.2 (#21420)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-22 19:29:54 +00:00
Jonathan Swoboda 7d62aae996 [wifi] Allow the lease cache and a second WISP DNS server on the ESP32 Arduino framework (#21412) 2026-10-22 15:27:16 -04:00
c12baaa337 [mqtt] Support stop tilt on covers (#20251)
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-22 19:14:24 +00:00
Jonathan Swoboda cb9eda3681 [spi] Gate psram_dma on the ESP-IDF version rather than the framework (#21411) 2026-10-22 14:48:04 -04:00
J. Nick Koston 4e27957c74 [ci] Add the UART loopback, WISP bridge, raw API action and LD2412 flows to the hardware-test skill (#21356) 2026-10-22 08:46:37 -10:00
Roman PriesolandJ. Nick Koston 0856211b9b [voice_assistant] Allow IPv4 to be disabled (#21389)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-22 08:46:26 -10:00
J. Nick Koston ff4ff5c5f5 [restart][shutdown][factory_reset][safe_mode] Use Apply buttons (#21158) 2026-10-22 08:37:53 -10:00
J. Nick Koston 980b336668 [core] Let Apply entities run a free statement without a parent hub (#21157) 2026-10-22 08:37:52 -10:00
J. Nick Kostonandpre-commit-ci-lite[bot] a3cb3732ee [sx127x][sx126x][cc1101][udp][uart] Send byte payloads with ApplyBytes (#21263)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-22 08:19:09 -10:00
Jonathan Swoboda ab50b3830a [wifi] Allow WISP mode on the ESP32 Arduino framework (#21410) 2026-10-22 18:10:47 +00:00
Roman Priesol 3131cf28a3 [esphome] Fix OTA build with IPv4 disabled (#21388) 2026-10-22 08:07:17 -10:00
J. Nick Koston eb3394e332 Merge remote-tracking branch 'origin/dev' into neutral-ble-client
# Conflicts:
#	esphome/components/ble_client/ble_client.h
2026-10-21 07:23:08 -10:00
J. Nick Koston 88eedeec3f [ble_client] Run on_connect before the legacy engine releases the services 2026-10-20 19:08:38 -10:00
J. Nick Koston c9fe8b6b45 [ble_client] Run on_connect and on_disconnect as callback automations on both engines 2026-10-20 18:49:11 -10:00
J. Nick Koston b7e652399b Merge remote-tracking branch 'origin/dev' into pr-18205 2026-10-20 18:46:12 -10:00
J. Nick Koston 955998021c Merge remote-tracking branch 'origin/dev' into neutral-ble-client-fix
# Conflicts:
#	esphome/components/bluetooth_proxy/__init__.py
2026-10-20 15:28:55 -10:00
J. Nick Koston 5a6983822d Keep a reconnect from the disconnect trigger alive, and settle the cancel window
The neutral engine reports both the user disconnect trigger and the
connect action's abort hook from one callback list, so an on_disconnect
automation that calls ble_client.connect cancelled the attempt it had
just started and never ran the rest of its chain. The legacy engine
avoided this by keying the two on different events (CLOSE_EVT for the
trigger, DISCONNECT_EVT for the abort); the hook now skips the abort
while a fresh attempt is in flight.

connected() no longer reports a link with an accepted teardown in
flight, so a connect action started in that window stops taking the
already-connected fast path, and a second disconnect() in that window
is ignored rather than settling the link twice.

The write/notify completion contract claimed every completion arrives
from the event loop, which the rp2 backend contradicts for the two
local-only operations; it now names them and requires consumers to arm
their pending state before issuing an op.
2026-10-20 09:41:50 -10:00
J. Nick Koston 11a3372843 Merge remote-tracking branch 'origin/dev' into neutral-ble-client 2026-10-20 09:34:11 -10:00
J. Nick Koston e388c459b3 Merge remote-tracking branch 'origin/dev' into neutral-ble-client
# Conflicts:
#	esphome/components/ble_client/__init__.py
2026-10-18 07:35:20 -10:00
J. Nick Koston c83ea861eb Merge remote-tracking branch 'upstream/dev' into neutral-ble-client
# Conflicts:
#	esphome/components/ble_client/automation.cpp
2026-10-13 13:34:04 -10:00
J. Nick Koston 955afc4847 Merge remote-tracking branch 'upstream/dev' into neutral-ble-client 2026-10-13 10:12:14 -10:00
J. Nick Koston 7a01c71022 Merge remote-tracking branch 'upstream/dev' into neutral-ble-client
# Conflicts:
#	esphome/components/ble_client/__init__.py
2026-10-10 09:25:46 -10:00
J. Nick Koston 9f254c69a1 Merge remote-tracking branch 'upstream/dev' into neutral-ble-client 2026-10-06 21:32:51 -10:00
J. Nick Koston 1b6cc0bf18 Merge remote-tracking branch 'upstream/dev' into neutral-ble-client 2026-10-02 08:15:50 -05:00
J. Nick Koston 0801211ba1 Merge remote-tracking branch 'upstream/dev' into HEAD 2026-09-17 08:48:59 -05:00
J. Nick Koston 5a2f059be7 Match the backend's user-provided default constructor convention 2026-09-16 09:23:15 -05:00
J. Nick Koston 14998c0905 Merge remote-tracking branch 'upstream/dev' into neutral-ble-client 2026-09-16 09:20:38 -05:00
J. Nick Koston d69a1e9bf9 Charge the backoff on a degraded build, drop raced completions, one slot-budget map
A table-build failure that keeps the link for legacy nodes now charges the
backoff, so an unwalkable database stops retrying at advertisement cadence.
The write action re-resolves from the current table only. The neutral engine
drops a discovery completion that raced a teardown. Slot charging and the
cap-check skip read one mapping, so a platform cannot be skipped uncharged.
2026-09-14 11:54:14 -05:00
J. Nick Koston 8eb5d17f3d Merge remote-tracking branch 'origin/dev' into neutral-ble-client
# Conflicts:
#	esphome/components/ble_client/automation.h
2026-09-14 10:58:23 -05:00
J. Nick Koston 808afc024d Merge remote-tracking branch 'upstream/dev' into neutral-ble-client 2026-09-02 11:34:17 +02:00
J. Nick Koston 0bb98b715f Merge remote-tracking branch 'upstream/dev' into neutral-ble-client 2026-08-26 19:49:11 -05:00
J. Nick Koston 90af765fae Merge remote-tracking branch 'upstream/dev' into neutral-ble-client 2026-08-24 10:27:17 -05:00
J. Nick Koston f17b18d51a Merge remote-tracking branch 'upstream/dev' into neutral-ble-client 2026-08-20 00:45:33 -05:00
J. Nick Koston 5f11c430cc Emit the materializer define only for the backend that has one
The registry entry now carries its materializer gate, so rp2 builds stop
receiving a Bluedroid define. Also: parent before the capacity check in
register_gatt_node (neutral parity) and BluedroidServiceTable is
non-copyable (owning raw pointer).
2026-08-19 10:30:43 -05:00
J. Nick Koston baf57195e2 Name the materializer define for what it gates
USE_BLUEDROID_GATT_SERVICE_TABLE compiles the Bluedroid materializer and
nothing else; the old name read like a cross-backend consumer capability
(rp2 always materializes and ignores it).
2026-08-19 09:15:17 -05:00
J. Nick Koston 27a97f36af Key backend and engine selection on their own defines
Each backend arm now has a codegen-emitted USE_BLE_GATT_BACKEND_<NAME>
define, making the alias ladder order-independent and letting the proxy
capability constants describe the backend instead of the platform (a second
esp32 backend carries its own facts). The legacy raw-gattc engine is
selected by USE_BLE_CLIENT_LEGACY_ENGINE instead of platform identity, so
esp32's future move to the neutral engine is one line per language. The
address-scoped maintenance calls key on the stack define they wrap.
2026-08-19 09:14:17 -05:00
J. Nick Koston 0a28a5b023 Document the contract facts a backend author needs
Connect-timing ownership (the backend integrates with its platform's
scan/connect arbitration), the asynchronous-completion rule for accepted
ops, and the reserved error ranges with the wire-frozen values - all learned
the hard way by the first out-of-tree backend.
2026-08-19 09:10:05 -05:00
J. Nick Koston 5c6e62ffe0 Pin the neutral cap branch a future hub platform will take 2026-08-12 19:28:21 -05:00
J. Nick Koston 0801b29617 Surface a degraded mixed client in component status
The table-build skip sets a warning cleared by the next successful fan-out.
2026-08-12 19:12:09 -05:00
J. Nick Koston b00ad7ca8a Name the mixed-client invariant and fold the promote loop
has_legacy_nodes_() lives next to the invariant it derives from; both
bailout outcomes reach one promotion loop; the carve assert relaxes to the
descending chain the comment claims; the notify op string names the IDF
call like its siblings.
2026-08-12 19:03:16 -05:00
J. Nick Koston 7ad51b4b8f Pin the carve alignment, name the node-abort contract, fix strays
The static_assert enforces what the carve comment claims; the node header
now states that aborting inside on_connected() can leave on_disconnect
unpaired; the passive-proxy test comment names the current gate; the
pending-regs comment lands on the right member (lost to an aborted batch).
2026-08-12 18:58:56 -05:00
J. Nick Koston 65c1b06f7f Keep legacy nodes connected through a table-build failure
A failed search still poisons the whole discovery, but an allocation or walk
failure is table-specific: on a mixed client the legacy nodes keep the link
and only the gatt nodes skip that connection.
2026-08-12 18:42:03 -05:00
J. Nick Koston e158205e4c Surface the node-capacity guard in component status and log sync register failures 2026-08-12 18:38:04 -05:00
J. Nick Koston f5debdad32 Drop the orphaned Automation TAG box and fix the tidy naming
The write action's own tag left Automation::TAG with zero readers;
automation.cpp existed only to define it. Also notes the unpaired legacy
disconnect trigger on the failed-discovery path and re-anchors the
gatt_nodes_ comment.
2026-08-12 18:16:40 -05:00
J. Nick Koston 2a95fa44dc Pack ConnectBackoff into 4 bytes and regroup the table walk context
256 ms ticks in a uint16_t drop the struct from 8 bytes (3 padding) to 4 on
both engines; the materializer's uint8 fields move behind its uint16s.
2026-08-12 18:07:44 -05:00
J. Nick Koston 314557f83b Deduplicate the review-round additions
One ConnectBackoff shared by both engines replaces the forked hold-off
fields and constants (the duration is derived from the failure count). The
pending-registration scan lives once, sized by its own bound instead of the
node counter. The failure-suppressing SEARCH_CMPL arm moves to the call site
so the dispatcher stays void, notify-state logging is one helper on the
neutral wording, and the op-check helper takes the backend's spelling.
2026-08-12 17:56:29 -05:00
J. Nick Koston e4e464a3b9 Log GATT failures the bridge previously fanned out silently
Failed notify enables, reads, writes and unsubscribes now leave the same
breadcrumbs as the neutral engine; the shared-completion guarantee moves to
the node contract header.
2026-08-12 17:47:07 -05:00
J. Nick Koston a41dbed69c Clear the bridge backoff on re-enable and fail the table build closed
set_enabled(true) resets the hold-off (neutral-engine parity), and every
failing build() return now resets the counts so a failed build can never
present a non-empty view.
2026-08-12 17:24:34 -05:00
J. Nick Koston ba10832ed7 Tighten the bridge comments to repo style 2026-08-12 17:09:37 -05:00
J. Nick Koston d6b35f8ccf Keep the on_connect trigger out of a failed discovery's teardown
A discovery the bridge classifies as failed now suppresses the legacy
SEARCH_CMPL fan-out, so user automations never observe the doomed link as a
connection (neutral-engine parity, including the mid-fan-out node abort).
Also documents that same-handle subscriptions from mixed node kinds are
unsupported during the migration window.
2026-08-12 17:04:46 -05:00
J. Nick Koston 03efcf6bb5 Hold off reconnects after materializer failures on the esp32 bridge
A peer that reliably fails discovery (walk error, allocation failure) would
otherwise reconnect on its next sighting forever, holding a controller slot
each cycle - the neutral engine already backs off on this path.
2026-08-12 16:58:34 -05:00
J. Nick Koston 0d46b1047e Name the failure in the discovery-bailout log
A failed search or count was indistinguishable from a service-less peer.
2026-08-12 16:38:33 -05:00
J. Nick Koston 456f5b3042 Apply the simplify pass to the single-interface rework
Neutral fan-out runs before the legacy nodes so on_connect triggers observe
resolved gatt nodes (the neutral engine's order). Pending notify
registrations move to a fixed array (no post-setup heap). Gatt nodes also
join nodes_, collapsing the twin state loops. One define now means 'the
neutral node surface is compiled in' on both engines. The service count
moves into BluedroidServiceTable, free() resets it, bridge ops log failures,
the write action carries its own tag, and the automation headers drop
includes that left with the write actions.
2026-08-12 16:32:46 -05:00
J. Nick Koston 83c9603b4b Address review: one overcommit message per platform and a stall breadcrumb
Platforms whose BLE stack owns a connection budget (esp32, rp2) report an
overcommit exactly once through that stack; the neutral cap check covers
future backend platforms without one. The write action logs unmatched
completions while a chain is parked, and the schema-dump gap is documented
as deliberate.
2026-08-12 16:11:20 -05:00
J. Nick Koston ad9a41ed61 Cover the engine feature gates in the schema tests 2026-08-12 16:04:57 -05:00
J. Nick Koston e6b7b3a028 One ble_write action on the neutral node interface
The raw-gattc and neutral twins collapse into ble_write_action.h, registered
through register_gatt_node on both engines. On esp32 this is the in-tree
exerciser of the node bridge; builds without a ble_write action compile no
bridge code.
2026-08-12 16:02:50 -05:00
J. Nick Koston 9a4a550fb9 Express ble_client engine capabilities per platform
BLEClientFeatures (gatt_node / raw_gattc / security) with a per-platform
provider set, capability-named validation errors, NODE_BLE_CLIENT_SCHEMA +
register_gatt_node for migrated components, and the security actions gated on
the feature instead of the platform. esp32 auto-loads bluetooth_connection
for the shared materializer (compiles empty without a neutral node).
2026-08-12 15:58:25 -05:00
J. Nick Koston 42dc4025c9 Deliver the neutral node callbacks from the esp32 engine
Nodes registered through register_gatt_node() get the neutral surface:
service table at SEARCH_CMPL (honoring the search status the base ignores),
translated completion/notify fan-outs, pairing results, and teardown via
on_disconnect_complete. Bridge-initiated notify registrations are intercepted
before the base's automatic CCCD write - the CCCD is the node's job on the
neutral contract. Compiled only under USE_BLE_CLIENT_GATT_NODES; builds
without a neutral node are unchanged.
2026-08-12 15:55:56 -05:00
J. Nick Koston 901fd35a7e Extract the Bluedroid service-table materializer into a shared class
BluedroidServiceTable carries the two-pass build verbatim in its own TU; the
backend embeds one instance. ble_client's esp32 engine becomes the second
consumer next.
2026-08-12 15:53:14 -05:00
J. Nick Koston 4344ffd682 Share one BLEClientNode between the engines and round out the neutral surface
The neutral callbacks (plus a new on_pairing_result hook) live unconditionally
in ble_client_node.h; the raw esp32 surface stays beneath them for unmigrated
nodes. Adds find_descriptor beside find_cccd and pair()/unpair() on the
neutral client. Drops the node 'client' pointer and address, which nothing
reads.
2026-08-12 15:47:43 -05:00
J. Nick Koston 1c671be1bd Use the merged ledger accessor in the explicit-connections test 2026-08-12 15:04:26 -05:00
J. Nick Koston 52dc51c7c6 Merge remote-tracking branch 'upstream/dev' into neutral-ble-client
# Conflicts:
#	esphome/components/bluetooth_connection/__init__.py
#	esphome/components/bluetooth_connection/bluetooth_connection.cpp
#	esphome/components/bluetooth_connection/bluetooth_connection.h
#	esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp
#	esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h
#	esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp
#	esphome/components/bluetooth_connection/bluetooth_connection_hub.h
#	esphome/components/bluetooth_proxy/__init__.py
#	esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
#	esphome/components/bluetooth_proxy/bluetooth_proxy.h
#	esphome/core/defines.h
#	tests/components/bluetooth_connection/__init__.py
2026-08-12 15:04:09 -05:00
J. Nick Koston 671b6216a7 Merge branch 'dev' into neutral-ble-client 2026-08-10 14:30:42 -05:00
J. Nick Koston 5764ca607d Merge remote-tracking branch 'upstream/dev' into neutral-ble-client
# Conflicts:
#	esphome/components/ble_device_base/ble_gatt_client.h
#	esphome/components/bluetooth_connection/__init__.py
#	esphome/components/bluetooth_connection/bluetooth_connection.cpp
#	esphome/components/bluetooth_connection/bluetooth_connection.h
#	esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp
#	esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h
#	esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp
#	esphome/components/bluetooth_connection/bluetooth_connection_hub.h
#	esphome/components/bluetooth_proxy/__init__.py
#	esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
#	esphome/core/defines.h
#	tests/component_tests/bluetooth_proxy/test_platform_gates.py
#	tests/components/ble_device_base/__init__.py
#	tests/components/bluetooth_proxy/test-passive.esp32-c6-idf.yaml
2026-08-10 10:50:00 -05:00
J. Nick Koston 273c80ecd3 Address review: state the table lifetime where it is freed
Every teardown path routes through release_services(), so the
materialized table cannot outlive its link - said at free_service_table_
so the next reviewer need not re-derive it.
2026-08-09 19:59:06 -05:00
J. Nick Koston 1933126ad7 Address review: de-vacuous the reject test, loud node overflow, walker logs
- The rejection test registers the hub and matches the extra-keys error,
  so the missing-tracker error can no longer satisfy it vacuously
- register_ble_node logs and drops loudly at capacity: a push_back past
  a StaticVector's bound is a silent no-op, and an undersized slot count
  must show at boot, not as an unresolvable node
- Every walker failure names the failing call and status; the descriptor
  cap gets its own message so a >64-descriptor characteristic is
  diagnosable instead of a permanent silent backoff loop
2026-08-09 19:37:32 -05:00
J. Nick Koston 64034b5b23 Use the set_core_config fixture instead of hand-poking CORE.data
The conftest fixture is the one spelling for platform setup; the
ledger tests already use it.
2026-08-09 19:16:18 -05:00
J. Nick Koston 830854d661 Address review: pin the neutral arm's rejection of esp32-only keys
The schema split's accept side was tested; the reject side was not - a
key leaking from the legacy arm into the neutral schema would have
passed silently.
2026-08-09 19:12:52 -05:00
J. Nick Koston 87b7076cd1 Address review: fail the empty table before CONNECTED, one slot spelling
- The empty-table bailout ran after state_ went CONNECTED, so its
  teardown's terminal report read as a completed connection and fired
  on_disconnect with no preceding on_connect - the exact trigger
  contract violation the sibling error branch avoids. The check now
  runs first and the teardown resolves through connect_failed; a
  node-less client still reaches CONNECTED through the single
  assignment after the guard
- The esp32 arm claims its slot through consume_gatt_slot like every
  other claimant (behavior-identical: it forwards to the same esp32
  validator), so the ledger's one-spelling contract holds
2026-08-09 18:52:49 -05:00
J. Nick Koston 5ec7047151 Address review: empty-table failure semantics, async abort guard, StaticVector nodes
- A zero-service table on a client with nodes is treated as a failed
  discovery (warn, release, charge the backoff, disconnect): a real GATT
  peer always exposes at least GAP/GATT, so an empty table means the
  materialization failed and the connection must retry instead of
  sitting inert behind a successful on_connect
- The fan-out abort guard also tests cancel_requested_, covering the
  normal async teardown a node starts from on_connected (the state-only
  check caught just the synchronous refusal)
- nodes_ becomes a StaticVector sized by ESPHOME_BLE_CLIENT_MAX_NODES:
  the client requests a baseline slot, each neutral write action (which
  registers itself in its constructor) requests one, and the define
  rides defines.h for analysis - no realloc machinery on the rp2 target
- The Bluedroid empty-table return logs its quiet cases; the
  service_table=False stub notes the misconfiguration it implies
2026-08-09 18:34:13 -05:00
J. Nick Koston 45d921f77a Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 17:34:37 -05:00
J. Nick Koston 321b00143d Address review: latch the retry only for the current subscriber
A gate-refused send to a peer that is not api_connection_ (the
subscribe-time path during a handover) must not arm a retry that
loop() would aim at a different connection.
2026-08-09 17:34:33 -05:00
J. Nick Koston e6e9016ed4 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 17:07:45 -05:00
J. Nick Koston a292aa2654 Drop the disconnect-notification retry latch
Overkill for its window: the address latch, drain, reuse guard and
session hygiene bought coverage for a case the client's own timeouts
and the retried connections-free state already heal. The notification
goes out when it can; the connections-free retry (the hardware-verified
fix) stays.
2026-08-09 17:07:42 -05:00
J. Nick Koston 4353f5ac9c Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 17:05:05 -05:00
J. Nick Koston 4925f6648b Address review: retry-latch hygiene on subscriber change, parity note
- Both retry latches clear on subscribe/unsubscribe: a new subscriber
  must not receive a connected=false for an address from the previous
  session (it resyncs through its own subscribe requests)
- The app-register failure path states its BLEClientBase parity so the
  no-retry behavior reads as deliberate
2026-08-09 17:05:01 -05:00
J. Nick Koston 47bd8f5ffc Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 17:03:13 -05:00
J. Nick Koston 31771da92c Address review: gate the cache clean where the paths converge
The stack-down settle skipped release_services() locally, but its
report routes into the wrapper's reset which calls it anyway - the
warn survived. Gating the clean on an active stack inside
release_services() covers every path, and the stack-down branch goes
back to the plain call.
2026-08-09 17:03:09 -05:00
J. Nick Koston 1978f203bc Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 16:32:41 -05:00
J. Nick Koston ae906ada94 Address review: quiet stack-down teardown, pin bitfield widths
- The stack-down settle resets the stream latches directly instead of
  calling release_services(): the dying stack invalidates its own cache,
  and the newly checked cache_clean would warn on every OTA or
  ble.disable with a live connection
- static_asserts pin the exactly-sized state bitfields so a future
  enumerator truncates loudly at compile time
2026-08-09 16:32:37 -05:00
J. Nick Koston 6a2465da63 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 16:24:06 -05:00
J. Nick Koston 9a5aab63d5 Address review: retry a dropped freed-slot notification too
The connections-free retry left its sibling asymmetric: on the same
full buffer the slot could report free while the device still read as
connected, with nothing resending the connected=false. A one-deep latch
(address + error) drains at the same 100 ms cadence and re-latches on a
repeat failure; a re-reservation of the address clears it, since the
client re-requesting the connect has already acted on the disconnect
and a late resend would shadow the new connection. A lost
connected=true stays client-timeout territory.
2026-08-09 16:24:03 -05:00
J. Nick Koston deeec17d0d Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 16:07:38 -05:00
J. Nick Koston 735f810f08 Cover the unknown-platform raise in frameworks_for_platforms 2026-08-09 16:07:35 -05:00
J. Nick Koston cd48023a56 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 16:03:30 -05:00
J. Nick Koston 9330204908 Address review: unknown platforms raise, the filter pin asserts equality
An unknown or misspelled platform name yielded an empty set - the file
would be filtered out of every build (link failure at the end of a long
compile) and the subset guard passed vacuously.
2026-08-09 16:03:25 -05:00
J. Nick Koston 45501056d8 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 15:58:59 -05:00
J. Nick Koston 2e6ad94bd1 Tighten the listener doc to repo style 2026-08-09 15:58:56 -05:00
J. Nick Koston 5994105d34 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 15:57:24 -05:00
J. Nick Koston 0ac9a0f8bd Check the cache-clean status in release_services()
The one IDF call in the backend that skipped check_and_log_error_. A
failed clean leaves a stale database the next connection could serve as
authoritative, with nothing in the log to point at it. (The call fails
only at dispatch - stack down - not for an empty cache, so this cannot
warn-spam routine teardowns.)
2026-08-09 15:57:20 -05:00
J. Nick Koston 77b8b384bd Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 15:55:00 -05:00
J. Nick Koston 3728669fa6 Restore the reconnect-during-teardown cancel fast path
The collapse of the CONNECTING branch dropped a real race fix: HA
re-requesting a connect while a scheduled teardown was still pending
used to cancel the teardown and let the in-flight open complete;
without it the request was ignored and HA paid a teardown plus a fresh
connect during exactly the reconnect churn this path sees most.

cancel_gatt_disconnect() joins the contract: true only for a scheduled
teardown that has not started closing (Bluedroid clears the latched
want_disconnect_); rp2 and the stubs return false since their teardowns
start inside gatt_disconnect(). The wrapper's cancel_teardown() returns
its state to CONNECTING and the proxy handler carries the dev branch
verbatim.
2026-08-09 15:54:56 -05:00
J. Nick Koston 578e4774a9 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 15:44:01 -05:00
J. Nick Koston 8696255ccc Give connection_index_ an in-class initializer like every sibling 2026-08-09 15:43:57 -05:00
J. Nick Koston 594931265b Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 15:15:37 -05:00
J. Nick Koston b0f753a7a2 Address review: check prefer-conn-params, pace the retry drain
- Both esp_ble_gap_set_prefer_conn_params calls route through
  check_and_log_error_ like the class this PR replaces did; a rejected
  preference leaves the link on the controller default interval, which
  is exactly the WiFi-coex failure the shared constants exist to avoid,
  so it must be visible in logs
- The connections-free retry moves below the 100 ms gate so a full TCP
  buffer is retried at the loop cadence instead of every iteration
2026-08-09 15:15:33 -05:00
J. Nick Koston fc69d8543e Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 14:54:05 -05:00
J. Nick Koston d586a6c8d4 Address review: check the reclaim close, log the mid-stream park
- The late-OPEN_EVT reclaim was the one unchecked IDF call in the
  backend; a failed close there leaks a live link nothing tracks
- The mid-stream services_released_ park now leaves a trace like its
  API-lost sibling, so a stuck GetServices is explicable from logs
2026-08-09 14:54:01 -05:00
J. Nick Koston 9301391792 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 14:46:35 -05:00
J. Nick Koston 1b801427c4 Dry-pass round 2: comment accuracy polish
Round 2 found no code issues - the round-1 mechanisms verify clean on
disk (every IDLE transition through the one door, the teardown-guard
trio minimal-complete per event, the error latch airtight, the reset
lists disjoint by construction). What remained was comment drift:
disconnect_pending() spelling, the OPEN_EVT and cached-MTU comments,
the contract header re-wrap, the two claim-a-slot docstrings
disambiguated, and ragged wraps from earlier text excisions.
2026-08-09 14:46:30 -05:00
J. Nick Koston be391feaa9 Merge branch 'esp32-gatt-backend' into neutral-ble-client
# Conflicts:
#	esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp
#	esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h
2026-08-09 14:38:33 -05:00
J. Nick Koston 7e369f332c Dry-pass round 1: one door to IDLE, complete teardown guards, residue
- set_idle_() is the single door back to IDLE (bootstrap, open-fail and
  the DISCOVERED park went through bare set_state), so the per-attempt
  reset list holds only per-attempt latches
- CFG_MTU joins OPEN_EVT and SEARCH_CMPL in suppressing reports while a
  teardown owns the link - one spelling of the guard across all three
  events, and the wrapper's race arm becomes defense instead of the only
  cover
- The connections-free retry drain compiles on every proxy build (the
  advertisement-only arm sends the message too; the latch already did)
- latch_pending_error_ makes first-cause-wins the mechanism at all three
  latch sites; the dead freed-slot refused branch and its retired
  vocabulary go; the initiate_connection/start_connect_ pair collapses
- UNSET_CONN_ID hoisted next to the field it initializes; count-status
  shadow renamed; stale busy-error rationale replaced with the real one
  (a repeat call would re-arm the teardown timer)
- Fixture states the batch-grouping caveat like its rp2 sibling; the
  get_service_table stub carries a greppable direct-consumer warning
2026-08-09 14:37:57 -05:00
J. Nick Koston a40ecfdf9c Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 14:22:40 -05:00
J. Nick Koston 9a6765eb8f Log the deferred connections-free resend at verbose
Log lines ride the same API connection; a debug line at the exact
moment the TCP buffer is full adds traffic when it can least afford it
(the api layer's own buffer-full log is verbose for the same reason).
2026-08-09 14:22:37 -05:00
J. Nick Koston 1d0a87daa8 Address review: an empty service table with nodes logs a warning
A service-less device or a failed materialization both leave the nodes
unable to resolve; the client no longer sits inert without a trace.
2026-08-09 14:20:31 -05:00
J. Nick Koston c98dd74e11 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 14:13:53 -05:00
J. Nick Koston aceff27948 Retry a dropped connections-free update
Field bug on a Pico W: the slot freed device-side but the API client
kept free=0 with the address still allocated. send_message drops the
response when the TCP buffer is full (a boot storm makes that likely
right when HA reconnects), and nothing ever resent it - the client's
slot state is verbatim the last response received, so it stayed stale
until reboot. The cached response is current by construction, so a
pending bit retried from loop() is an idempotent resync; the
subscribe-time send heals through the same bit.
2026-08-09 14:13:50 -05:00
J. Nick Koston 211337cd6d Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 14:03:07 -05:00
J. Nick Koston 4276815734 Address review: scope the rp2 defines pin, contract-back the teardown semantics
- The esp32 arm's ESPHOME_BLE_GATT_CLIENT_COUNT shadowed the unanchored
  regex; the pin now searches the USE_RP2 platform block
- The contract states what the wrapper relies on: nonzero from
  gatt_disconnect means nothing to tear down, an accepted teardown
  always reaches a terminal report
2026-08-09 14:03:04 -05:00
J. Nick Koston 3c57064bf2 Address review: skip table materialization when no node will read it
A client with only connect/disconnect automations paid the build/free
cycle on every reconnect for nothing - post-setup heap churn on the
Bluedroid direct-consumer path. release_services() stays unconditional.
2026-08-09 13:45:33 -05:00
J. Nick Koston f9be82d1a6 Address review: on_notify_state joins the node interface
notify_characteristic() had no completion path to the node that asked;
the hook lands now while the surface has no external users, with the
client fanning out after its breadcrumb.
2026-08-09 13:30:08 -05:00
J. Nick Koston 10a0bf0ff8 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 13:29:27 -05:00
J. Nick Koston 484674b85d Derive the bluedroid source-filter entry from the shared helper
frameworks_for_platforms() gains its production caller in the same file
as the platform registry; the derived set is identical to the hand list.
2026-08-09 13:29:00 -05:00
J. Nick Koston 49c1547fda Compile the gated lookup helpers into the ble_device_base test build
The contract test's own #define only covers its translation unit;
ble_gatt_client.cpp needs the define from the build to link the
find_characteristic/find_cccd definitions the lookup tests call.
2026-08-09 13:25:52 -05:00
J. Nick Koston 7b4353b417 Import the service-table materializer from #18198
The Bluedroid on-demand materializer, the neutral lookup helpers' host
tests, and the USE_BLE_GATT_SERVICE_TABLE define move here, next to
their first consumer (the neutral engine's table resolution and the
service_table codegen flag). One behavior fix rides along: hitting
MAX_DESCRIPTORS_PER_CHARACTERISTIC now fails the walk like every other
inconsistency instead of truncating the table silently.
2026-08-09 13:23:44 -05:00
J. Nick Koston 5f20346d54 Merge branch 'esp32-gatt-backend' into neutral-ble-client
# Conflicts:
#	esphome/components/ble_device_base/ble_gatt_client.h
2026-08-09 13:23:17 -05:00
J. Nick Koston 356c1d9134 Guard every GATT op on an unset conn id
discover_services() already returned the contract's not-connected value;
the read/write/notify/pair ops fell through to an arbitrary stack error
instead. Dead for the proxy (the wrapper gates on connected()), live for
a direct consumer racing an op against a teardown.
2026-08-09 13:22:28 -05:00
J. Nick Koston 9534a1df55 Move the dark service-table materializer and lookup helpers to #18205
Nothing in this PR emits USE_BLE_GATT_SERVICE_TABLE, so the Bluedroid
materializer, the neutral find_service/find_characteristic/find_cccd
helpers, their host tests, and the defines.h entry were scaffolding with
no caller here. They move to the PR that introduces their first
consumer. The contract op stays: get_service_table() keeps its {} stub
(the proxy streams in place).
2026-08-09 13:21:36 -05:00
J. Nick Koston fb64b18047 Address review: notify/pairing breadcrumbs, aborted fan-out trace
- on_notify_state and on_pairing_result are handled on the neutral
  client: a failed registration or pairing logs instead of vanishing
  into the default no-op on a frozen surface
- A node tearing the link down during the connect fan-out leaves a
  warning, since on_disconnect then fires with no preceding on_connect
2026-08-09 12:59:24 -05:00
J. Nick Koston 5572bf77dd Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 12:57:42 -05:00
J. Nick Koston d8ec76e02b Fail on premature enumeration terminators; explicit terminal states
- An INVALID_OFFSET/NOT_FOUND before the count from the same cache is a
  contradiction, not an end-of-range: the walker fails the build and the
  streamer aborts with the real cause instead of silently truncating
  (the walker's descriptor loop keeps its terminator - the 64 cap is its
  only bound)
- unconditional_disconnect_'s unset-conn-id path drives the slot to a
  terminal reported state instead of leaning on the scheduled-teardown
  timer
- find_characteristic/find_cccd log a corrupt range before returning the
  not-found sentinel (moved out of line - no ESP_LOG in headers)
2026-08-09 12:57:36 -05:00
J. Nick Koston cf5faa454c Address review: rejection logs, honest service_table docstring, terse sweep
- connect()/disconnect() log a rejected user action like the legacy
  engine instead of silently ignoring it
- The service_table docstring states the flag is Bluedroid-only forward
  scaffolding and that rp2's proxy hub must keep its materializer
  regardless (it streams through get_service_table())
- Review-round comments tightened to repo style
2026-08-09 12:47:17 -05:00
J. Nick Koston 55d14c6fbf Merge branch 'esp32-gatt-backend' into neutral-ble-client
# Conflicts:
#	esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp
2026-08-09 12:43:57 -05:00
J. Nick Koston d86bdb36cf Address review: honor SEARCH_CMPL status, close a late OPEN, backstops
- handle_search_cmpl_ takes the event status: a failed discovery no
  longer reads as a clean zero-service success (the empty cached
  database satisfies the count calls), and the table invalidation plus
  param step-down still run on the failure path
- A late successful OPEN_EVT on a slot the teardown net already gave up
  is closed instead of leaking a live controller link with no conn id
- The counting pass logs its failure like the two paths below it
- static_assert pins the wrapper's compile-time streamer detection; a
  signature drift would silently fall back to a table proxy builds
  compile without
- find_characteristic/find_cccd range math widened to 32-bit (correct by
  type; the wrapped-end case was already loop-safe)
2026-08-09 12:42:17 -05:00
J. Nick Koston c64684dd00 Tighten review-round comments to repo style 2026-08-09 12:36:55 -05:00
J. Nick Koston ed002feee9 Address review: mid-fan-out release, self-enforcing cap, scaffolding note
- The fan-out early return releases the borrowed table itself instead of
  relying on the backend's own teardown release
- A registered non-esp32 backend platform without a slot cap now fails
  validation loudly instead of failing open (the test pin remains)
- new_gatt_backend's docstring says the service_table flag is forward
  scaffolding for the first esp32 direct consumer, not load-bearing on
  any current build
2026-08-09 12:19:43 -05:00
J. Nick Koston 38f31d85b6 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 12:19:14 -05:00
J. Nick Koston d04ef2378e Contract doc: writes without response also report on_write_result
Both backends report the no-response completion (Bluedroid at
WRITE_CHAR_EVT, rp2 synthesized or via the can-send path); the neutral
write action already relies on it.
2026-08-09 12:18:35 -05:00
J. Nick Koston 5e26633231 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 12:07:13 -05:00
J. Nick Koston 6c78b480c4 Return the wrapper to 48 bytes - exact per-slot RAM parity with dev
The measured layout showed 56+48 = 104 B/slot vs dev's 96. Two changes
that only pay together (the wrapper is 8-aligned, so nothing under a
full 8 helps):

- The tail packs into 2 bytes of bitfields; remote_addr_type_ becomes a
  start_connect_ parameter (written and read on adjacent lines only)
- The wrapper's duplicate 10 s teardown timer is gone: the backend owns
  the whole safety window. Its timer now also arms for a teardown
  scheduled during CONNECTING (the one case the wrapper covered alone),
  a late OPEN_EVT on a given-up slot closes the link instead of
  resurrecting it, and the wrapper's transient-refusal branch collapses
  because both backends return nonzero only when already idle

48 + 48 = 96 B/slot, the split at dev parity.
2026-08-09 12:07:10 -05:00
J. Nick Koston 3d33a16b71 Return the wrapper to 48 bytes - exact per-slot RAM parity with dev
The measured layout showed 56+48 = 104 B/slot vs dev's 96. Two changes
that only pay together (the wrapper is 8-aligned, so nothing under a
full 8 helps):

- The tail packs into 2 bytes of bitfields; remote_addr_type_ becomes a
  start_connect_ parameter (written and read on adjacent lines only)
- The wrapper's duplicate 10 s teardown timer is gone: the backend owns
  the whole safety window. Its timer now also arms for a teardown
  scheduled during CONNECTING (the one case the wrapper covered alone),
  a late OPEN_EVT on a given-up slot closes the link instead of
  resurrecting it, and the wrapper's transient-refusal branch collapses
  because both backends return nonzero only when already idle

48 + 48 = 96 B/slot, the split at dev parity.
2026-08-09 12:06:44 -05:00
J. Nick Koston e3aa8773e6 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 11:50:26 -05:00
J. Nick Koston be5e00aefd Address review: discovery status, loop lifetime, event conn id
- SEARCH_CMPL honors the event's own status: a failed discovery leaves
  an empty cached database that the count calls read as a clean zero,
  which would have become an authoritative empty service list
- The loop stays enabled while a link exists and settles only at IDLE -
  the stack-down recovery needs a tick to run in ESTABLISHED, which the
  settle-on-established optimization was silently blocking
- The pre-started search uses the OPEN_EVT conn id (replaced-class
  parity)
2026-08-09 11:50:22 -05:00
J. Nick Koston 9c5ebdffc9 Address review: complete the node-facing surface, wiring test, comment placement
- read_descriptor and notify_characteristic passthroughs join the frozen
  surface so the first migrated node can subscribe without extending the
  client; the comment now says which ops have callers today
- The choke-point test also validates through the public
  BLE_CLIENT_SCHEMA, so removing the cv.All wiring fails a test
- DOMAIN no longer sits between the BTstack comment and the constant it
  explains
2026-08-09 11:26:47 -05:00
J. Nick Koston 9554e1a212 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 11:19:58 -05:00
J. Nick Koston 28491f83b4 Close the silent-failure paths from review
- A refused MTU request no longer wedges the connection: OPEN_EVT
  reports with the default MTU so the consumer proceeds (previously no
  CFG_MTU_EVT meant no connected report and a slot that never freed)
- Disabling the BLE stack settles a live link with a connected=false
  report before forgetting it, so the consumer frees its slot instead of
  holding a phantom connection
- A refused security response answers the pairing request with the
  failure instead of hanging it
- The service-table walk mismatch logs its reason before discarding
- The shared streamer's two bounds-check aborts use abort_service_stream
  with an ATT Unlikely Error cause (new shared GATT_ERR_UNLIKELY)
2026-08-09 11:19:55 -05:00
J. Nick Koston 2f71ec16d7 Close the silent-failure paths from review
- A refused MTU request no longer wedges the connection: OPEN_EVT
  reports with the default MTU so the consumer proceeds (previously no
  CFG_MTU_EVT meant no connected report and a slot that never freed)
- Disabling the BLE stack settles a live link with a connected=false
  report before forgetting it, so the consumer frees its slot instead of
  holding a phantom connection
- A refused security response answers the pairing request with the
  failure instead of hanging it
- The service-table walk mismatch logs its reason before discarding
- The shared streamer's two bounds-check aborts use abort_service_stream
  with an ATT Unlikely Error cause (new shared GATT_ERR_UNLIKELY)
2026-08-09 11:19:25 -05:00
J. Nick Koston 3daa1d2450 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 10:56:31 -05:00
J. Nick Koston a513cef4e2 Encode the pre-started search lifecycle as a state machine
The three coordinated flags become one SearchState (NONE / PRESTARTED /
PRESTART_DONE / CLAIMED / REPORT_PENDING): illegal combinations are
unrepresentable, delivery and the two reset sites collapse to single
assignments, and the requested-meets-done subtlety becomes a named
state. A second discover_services() while one is in flight now returns 0
instead of issuing a duplicate search (one completion is already owed).
Same RAM: the enum sits in a 4-bit bitfield in the same flags byte.
2026-08-09 10:56:27 -05:00
J. Nick Koston fa17215053 Address review: fan-out guard, refusal warning, sighting-less connect hint
- The connected fan-out stops if a node tears the link down mid-loop
  (latent until a backend settles a teardown synchronously)
- A synchronously refused disconnect logs its own warning - a backend/
  client state divergence is no longer the most quietly logged outcome
- An action-initiated connect before any sighting warns that the public
  address type is assumed (an opaque repeated failure becomes
  self-diagnosing); the connect() doc no longer mentions a config key
  that does not exist
- The explicit-connections ledger test asserts the exact charged count;
  the host-test override comment names the current gate macro
2026-08-09 10:43:21 -05:00
J. Nick Koston 073d9a72a0 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 10:22:39 -05:00
J. Nick Koston 3a24d0b03b Reset the search latches per connection attempt
The latches were cleared only in set_idle_(), but the stack-down branch
in loop() reaches IDLE through set_state() without it, so after a
ble.disable/enable cycle the next connection's discover_services() would
complete immediately from the previous connection's latched result and
HA would cache an authoritative empty service list. tracker_connect_()
now resets the latch alongside the two per-attempt flags it already
clears, and delivery consumes the whole latch so a re-discovery on a
live link issues a real search instead of re-reporting the first
result.
2026-08-09 10:22:35 -05:00
J. Nick Koston b229bd4d8a Address review: explicit connections claim slots, re-enable clears backoff
- Explicit `connections:` entries now charge the slot ledger like the
  generated ones (dev let them evade the controller budget); pinned by a
  test
- set_enabled(true) clears the connect backoff - a re-enable is an
  explicit try-again
- The neutral engine's source filter uses the shared
  frameworks_for_platforms helper; ledger tests use the conftest fixture
2026-08-09 10:09:20 -05:00
J. Nick Koston c8e240198d Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 10:07:42 -05:00
J. Nick Koston 0512339c58 Deduplicate the stream-abort sequence and small cleanups
- abort_service_stream() on the wrapper (which owns the latch discipline)
  replaces five identical field-poke sequences in the streamer
- The pre-start reuses check_and_log_error_ like the serialized path
- discover_services() sets the requested flag where it sticks instead of
  set-then-rollback; deliver_pending_search_() returns void (no caller
  reads it); loop()'s IDLE arm folds into the settle arm
- SEARCH_CMPL during a teardown skips the param/count work whose result
  is never delivered
- frameworks_for_platforms() in config_helpers derives framework sets
  from platform lists; the hub.cpp filter entry is now a named map pinned
  by a test against the proxy's platform list
2026-08-09 10:05:42 -05:00
J. Nick Koston e95bebb7d0 Merge branch 'esp32-gatt-backend' into neutral-ble-client
# Conflicts:
#	esphome/components/bluetooth_connection/bluetooth_connection.h
2026-08-09 09:54:08 -05:00
J. Nick Koston 084ff9d6e3 Restore the discovery/MTU overlap and latch streamer abort causes
The split serialized service discovery behind the MTU exchange: the
backend reported connected only at CFG_MTU_EVT and the wrapper started
discovery on that report, costing one ATT round trip per uncached
connection. The backend now pre-starts the search at OPEN_EVT (it knows
the connection type) and completes the consumer's discover_services()
from it: SEARCH_CMPL latches silently until requested, the flush after
the connected report delivers in the same event drain, and a refused
pre-start falls back to the serialized path. Net RAM cost is zero (the
flags pack into one bitfield byte plus a status byte, replacing the two
existing bools).

Also from review: SEARCH_CMPL no longer clobbers a teardown in progress
with ESTABLISHED, the five streamer abort sites latch pending_error_ so
HA sees the real Bluedroid status instead of a generic HCI reason, and a
completed re-discovery frees a materialized service table before the
counts its offsets derive from move.
2026-08-09 09:52:58 -05:00
J. Nick Koston 4a9e5f5da9 Require the proxy in the wrapper's compile gate
The hub wrapper serves the proxy's API surface, but its gate keyed on the
backend define alone while the backend guarded its own proxy pieces on
USE_BLUETOOTH_PROXY. Narrow BLUETOOTH_CONNECTION_HAS_GATT to require both
so a future backend-only consumer build agrees with the backend's guards.
No change to any build this PR can produce (only the proxy emits
USE_BLE_GATT_CLIENT here).
2026-08-09 09:44:23 -05:00
J. Nick Koston f6158597ac Address review: slot-cap testing mode, derived source filter, breadcrumbs
- Skip the hub-platform GATT slot cap in testing mode (mirrors
  esp32_ble.validate_connection_slots) so grouped builds validate
- Derive the neutral engine's source filter from the backend registry so
  a new platform cannot validate and then fail at link
- Log the real code on refused service discovery and cancelled connects;
  read completions get the same breadcrumb writes have
- Comment accuracy: the dumper arm is the legacy shape, not a superset
- Ledger tests: real-validator end-to-end path and the testing-mode skip
2026-08-09 09:42:50 -05:00
J. Nick Koston 8924640593 Merge branch 'esp32-gatt-backend' into neutral-ble-client
# Conflicts:
#	esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h
#	esphome/components/bluetooth_proxy/__init__.py
2026-08-09 09:37:59 -05:00
J. Nick Koston 26c000c417 Collapse shim-era indirection left by the merge of the two halves
- Fold the one-line event-handler forwarders, the state accessor renames,
  and the report helper into direct calls; drop the dead forward
  declaration and the tracker_client() accessor (the backend is the
  tracker client)
- Hoist DEFAULT_ATT_MTU next to the other shared GATT constants and use
  it in all three engines
- Emit BLUETOOTH_PROXY_MAX_CONNECTIONS once, in _connections_to_code
- Drop the unused esp_gatt_common_api.h include and DOMAIN constant
- Comment accuracy: stale shim/esp32-parity wording, registry roles,
  request_gatt_client() caller
2026-08-09 09:36:17 -05:00
J. Nick Koston 7cb32f9546 Clean up iteration residue from the review rounds
- Drop unused includes (ble_client_state.h, <utility>) and the unused
  notify/indicate property constants (no CCCD path yet)
- Note the frozen node-facing surface on the uncalled backend forwarders
- Keep the legacy-only 'name' key out of the neutral schema
- Move the slot-ledger test to tests/component_tests/bluetooth_connection
- Comment accuracy fixes (choke-point wording, wrapped lines, backoff note)
2026-08-09 09:31:21 -05:00
J. Nick Koston 13de0275c8 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 09:19:35 -05:00
J. Nick Koston 22af86a65b Tighten new comments to repo style 2026-08-09 09:19:32 -05:00
J. Nick Koston 83b099d708 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 09:17:42 -05:00
J. Nick Koston 6957e2ef15 Type gattc_if_ at its real width instead of shrinking the timer 2026-08-09 09:17:38 -05:00
J. Nick Koston 743a10fa3e Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 09:04:35 -05:00
J. Nick Koston f7b1e9ae65 Shrink the disconnect timestamp to a tick so the backend packs to 48 bytes 2026-08-09 09:04:31 -05:00
J. Nick Koston 53a5c7b051 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 09:00:25 -05:00
J. Nick Koston 54c50c07c5 Merge remote-tracking branch 'origin/dev' into esp32-gatt-backend
# Conflicts:
#	esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
#	esphome/components/bluetooth_proxy/bluetooth_proxy.h
#	esphome/core/defines.h
2026-08-09 08:59:12 -05:00
J. Nick Koston 64df0c1da1 The fixture comment follows the renamed predicate here 2026-08-09 08:56:18 -05:00
J. Nick Koston 9c31e08e95 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 08:55:53 -05:00
J. Nick Koston 93124f1f1f Align two comments with the code they describe 2026-08-09 08:55:49 -05:00
J. Nick Koston 8abb5f102d Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 04:20:42 -05:00
J. Nick Koston 12fb1b0697 Name the MTU default and bound the descriptor walk sensibly 2026-08-09 04:20:39 -05:00
J. Nick Koston 81216da8e7 Apply review: honest slot docstring, cap-coverage pin, write breadcrumb 2026-08-09 04:02:45 -05:00
J. Nick Koston ed2cdad9b4 Merge branch 'esp32-gatt-backend' into neutral-ble-client 2026-08-09 03:44:14 -05:00
J. Nick Koston be818a5bf2 Apply review polish: bounded walk, real statuses, guarded settles 2026-08-09 03:44:10 -05:00
J. Nick Koston 444a217ee7 Apply review: honest teardown status and unresolved-write cause 2026-08-09 03:29:20 -05:00
J. Nick Koston 93f9d4a1ad Apply review: state before fan-out, refusal breadcrumbs, write status log 2026-08-09 03:13:06 -05:00
J. Nick Koston ac7d39a8c1 Merge branch 'esp32-gatt-backend' into neutral-ble-client
# Conflicts:
#	esphome/components/bluetooth_connection/__init__.py
#	esphome/components/bluetooth_connection/bluetooth_connection_rp2.h
#	tests/component_tests/bluetooth_proxy/test_platform_gates.py
2026-08-09 03:09:32 -05:00
J. Nick Koston 612033062f Apply combined review: wire MTU parity, abort teardown parity, closure ownership 2026-08-09 03:08:47 -05:00
J. Nick Koston f9dbd24a2a Apply review: settle refused teardowns, per-platform schema cache, ledger tests 2026-08-09 02:50:01 -05:00
J. Nick Koston 833a835e0f Combine the flip into the backend PR and put the table define in the esp32 arm 2026-08-09 02:27:53 -05:00
J. Nick Koston 399dc26236 Follow the contract op rename 2026-08-09 02:17:36 -05:00
J. Nick Koston 0d9aea9594 Merge branch 'esp32-proxy-flip' into neutral-ble-client 2026-08-09 02:17:08 -05:00
J. Nick Koston f2261d6632 Merge branch 'esp32-gatt-backend' into esp32-proxy-flip 2026-08-09 02:16:17 -05:00
J. Nick Koston d224a82e38 Fold the tracker shim into the backend and pass the MTU through 2026-08-09 02:16:14 -05:00
J. Nick Koston ce436ff8b4 Apply review: refusal resolves waiters, cancel skips backoff, wrap-safe hold-off 2026-08-09 01:54:16 -05:00
J. Nick Koston cbda1e71be Merge branch 'esp32-proxy-flip' into neutral-ble-client 2026-08-09 01:53:48 -05:00
J. Nick Koston ceabfeb6f4 Include the header that declares the bond removal 2026-08-09 01:52:39 -05:00
J. Nick Koston 5e11a886f1 Merge branch 'esp32-gatt-backend' into esp32-proxy-flip 2026-08-09 01:51:59 -05:00
J. Nick Koston e771b2522a Terminate a released stream without services-done and keep the net armed 2026-08-09 01:51:42 -05:00
J. Nick Koston ee96293c4c Platform-split ble_client codegen and gate the raw-gattc node family 2026-08-09 01:45:34 -05:00
J. Nick Koston fa18f5a0ad Neutral ble_client engine on the GATT contract 2026-08-09 01:45:11 -05:00
J. Nick Koston 269d52ac2e Revive backend-only builds, the slot ledger, and the table gate 2026-08-09 01:33:45 -05:00
J. Nick Koston 06a52f682f Name the platform explicitly in schema builders for the dumper 2026-08-09 01:01:21 -05:00
J. Nick Koston 409ff027cc Collapse the identical ignore branches 2026-08-09 00:59:16 -05:00
J. Nick Koston 3c7e5000a5 Merge branch 'esp32-gatt-backend' into esp32-proxy-flip 2026-08-09 00:44:41 -05:00
J. Nick Koston 7af211ab54 Deliver completions through a GattClientListener interface 2026-08-09 00:44:35 -05:00
J. Nick Koston cc81069d02 Keep the esp32 maintenance functions outside the gated surface 2026-08-09 00:28:54 -05:00
J. Nick Koston ba567cecc3 Shared backend codegen registry and flip cleanups 2026-08-09 00:23:34 -05:00
J. Nick Koston ee7fb83334 Merge branch 'esp32-gatt-backend' into esp32-proxy-flip
# Conflicts:
#	esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp
#	esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h
2026-08-09 00:20:09 -05:00
J. Nick Koston 9db31db24d Multi-consumer sink, table materializer, and review fixes for the backend 2026-08-09 00:19:39 -05:00
J. Nick Koston ceca66c01a Merge branch 'esp32-gatt-backend' into esp32-proxy-flip 2026-08-08 23:53:50 -05:00
J. Nick Koston dc97c5a161 Merge branch 'esp32-hub-devirtualize' into esp32-gatt-backend 2026-08-08 23:53:48 -05:00
J. Nick Koston 97bbcf55d9 Describe the ladder arm's real safety net in the test docstring 2026-08-08 23:53:45 -05:00
J. Nick Koston d135ac97e5 Report teardown once at CLOSE_EVT and refuse connects on a busy slot 2026-08-08 23:27:04 -05:00
J. Nick Koston 296dd19361 Merge branch 'esp32-gatt-backend' into esp32-proxy-flip 2026-08-08 23:21:58 -05:00
J. Nick Koston 138304cfbe Fail discovery on count errors and free idle slots at once 2026-08-08 23:21:57 -05:00
J. Nick Koston 9fef1862be Merge branch 'esp32-gatt-backend' into esp32-proxy-flip 2026-08-08 23:09:18 -05:00
J. Nick Koston 80758f11aa Merge branch 'esp32-hub-devirtualize' into esp32-gatt-backend 2026-08-08 23:09:16 -05:00
J. Nick Koston 9d8362738f Use the canonical board id and the sibling test conventions 2026-08-08 23:09:13 -05:00
J. Nick Koston b2c22b5802 Merge branch 'esp32-gatt-backend' into esp32-proxy-flip 2026-08-08 22:45:31 -05:00
J. Nick Koston f62009ecc9 Merge branch 'esp32-hub-devirtualize' into esp32-gatt-backend 2026-08-08 22:45:29 -05:00
J. Nick Koston c09e3e0d45 Check the alias defines through real codegen 2026-08-08 22:42:56 -05:00
J. Nick Koston cdd7bef74a Check the alias defines through real codegen 2026-08-08 22:40:36 -05:00
J. Nick Koston 0e1321a813 Merge branch 'esp32-gatt-backend' into esp32-proxy-flip 2026-08-08 22:38:31 -05:00
J. Nick Koston 022b612366 Merge branch 'esp32-hub-devirtualize' into esp32-gatt-backend 2026-08-08 22:38:29 -05:00
J. Nick Koston 2dc9fbec5e Pin the tracker alias three-place invariant 2026-08-08 22:38:26 -05:00
J. Nick Koston 9dafff6a61 Apply the simplify findings and fix the passive esp32 build 2026-08-08 22:32:36 -05:00
J. Nick Koston 3c7a1062c8 Flip the esp32 proxy onto the hub wrapper 2026-08-08 22:21:38 -05:00
J. Nick Koston 8164cdd018 Stream in place through a backend cut-through 2026-08-08 22:08:16 -05:00
J. Nick Koston 4ea491d46f Bound the streaming peak to one batch window 2026-08-08 21:59:08 -05:00
J. Nick Koston 7fdb6cd2d5 Merge branch 'esp32-hub-devirtualize' into esp32-gatt-backend 2026-08-08 21:41:25 -05:00
J. Nick Koston 2d50e07e90 Fold the push-hub surface into the concept 2026-08-08 21:41:23 -05:00
J. Nick Koston 78a451a1c0 Apply the simplify findings 2026-08-08 21:39:48 -05:00
J. Nick Koston 2483621500 Bluedroid backend for the GATT client contract 2026-08-08 21:31:09 -05:00
J. Nick Koston 4f086b6b25 Drive the push and poll halves off one define 2026-08-08 21:23:54 -05:00
J. Nick Koston c4850a9320 Note the tracker-native calls in the esp32 arm 2026-08-08 21:03:19 -05:00
J. Nick Koston 91bcb14c09 Restore merged pairing dispatch and capability doc 2026-08-08 21:02:10 -05:00
J. Nick Koston 85e4a6cc33 Merge remote-tracking branch 'origin/dev' into esp32-hub-devirtualize 2026-08-08 21:00:53 -05:00
J. Nick Koston 149d7cd801 Merge branch 'esp32-tracker-retire-scanner-listener' into esp32-hub-devirtualize
# Conflicts:
#	esphome/components/bluetooth_proxy/bluetooth_proxy.h
#	esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
2026-08-08 19:14:28 -05:00
J. Nick Koston ce2e113a6d Update stale comments left by the listener removal 2026-08-08 19:13:11 -05:00
J. Nick Koston ae5b6d271f Retire the scanner-state listener interface 2026-08-08 19:13:11 -05:00
J. Nick Koston e1572d7520 Drop the duplicated scanner-state define 2026-08-08 16:36:16 -05:00
J. Nick Koston 536afce394 Merge branch 'esp32-tracker-retire-scanner-listener' into esp32-hub-devirtualize
# Conflicts:
#	esphome/components/ble_device_base/ble_hub.h
#	esphome/components/bluetooth_proxy/__init__.py
#	esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
#	esphome/components/bluetooth_proxy/bluetooth_proxy.h
#	esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
2026-08-08 16:02:18 -05:00
J. Nick Koston 152b6a5d50 Update stale comments left by the listener removal 2026-08-08 15:59:54 -05:00
J. Nick Koston f68ebcf7d1 Retire the scanner-state listener interface 2026-08-08 15:59:54 -05:00
J. Nick Koston ca5fa6dfaf Use the shared MAC validity helper 2026-08-08 15:59:53 -05:00
J. Nick Koston 962a82d157 Make the adapter MAC accessor const 2026-08-08 15:59:53 -05:00
J. Nick Koston c6d4ca7377 Use one stack query for the adapter MAC 2026-08-08 15:59:53 -05:00
J. Nick Koston 465a6a3596 Let esp32_ble own the adapter MAC lookup 2026-08-08 15:59:53 -05:00
J. Nick Koston 03c9b001f4 Ask the stack for the adapter MAC on hosted controllers 2026-08-08 15:59:53 -05:00
J. Nick Koston d680200e5a Read the adapter MAC through esp_read_mac 2026-08-08 15:59:53 -05:00
J. Nick Koston 98dae49440 Remove redundant access specifier 2026-08-08 15:59:53 -05:00
J. Nick Koston 7ec366c1ba Monotonic parse flag, client-scoped opt-out, drop dead include 2026-08-08 15:59:53 -05:00
J. Nick Koston b1e1b0d7c5 Retire the raw listener path and parser-type enum from the tracker 2026-08-08 15:59:53 -05:00
J. Nick Koston ec49eb12dd Finish the docstring reflow 2026-08-08 11:28:16 -05:00
J. Nick Koston 949474efc6 Name the alias header in the module docstring 2026-08-08 10:55:27 -05:00
J. Nick Koston 708f095b6f Merge branch 'esp32-tracker-retire-scanner-listener' into esp32-hub-devirtualize 2026-08-08 10:55:27 -05:00
J. Nick Koston bf4af703b6 Merge branch 'esp32-tracker-parser-cleanup' into esp32-tracker-retire-scanner-listener 2026-08-08 10:55:25 -05:00
J. Nick Koston 541616e19c Use the shared MAC validity helper 2026-08-08 10:55:22 -05:00
J. Nick Koston 952c3e6584 Merge branch 'esp32-tracker-retire-scanner-listener' into esp32-hub-devirtualize 2026-08-08 09:52:26 -05:00
J. Nick Koston 2ea55bee7b Merge branch 'esp32-tracker-parser-cleanup' into esp32-tracker-retire-scanner-listener 2026-08-08 09:52:24 -05:00
J. Nick Koston 1931ef32ee Merge branch 'esp32-proxy-hub-scanner-state' into esp32-tracker-parser-cleanup 2026-08-08 09:52:12 -05:00
J. Nick Koston f2da6c1a56 Merge remote-tracking branch 'origin/dev' into esp32-proxy-hub-scanner-state
# Conflicts:
#	esphome/components/bluetooth_proxy/__init__.py
#	esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
#	esphome/components/bluetooth_proxy/bluetooth_proxy.h
#	tests/component_tests/ble_device_base/test_slot_counter.py
2026-08-08 09:51:50 -05:00
J. Nick Koston 6e9e83bdfd Include defines.h where its define is tested 2026-08-08 04:05:45 -05:00
J. Nick Koston fe29a53b71 Require the push slot in the concept where it exists 2026-08-08 03:20:11 -05:00
J. Nick Koston fa2dafcbca Merge branch 'esp32-tracker-retire-scanner-listener' into esp32-hub-devirtualize 2026-08-08 02:43:49 -05:00
J. Nick Koston dc18fced19 Merge branch 'esp32-tracker-parser-cleanup' into esp32-tracker-retire-scanner-listener 2026-08-08 02:43:47 -05:00
J. Nick Koston 389d3629c2 Make the adapter MAC accessor const 2026-08-08 02:43:45 -05:00
J. Nick Koston 36a4d87611 Merge branch 'esp32-proxy-hub-scanner-state' into esp32-tracker-parser-cleanup 2026-08-08 02:43:21 -05:00
J. Nick Koston 65db614f22 Pin the scanner-state callback slot semantics 2026-08-08 02:43:09 -05:00
J. Nick Koston 591856e7dc Constrain the contract's return types 2026-08-08 02:30:10 -05:00
J. Nick Koston b680482092 Trim the surface doc to the semantics 2026-08-08 02:01:49 -05:00
J. Nick Koston 7fe447a4a2 Enforce the hub surface with a concept 2026-08-08 02:00:17 -05:00
J. Nick Koston 8fae4f6767 Merge branch 'esp32-tracker-retire-scanner-listener' into esp32-hub-devirtualize 2026-08-08 01:54:40 -05:00
J. Nick Koston 3d458bd9c8 Merge branch 'esp32-tracker-parser-cleanup' into esp32-tracker-retire-scanner-listener 2026-08-08 01:54:38 -05:00
J. Nick Koston fd7510d6e5 Use one stack query for the adapter MAC 2026-08-08 01:54:36 -05:00
J. Nick Koston ffe55f7691 Trim the new comments to repo style 2026-08-08 01:52:40 -05:00
J. Nick Koston 40ac344bf2 Merge branch 'esp32-tracker-retire-scanner-listener' into esp32-hub-devirtualize
# Conflicts:
#	esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
2026-08-08 01:51:20 -05:00
J. Nick Koston 667ffe98d9 Merge branch 'esp32-tracker-parser-cleanup' into esp32-tracker-retire-scanner-listener 2026-08-08 01:50:51 -05:00
J. Nick Koston 270f776675 Let esp32_ble own the adapter MAC lookup 2026-08-08 01:50:49 -05:00
J. Nick Koston 965bbd4425 Merge branch 'esp32-tracker-retire-scanner-listener' into esp32-hub-devirtualize 2026-08-08 01:46:50 -05:00
J. Nick Koston e485e5af90 Merge branch 'esp32-tracker-parser-cleanup' into esp32-tracker-retire-scanner-listener 2026-08-08 01:46:48 -05:00
J. Nick Koston c795d0734d Ask the stack for the adapter MAC on hosted controllers 2026-08-08 01:46:45 -05:00
J. Nick Koston 0e77d47df4 Merge branch 'esp32-tracker-retire-scanner-listener' into esp32-hub-devirtualize 2026-08-08 01:38:39 -05:00
J. Nick Koston 47c2f8f31a Merge branch 'esp32-tracker-parser-cleanup' into esp32-tracker-retire-scanner-listener 2026-08-08 01:38:36 -05:00
J. Nick Koston fe53d24e46 Read the adapter MAC through esp_read_mac 2026-08-08 01:38:33 -05:00
J. Nick Koston daf988fde8 Merge branch 'esp32-tracker-retire-scanner-listener' into esp32-hub-devirtualize 2026-08-08 01:31:26 -05:00
J. Nick Koston 6604a9cca7 Update stale comments left by the listener removal 2026-08-08 01:31:22 -05:00
J. Nick Koston ddd45e393b Merge branch 'esp32-tracker-retire-scanner-listener' into esp32-hub-devirtualize 2026-08-08 01:26:09 -05:00
J. Nick Koston 0974e8a1f5 Merge branch 'esp32-tracker-parser-cleanup' into esp32-tracker-retire-scanner-listener 2026-08-08 01:26:07 -05:00
J. Nick Koston 1478307280 Merge branch 'esp32-proxy-hub-scanner-state' into esp32-tracker-parser-cleanup 2026-08-08 01:26:06 -05:00
J. Nick Koston fc77cdcb7d Drop a leftover declaration and fix a stale test name 2026-08-08 01:26:03 -05:00
J. Nick Koston fcb9da489e Call the static capability query through the type 2026-08-08 01:22:53 -05:00
J. Nick Koston ab458fffab Bind BLEHub to the build's tracker at compile time 2026-08-08 01:18:21 -05:00
J. Nick Koston c0c3afe6b1 Merge branch 'esp32-tracker-parser-cleanup' into esp32-tracker-retire-scanner-listener 2026-08-08 01:02:41 -05:00
J. Nick Koston b092f64744 Remove redundant access specifier 2026-08-08 01:02:39 -05:00
J. Nick Koston 6b2938c1f7 Retire the scanner-state listener interface 2026-08-08 00:56:59 -05:00
J. Nick Koston 0e5d902c95 Monotonic parse flag, client-scoped opt-out, drop dead include 2026-08-08 00:53:31 -05:00
J. Nick Koston 11fc8e4c07 Merge branch 'esp32-proxy-hub-scanner-state' into esp32-tracker-parser-cleanup 2026-08-08 00:52:35 -05:00
J. Nick Koston d9f0f40373 Simplify scanner state: aliased enum, define-gated slot, one guarded sender 2026-08-08 00:52:12 -05:00
J. Nick Koston e671a602e9 Merge branch 'esp32-proxy-hub-scanner-state' into esp32-tracker-parser-cleanup 2026-08-08 00:41:34 -05:00
J. Nick Koston 7f44b9b826 Merge branch 'esp32-proxy-hub-advertisements' into esp32-proxy-hub-scanner-state
# Conflicts:
#	esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
2026-08-08 00:41:33 -05:00
J. Nick Koston 47804f8d08 Address review: one tracker pointer, portable format, doc fixes 2026-08-08 00:40:43 -05:00
J. Nick Koston 67cc43c8ef Retire the raw listener path and parser-type enum from the tracker 2026-08-08 00:36:22 -05:00
J. Nick Koston 3e9808162b Deliver scanner state through a hub push callback 2026-08-08 00:31:18 -05:00
J. Nick Koston a6f81f3185 Carry the address as uint64 in RawAdvertisement and unify the proxy setup 2026-08-08 00:12:50 -05:00
J. Nick Koston 5296539849 Pin the removed listener slot 2026-08-08 00:01:43 -05:00
J. Nick Koston de9074fb5e Move the esp32 proxy advertisement path to the hub raw callback 2026-08-08 00:00:57 -05:00
1825 changed files with 67194 additions and 19605 deletions

No files matched your search

+17 -3
View File
@@ -111,11 +111,11 @@ ESP-IDF OTA image: `.esphome/build/<name>/build/firmware.ota.bin`. LibreTiny OTA
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/`
(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), `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).
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, 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), `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, 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).
## Templates in `$SKILL/templates`
@@ -132,7 +132,21 @@ 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) `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).
## 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
@@ -7,6 +7,9 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
- Opening the port usually resets the board through DTR, but not always; `serial_capture.py --reset`
pulses RTS to capture from boot. 74880 baud shows the ROM boot messages (`rst cause`, `boot mode`).
- UART0 can be looped back onto itself (`USC0(0) |= (1 << UCLBE)` in an `on_boot` lambda) so a UART
component is testable without its device: frames written with `uart.write` arrive on RX. Log over the
API with `logger: baud_rate: 0`, since the serial port carries the component's bytes.
- `esphome logs` on some CH340s emits thousands of duplicated garbage lines right after the RTS reset;
a plain pyserial capture at 115200 is reliable.
- A failed esptool write can wedge the adapter (`termios` error 22, or an open that hangs) until replug.
@@ -47,6 +50,9 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
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).
- 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)
@@ -80,6 +86,8 @@ 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.
## BK7238, WB3S, CB3S (BK7231N) (LibreTiny bk72xx, CH340, RTS on CEN)
@@ -110,6 +118,13 @@ 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.
- 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.
@@ -4,23 +4,27 @@ 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
- [`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
- [`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
- [`ld2450.md`](ld2450.md): polygon zones, zone number round trip through the radar's readback, ESP8266 loopback harness without a radar
- [`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
- [`modem.md`](modem.md): PPPoS modem component boot check with no modem attached, loop blocking and disable timing
- [`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
- [`online_image.md`](online_image.md): stalled and malformed downloads
- [`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
- [`rtsp_server.md`](rtsp_server.md): camera streaming on the Waveshare ESP32-S3-ETH
- [`scheduler.md`](scheduler.md): loop wake A/B
- [`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)
- [`wifi.md`](wifi.md): reboot_timeout with a fallback AP, DHCP lease cache, Easy Connect (DPP), WISP bridge with an ethernet upstream
- [`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
@@ -30,3 +34,7 @@ One file per component, named after it:
- [`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 on the S3
- [`remote_base.md`](remote_base.md): remote_receiver protocol decoding through a one-pin loopback on ESP32 and ESP8266, no IR hardware
@@ -28,3 +28,31 @@ connected first, and the loop-time maxima. For the loop cost of polling, repeat
so after a drop every dial is closed and HA's incoming reconnect wins 11 to 33 s later; the measurable effect is the
first dial landing in the loop the network flag flips.
Results: https://github.com/esphome/esphome/pull/21085#issuecomment-6254742791
## User defined action listing and execution without a client library (#18882)
`scripts/api/rawactions.py HOST list` speaks the plaintext native API over a raw socket and decodes every
`ListEntitiesServicesResponse` by field number (argument fields 1 name, 2 type, 3 description, 4 example, 5 optional,
6 default_value), so new wire fields show up without waiting for the client library. `rawactions.py HOST exec ACTION
k=v ...` sends `ExecuteServiceRequest` with `sint32` ints (field 5, not the legacy int32 field 2), fixed32 floats,
length delimited strings and `int[]` as repeated sint32 (field 7); `ALLOW_MISSING=1` leaves arguments out on purpose
(the device then drops the call). `templates/api-actions-dut.yaml` (D1 mini, three actions, optional variables with
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
@@ -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,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,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,13 @@
# ld2412
## Output pin level and the basic configuration command (#21353)
`templates/ld2412-select-only-dut.yaml` (ESP32-S3, the module on GPIO43 and GPIO44 at 115200, only the output pin
level select, the module's OUT pin on GPIO38 as a binary sensor plus the presence sensor) with `uart: debug:
direction: BOTH`. Drive the select with `scripts/ld2450/lddrive.py HOST KEY LOGFILE sel:NAME=OPTION` and read the TX
dump for command `02` carrying the level byte, then the OUT pin binary sensor against presence: with level high the
pin follows presence, with level low it is inverted. A module that never answers the boot query (no `<<<` lines)
only shows the held back path: `Command 02 held back until the module reports its settings`, a query `12`, and after
2 s `Command 02 not sent, the module did not report its settings` with the select republished to the last known
level. With the full set of numbers configured a select change also needs every number to have a state, so wait
for the boot query answer before judging.
@@ -13,3 +13,30 @@ selects over the API and prints every LD2450 state update. After each set the hu
the radar and republishes all twelve coordinates from the answer, so set each coordinate of each zone to a distinct
value and check the readbacks; no target is needed. Readbacks can arrive late over Wi-Fi, so match them by content.
Results: https://github.com/esphome/esphome/pull/21146#issuecomment-6278026922
## ESP8266 loopback harness, no radar needed (#21349)
`templates/ld2450-loopback.yaml` (package; substitutions `name` and `baud`) runs the hub on UART0 (GPIO1 and GPIO3)
with `logger: baud_rate: 0` and sets the UART0 loopback bit in `on_boot` (`USC0(0) |= (1 << UCLBE)`), so everything
the hub or a `uart.write` action sends comes back on RX. Template buttons inject periodic frames (all four direction
strings, targets 2 and 3), query zone acks (zone type 0, 1 and 2 for the select readback) and call the name based
setters with a valid and a bogus name. `uart: debug: direction: TX` delimited on the command footer dumps every
command the hub sends. Drive it with `scripts/ld2450/lddrive.py HOST KEY LOGFILE press:NAME sel:NAME=OPTION wait:S`
(logs over the API at VERBOSE, 2 s between steps because the API log stream drops lines while `send_command_`
blocks), then `txsum.py LOGFILE` lists the TX frames per step; diff the summaries of the two builds. The hub reads its
own commands back and logs `Invalid status` for each, which is harmless. The boot readback of the baud rate select
comes from the UART setting, so a second build at `baud: "9600"` checks the flash table lookup.
Results: https://github.com/esphome/esphome/pull/21349#issuecomment-6332320026
## Frame splitter collisions (#21357)
The loopback template also carries the zone 1 numbers and seven collision buttons: a periodic frame with the
command footer inside target 1, zone acks with `55 CC` and with `04 03 02 01` inside the coordinates, a periodic
frame cut after 12 bytes followed by a zone ack, a frame missing its last byte followed by a complete one, a bogus
255 byte length then a frame, and garbage then a frame. Judge by the state updates plus the `Handling Periodic` and
`Handling Ack` lines. Through a real radar the set zone ack is sometimes corrupted by the radar itself (its tail
replaced by the periodic frame it was sending, for example `04 03 03 03 00 ... 55 CC`), so count readbacks over at
least 24 selects per build and read the raw bytes with `uart: debug: direction: RX` (`!extend` the package's uart
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
@@ -0,0 +1,10 @@
# 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.
@@ -0,0 +1,18 @@
# 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
@@ -51,3 +51,31 @@ 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
@@ -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.
@@ -0,0 +1,30 @@
# 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: the transmitter sends with interrupts off, so a receiver on the same pin sees nothing. `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.
@@ -0,0 +1,19 @@
# 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`, 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).
@@ -23,3 +23,17 @@ Only an Android phone configures: Settings, network details, Add device, Share W
QR scanner makes the phone an enrollee). Render the URI with the `qrcode` module and open the PNG.
`esp32: framework: log_level: DEBUG` plus `sdkconfig_options: CONFIG_WPA_DEBUG_PRINT: y` prints the
supplicant state machine.
## WISP bridge with an ethernet upstream (#7147)
`templates/wisp-ethernet-dut.yaml` (ESP32-S3 with a W5500, `wifi: ap:` with `ap_mode: WISP` and no station) and
`templates/wisp-client.yaml` (D1 mini joining the bridge, `http_request` to a web server on the wired LAN and to
`http://example.com/` every 15 s, `time: sntp`). Secrets: `wisp_psk` and `lan_http` (serve a text file on the host
with `python -m http.server 8787 --bind 0.0.0.0` from a scratch directory). Judge by the client's `GET status 200`
lines, the web server log showing the device's ethernet address as the source (NAT), the client's DHCP info (DNS1 is
the LAN resolver) and `SNTP synced`. The device logs `Starting AP` and `DHCP DNS server: <resolver>` on its USB
console; flashing over the air reboots it for repeat boots. Before the AP address and DHCP server moved ahead of the
AP start (the fix in that PR), the first AP start failed on every boot with `esp_netif_dhcps_option failed: 20484`
(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.
@@ -0,0 +1,211 @@
"""Plaintext native API client that lists user actions with their raw argument fields and executes one.
usage: rawactions.py HOST list
rawactions.py HOST exec ACTION key=value [...] (every argument must be given; types from the listing)
Fields of ListEntitiesServicesArgument: 1 name, 2 type, 3 description, 4 example, 5 optional, 6 default_value.
"""
import os
import socket
import struct
import sys
HOST = sys.argv[1]
CMD = sys.argv[2]
ARGS = sys.argv[3:]
TYPE_NAMES = {
0: "bool",
1: "int",
2: "float",
3: "string",
4: "bool[]",
5: "int[]",
6: "float[]",
7: "string[]",
}
def varint(v: int) -> bytes:
out = bytearray()
while True:
b = v & 0x7F
v >>= 7
if v:
out.append(b | 0x80)
else:
out.append(b)
return bytes(out)
def svarint(v: int) -> bytes:
return varint((v << 1) ^ (v >> 63))
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) -> list[tuple[int, int, object]]:
out = []
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] # fixed32, caller unpacks
pos += 4
else:
raise ValueError(f"wire type {wt}")
out.append((fnum, wt, v))
return out
def frame(mtype: int, payload: bytes) -> bytes:
return b"\x00" + varint(len(payload)) + varint(mtype) + payload
def field_bytes(fnum: int, data: bytes) -> bytes:
return varint((fnum << 3) | 2) + varint(len(data)) + data
def field_varint(fnum: int, v: int) -> bytes:
return varint((fnum << 3) | 0) + varint(v)
def recv_frames(
sock: socket.socket, until: set[int], limit: float = 5.0
) -> list[tuple[int, bytes]]:
sock.settimeout(limit)
buf = bytearray()
frames = []
while True:
try:
chunk = sock.recv(4096)
except TimeoutError:
return frames
if not chunk:
return frames
buf += chunk
while True:
if len(buf) < 3:
break
if buf[0] != 0:
raise ValueError("not plaintext")
try:
ln, p = decode_varint(buf, 1)
mt, p = decode_varint(buf, p)
except IndexError:
break
if len(buf) < p + ln:
break
frames.append((mt, bytes(buf[p : p + ln])))
del buf[: p + ln]
if mt in until:
return frames
def main() -> None:
sock = socket.create_connection((HOST, 6053), timeout=5)
sock.sendall(
frame(
1, field_bytes(1, b"rawactions") + field_varint(2, 1) + field_varint(3, 10)
)
) # HelloRequest
recv_frames(sock, {2})
sock.sendall(frame(3, b"")) # ConnectRequest, no password
recv_frames(sock, {4})
sock.sendall(frame(11, b"")) # ListEntitiesRequest
frames = recv_frames(sock, {19})
services = {}
for mt, payload in frames:
if mt != 41:
continue
name = key = None
args = []
for fnum, _, v in fields(payload):
if fnum == 1:
name = v.decode()
elif fnum == 2:
key = struct.unpack("<I", v)[0]
elif fnum == 3:
a = {
"name": None,
"type": 0,
"description": None,
"example": None,
"optional": None,
"default": None,
}
for af, _, av in fields(v):
if af == 1:
a["name"] = av.decode()
elif af == 2:
a["type"] = av
elif af == 3:
a["description"] = av.decode()
elif af == 4:
a["example"] = av.decode()
elif af == 5:
a["optional"] = bool(av)
elif af == 6:
a["default"] = av.decode()
else:
a[f"field{af}"] = av
args.append(a)
services[name] = (key, args)
if CMD == "list":
for name, (key, args) in services.items():
print(f"action {name} key={key}")
for a in args:
extra = {
k: v
for k, v in a.items()
if k not in ("name", "type") and v is not None
}
print(f" {a['name']}: {TYPE_NAMES.get(a['type'], a['type'])} {extra}")
return
name = ARGS[0]
key, args = services[name]
payload = varint((1 << 3) | 5) + struct.pack("<I", key)
given = dict(kv.split("=", 1) for kv in ARGS[1:])
for a in args:
if a["name"] not in given:
if os.environ.get("ALLOW_MISSING"):
continue # leave the argument out of the request on purpose
sys.exit(f"missing {a['name']}")
raw = given[a["name"]]
t = a["type"]
if t == 0:
av = field_varint(1, 1 if raw.lower() == "true" else 0)
elif t == 1:
av = varint((5 << 3) | 0) + svarint(int(raw)) # sint32 int_
elif t == 2:
av = varint((3 << 3) | 5) + struct.pack("<f", float(raw))
elif t == 3:
av = field_bytes(4, raw.encode())
elif t == 5:
av = b"".join(
varint((7 << 3) | 0) + svarint(int(x)) for x in raw.split(",") if x
) # repeated sint32 int_array
else:
sys.exit(f"type {t} not supported here")
payload += field_bytes(2, av)
sock.sendall(frame(42, payload)) # ExecuteServiceRequest
print(f"sent {name} with {given}")
sock.close()
main()
@@ -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,44 @@
"""Run the burst action once and print each queued request as it completes.
Usage: burstonly.py IP URL N"""
import asyncio
import re
import sys
import time
import aioesphomeapi
IP, URL, N = sys.argv[1], sys.argv[2], int(sys.argv[3])
async def main():
c = aioesphomeapi.APIClient(IP, 6053, None)
await c.connect(login=True)
_, svcs = await c.list_entities_services()
s = {x.name: x for x in svcs}
t0 = time.monotonic()
done = []
errors = []
def on_log(m):
t = re.sub(r"\x1b\[[0-9;]*m", "", m.message.decode(errors="replace"))
if "BURSTRESP status=200 " in t:
done.append(t)
elif "BURSTRESP" in t or "BURSTERR" in t:
errors.append(t)
if "hrt" in t or "Too many" in t:
print(f"{time.monotonic() - t0:6.2f} {t[:120]}", flush=True)
c.subscribe_logs(on_log, log_level=aioesphomeapi.LogLevel.LOG_LEVEL_INFO)
await asyncio.sleep(1)
t0 = time.monotonic()
await c.execute_service(s["burst"], {"url": URL, "count": N})
await asyncio.sleep(14)
await c.disconnect()
print(
f"{len(done)} of {N} burst requests answered, {len(errors)} failed", flush=True
)
return 0 if len(done) >= N else 1
sys.exit(asyncio.run(main()))
@@ -0,0 +1,159 @@
"""Drive the http_request action test device over the API and collect its hrt log lines.
Usage: hrdrive.py DEVICE_IP SERVER_BASE LABEL [https]"""
import asyncio
import os
import re
import sys
import time
import aioesphomeapi
IP, BASE, LABEL = sys.argv[1], sys.argv[2], sys.argv[3]
HTTPS = len(sys.argv) > 4
CASES = [
("get", "fast", BASE + "/fast"),
("get", "slow2s", BASE + "/slow?d=2"),
("get", "big70k", BASE + "/big?n=70000&rate=20000"),
("get", "chunked70k", BASE + "/chunked?n=70000&rate=20000"),
("post", "post500", BASE + "/post"),
("get", "redirect", BASE + "/redirect"),
("get", "hang", BASE + "/hang?d=20"),
("get", "unreachable", "http://192.0.2.1:81/"),
]
if HTTPS:
CASES += [
("get", "https_get", "https://www.howsmyssl.com/a/check"),
("post", "https_post", "https://httpbin.org/post"),
]
ONLY = os.environ.get("ONLY")
if ONLY:
CASES = [c for c in CASES if c[1] in ONLY.split(",")]
ROUNDS = int(os.environ.get("ROUNDS", "2"))
# cases whose request must fail (past the 8 s timeout, or an address that never answers)
EXPECT_ERROR = {"hang", "unreachable"}
EXTRAS = not ONLY
timeouts: list[str] = []
mismatches: list[str] = []
async def main() -> int:
c = aioesphomeapi.APIClient(IP, 6053, None)
await c.connect(login=True)
_, services = await c.list_entities_services()
svc = {s.name: s for s in services}
lines = []
def on_log(m):
t = (
m.message.decode(errors="replace")
if isinstance(m.message, bytes)
else m.message
)
t = re.sub(r"\x1b\[[0-9;]*m", "", t)
if (
"[hrt" in t
or "http_request" in t
or "Script" in t
or "already running" in t
):
lines.append(t)
print(f"{LABEL} {t}", flush=True)
def bad_burst(ln: str) -> bool:
return "BURSTERR" in ln or ("BURSTRESP" in ln and "status=200 " not in ln)
def mismatch(what: str, why: str) -> None:
print(f"{LABEL} {what} MISMATCH: {why}", flush=True)
mismatches.append(what)
async def wait_until(done, secs: float, what: str) -> None:
"""Poll done() until it holds; print a TIMEOUT marker when the deadline passes first."""
end = time.monotonic() + secs
while not done():
if time.monotonic() >= end:
print(f"{LABEL} {what} TIMEOUT", flush=True)
timeouts.append(what)
return
await asyncio.sleep(0.2)
c.subscribe_logs(on_log, log_level=aioesphomeapi.LogLevel.LOG_LEVEL_DEBUG)
await asyncio.sleep(1)
for rnd in range(1, ROUNDS + 1):
for kind, name, url in CASES:
n0 = len(lines)
print(f"{LABEL} == {name} round {rnd}", flush=True)
if kind == "get":
await c.execute_service(svc["req_get"], {"url": url})
else:
await c.execute_service(
svc["req_post"], {"url": url, "payload": "x" * 500}
)
await wait_until(
lambda n0=n0: any("RESULT" in ln for ln in lines[n0:]),
40,
f"{name} round {rnd}",
)
result = next((ln for ln in lines[n0:] if "RESULT" in ln), None)
if result is not None and (
("status=200 " not in result)
if name not in EXPECT_ERROR
else (" error " not in result)
):
mismatch(f"{name} round {rnd}", result)
await asyncio.sleep(1.5)
if not EXTRAS:
continue
print(f"{LABEL} == burst round {rnd}", flush=True)
n0 = len(lines)
await c.execute_service(svc["burst"], {"url": BASE + "/slow?d=1", "count": 6})
await wait_until(
lambda n0=n0: (
sum("BURSTRESP" in ln or "BURSTERR" in ln for ln in lines[n0:]) >= 6
),
30,
f"burst round {rnd}",
)
if any(bad_burst(ln) for ln in lines[n0:]):
mismatch(f"burst round {rnd}", "a burst request failed or was not 200")
await asyncio.sleep(2)
print(f"{LABEL} == cancel round {rnd}", flush=True)
n0 = len(lines)
await c.execute_service(svc["cancel"], {"url": BASE + "/slow?d=2"})
await asyncio.sleep(9)
if not any("CANCEL stopped running=0" in ln for ln in lines[n0:]):
mismatch(
f"cancel round {rnd}", "script.stop did not leave the script stopped"
)
print(f"{LABEL} == cross round {rnd}", flush=True)
n0 = len(lines)
await c.execute_service(svc["req_get"], {"url": BASE + "/slow?d=2&tag=cross"})
await c.execute_service(
svc["burst"], {"url": BASE + "/slow?d=1&tag=crossburst", "count": 3}
)
await wait_until(
lambda n0=n0: (
sum("BURSTRESP" in ln or "BURSTERR" in ln for ln in lines[n0:]) >= 3
and any("RESULT" in ln for ln in lines[n0:])
),
30,
f"cross round {rnd}",
)
if any(
bad_burst(ln) or ("RESULT" in ln and "status=200 " not in ln)
for ln in lines[n0:]
):
mismatch(f"cross round {rnd}", "a request failed")
await asyncio.sleep(2)
await c.disconnect()
if timeouts:
print(f"{LABEL} TIMEOUTS: {', '.join(timeouts)}", flush=True)
if mismatches:
print(f"{LABEL} MISMATCHES: {', '.join(mismatches)}", flush=True)
return 1 if timeouts or mismatches else 0
sys.exit(asyncio.run(main()))
@@ -0,0 +1,78 @@
"""Test server for http_request actions: deterministic bodies, slow and chunked responses."""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import sys
import time
from urllib.parse import parse_qs, urlparse
def body_bytes(n):
return bytes((i * 7 + 3) & 0xFF for i in range(n))
class H(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, fmt, *a):
sys.stdout.write(f"{time.time():.3f} {self.client_address[0]} {fmt % a}\n")
sys.stdout.flush()
def _q(self):
u = urlparse(self.path)
return u.path, {k: v[0] for k, v in parse_qs(u.query).items()}
def _send(self, data, code=200, ctype="application/octet-stream"):
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_POST(self):
n = int(self.headers.get("Content-Length", 0))
got = self.rfile.read(n)
self._send(
b"posted %d %d" % (len(got), sum(got) & 0xFFFFFFFF), ctype="text/plain"
)
def do_GET(self):
path, q = self._q()
if path == "/fast":
self._send(b"hello", ctype="text/plain")
elif path == "/slow":
time.sleep(float(q.get("d", "2")))
self._send(b"slow ok", ctype="text/plain")
elif path == "/redirect":
self.send_response(302)
self.send_header("Location", "/fast")
self.send_header("Content-Length", "0")
self.end_headers()
elif path == "/hang":
time.sleep(float(q.get("d", "30")))
elif path in ("/big", "/chunked"):
n = int(q.get("n", "70000"))
rate = float(q.get("rate", "20000"))
data = body_bytes(n)
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
if path == "/big":
self.send_header("Content-Length", str(n))
else:
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
step = 1000
for i in range(0, n, step):
part = data[i : i + step]
if path == "/big":
self.wfile.write(part)
else:
self.wfile.write(b"%x\r\n" % len(part) + part + b"\r\n")
self.wfile.flush()
time.sleep(len(part) / rate)
if path == "/chunked":
self.wfile.write(b"0\r\n\r\n")
else:
self._send(b"nope", 404)
ThreadingHTTPServer(("0.0.0.0", int(sys.argv[1])), H).serve_forever()
@@ -0,0 +1,58 @@
"""Serve update manifests: stable.json, beta.json and slow.json (answered after a delay); logs every request."""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import sys
import time
PORT = int(sys.argv[1])
DELAY = float(sys.argv[2]) if len(sys.argv) > 2 else 4.0
def manifest(name: str, version: str) -> bytes:
builds = [
{
"chipFamily": fam,
"ota": {
"path": f"/{name}.bin",
"md5": "d41d8cd98f00b204e9800998ecf8427e",
"summary": f"{name} channel",
},
}
for fam in ("ESP8266", "ESP32")
]
return json.dumps(
{"name": f"Bench {name}", "version": version, "builds": builds}
).encode()
DOCS = {
"/stable.json": manifest("stable", "1.0.0"),
"/beta.json": manifest("beta", "2.0.0-beta1"),
"/slow.json": manifest("slow", "3.0.0-slow"),
}
class Handler(BaseHTTPRequestHandler):
def do_GET(self) -> None: # noqa: N802
body = DOCS.get(self.path)
print(
f"{time.strftime('%H:%M:%S')} {self.client_address[0]} GET {self.path} -> {404 if body is None else 200}",
flush=True,
)
if body is None:
self.send_error(404)
return
if self.path == "/slow.json":
time.sleep(DELAY)
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args) -> None: # quiet default log
return
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
@@ -0,0 +1,10 @@
esphome:
name: ho32base
esp32:
board: esp32dev
framework:
type: esp-idf
packages:
common: !include ../../ho-common.yaml
@@ -0,0 +1,11 @@
esphome:
name: ho66base
esp8266:
board: d1_mini
packages:
common: !include ../../ho-common.yaml
http_request:
verify_ssl: false
@@ -0,0 +1,32 @@
"""Serve a www dir for ota.http_request tests; /auth/ needs basic auth otauser:otapass.
Usage: fwsrv.py WWW PORT"""
import base64
import functools
import http.server
import sys
WWW, PORT = sys.argv[1], int(sys.argv[2])
GOOD = "Basic " + base64.b64encode(b"otauser:otapass").decode()
class H(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path.startswith("/auth/") and self.headers.get("Authorization") != GOOD:
self.send_response(401)
self.send_header("WWW-Authenticate", 'Basic realm="ota"')
self.send_header("Content-Length", "0")
self.end_headers()
return
super().do_GET()
def log_message(self, fmt, *args):
print(
f"{self.address_string()} auth={'Authorization' in self.headers} {fmt % args}",
flush=True,
)
http.server.ThreadingHTTPServer(
("0.0.0.0", PORT), functools.partial(H, directory=WWW)
).serve_forever()
@@ -0,0 +1,60 @@
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
api:
reboot_timeout: 0s
actions:
- action: fl_md5url
variables: {url: string, md5url: string}
then:
- ota.http_request.flash:
url: !lambda return url;
md5_url: !lambda return md5url;
- action: fl_md5
variables: {url: string, sum: string}
then:
- ota.http_request.flash:
url: !lambda return url;
md5: !lambda return sum;
- action: fl_auth
variables: {url: string, md5url: string, user: string, pw: string}
then:
- ota.http_request.flash:
url: !lambda return url;
md5_url: !lambda return md5url;
username: !lambda return user;
password: !lambda return pw;
- action: fl_nourl
then:
- lambda: id(hota).flash();
- action: fl_badurl
then:
- lambda: |-
id(hota).set_url("not a url");
id(hota).flash();
- action: getters
then:
- lambda: |-
ESP_LOGI("hot", "getters computed='%s' expected='%s'", id(hota).md5_computed().c_str(),
id(hota).md5_expected().c_str());
esphome:
on_boot:
priority: -100
then:
- lambda: |-
ESP_LOGI("hot", "boot getters computed='%s' expected='%s'", id(hota).md5_computed().c_str(),
id(hota).md5_expected().c_str());
http_request:
timeout: 10s
ota:
- platform: esphome
- platform: http_request
id: hota
@@ -0,0 +1,142 @@
"""Drive the ota.http_request cases over the API.
Usage: hodrive.py IP BASEURL MD5FILE (BASEURL serves fw.bin, fw.md5, bad.md5, auth/fw.bin, auth/fw.md5)"""
import asyncio
import pathlib
import re
import sys
import time
import aioesphomeapi
if len(sys.argv) != 4:
sys.exit(__doc__)
IP, BASE = sys.argv[1:3]
SUM = pathlib.Path(sys.argv[3]).read_text(encoding="utf-8").strip()
T0 = time.monotonic()
lines: list[str] = []
failed: list[str] = []
def out(s: str) -> None:
print(f"{time.monotonic() - T0:7.2f} {s}", flush=True)
async def connect() -> tuple[aioesphomeapi.APIClient, dict]:
last: Exception | None = None
for _ in range(90):
c = aioesphomeapi.APIClient(IP, 6053, None)
try:
await c.connect(login=True)
except (
aioesphomeapi.InvalidAuthAPIError,
aioesphomeapi.InvalidEncryptionKeyAPIError,
aioesphomeapi.RequiresEncryptionAPIError,
):
raise # a key or password problem never fixes itself
except Exception as exc: # noqa: BLE001
last = exc
await asyncio.sleep(2)
continue
i = await c.device_info()
out(f"connected {i.name} {i.compilation_time}")
_, svcs = await c.list_entities_services()
def on_log(m):
t = re.sub(r"\x1b\[[0-9;]*m", "", m.message.decode(errors="replace"))
if re.search(r"\[hot|http_request|ota|OTA|MD5|md5", t):
lines.append(t)
out(t[:170])
c.subscribe_logs(on_log, log_level=aioesphomeapi.LogLevel.LOG_LEVEL_DEBUG)
return c, {x.name: x for x in svcs}
raise SystemExit(f"device never came back (last error {last!r})")
async def case(c, s, name, args, want, timeout=60.0):
out(f"=== {name} {args} (want {want})")
start = len(lines)
await c.execute_service(s[name], args)
end = time.monotonic() + timeout
while time.monotonic() < end:
if any(re.search(want, x) for x in lines[start:]):
out(f"RESULT {name} PASS")
return True
await asyncio.sleep(0.2)
out(f"RESULT {name} FAIL (no '{want}')")
failed.append(name)
return False
async def flash_ok(c, s, name, args):
"""A good flash: wait for the device to drop and come back."""
out(f"=== {name} {args} (want reboot)")
start = len(lines)
await c.execute_service(s[name], args)
end = time.monotonic() + 120
while (
time.monotonic() < end
and c._connection is not None
and c._connection.is_connected
): # noqa: SLF001
await asyncio.sleep(0.5)
# a good flash needs both the device's own success line and the connection dropping for the reboot
dropped = c._connection is None or not c._connection.is_connected # noqa: SLF001
logged = any(
re.search(r"(?i)update complete|rebooting|successful", x) for x in lines[start:]
)
if not dropped:
out(f"{name}: the device never dropped the connection within 120 s")
ok = logged and dropped
await c.disconnect()
await asyncio.sleep(5)
c, s = await connect()
out(f"RESULT {name} {'PASS' if ok else 'FAIL'}")
if not ok:
failed.append(name)
return c, s
async def main():
c, s = await connect()
u, m = f"{BASE}/fw.bin", f"{BASE}/fw.md5"
await case(c, s, "getters", {}, r"computed='' expected=''")
await case(c, s, "fl_nourl", {}, r"URL not set")
await case(c, s, "fl_badurl", {}, r"URL not set")
await case(c, s, "getters", {}, r"computed='' expected=''")
await case(c, s, "fl_md5", {"url": u, "sum": "0" * 32}, r"MD5 mismatch")
await case(c, s, "getters", {}, r"getters computed=")
await case(
c,
s,
"fl_auth",
{
"url": f"{BASE}/auth/fw.bin",
"md5url": f"{BASE}/auth/fw.md5",
"user": "otauser",
"pw": "wrong",
},
r"HTTP Request failed; URL: \S*/auth/fw\.md5; Code: 401",
)
c, s = await flash_ok(
c,
s,
"fl_auth",
{
"url": f"{BASE}/auth/fw.bin",
"md5url": f"{BASE}/auth/fw.md5",
"user": "otauser",
"pw": "otapass",
},
)
c, s = await flash_ok(c, s, "fl_md5url", {"url": u, "md5url": m})
c, s = await flash_ok(c, s, "fl_md5", {"url": u, "sum": SUM})
await case(c, s, "getters", {}, r"computed='' expected=''")
await c.disconnect()
out(
f"SUMMARY {'all cases passed' if not failed else 'FAILED: ' + ', '.join(failed)}"
)
return 1 if failed else 0
sys.exit(asyncio.run(main()))
@@ -0,0 +1,10 @@
esphome:
name: ho32pr
esp32:
board: esp32dev
framework:
type: esp-idf
packages:
common: !include ../../ho-common.yaml
@@ -0,0 +1,11 @@
esphome:
name: ho66pr
esp8266:
board: d1_mini
packages:
common: !include ../../ho-common.yaml
http_request:
verify_ssl: false
@@ -0,0 +1,30 @@
#!/bin/bash
# run.sh BUILD(pr|base) BOARD(32|66) : serial flash ho<BOARD><BUILD>, serve its own image, drive the cases
# Copy this directory to a scratch directory (secrets.yaml next to ho-common.yaml) and run it there.
# env: TREE (esphome checkout for BUILD), P32 or P66 (serial port of the board), SC (path to serial_capture.py),
# HOST_IP (this host's address as the device reaches it), PY (checkout python)
D=$(cd "$(dirname "$0")" && pwd); V=$1; B=$2; N=ho$B$V; T=${TREE:?set TREE}; PY=${PY:-$T/venv/bin/python}
SC=${SC:?set SC}; HOST=${HOST_IP:?set HOST_IP}
if [ "$B" = 32 ]; then P=${P32:?set P32}; else P=${P66:?set P66}; fi
OUT=$D/out-$V-$B; rm -rf "$OUT"; mkdir -p "$OUT"
pkill -f "[s]erial_capture.py $P"
(cd "$T" && $PY -m esphome compile "$D/$V/$B/$N.yaml" > "$OUT/compile.log" 2>&1) || { echo "compile failed"; exit 1; }
(cd "$T" && $PY -m esphome upload "$D/$V/$B/$N.yaml" --device "$P" > "$OUT/upload.log" 2>&1) || { echo "upload failed"; exit 1; }
# the exact image of this config, just compiled, never one found elsewhere
if [ "$B" = 32 ]; then IMG=$D/$V/$B/.esphome/build/$N/build/firmware.ota.bin
else IMG=$D/$V/$B/.esphome/build/$N/.pioenvs/$N/firmware.bin; fi
[ -f "$IMG" ] || { echo "no image for $N at $IMG"; exit 1; }
echo "image $IMG"
W=$OUT/www; mkdir -p "$W/auth"; cp "$IMG" "$W/fw.bin"; cp "$IMG" "$W/auth/fw.bin"
$PY -c "import hashlib,sys;print(hashlib.md5(open(sys.argv[1],'rb').read()).hexdigest())" "$IMG" > "$W/fw.md5"
cp "$W/fw.md5" "$W/auth/fw.md5"; echo 00000000000000000000000000000000 > "$W/bad.md5"
$PY "$SC" "$P" 400 "$OUT/serial.txt" --reset > /dev/null 2>&1 & CAP=$!
trap 'kill $CAP $SRV 2>/dev/null' EXIT
pkill -f "[f]wsrv.py"; $PY "$D/fwsrv.py" "$W" 8093 > "$OUT/server.txt" 2>&1 & SRV=$!
for i in $(seq 60); do IP=$(grep -a -o 'IP Address: [0-9.]*' "$OUT/serial.txt" | tail -1 | cut -d' ' -f3); [ -z "$IP" ] && IP=$(python3 -c "import socket;print(socket.gethostbyname('$N.local'))" 2>/dev/null); [ -n "$IP" ] && break; sleep 2; done
[ -n "$IP" ] || { echo "device not found"; exit 1; }
echo "device up"
$PY "$D/hodrive.py" "$IP" "http://$HOST:8093" "$W/fw.md5" > "$OUT/drive.txt" 2>&1; RC=$?
kill $SRV; pkill -f "[s]erial_capture.py $P"
grep -E 'RESULT|SUMMARY|never came back' "$OUT/drive.txt"; echo "server requests: $(grep -c . "$OUT/server.txt")"
exit $RC
@@ -0,0 +1,74 @@
"""Trigger ota.http_request.flash over the API, optionally during a burst of action requests.
Usage: otadrive.py IP FWURL MD5 BASE [burst]"""
import asyncio
import contextlib
import re
import sys
import time
import aioesphomeapi
IP, FW, SUM, BASE = sys.argv[1:5]
BURST = len(sys.argv) > 5
async def main():
c = aioesphomeapi.APIClient(IP, 6053, None)
await c.connect(login=True)
i = await c.device_info()
print("before:", i.name, i.compilation_time, flush=True)
_, svcs = await c.list_entities_services()
s = {x.name: x for x in svcs}
t0 = time.monotonic()
seen: list[str] = []
def on_log(m):
t = re.sub(r"\x1b\[[0-9;]*m", "", m.message.decode(errors="replace"))
seen.append(t)
if re.search(r"hrt|http_request|ota|OTA|Too many", t):
print(f"{time.monotonic() - t0:7.2f} {t[:170]}", flush=True)
c.subscribe_logs(on_log, log_level=aioesphomeapi.LogLevel.LOG_LEVEL_INFO)
await asyncio.sleep(1)
if BURST:
await c.execute_service(s["burst"], {"url": BASE + "/slow?d=1", "count": 6})
await asyncio.sleep(0.5)
await c.execute_service(s["otaflash"], {"url": FW, "sum": SUM})
end = time.monotonic() + 45
while (
time.monotonic() < end
and c._connection is not None # noqa: SLF001
and c._connection.is_connected # noqa: SLF001
):
await asyncio.sleep(0.5)
dropped = c._connection is None or not c._connection.is_connected # noqa: SLF001
with contextlib.suppress(Exception):
await c.disconnect()
for _ in range(30):
try:
c2 = aioesphomeapi.APIClient(IP, 6053, None)
await c2.connect(login=True)
i = await c2.device_info()
print("after:", i.name, i.compilation_time, flush=True)
await c2.disconnect()
break
except Exception: # noqa: BLE001
await asyncio.sleep(2)
else:
print("RESULT FAIL: not reachable after the flash", flush=True)
return 1
if not dropped:
print("RESULT FAIL: the device never rebooted", flush=True)
return 1
if any("OTAFLASH returned" in t for t in seen):
print("RESULT FAIL: the flash action returned, the update failed", flush=True)
return 1
if not any(re.search(r"(?i)update complete|rebooting|successful", t) for t in seen):
print("RESULT FAIL: no update complete line before the reboot", flush=True)
return 1
print("RESULT PASS", flush=True)
return 0
sys.exit(asyncio.run(main()))
@@ -0,0 +1,68 @@
"""Call reboot_during (a slow request in flight, another waiting, then App.safe_reboot) and time the reboot.
Usage: reboottest.py IP URL"""
import asyncio
import contextlib
import re
import sys
import time
import aioesphomeapi
IP, URL = sys.argv[1], sys.argv[2]
async def main():
c = aioesphomeapi.APIClient(IP, 6053, None)
await c.connect(login=True)
_, svcs = await c.list_entities_services()
s = {x.name: x for x in svcs}
t0 = time.monotonic()
def on_log(m):
t = re.sub(r"\x1b\[[0-9;]*m", "", m.message.decode(errors="replace"))
if re.search(
r"hrt|http_request|safe_mode|shutdown|Rebooting|reboot", t, re.IGNORECASE
):
print(f"{time.monotonic() - t0:7.2f} {t[:150]}", flush=True)
c.subscribe_logs(on_log, log_level=aioesphomeapi.LogLevel.LOG_LEVEL_DEBUG)
await asyncio.sleep(1)
t0 = time.monotonic()
await c.execute_service(s["reboot_during"], {"url": URL})
dropped = False
while time.monotonic() - t0 < 30:
try:
await asyncio.wait_for(c.device_info(), 1.0)
await asyncio.sleep(0.2)
except TimeoutError:
continue # a slow reply is not a reboot; only a closed connection counts
except Exception: # noqa: BLE001
dropped = True
print(f"{time.monotonic() - t0:7.2f} API connection gone", flush=True)
break
with contextlib.suppress(Exception):
await c.disconnect()
if not dropped:
print(f"{time.monotonic() - t0:7.2f} NO REBOOT within 30 s", flush=True)
return 1
last: Exception | None = None
for _ in range(60):
try:
c2 = aioesphomeapi.APIClient(IP, 6053, None)
await asyncio.wait_for(c2.connect(login=True), 3)
i = await c2.device_info()
print(f"{time.monotonic() - t0:7.2f} back: {i.name}", flush=True)
await c2.disconnect()
return 0
except Exception as exc: # noqa: BLE001
last = exc
await asyncio.sleep(1)
print(
f"{time.monotonic() - t0:7.2f} NOT BACK after reboot (last error {last!r})",
flush=True,
)
return 1
sys.exit(asyncio.run(main()))
@@ -0,0 +1,66 @@
"""Run N update checks over the API and print the update state plus http_request/update/long-operation log lines.
Usage: updcheck.py IP LABEL N"""
import asyncio
import re
import sys
import time
import aioesphomeapi
IP, LABEL, N = sys.argv[1], sys.argv[2], int(sys.argv[3])
async def main():
c = aioesphomeapi.APIClient(IP, 6053, None)
await c.connect(login=True)
info = await c.device_info()
print(
f"{LABEL} device {info.name} {info.esphome_version} {info.compilation_time}",
flush=True,
)
ents, _ = await c.list_entities_services()
upd = next(e for e in ents if isinstance(e, aioesphomeapi.UpdateInfo))
states = []
def on_state(s):
if s.key == upd.key:
states.append(s)
print(
f"{LABEL} {time.monotonic() - t0:7.2f} UPDATE in_progress={s.in_progress} current={s.current_version} latest={s.latest_version} title={s.title!r}",
flush=True,
)
def on_log(m):
t = (
m.message.decode(errors="replace")
if isinstance(m.message, bytes)
else m.message
)
t = re.sub(r"\x1b\[[0-9;]*m", "", t)
if re.search(
r"http_request|update|took a long time|Guru|abort|heap|Free",
t,
re.IGNORECASE,
):
print(f"{LABEL} {time.monotonic() - t0:7.2f} {t[:200]}", flush=True)
t0 = time.monotonic()
c.subscribe_states(on_state)
c.subscribe_logs(on_log, log_level=aioesphomeapi.LogLevel.LOG_LEVEL_DEBUG)
await asyncio.sleep(3)
missing = []
for i in range(N):
print(f"{LABEL} {time.monotonic() - t0:7.2f} CHECK {i + 1}", flush=True)
before = len(states)
c.update_command(upd.key, aioesphomeapi.UpdateCommand.CHECK)
await asyncio.sleep(25)
if not any(not st.in_progress for st in states[before:]):
print(f"{LABEL} CHECK {i + 1} got no finished update state", flush=True)
missing.append(i + 1)
await c.disconnect()
print(f"{LABEL} {N - len(missing)} of {N} checks reported a state", flush=True)
return 1 if missing else 0
sys.exit(asyncio.run(main()))
@@ -0,0 +1,129 @@
"""Drive the http_request update harness over the API: press buttons, print every update state and save the log.
usage: updrive.py HOST KEY(- for plaintext) LOGFILE OP [OP ...]
OP: list | press:NAME | set:NAME=VALUE | sel:NAME=OPTION | wait:SECONDS
"""
import asyncio
import pathlib
import sys
import time
from aioesphomeapi import (
APIClient,
BinarySensorState,
ButtonInfo,
LogLevel,
NumberInfo,
NumberState,
SelectInfo,
SelectState,
SensorInfo,
SensorState,
TextSensorInfo,
TextSensorState,
UpdateState,
)
HOST, KEY, LOGFILE = sys.argv[1], sys.argv[2], sys.argv[3]
OPS = sys.argv[4:]
T0 = time.time()
def stamp() -> str:
return f"{time.time() - T0:7.3f}"
async def main() -> None:
with pathlib.Path(LOGFILE).open("w", encoding="utf-8") as log:
c = APIClient(HOST, 6053, None, noise_psk=None if KEY == "-" else KEY)
await c.connect(login=True)
info = await c.device_info()
print(
f"device {info.name} esphome {info.esphome_version} compiled {info.compilation_time}"
)
ents, _ = await c.list_entities_services()
by_key = {e.key: e for e in ents}
buttons = {e.name: e for e in ents if isinstance(e, ButtonInfo)}
selects = {e.name: e for e in ents if isinstance(e, SelectInfo)}
def on_state(s) -> None:
e = by_key.get(s.key)
if e is None:
return
if isinstance(s, TextSensorState):
print(
f"{stamp()} TXT {e.name} = {s.state!r}{' (missing)' if s.missing_state else ''}",
flush=True,
)
elif isinstance(s, UpdateState):
print(
f"{stamp()} UPD {e.name} latest={s.latest_version!r} title={s.title!r} summary={s.release_summary!r} in_progress={s.in_progress}{' (missing)' if s.missing_state else ''}",
flush=True,
)
elif isinstance(s, BinarySensorState):
print(
f"{stamp()} BIN {e.name} = {s.state}{' (missing)' if s.missing_state else ''}",
flush=True,
)
elif isinstance(s, NumberState):
print(
f"{stamp()} NUM {e.name} = {s.state:.0f}{' (missing)' if s.missing_state else ''}",
flush=True,
)
elif isinstance(s, SelectState):
print(
f"{stamp()} SEL {e.name} = {s.state!r}{' (missing)' if s.missing_state else ''}",
flush=True,
)
elif isinstance(s, SensorState) and isinstance(e, SensorInfo):
print(f"{stamp()} SEN {e.name} = {s.state:.0f}", flush=True)
def on_log(msg) -> None:
log.write(f"{stamp()} {msg.message.decode(errors='replace')}\n")
c.subscribe_states(on_state)
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_VERBOSE)
await asyncio.sleep(1.5)
for op in OPS:
if op == "list":
for n in sorted(buttons):
print("button:", n)
for n in sorted(selects):
print("select:", n, selects[n].options)
for e in ents:
if isinstance(e, TextSensorInfo):
print("text_sensor:", e.name)
continue
kind, _, rest = op.partition(":")
if kind == "wait":
await asyncio.sleep(float(rest))
continue
if kind == "press":
if rest not in buttons:
sys.exit(f"no button named {rest!r}; have {sorted(buttons)}")
print(f"{stamp()} >>> press {rest}", flush=True)
log.write(f"{stamp()} >>> press {rest}\n")
c.button_command(buttons[rest].key)
elif kind == "set":
name, _, value = rest.partition("=")
numbers = {e.name: e for e in ents if isinstance(e, NumberInfo)}
if name not in numbers:
sys.exit(f"no number named {name!r}")
print(f"{stamp()} >>> set {name} = {value}", flush=True)
log.write(f"{stamp()} >>> set {name} = {value}\n")
c.number_command(numbers[name].key, float(value))
elif kind == "sel":
name, _, value = rest.partition("=")
if name not in selects:
sys.exit(f"no select named {name!r}; have {sorted(selects)}")
print(f"{stamp()} >>> select {name} = {value}", flush=True)
log.write(f"{stamp()} >>> select {name} = {value}\n")
c.select_command(selects[name].key, value)
else:
sys.exit(f"unknown op {op!r}")
await asyncio.sleep(2.0)
await asyncio.sleep(1)
await c.disconnect()
asyncio.run(main())
@@ -0,0 +1,123 @@
"""Drive an LD24xx harness over the API: press buttons, set selects and numbers, print state updates, save the log.
usage: lddrive.py HOST KEY LOGFILE OP [OP ...] (KEY of - means a plaintext API)
OP: list | press:NAME | sel:NAME=OPTION | set:NAME=VALUE | wait:SECONDS
Steps are 2 s apart: the API log stream drops lines while the hub blocks in send_command_.
"""
import asyncio
from pathlib import Path
import sys
import time
from aioesphomeapi import (
APIClient,
BinarySensorState,
ButtonInfo,
LogLevel,
NumberInfo,
NumberState,
SelectInfo,
SelectState,
SensorInfo,
SensorState,
TextSensorInfo,
TextSensorState,
)
HOST, KEY, LOGFILE = sys.argv[1], sys.argv[2], sys.argv[3]
OPS = sys.argv[4:]
T0 = time.time()
STEP_GAP_S = 2.0
def stamp() -> str:
return f"{time.time() - T0:7.3f}"
async def run(c: APIClient, log) -> None:
info = await c.device_info()
print(
f"device {info.name} esphome {info.esphome_version} compiled {info.compilation_time}"
)
ents, _ = await c.list_entities_services()
by_key = {e.key: e for e in ents}
buttons = {e.name: e for e in ents if isinstance(e, ButtonInfo)}
selects = {e.name: e for e in ents if isinstance(e, SelectInfo)}
numbers = {e.name: e for e in ents if isinstance(e, NumberInfo)}
def on_state(s) -> None:
e = by_key.get(s.key)
if e is None:
return
missing = " (missing)" if getattr(s, "missing_state", False) else ""
if isinstance(s, TextSensorState):
print(f"{stamp()} TXT {e.name} = {s.state!r}{missing}", flush=True)
elif isinstance(s, BinarySensorState):
print(f"{stamp()} BIN {e.name} = {s.state}{missing}", flush=True)
elif isinstance(s, NumberState):
print(f"{stamp()} NUM {e.name} = {s.state:.0f}{missing}", flush=True)
elif isinstance(s, SelectState):
print(f"{stamp()} SEL {e.name} = {s.state!r}{missing}", flush=True)
elif (
isinstance(s, SensorState)
and isinstance(e, SensorInfo)
and "Count" in e.name
):
print(f"{stamp()} CNT {e.name} = {s.state:.0f}", flush=True)
def on_log(msg) -> None:
log.write(f"{stamp()} {msg.message.decode(errors='replace')}\n")
c.subscribe_states(on_state)
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_VERBOSE)
await asyncio.sleep(1.5)
for op in OPS:
if op == "list":
for n in sorted(buttons):
print("button:", n)
for n in sorted(selects):
print("select:", n, selects[n].options)
for e in ents:
if isinstance(e, TextSensorInfo):
print("text_sensor:", e.name)
continue
kind, _, rest = op.partition(":")
if kind == "wait":
await asyncio.sleep(float(rest))
continue
if kind == "press":
if rest not in buttons:
sys.exit(f"no button named {rest!r}; have {sorted(buttons)}")
print(f"{stamp()} >>> press {rest}", flush=True)
log.write(f"{stamp()} >>> press {rest}\n")
c.button_command(buttons[rest].key)
elif kind == "set":
name, _, value = rest.partition("=")
if name not in numbers:
sys.exit(f"no number named {name!r}; have {sorted(numbers)}")
print(f"{stamp()} >>> set {name} = {value}", flush=True)
log.write(f"{stamp()} >>> set {name} = {value}\n")
c.number_command(numbers[name].key, float(value))
elif kind == "sel":
name, _, value = rest.partition("=")
if name not in selects:
sys.exit(f"no select named {name!r}; have {sorted(selects)}")
print(f"{stamp()} >>> select {name} = {value}", flush=True)
log.write(f"{stamp()} >>> select {name} = {value}\n")
c.select_command(selects[name].key, value)
else:
sys.exit(f"unknown op {op!r}")
await asyncio.sleep(STEP_GAP_S)
await asyncio.sleep(1)
async def main() -> None:
c = APIClient(HOST, 6053, None, noise_psk=None if KEY == "-" else KEY)
await c.connect(login=True)
with Path(LOGFILE).open("w", encoding="utf-8") as log:
await run(c, log)
await c.disconnect()
asyncio.run(main())
@@ -0,0 +1,33 @@
"""Summarize an lddrive.py log: for each step, the LD2450 TX frames, commands and publishes.
usage: txsum.py LOGFILE (diff the output of two builds)
"""
from pathlib import Path
import re
import sys
ANSI = re.compile(r"\x1b\[[0-9;]*m")
out: list[str] = []
with Path(sys.argv[1]).open(encoding="utf-8", errors="replace") as log:
for raw in log:
line = ANSI.sub("", raw.rstrip("\n"))
m = re.match(r"\s*[\d.]+ (.*)", line)
if not m:
continue
text = m.group(1)
if text.startswith(">>>"):
out.append(f"\n# {text[4:]}")
elif "[uart_debug" in text:
out.append(f" TX {text.split('>>> ')[1]}")
elif "[ld2450" in text and (
"Sending COMMAND" in text
or "zone type" in text.lower()
or "Query zone" in text
):
out.append(f" LOG {text.split(']: ', 1)[1]}")
elif "[select:" in text and ">>" in text:
out.append(f" SEL {text.split(']: ', 1)[1]}")
elif "[text_sensor:098]" in text:
out.append(f" TXT {text.split(']: ', 1)[1]}")
print("\n".join(out))
@@ -0,0 +1,86 @@
"""Drive the modem test build over the API: press buttons, print sensor states, save the device log.
usage: mdrive.py HOST LOGFILE OP [OP ...]
OP: list | press:NAME | wait:SECONDS
"""
import asyncio
import pathlib
import sys
import time
from aioesphomeapi import (
APIClient,
ButtonInfo,
LogLevel,
SensorInfo,
SensorState,
TextSensorInfo,
TextSensorState,
)
HOST, LOGFILE = sys.argv[1], sys.argv[2]
OPS = sys.argv[3:]
T0 = time.time()
def stamp() -> str:
return f"{time.time() - T0:7.3f}"
async def main() -> None:
with pathlib.Path(LOGFILE).open("w", encoding="utf-8") as log:
c = APIClient(HOST, 6053, None)
await c.connect(login=True)
info = await c.device_info()
print(
f"device {info.name} esphome {info.esphome_version} compiled {info.compilation_time}"
)
ents, _ = await c.list_entities_services()
by_key = {e.key: e for e in ents}
buttons = {e.name: e for e in ents if isinstance(e, ButtonInfo)}
def on_state(s) -> None:
e = by_key.get(s.key)
if e is None:
return
if isinstance(s, TextSensorState):
print(
f"{stamp()} TXT {e.name} = {s.state!r}{' (missing)' if s.missing_state else ''}",
flush=True,
)
elif isinstance(s, SensorState) and isinstance(e, SensorInfo):
print(f"{stamp()} SEN {e.name} = {s.state:.1f}", flush=True)
def on_log(msg) -> None:
log.write(f"{stamp()} {msg.message.decode(errors='replace')}\n")
log.flush()
c.subscribe_states(on_state)
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_DEBUG)
await asyncio.sleep(1.5)
for op in OPS:
if op == "list":
for n in sorted(buttons):
print("button:", n)
for e in ents:
if isinstance(e, (TextSensorInfo, SensorInfo)):
print("entity:", type(e).__name__, e.name)
continue
kind, _, rest = op.partition(":")
if kind == "wait":
await asyncio.sleep(float(rest))
continue
if kind == "press":
if rest not in buttons:
sys.exit(f"no button named {rest!r}; have {sorted(buttons)}")
print(f"{stamp()} >>> press {rest}", flush=True)
log.write(f"{stamp()} >>> press {rest}\n")
c.button_command(buttons[rest].key)
else:
sys.exit(f"unknown op {op!r}")
await asyncio.sleep(2.0)
await asyncio.sleep(1)
await c.disconnect()
asyncio.run(main())
@@ -0,0 +1,8 @@
substitutions: {name: mqabase, suffix: "true"}
esp32:
board: esp32dev
framework: {type: esp-idf}
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
@@ -0,0 +1,9 @@
substitutions: {name: mqbbase, suffix: "true"}
esp32:
board: esp32dev
framework: {type: esp-idf}
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
topic_prefix: custpre32
@@ -0,0 +1,18 @@
substitutions: {name: mqcbase, suffix: "true"}
esp32:
board: esp32dev
framework: {type: esp-idf}
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
log_topic: explicit32/log
birth_message:
topic: explicit32/avail
payload: up
will_message:
topic: explicit32/avail
payload: down
shutdown_message:
topic: explicit32/avail
payload: bye
@@ -0,0 +1,8 @@
substitutions: {name: mqd32base, suffix: "false"}
esp32:
board: esp32dev
framework: {type: esp-idf}
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
@@ -0,0 +1,7 @@
substitutions: {name: mqabase, suffix: "true"}
esp8266:
board: d1_mini
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
@@ -0,0 +1,8 @@
substitutions: {name: mqbbase, suffix: "true"}
esp8266:
board: d1_mini
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
topic_prefix: custpre66
@@ -0,0 +1,17 @@
substitutions: {name: mqcbase, suffix: "true"}
esp8266:
board: d1_mini
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
log_topic: explicit66/log
birth_message:
topic: explicit66/avail
payload: up
will_message:
topic: explicit66/avail
payload: down
shutdown_message:
topic: explicit66/avail
payload: bye
@@ -0,0 +1,7 @@
substitutions: {name: mqd66base, suffix: "false"}
esp8266:
board: d1_mini
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
@@ -0,0 +1,24 @@
# Package for esphome#9315, included by the per-variant files under pr/ and base/ (substitutions name, suffix).
esphome:
name: ${name}
name_add_mac_suffix: ${suffix}
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
ota:
- platform: esphome
button:
- platform: restart
name: Restart
interval:
- interval: 10s
then:
- logger.log: "alive"
@@ -0,0 +1,8 @@
substitutions: {name: mqapr, suffix: "true"}
esp32:
board: esp32dev
framework: {type: esp-idf}
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
@@ -0,0 +1,9 @@
substitutions: {name: mqbpr, suffix: "true"}
esp32:
board: esp32dev
framework: {type: esp-idf}
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
topic_prefix: custpre32
@@ -0,0 +1,18 @@
substitutions: {name: mqcpr, suffix: "true"}
esp32:
board: esp32dev
framework: {type: esp-idf}
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
log_topic: explicit32/log
birth_message:
topic: explicit32/avail
payload: up
will_message:
topic: explicit32/avail
payload: down
shutdown_message:
topic: explicit32/avail
payload: bye
@@ -0,0 +1,8 @@
substitutions: {name: mqd32pr, suffix: "false"}
esp32:
board: esp32dev
framework: {type: esp-idf}
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
@@ -0,0 +1,7 @@
substitutions: {name: mqapr, suffix: "true"}
esp8266:
board: d1_mini
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
@@ -0,0 +1,8 @@
substitutions: {name: mqbpr, suffix: "true"}
esp8266:
board: d1_mini
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
topic_prefix: custpre66
@@ -0,0 +1,17 @@
substitutions: {name: mqcpr, suffix: "true"}
esp8266:
board: d1_mini
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
log_topic: explicit66/log
birth_message:
topic: explicit66/avail
payload: up
will_message:
topic: explicit66/avail
payload: down
shutdown_message:
topic: explicit66/avail
payload: bye
@@ -0,0 +1,7 @@
substitutions: {name: mqd66pr, suffix: "false"}
esp8266:
board: d1_mini
packages: {mq: !include ../../mq-common.yaml}
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
@@ -0,0 +1,51 @@
#!/bin/bash
# run.sh X BUILD : flash variant X (a..d) of BUILD (pr|base) on both boards, record broker traffic, restart, hard reset
# Copy this directory to a scratch directory and run it there; secrets.yaml (wifi_ssid, wifi_password, broker_ip)
# goes next to mq-common.yaml and in every board folder.
# env: TREE (esphome checkout for BUILD), P32 and P66 (ESP32 and ESP8266 serial ports), SC (path to
# serial_capture.py), PY (checkout python)
M=$(cd "$(dirname "$0")" && pwd); X=$1; V=$2; T=${TREE:?set TREE to the checkout under test}
P32=${P32:?set P32}; P66=${P66:?set P66}; SC=${SC:?set SC}
PY=${PY:-$T/venv/bin/python}; OUT=$M/out-$X-$V; mkdir -p $OUT
pkill -f "[s]erial_capture.py $P32"; pkill -f "[s]erial_capture.py $P66"; pkill -f "[m]osquitto_sub -h 127.0.0.1"
# compile one at a time (shared tool caches), then flash both boards at once; upload alone never builds
for b in 32 66; do
(cd $T && $PY -m esphome compile $M/$V/$b/mq${X}$b$V.yaml > $OUT/c$b.log 2>&1) || { echo "compile$b failed, see $OUT/c$b.log"; exit 1; }
done
(cd $T && $PY -m esphome upload $M/$V/32/mq${X}32$V.yaml --device $P32 > $OUT/u32.log 2>&1) & U32=$!
(cd $T && $PY -m esphome upload $M/$V/66/mq${X}66$V.yaml --device $P66 > $OUT/u66.log 2>&1) & U66=$!
wait $U32 || { echo "upload32 failed, see $OUT/u32.log"; exit 1; }
wait $U66 || { echo "upload66 failed, see $OUT/u66.log"; exit 1; }
mosquitto_sub -h 127.0.0.1 -v -t '#' -F '@H:@M:@S r=%r %t %p' > $OUT/broker.txt 2>&1 &
SUB=$!
$PY $SC $P32 170 $OUT/s32.txt --reset > /dev/null 2>&1 &
$PY $SC $P66 170 $OUT/s66.txt > /dev/null 2>&1 &
sleep 60
TOPICS=$(grep "homeassistant/button/" $OUT/broker.txt | grep "\"name\":\"mq${X}" | grep -o '"cmd_t":"[^"]*"' | cut -d'"' -f4 | sort -u)
ERR=0
[ "$(echo "$TOPICS" | grep -c .)" -ge 2 ] || { echo "ERROR: expected a Restart command topic per board, found: ${TOPICS:-none}"; ERR=1; }
for t in $TOPICS; do
mosquitto_pub -h 127.0.0.1 -t "$t" -m PRESS && echo "$(date +%T) press $t" || { echo "$(date +%T) ERROR: publish to $t failed"; ERR=1; }
done
sleep 45
pkill -f "[s]erial_capture.py $P32"; pkill -f "[s]erial_capture.py $P66"; sleep 1
echo "$(date +%T) hold both in the bootloader for 40 s (will test)"
$PY -m esptool --port $P32 --after no-reset chip-id > $OUT/hold32.log 2>&1 & H32=$!
$PY -m esptool --port $P66 --after no-reset chip-id > $OUT/hold66.log 2>&1 & H66=$!
wait $H32 || { echo "ERROR: ESP32 not held in the bootloader, no will expected (see $OUT/hold32.log)"; ERR=1; }
wait $H66 || { echo "ERROR: ESP8266 not held in the bootloader, no will expected (see $OUT/hold66.log)"; ERR=1; }
sleep 40
REL=$(date +%T); echo "$REL release both"
$PY $SC $P32 40 $OUT/s32b.txt --reset > /dev/null 2>&1 &
$PY $SC $P66 40 $OUT/s66b.txt --reset > /dev/null 2>&1 &
sleep 42
kill $SUB
for f in s32 s66; do echo "== $f"; grep -a -E "Topic Prefix|Log Topic|Availability|Client ID" $OUT/$f.txt | head -4 | sed 's/^\[[0-9:]*\]//'; done
echo "== broker (excluding discovery and entity state)"
grep -v -E " homeassistant/| [^ ]*/(sensor|button|binary_sensor)/| esphome/discover/" $OUT/broker.txt | awk '$3 ~ /(debug|log)$/ { if (seen[$3]++) next; print $1, $2, $3, "(log lines:", "see file)"; next } { print }' | head -40
echo "== log line counts per topic"; awk '$3 ~ /(debug|log)$/ {c[$3]++} END {for (t in c) print t, c[t]}' $OUT/broker.txt
# each board must publish its birth message (default "online", explicit variant "up") after the release;
# a missing will is the measured result and does not fail the run
BIRTHS=$(awk -v t="$REL" '$1 >= t && ($4 == "online" || $4 == "up") {print $3}' $OUT/broker.txt | sort -u | grep -c .)
[ "$BIRTHS" -ge 2 ] || { echo "ERROR: $BIRTHS of 2 boards published a birth message after the release"; ERR=1; }
exit $ERR
@@ -0,0 +1,9 @@
substitutions: {name: sd32base}
esphome:
name: sd32base
esp32:
board: esp32dev
framework: {type: esp-idf}
deep_sleep:
id: ds
packages: {c: !include ../sd-common.yaml}
@@ -0,0 +1,8 @@
substitutions: {name: sd66base}
esphome:
name: sd66base
esp8266:
board: d1_mini
deep_sleep:
id: ds
packages: {c: !include ../sd-common.yaml}
@@ -0,0 +1,33 @@
"""match.py OUTDIR: for each event, say whether a bye/dead for that board arrived within 5 s."""
import pathlib
import sys
d = pathlib.Path(sys.argv[1])
def secs(stamp: str) -> int:
h, m, s = (int(x) for x in stamp.split(":"))
return h * 3600 + m * 60 + s
ev = [ln.split() for ln in (d / "events.txt").read_text(encoding="utf-8").splitlines()]
br = [
f
for f in (
ln.split() for ln in (d / "broker.txt").read_text(encoding="utf-8").splitlines()
)
if len(f) >= 3 and f[2] in ("bye", "dead")
]
rows = {}
for ts, kind, b in ev:
hit = [
p
for s, top, p in br
if top.startswith(f"sd{b}") and 0 <= secs(s) - secs(ts) <= 5
]
rows.setdefault((b, kind), []).append(
"bye" if "bye" in hit else (hit[0] if hit else "-")
)
for (b, kind), r in sorted(rows.items()):
print(f"{b} {kind:8s} {r.count('bye')}/{len(r)} bye {' '.join(r)}")
@@ -0,0 +1,9 @@
substitutions: {name: sd32pr}
esphome:
name: sd32pr
esp32:
board: esp32dev
framework: {type: esp-idf}
deep_sleep:
id: ds
packages: {c: !include ../sd-common.yaml}
@@ -0,0 +1,8 @@
substitutions: {name: sd66pr}
esphome:
name: sd66pr
esp8266:
board: d1_mini
deep_sleep:
id: ds
packages: {c: !include ../sd-common.yaml}
@@ -0,0 +1,40 @@
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
mqtt:
broker: !secret broker_ip
reboot_timeout: 0s
discovery: false
birth_message: {topic: "${name}/status", payload: hello}
will_message: {topic: "${name}/status", payload: dead}
shutdown_message: {topic: "${name}/status", payload: bye}
on_message:
- topic: ${name}/cmd
payload: restart
then:
- button.press: restart_btn
- topic: ${name}/cmd
payload: disable
then:
- mqtt.disable:
- delay: 5s
- mqtt.enable:
- topic: ${name}/cmd
payload: sleep
then:
- deep_sleep.enter:
id: ds
sleep_duration: 5s
ota:
- platform: esphome
button:
- platform: restart
id: restart_btn
name: Restart
@@ -0,0 +1,44 @@
#!/bin/bash
# sdrun.sh V(pr|base) [N] : flash both boards, restart each N times over MQTT, then disable, then deep sleep (ESP32)
# Copy this directory to a scratch directory (secrets.yaml with wifi_ssid, wifi_password and broker_ip next to
# sd-common.yaml) and run it there with a local mosquitto on this host.
# env: TREE (esphome checkout for V), P32 and P66 (ESP32 and ESP8266 serial ports), PY (checkout python)
S=$(cd "$(dirname "$0")" && pwd); V=$1; N=${2:-10}; T=${TREE:?set TREE}; PY=${PY:-$T/venv/bin/python}
P32=${P32:?set P32}; P66=${P66:?set P66}; OUT=$S/out-$V; rm -rf "$OUT"; mkdir -p "$OUT"
pkill -f "[s]erial_capture.py $P32"; pkill -f "[s]erial_capture.py $P66"
# compile one at a time (shared tool caches), then flash both boards at once; upload alone never builds
for b in 32 66; do
(cd "$T" && $PY -m esphome compile "$S/$V/sd$b$V.yaml" > "$OUT/c$b.log" 2>&1) || { echo "compile$b failed, see $OUT/c$b.log"; exit 1; }
done
(cd "$T" && $PY -m esphome upload "$S/$V/sd32$V.yaml" --device "$P32" > "$OUT/u32.log" 2>&1) & U32=$!
(cd "$T" && $PY -m esphome upload "$S/$V/sd66$V.yaml" --device "$P66" > "$OUT/u66.log" 2>&1) & U66=$!
wait $U32 || { echo "upload32 failed"; exit 1; }; wait $U66 || { echo "upload66 failed"; exit 1; }
mosquitto_sub -h 127.0.0.1 -R -v -t "sd32$V/status" -t "sd66$V/status" -F '@H:@M:@S %t %p' > "$OUT/broker.txt" 2>&1 & SUB=$!
# cmd KIND BOARD: publish the command and record the event only when the publish worked
ERR=0
cmd() {
local ts; ts=$(date +%T)
if mosquitto_pub -h 127.0.0.1 -t "sd$2$V/cmd" -m "$1"; then echo "$ts $1 $2" >> "$OUT/events.txt"
else echo "$ts ERROR: publish $1 to board $2 failed"; ERR=1; fi
}
sleep 40
for b in 32 66; do
grep -q "sd$b$V/status hello" "$OUT/broker.txt" || { echo "ERROR: sd$b$V never sent its birth message"; kill $SUB; exit 1; }
done
for i in $(seq "$N"); do
for b in 32 66; do cmd restart $b; done
sleep 25
done
for b in 32 66; do cmd disable $b; done
sleep 30
for i in 1 2 3; do cmd sleep 32; sleep 30; done
kill -0 $SUB 2>/dev/null || { echo "ERROR: the broker subscriber died, counts are incomplete"; ERR=1; }
kill $SUB 2>/dev/null
for b in 32 66; do echo "sd$b$V: $(grep -c "sd$b$V/status hello" "$OUT/broker.txt") hello, $(grep -c "sd$b$V/status bye" "$OUT/broker.txt") bye, $(grep -c "sd$b$V/status dead" "$OUT/broker.txt") dead"; done
# each restart, the disable (a reconnect) and each ESP32 deep sleep is followed by a new birth message
for b in 32 66; do
want=$((N + 2)); [ $b = 32 ] && want=$((want + 3))
got=$(grep -c "sd$b$V/status hello" "$OUT/broker.txt")
[ "$got" -ge "$want" ] || { echo "ERROR: sd$b$V sent $got birth messages, expected $want (a board did not come back)"; ERR=1; }
done
exit $ERR
@@ -0,0 +1,115 @@
"""Reboot the DUT before each fetch and report the longest online_image loop pass of that one fetch.
usage: firstpass.py HOST KEY BASEURL LABEL IMAGE...
The component warning prints only on a new maximum, so a fresh boot per image gives that image's own maximum.
"""
import asyncio
import contextlib
import re
import sys
import time
from aioesphomeapi import (
APIClient,
ButtonInfo,
InvalidAuthAPIError,
InvalidEncryptionKeyAPIError,
LogLevel,
RequiresEncryptionAPIError,
UserService,
)
HOST, KEY, BASE, LABEL = sys.argv[1:5]
IMAGES = sys.argv[5:]
TOOK = re.compile(r"online_image\.image took a long time for an operation \((\d+) ms\)")
OUTCOME = re.compile(r"download (finished|error): img\b")
async def connect() -> tuple[APIClient, dict, int]:
last: Exception | None = None
for _ in range(30):
c = APIClient(HOST, 6053, None, noise_psk=KEY)
try:
await c.connect(login=True)
break
except (
InvalidAuthAPIError,
InvalidEncryptionKeyAPIError,
RequiresEncryptionAPIError,
):
raise # a wrong key never fixes itself
except Exception as exc: # noqa: BLE001
last = exc
await asyncio.sleep(2)
else:
raise RuntimeError(f"device did not come back (last error {last!r})")
ents, services = await c.list_entities_services()
reboot = next(
(e.key for e in ents if isinstance(e, ButtonInfo) and e.name == "Reboot"), None
)
if reboot is None:
raise RuntimeError("the device has no button named Reboot")
return c, {s.name: s for s in services if isinstance(s, UserService)}, reboot
async def rebooted(c: APIClient, reboot: int) -> bool:
"""Press Reboot and wait up to 15 s for the API connection to drop."""
c.button_command(reboot)
end = time.monotonic() + 15
while time.monotonic() < end:
try:
await asyncio.wait_for(c.device_info(), 1.0)
except TimeoutError:
continue # a slow reply is not a reboot; only a closed connection counts
except Exception: # noqa: BLE001
return True
await asyncio.sleep(0.2)
return False
async def main() -> int:
failed = []
for img in IMAGES:
c, actions, reboot = await connect()
ok = await rebooted(c, reboot)
with contextlib.suppress(Exception):
await c.disconnect()
if not ok:
print(f"{LABEL} {img:22} SKIPPED: no reboot within 15 s", flush=True)
failed.append(img)
continue
await asyncio.sleep(8)
c, actions, reboot = await connect()
lines: list[str] = []
c.subscribe_logs(
lambda m, lines=lines: lines.append(m.message.decode(errors="replace")),
log_level=LogLevel.LOG_LEVEL_DEBUG,
)
await asyncio.sleep(3)
lines.clear()
t0 = time.monotonic()
await c.execute_service(actions["fetch"], {"url": f"{BASE}/{img}"})
outcome = None
while time.monotonic() - t0 < 60 and outcome is None:
await asyncio.sleep(0.2)
outcome = next(
(m.group(1) for ln in lines if (m := OUTCOME.search(ln))), None
)
took = time.monotonic() - t0
await asyncio.sleep(1)
passes = [int(m.group(1)) for ln in lines if (m := TOOK.search(ln))]
print(
f"{LABEL} {img:22} {outcome or 'none':9} {took:5.1f}s longest pass {max(passes) if passes else '<50'} ms {passes}",
flush=True,
)
if (
outcome != "finished"
): # a decode timing is only meaningful for a complete image
failed.append(img)
await c.disconnect()
if failed:
print(f"{LABEL} not finished: {', '.join(failed)}", flush=True)
return 1 if failed else 0
sys.exit(asyncio.run(main()))
@@ -0,0 +1,32 @@
"""Build the large PNGs for the decode time test: a compressible gradient (also saved as ok.png so every server case
carries a multi-row PNG), a flat 640x480 image whose few KB hold 307200 pixels, incompressible noise, and a JPEG."""
from pathlib import Path
import random
import sys
from PIL import Image
www = Path(sys.argv[1])
im = Image.new("RGB", (320, 240))
px = im.load()
for y in range(240):
for x in range(320):
px[x, y] = (x * 255 // 319, y * 255 // 239, 128)
im.save(www / "ok.png", "PNG", optimize=True)
im.save(www / "gradient.png", "PNG", optimize=True)
im.save(www / "gradient.jpg", "JPEG", quality=85)
Image.new("RGB", (640, 480), (40, 90, 200)).save(www / "flat.png", "PNG", optimize=True)
random.seed(1)
noise = Image.new("RGB", (320, 240))
npx = noise.load()
for y in range(240):
for x in range(320):
npx[x, y] = (
random.randrange(256),
random.randrange(256),
random.randrange(256),
)
noise.save(www / "noise.png", "PNG")
for p in sorted(www.iterdir()):
print(p.name, p.stat().st_size)
@@ -0,0 +1,93 @@
"""Write big BMP (1, 8, 24 bit) and QOI test images into a www directory."""
from pathlib import Path
import random
import struct
import sys
from PIL import Image
www = Path(sys.argv[1])
def gradient(w, h):
im = Image.new("RGB", (w, h))
im.putdata(
[
((x * 255) // w, (y * 255) // h, ((x + y) * 255) // (w + h))
for y in range(h)
for x in range(w)
]
)
return im
def checker(w, h):
im = Image.new("1", (w, h))
im.putdata(
[255 if ((x // 16) + (y // 16)) % 2 else 0 for y in range(h) for x in range(w)]
)
return im
checker(640, 480).save(www / "bmp1_640.bmp")
gradient(320, 240).convert("P", palette=Image.ADAPTIVE).save(www / "bmp8_320.bmp")
gradient(320, 240).save(www / "bmp24_320.bmp")
def qoi(im, path):
im = im.convert("RGBA")
w, h = im.size
out = bytearray(b"qoif" + struct.pack(">IIBB", w, h, 4, 0))
idx = [(0, 0, 0, 0)] * 64
prev = (0, 0, 0, 255)
run = 0
px = list(im.getdata())
for i, p in enumerate(px):
if p == prev:
run += 1
if run == 62 or i == len(px) - 1:
out.append(0xC0 | (run - 1))
run = 0
continue
if run:
out.append(0xC0 | (run - 1))
run = 0
h_ = (p[0] * 3 + p[1] * 5 + p[2] * 7 + p[3] * 11) % 64
if idx[h_] == p:
out.append(h_)
else:
idx[h_] = p
if p[3] == prev[3]:
dr, dg, db = (
((p[0] - prev[0] + 128) % 256) - 128,
((p[1] - prev[1] + 128) % 256) - 128,
((p[2] - prev[2] + 128) % 256) - 128,
)
drg, dbg = dr - dg, db - dg
if -2 <= dr <= 1 and -2 <= dg <= 1 and -2 <= db <= 1:
out.append(0x40 | (dr + 2) << 4 | (dg + 2) << 2 | (db + 2))
elif -32 <= dg <= 31 and -8 <= drg <= 7 and -8 <= dbg <= 7:
out += bytes([0x80 | (dg + 32), (drg + 8) << 4 | (dbg + 8)])
else:
out += bytes([0xFE, p[0], p[1], p[2]])
else:
out += bytes([0xFF, *p])
prev = p
out += b"\x00" * 7 + b"\x01"
path.write_bytes(out)
qoi(
Image.new("RGB", (640, 480), (0, 0, 0)).copy(), www / "qoi_flat.qoi"
) # all runs, 62 pixels a byte
qoi(checker(640, 480).convert("RGB"), www / "qoi_check.qoi")
qoi(gradient(320, 240), www / "qoi_grad.qoi")
random.seed(1)
noise = Image.frombytes(
"RGB", (320, 240), bytes(random.getrandbits(8) for _ in range(320 * 240 * 3))
)
qoi(noise, www / "qoi_noise.qoi")
for f in sorted(www.glob("*_*.*")):
print(f.name, f.stat().st_size)
@@ -0,0 +1,42 @@
"""Build the JPEGs for the streaming decode test: noise 320x240 (also ok.jpg) and 640x480, a 640x480 gradient, the
320x240 noise with a 12 KB EXIF segment in front of the frame header, and a progressive one."""
from pathlib import Path
import random
import sys
from PIL import Image
www = Path(sys.argv[1])
random.seed(2)
def noise(w: int, h: int) -> Image.Image:
im = Image.new("RGB", (w, h))
px = im.load()
for y in range(h):
for x in range(w):
px[x, y] = (
random.randrange(256),
random.randrange(256),
random.randrange(256),
)
return im
n320 = noise(320, 240)
n320.save(www / "noise320.jpg", "JPEG", quality=90)
n320.save(www / "ok.jpg", "JPEG", quality=90)
noise(640, 480).save(www / "noise640.jpg", "JPEG", quality=85)
g = Image.new("RGB", (640, 480))
px = g.load()
for y in range(480):
for x in range(640):
px[x, y] = (x * 255 // 639, y * 255 // 479, 128)
g.save(www / "gradient640.jpg", "JPEG", quality=85)
n320.save(
www / "bigexif320.jpg", "JPEG", quality=90, exif=b"Exif\x00\x00" + bytes(12000)
)
n320.save(www / "progressive320.jpg", "JPEG", quality=90, progressive=True)
for p in sorted(www.iterdir()):
print(p.name, p.stat().st_size)
@@ -5,7 +5,16 @@ from pathlib import Path
import sys
import time
WWW = Path(sys.argv[1])
WWW = Path(sys.argv[1]).resolve()
NOT_FOUND = b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
def www_file(rel: str) -> Path | None:
"""The file under WWW named by a request path, or None if missing or outside WWW."""
file = (WWW / rel).resolve()
return file if file.is_relative_to(WWW) and file.is_file() else None
PORT = int(sys.argv[2])
LOG = Path(sys.argv[3]).open("a", encoding="utf-8", buffering=1) # noqa: SIM115
@@ -108,8 +117,34 @@ async def respond(path: str, req: bytes, w: asyncio.StreamWriter) -> str:
if name == "huge": # claims 20 MB so the JPEG buffer resize fails
w.write(headers(ctype, [b"Content-Length: 20000000"]) + body)
return f"claims 20000000, sent {len(body)} bytes"
w.write(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
return "404"
prefix, _, rest = name.partition("_")
if prefix in ("half", "noclen", "chunked", "stall") and rest:
# 404 when the base file is missing, so a typo never passes as the misbehaviour under test
file = www_file(rest + "." + ext)
else:
prefix, file = "", www_file(path.lstrip("/"))
if file is None:
w.write(NOT_FOUND)
return "404"
data = file.read_bytes()
if prefix == "half": # the first half of a file, Content-Length matching
cut = data[: len(data) // 2]
w.write(headers(ctype, [b"Content-Length: %d" % len(cut)]) + cut)
return f"half {len(cut)} of {len(data)} bytes"
if prefix == "noclen": # a file without Content-Length, delimited by close
w.write(headers(ctype, []) + data)
return f"no content-length, full {len(data)} bytes"
if prefix == "chunked": # a file in 1 KB chunks
w.write(headers(ctype, [b"Transfer-Encoding: chunked"]) + chunked(data, 1024))
return f"chunked full {len(data)} bytes"
if prefix == "stall": # claims the full length, sends a third, holds the socket
part = data[: len(data) // 3]
w.write(headers(ctype, [b"Content-Length: %d" % len(data)]) + part)
await w.drain()
await asyncio.sleep(40)
return f"stalled after {len(part)} of {len(data)} bytes"
w.write(headers(ctype, [b"Content-Length: %d" % len(data), *tag]) + data)
return f"file {file.name} {len(data)} bytes"
async def handle(r: asyncio.StreamReader, w: asyncio.StreamWriter) -> None:
@@ -0,0 +1,261 @@
"""Fetch images through the online_image API actions and report outcome, time, loop time maximum and heap minimum per case.
usage: [CASES=png|noise|end|bmpqoi|short|jpeg] pngdrive.py HOST KEY LOGFILE BASEURL LABEL
"""
import asyncio
import contextlib
import os
import pathlib
import re
import sys
import time
import urllib.error
import urllib.request
from aioesphomeapi import APIClient, LogLevel, SensorInfo, SensorState, UserService
HOST, KEY, LOGFILE, BASE, LABEL = sys.argv[1:6]
CAP_S = 60
OUTCOME = re.compile(r"download (finished|error): (\w+)")
INTEREST = re.compile(
r"took a long time|Error|error|Download ended|No data received|Waiting for more|Decoded|cut off|already being|headers do not fit|buffer_size|Downloading image|fully downloaded|allocat|not caching"
)
PNG = [
("gradient 320x240 png", "fetch", "gradient.png", "finished"),
("gradient again", "fetch", "gradient.png", "finished"),
("gradient again", "fetch", "gradient.png", "finished"),
("flat 640x480 png", "fetch", "flat.png", "finished"),
("flat again", "fetch", "flat.png", "finished"),
("flat again", "fetch", "flat.png", "finished"),
("gradient jpeg", "fetch", "gradient.jpg", "finished"),
("ok bmp 16x16", "fetch", "ok.bmp", "finished"),
("ok png 16x16 body = gradient", "fetch", "ok.png", "finished"),
("chunked png", "fetch", "chunked.png", "finished"),
("chunked png cut in half", "fetch", "chunked_cut.png", "any"),
("closed early png", "fetch", "closed_early.png", "error"),
("stalled png", "fetch", "stall.png", "error"),
("slow png", "fetch", "slow.png", "finished"),
("empty png", "fetch", "empty.png", "error"),
("no content-length png", "fetch", "noclen.png", "any"),
("truncated bmp", "fetch", "truncated.bmp", "error"),
("gradient after errors", "fetch", "gradient.png", "finished"),
("gradient refetch 304", "refetch", "", "finished"),
("noise 320x240 png", "fetch", "noise.png", "finished"),
("noise again", "fetch", "noise.png", "finished"),
]
NOISE = [("noise 320x240 png", "fetch", "noise.png", "finished")] * 3
END = [
(f"{pre}{img}", "fetch", f"{pre}{img}", exp)
for img in ("flat.png", "gradient.png")
for pre, exp in (
("", "finished"),
("noclen_", "finished"),
("chunked_", "finished"),
("half_", "any"),
("stall_", "error"),
)
] + [("gradient after", "fetch", "gradient.png", "finished")]
BMPQOI = [
(f"{pre}{img}", "fetch", f"{pre}{img}", exp)
for img in (
"bmp1_640.bmp",
"bmp8_320.bmp",
"bmp24_320.bmp",
"qoi_flat.qoi",
"qoi_check.qoi",
"qoi_grad.qoi",
"qoi_noise.qoi",
)
for pre, exp in (
("", "finished"),
("", "finished"),
("noclen_", "any"),
("chunked_", "any"),
("half_", "error"),
)
] + [
("ok bmp 16x16", "fetch", "ok.bmp", "finished"),
("ok qoi 16x16", "fetch", "ok.qoi", "finished"),
("truncated bmp", "fetch", "truncated.bmp", "error"),
("stall_bmp1_640.bmp", "fetch", "stall_bmp1_640.bmp", "error"),
("stall_qoi_flat.qoi", "fetch", "stall_qoi_flat.qoi", "error"),
("qoi_flat after", "fetch", "qoi_flat.qoi", "finished"),
]
SHORT = [
("gradient 320x240 png", "fetch", "gradient.png", "finished"),
("gradient again", "fetch", "gradient.png", "finished"),
("noise 320x240 png", "fetch", "noise.png", "finished"),
("noise again", "fetch", "noise.png", "finished"),
("noise again", "fetch", "noise.png", "finished"),
("gradient after noise", "fetch", "gradient.png", "finished"),
]
JPEG = [
("noise 320x240 jpeg 69 KB", "fetch", "noise320.jpg", "finished"),
("noise 320x240 again", "fetch", "noise320.jpg", "finished"),
("noise 320x240 again", "fetch", "noise320.jpg", "finished"),
("noise 640x480 jpeg 233 KB", "fetch", "noise640.jpg", "finished"),
("noise 640x480 again", "fetch", "noise640.jpg", "finished"),
("gradient 640x480 jpeg 14 KB", "fetch", "gradient640.jpg", "finished"),
("4 KB buffer: noise 320x240", "fetch_small", "noise320.jpg", "finished"),
("4 KB buffer: noise 640x480", "fetch_small", "noise640.jpg", "finished"),
("4 KB buffer: 12 KB exif", "fetch_small", "bigexif320.jpg", "any"),
("64 KB buffer: 12 KB exif", "fetch", "bigexif320.jpg", "finished"),
("progressive jpeg", "fetch", "progressive320.jpg", "any"),
("no content-length", "fetch", "noclen_noise320.jpg", "finished"),
("chunked", "fetch", "chunked_noise320.jpg", "finished"),
("half with matching length", "fetch", "half_noise320.jpg", "error"),
("closed early", "fetch", "closed_early.jpg", "error"),
("stalled after a third", "fetch", "stall_noise320.jpg", "error"),
("empty", "fetch", "empty.jpg", "error"),
("noise 320x240 after errors", "fetch", "noise320.jpg", "finished"),
("refetch 304", "refetch", "", "finished"),
("ok png 16x16", "fetch", "ok.png", "finished"),
("ok bmp 16x16", "fetch", "ok.bmp", "finished"),
("4 KB buffer: noise 640x480 again", "fetch_small", "noise640.jpg", "finished"),
("4 KB buffer: half", "fetch_small", "half_noise320.jpg", "error"),
("4 KB buffer: stalled", "fetch_small", "stall_noise320.jpg", "error"),
(
"4 KB buffer: noise 320x240 after errors",
"fetch_small",
"noise320.jpg",
"finished",
),
]
CASE_SETS = {
"png": PNG,
"noise": NOISE,
"end": END,
"bmpqoi": BMPQOI,
"short": SHORT,
"jpeg": JPEG,
}
CASES = CASE_SETS[os.environ.get("CASES", "png")]
def missing_base_files() -> list[str]:
"""Base files behind prefixed cases that the server does not serve (any HTTP error, or no server).
A failed fetch ends as a download error on the device, which would pass the half_ and stall_ cases untested.
"""
builtin = {"chunked_cut", "closed_early", "large_header"}
missing = []
for _, _, path, _ in CASES:
stem = path.rsplit(".", 1)[0]
prefix, _, rest = path.partition("_")
if (
stem in builtin
or prefix not in ("half", "noclen", "chunked", "stall")
or not rest
):
continue
try:
with urllib.request.urlopen(f"{BASE}/{rest}", timeout=10):
pass
except (urllib.error.URLError, OSError) as exc: # HTTPError is a URLError
missing.append(f"{rest} ({exc})")
return sorted(set(missing))
async def main() -> int:
if missing := missing_base_files():
print(
f"server has no {', '.join(missing)}; run the image generators first",
flush=True,
)
return 2
c = APIClient(HOST, 6053, None, noise_psk=KEY)
await c.connect(login=True)
ents, services = await c.list_entities_services()
actions = {s.name: s for s in services if isinstance(s, UserService)}
loop_key = next(
e.key for e in ents if isinstance(e, SensorInfo) and e.name == "Loop Time"
)
heap_key = next(
e.key for e in ents if isinstance(e, SensorInfo) and e.name == "Heap Free"
)
state = {"loop": [], "heap": None, "heap_min": None, "lines": []}
def on_state(s) -> None:
if isinstance(s, SensorState) and s.key == loop_key:
state["loop"].append(s.state)
elif isinstance(s, SensorState) and s.key == heap_key:
state["heap"] = s.state
if state["heap_min"] is None or s.state < state["heap_min"]:
state["heap_min"] = s.state
def on_log(msg) -> None:
line = msg.message.decode(errors="replace")
log.write(f"{time.strftime('%H:%M:%S')} {line}\n")
state["lines"].append(line)
with pathlib.Path(LOGFILE).open("w", encoding="utf-8") as log:
c.subscribe_states(on_state)
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_DEBUG)
await asyncio.sleep(3)
print(f"{LABEL}: heap at start {state['heap']}")
rows = []
for label, action, path, expect in CASES:
img = "img_small" if action.endswith("_small") else "img"
state["loop"].clear()
state["lines"].clear()
state["heap_min"] = None
args = {} if action.startswith("refetch") else {"url": f"{BASE}/{path}"}
t0 = time.monotonic()
log.write(f">>> {label} {path}\n")
try:
await c.execute_service(actions[action], args)
except Exception as exc: # noqa: BLE001
print(f" api call failed ({exc!r}); reconnecting", flush=True)
with contextlib.suppress(Exception):
await c.disconnect()
await asyncio.sleep(10)
await c.connect(login=True)
ents, services = await c.list_entities_services()
actions = {s.name: s for s in services if isinstance(s, UserService)}
c.subscribe_states(on_state)
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_DEBUG)
await asyncio.sleep(2)
state["lines"].clear()
t0 = time.monotonic()
await c.execute_service(actions[action], args)
outcome = "no outcome"
while time.monotonic() - t0 < CAP_S:
await asyncio.sleep(0.25)
for line in state["lines"]:
m = OUTCOME.search(line)
if m and m.group(2) == img:
outcome = m.group(1)
if outcome != "no outcome":
break
took = time.monotonic() - t0
await asyncio.sleep(4.5) # two loop time reports after the case
notes = [
ln for ln in state["lines"] if INTEREST.search(ln) and "Loop" not in ln
]
loop_max = max(state["loop"]) if state["loop"] else float("nan")
ok = outcome == expect or (expect == "any" and outcome != "no outcome")
rows.append((label, path, expect, outcome, took, loop_max, ok))
print(
f"{'PASS' if ok else 'FAIL'} {label:30s} {path:18s} expected {expect:9s} got {outcome:10s} {took:5.1f}s loop max {loop_max:7.0f} ms heap min {state['heap_min']}",
flush=True,
)
for n in notes[:8]:
print(f" {n.strip()}", flush=True)
await c.disconnect()
print(
"\nSUMMARY",
"all expectations met"
if all(r[6] for r in rows)
else "MISMATCHES: " + ", ".join(r[0] for r in rows if not r[6]),
)
return 0 if all(r[6] for r in rows) else 1
sys.exit(asyncio.run(main()))
@@ -0,0 +1,75 @@
"""Run the tx_all script N times over the API and count what each sent frame decoded as.
Usage: rbdrive.py IP N"""
import asyncio
import collections
import re
import sys
import aioesphomeapi
IP, N = sys.argv[1], int(sys.argv[2])
lines: list[str] = []
async def main() -> None:
c = aioesphomeapi.APIClient(IP, 6053, None)
await c.connect(login=True)
i = await c.device_info()
print("device", i.name, i.compilation_time, flush=True)
_, svcs = await c.list_entities_services()
s = {x.name: x for x in svcs}
def on_log(m):
lines.append(re.sub(r"\x1b\[[0-9;]*m", "", m.message.decode(errors="replace")))
c.subscribe_logs(on_log, log_level=aioesphomeapi.LogLevel.LOG_LEVEL_DEBUG)
await asyncio.sleep(1)
complete = 0
for rnd in range(1, N + 1):
start = len(lines)
await c.execute_service(s["tx_all"], {})
for _ in range(200):
if any("tx done" in x for x in lines[start:]):
complete += 1
break
await asyncio.sleep(0.1)
else:
print(f"round {rnd}: no tx done within 20 s", flush=True)
await asyncio.sleep(1)
await c.disconnect()
# per sent protocol: what was decoded
table: dict[str, collections.Counter] = collections.defaultdict(collections.Counter)
cur = None
for x in lines:
if m := re.search(r"\]: tx (\w+)$", x):
cur = m[1]
continue
if cur is None or cur == "done":
continue
if m := re.search(r"Received ([A-Za-z0-9_ ]+?):", x):
table[cur][m[1]] += 1
elif "ext dumper saw" in x:
table[cur]["ext"] += 1
print(f"{complete} of {N} rounds complete", flush=True)
for proto, cnt in table.items():
print(
f"{proto:11s} " + ", ".join(f"{k}={v}" for k, v in sorted(cnt.items())),
flush=True,
)
for x in lines:
if "ext dumper saw" not in x and re.search(
r"rbt|error|Error|overflow|Guru|panic", x
):
print(" ", x[:160])
if not any(k != "ext" for cnt in table.values() for k in cnt):
print("nothing decoded", flush=True)
return 1
if not any(cnt["ext"] for cnt in table.values()):
print("the external dumper saw no frame", flush=True)
return 1
return 0 if complete == N else 1
sys.exit(asyncio.run(main()))
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
# run.sh LABEL B(32|66) [N] : write rb<B><LABEL> from templates/remote_base on every run, flash it, run rbdrive.py for N rounds
# Copy this directory and templates/remote_base into one scratch directory (secrets.yaml next to rb-common.yaml).
# env: TREE (esphome checkout to build from), P32 or P66 (serial port of the board), SC (path to
# serial_capture.py), PY (checkout python), EXPECT_REGISTER=1 (fail when the dumper registration was rejected)
R=$(cd "$(dirname "$0")" && pwd); V=$1; B=$2; N=${3:-3}; NAME=rb$B$V; T=${TREE:?set TREE}
PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}
if [ "$B" = 32 ]; then P=${P32:?set P32}; else P=${P66:?set P66}; fi
mkdir -p "$R/$V"; sed "s/@V@/$V/g" "$R/rb$B.tmpl" > "$R/$V/$NAME.yaml" # always from the current template
OUT=$R/out-$V-$B; rm -rf "$OUT"; mkdir -p "$OUT"
pkill -f "[s]erial_capture.py $P"
(cd "$T" && $PY -m esphome run --no-logs "$R/$V/$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 & CAP=$!
trap 'kill $CAP 2>/dev/null' EXIT
for i in $(seq 60); do IP=$(grep -a -o 'IP Address: [0-9.]*' "$OUT/serial.txt" 2>/dev/null | tail -1 | cut -d' ' -f3); [ -z "$IP" ] && IP=$(python3 -c "import socket;print(socket.gethostbyname('$NAME.local'))" 2>/dev/null); [ -n "$IP" ] && break; sleep 2; done
[ -n "$IP" ] || { echo "device not found"; exit 1; }
$PY "$R/rbdrive.py" "$IP" "$N" > "$OUT/drive.txt" 2>&1; RC=$?
pkill -f "[s]erial_capture.py $P"
[ -s "$OUT/serial.txt" ] || { echo "serial capture got nothing, crash check invalid"; exit 1; }
CRASH=$(grep -a -c -i 'guru\|panic\|exception\|rst cause' "$OUT/serial.txt")
REJECTED=$(grep -a -c 'No dumper slot' "$OUT/serial.txt")
cat "$OUT/drive.txt"; echo "serial: $(grep -a -c 'ext dumper register called' "$OUT/serial.txt") register calls, $REJECTED rejected, $CRASH crash lines"
if [ "${EXPECT_REGISTER:-0}" = 1 ] && [ "$REJECTED" != 0 ]; then echo "the dumper registration was rejected"; exit 1; fi
[ "$RC" = 0 ] && [ "$CRASH" = 0 ]
@@ -0,0 +1,103 @@
"""Drive the FrameReader simulation builds over the API: press buttons, set selects/numbers, print states.
usage: frdrive.py HOST LOGFILE OP [OP ...]
OP: list | press:NAME | sel:NAME=OPTION | num:NAME=VALUE | wait:SECONDS
"""
import asyncio
import math
import pathlib
import sys
import time
from aioesphomeapi import (
APIClient,
BinarySensorState,
ButtonInfo,
LogLevel,
NumberInfo,
SelectInfo,
SensorInfo,
SensorState,
)
HOST, LOGFILE = sys.argv[1], sys.argv[2]
OPS = sys.argv[3:]
T0 = time.time()
SKIP = {"Heap Free", "Loop Time"}
def stamp() -> str:
return f"{time.time() - T0:7.3f}"
async def main() -> int:
with pathlib.Path(LOGFILE).open("w", encoding="utf-8") as log:
c = APIClient(HOST, 6053, None)
await c.connect(login=True)
info = await c.device_info()
print(f"device {info.name} compiled {info.compilation_time}", flush=True)
ents, _ = await c.list_entities_services()
by_key = {e.key: e for e in ents}
def find(kind: type, name: str):
for e in ents:
if isinstance(e, kind) and e.name == name:
return e
sys.exit(f"no {kind.__name__} named {name!r}")
def on_state(s) -> None:
e = by_key.get(s.key)
if e is None or e.name in SKIP:
return
if isinstance(s, SensorState) and isinstance(e, SensorInfo):
v = "nan" if math.isnan(s.state) else f"{s.state:.2f}"
line = f"{stamp()} SEN {e.name} = {v}"
elif isinstance(s, BinarySensorState):
line = f"{stamp()} BIN {e.name} = {s.state}"
else:
return
print(line, flush=True)
log.write(line + "\n")
states.append(line)
def on_log(msg) -> None:
log.write(f"{stamp()} {msg.message.decode(errors='replace')}\n")
log.flush()
states: list[str] = []
c.subscribe_states(on_state)
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_VERBOSE)
await asyncio.sleep(2)
states.clear() # count only states published while the ops ran
for op in OPS:
kind, _, rest = op.partition(":")
if kind == "list":
for e in ents:
print(type(e).__name__, e.name)
continue
if kind == "wait":
await asyncio.sleep(float(rest))
continue
line = f"{stamp()} >>> {op}"
print(line, flush=True)
log.write(line + "\n")
if kind == "press":
c.button_command(find(ButtonInfo, rest).key)
elif kind == "sel":
name, _, value = rest.partition("=")
c.select_command(find(SelectInfo, name).key, value)
elif kind == "num":
name, _, value = rest.partition("=")
c.number_command(find(NumberInfo, name).key, float(value))
else:
sys.exit(f"unknown op {op!r}")
await asyncio.sleep(2.5)
await asyncio.sleep(1)
await c.disconnect()
if any(not op.startswith(("list", "wait")) for op in OPS) and not states:
print("no sensor state was published while the ops ran", flush=True)
return 1
return 0
sys.exit(asyncio.run(main()))
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# usage: run.sh NAME TREE SECONDS OP... flash over serial, capture serial, drive over the API
# Copy this, frdrive.py and the templates/uart_sim files into one scratch directory and run it there.
# env: PORT (the devkit serial port), SC (path to serial_capture.py), PY (checkout python); TREE is the esphome
# checkout to build from
D=$(cd "$(dirname "$0")" && pwd); N=$1; T=$2; S=$3; shift 3; P=${PORT:?set PORT}; PY=${PY:-$T/venv/bin/python}
(cd $T && PYTHONUNBUFFERED=1 timeout 1500 $PY -m esphome run --no-logs $D/$N.yaml --device $P > $D/$N-upload.log 2>&1) || { echo "build or upload failed"; exit 1; }
$PY ${SC:?set SC} $P $S $D/$N-cap.txt > $D/$N-cap.err 2>&1 &
CAP=$!
trap 'kill $CAP 2>/dev/null' EXIT
UP=0
for i in $(seq 1 40); do $PY -c "import socket;socket.create_connection(('$N.local',6053),2)" 2>/dev/null && { UP=1; break; }; sleep 2; done
[ $UP = 1 ] || { echo "device $N.local not reachable on port 6053"; exit 1; }
cd $D && PYTHONUNBUFFERED=1 timeout $((S - 5)) $PY frdrive.py $N.local $N-api.log "$@" > $N-drive.txt 2>&1; RC=$?; echo "drive rc=$RC"
wait $CAP; CAPRC=$?; trap - EXIT
[ "$CAPRC" = 0 ] || { echo "serial capture failed (rc $CAPRC), crash check invalid"; exit 1; }
CRASH=$(grep -acE 'Guru|abort\(\)|rst:0x' $D/$N-cap.txt)
echo "crash lines: $CRASH"
[ "$RC" = 0 ] && [ "$CRASH" = 0 ]
@@ -0,0 +1,65 @@
esphome:
name: ${name}
esp8266:
board: d1_mini
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
api:
reboot_timeout: 0s
actions:
- action: play_buzzer
description: Play a song or build one from parts
variables:
song_str:
type: string
description: Full RTTTL string
example: "scale:d=4,o=5,b=100:c,d,e"
required: false
notes:
type: string
required: false
octave:
type: int
default: 5
bpm:
type: int
default: 100
tempo:
type: float
default: 1.5
loud:
type: bool
default: true
label:
type: string
default: ""
then:
- lambda: |-
char s[40], n[40], l[40];
snprintf(s, sizeof(s), "%.*s", (int) song_str.size(), song_str.c_str());
snprintf(n, sizeof(n), "%.*s", (int) notes.size(), notes.c_str());
snprintf(l, sizeof(l), "%.*s", (int) label.size(), label.c_str());
ESP_LOGD("main", "play_buzzer song=%s(%d) notes=%s(%d) octave=%d bpm=%d tempo=%.1f loud=%d label=%s(%d)", s,
(int) song_str.size(), n, (int) notes.size(), octave, bpm, tempo, loud, l, (int) label.size());
- action: plain_action
variables:
value: int
then:
- logger.log:
format: "plain_action value=%d"
args: ["value"]
- action: list_action
variables:
values: int[]
then:
- logger.log:
format: "list_action n=%d"
args: ["values.size()"]
@@ -0,0 +1,28 @@
esphome:
name: ${name}
esp8266:
board: d1_mini
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
api:
reboot_timeout: 0s
actions:
- action: str_action
variables:
song_str: string
notes: string
octave: int
then:
- lambda: |-
char s[40], n[40];
snprintf(s, sizeof(s), "%.*s", (int) song_str.size(), song_str.c_str());
snprintf(n, sizeof(n), "%.*s", (int) notes.size(), notes.c_str());
ESP_LOGD("main", "str_action song=%s(%d) notes=%s(%d) octave=%d", s, (int) song_str.size(), n, (int) notes.size(), octave);
@@ -0,0 +1,150 @@
substitutions:
# the fake instrument's BLE address; pass it with `-s peri_mac AA:BB:CC:DD:EE:FF`
peri_mac: "00:00:00:00:00:00"
esphome:
name: midicli
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: VERBOSE
logs:
esp32_ble_tracker: DEBUG
ble_client: DEBUG
esp32_ble: DEBUG
api: DEBUG
scheduler: DEBUG
sensor: DEBUG
wifi: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
api:
reboot_timeout: 0s
esp32_ble_tracker:
ble_client:
- mac_address: ${peri_mac}
id: midi_client
ble_midi:
id: test_midi
ble_client_id: midi_client
max_sysex_size: 64
pair: false
on_connect:
- logger.log: "MIDI device connected"
- ble_midi.send_program_change:
id: test_midi
program: 0
on_disconnect:
- logger.log: "MIDI device disconnected"
on_note_on:
- logger.log:
format: "Note on %u velocity %u channel %u"
args: [note, velocity, channel]
on_note_off:
- logger.log:
format: "Note off %u channel %u"
args: [note, channel]
on_control_change:
- logger.log:
format: "CC %u = %u on channel %u"
args: [control_number, value, channel]
on_program_change:
- logger.log:
format: "Program %u on channel %u"
args: [program, channel]
on_pitch_bend:
- logger.log:
format: "Pitch bend %d on channel %u"
args: [value, channel]
on_sysex:
- lambda: |-
ESP_LOGI("midi", "SysEx of %u bytes: %s", (unsigned) data.size(), format_hex_pretty(data.data(), data.size()).c_str());
on_message:
- lambda: |-
ESP_LOGI("midi", "Raw message of %u bytes: %s", (unsigned) data.size(), format_hex_pretty(data.data(), data.size()).c_str());
sensor:
- platform: debug
free:
name: Heap Free
loop_time:
name: Loop Time
debug:
update_interval: 30s
button:
- platform: template
name: "MIDI send tests"
on_press:
- ble_midi.send_note_on:
id: test_midi
note: 60
- ble_midi.send_note_on:
id: test_midi
note: !lambda "return 60;"
velocity: !lambda "return 100;"
channel: !lambda "return 1;"
- ble_midi.send_note_off:
id: test_midi
note: 60
velocity: 0
channel: 1
- ble_midi.send_control_change:
id: test_midi
control_number: 7
value: 64
- ble_midi.send_pitch_bend:
id: test_midi
value: -8192
- ble_midi.send_sysex:
id: test_midi
payload: [0x7E, 0x7F, 0x06, 0x01]
- ble_midi.send_raw:
id: test_midi
data: !lambda "return std::vector<uint8_t>{0x90, 0x3C, 0x40};"
- if:
condition:
ble_midi.connected:
id: test_midi
then:
- logger.log: "Still connected"
else:
- logger.log: "Not connected"
- platform: template
name: "MIDI send out of range"
on_press:
- ble_midi.send_note_on:
id: test_midi
note: !lambda "return 200;"
- ble_midi.send_control_change:
id: test_midi
control_number: 1
value: 1
channel: !lambda "return 16;"
- ble_midi.send_pitch_bend:
id: test_midi
value: 8191
- ble_midi.send_sysex:
id: test_midi
payload: [0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7]
- ble_midi.send_raw:
id: test_midi
data: !lambda "return std::vector<uint8_t>(140, 0x00);"
- platform: template
name: "MIDI send long sysex"
on_press:
- ble_midi.send_sysex:
id: test_midi
payload: !lambda "return std::vector<uint8_t>(100, 0x11);"
@@ -0,0 +1,177 @@
esphome:
name: midiperi
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
logger:
hardware_uart: USB_SERIAL_JTAG
level: DEBUG
api:
reboot_timeout: 0s
ota:
- platform: esphome
ethernet:
type: W5500
clk_pin: GPIO13
mosi_pin: GPIO11
miso_pin: GPIO12
cs_pin: GPIO14
interrupt_pin: GPIO10
reset_pin: GPIO9
esp32_ble_server:
id: ble_server
manufacturer: FakeMIDI
on_connect:
- lambda: ESP_LOGI("peri", "central connected %d", id);
on_disconnect:
- lambda: ESP_LOGI("peri", "central disconnected %d", id);
services:
- uuid: 03B80E5A-EDE8-4B33-A751-6CE34EC4C700
advertise: true
characteristics:
- id: midi_io
uuid: 7772E5DB-3868-4112-A1A9-F2669D106BF3
read: true
write: true
write_no_response: true
notify: true
on_write:
- lambda: |-
ESP_LOGI("peri", "RX %u bytes: %s", (unsigned) x.size(), format_hex_pretty(x.data(), x.size()).c_str());
script:
- id: emit
parameters:
pkt: std::vector<uint8_t>
then:
- lambda: |-
ESP_LOGI("peri", "TX %u bytes: %s", (unsigned) pkt.size(), format_hex_pretty(pkt.data(), pkt.size()).c_str());
id(midi_io).set_value(std::vector<uint8_t>(pkt));
id(midi_io).notify();
button:
- platform: restart
name: P Restart
- platform: template
name: P Note On
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0x90, 0x3C, 0x64};'
- platform: template
name: P Note Off
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0x80, 0x3C, 0x00};'
- platform: template
name: P Control Change
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xB0, 0x07, 0x40};'
- platform: template
name: P Program Change
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xC5, 0x05};'
- platform: template
name: P Pitch Bend
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xE0, 0x7F, 0x7F};'
- platform: template
name: P Two Notes One Packet
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0x90, 0x3C, 0x64, 0x81, 0x91, 0x3E, 0x50};'
- platform: template
name: P Running Status
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0x90, 0x40, 0x64, 0x41, 0x64};'
- platform: template
name: P Sysex One Packet
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xF0, 0x7E, 0x7F, 0x06, 0x01, 0x80, 0xF7};'
- platform: template
name: P Sysex Split
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xF0, 0x41, 0x10, 0x42};'
- delay: 200ms
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x12, 0x00, 0x40, 0x80, 0xF7};'
- platform: template
name: P Sysex Oversize
on_press:
- script.execute:
id: emit
pkt: !lambda 'std::vector<uint8_t> v{0x80, 0x80, 0xF0}; for (int i = 0; i < 17; i++) v.push_back(i); return v;'
- delay: 200ms
- script.execute:
id: emit
pkt: !lambda 'std::vector<uint8_t> v{0x80}; for (int i = 0; i < 19; i++) v.push_back(0x20 + i); return v;'
- delay: 200ms
- script.execute:
id: emit
pkt: !lambda 'std::vector<uint8_t> v{0x80}; for (int i = 0; i < 19; i++) v.push_back(0x40 + i); return v;'
- delay: 200ms
- script.execute:
id: emit
pkt: !lambda 'std::vector<uint8_t> v{0x80}; for (int i = 0; i < 19; i++) v.push_back(0x60 + i); return v;'
- delay: 200ms
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xF7};'
- platform: template
name: P Sysex Cut Short
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xF0, 0x41, 0x10, 0x80, 0x90, 0x3C, 0x64};'
- platform: template
name: P Clock
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xF8};'
- platform: template
name: P Aftertouch
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0xD3, 0x45};'
- platform: template
name: P Orphan Data Byte
on_press:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0x3C, 0x64};'
- platform: template
name: P Burst
on_press:
- repeat:
count: 50
then:
- script.execute:
id: emit
pkt: !lambda 'return {0x80, 0x80, 0x90, 0x30, 0x40, 0x80, 0x80, 0x30, 0x00};'
- delay: 20ms
@@ -0,0 +1,162 @@
# Package for the http_request action test (esphome#21486). Needs substitutions: name, bufsize.
esphome:
name: ${name}
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
api:
reboot_timeout: 0s
actions:
- action: req_get
variables:
url: string
then:
- lambda: 'id(max_gap) = 0; id(t_start) = millis(); id(tag)++; ESP_LOGI("hrt", "START %d %s %s", id(tag), "GET", url.c_str());'
- http_request.get:
url: !lambda return url;
capture_response: true
max_response_buffer_size: ${bufsize}
on_response:
then:
- lambda: |-
uint32_t sum = 0;
for (char c : body) sum += (uint8_t) c;
ESP_LOGI("hrt", "RESULT %d status=%d len=%u sum=%u took=%u maxgap=%u head=%.20s", id(tag),
response->status_code, (unsigned) body.size(), (unsigned) sum,
(unsigned) (millis() - id(t_start)), (unsigned) std::max<uint32_t>(id(max_gap), millis() - id(last_tick)), body.c_str());
on_error:
then:
- lambda: 'ESP_LOGI("hrt", "RESULT %d error took=%u maxgap=%u", id(tag), (unsigned) (millis() - id(t_start)), (unsigned) std::max<uint32_t>(id(max_gap), millis() - id(last_tick)));'
- lambda: 'ESP_LOGI("hrt", "CHAIN %d after send", id(tag));'
- action: req_post
variables:
url: string
payload: string
then:
- lambda: 'id(max_gap) = 0; id(t_start) = millis(); id(tag)++; ESP_LOGI("hrt", "START %d %s %s", id(tag), "POST", url.c_str());'
- http_request.post:
url: !lambda return url;
body: !lambda return payload;
capture_response: true
max_response_buffer_size: ${bufsize}
on_response:
then:
- lambda: |-
uint32_t sum = 0;
for (char c : body) sum += (uint8_t) c;
ESP_LOGI("hrt", "RESULT %d status=%d len=%u sum=%u took=%u maxgap=%u head=%.20s", id(tag),
response->status_code, (unsigned) body.size(), (unsigned) sum,
(unsigned) (millis() - id(t_start)), (unsigned) std::max<uint32_t>(id(max_gap), millis() - id(last_tick)), body.c_str());
on_error:
then:
- lambda: 'ESP_LOGI("hrt", "RESULT %d error took=%u maxgap=%u", id(tag), (unsigned) (millis() - id(t_start)), (unsigned) std::max<uint32_t>(id(max_gap), millis() - id(last_tick)));'
- lambda: 'ESP_LOGI("hrt", "CHAIN %d after send", id(tag));'
- action: burst
variables:
url: string
count: int
then:
- lambda: 'id(max_gap) = 0; id(t_start) = millis(); ESP_LOGI("hrt", "BURST %d", count);'
- repeat:
count: !lambda return count;
then:
- script.execute:
id: burst_script
url: !lambda return url;
- action: cancel
variables:
url: string
then:
- lambda: 'id(max_gap) = 0; id(t_start) = millis(); ESP_LOGI("hrt", "CANCEL start");'
- script.execute:
id: single_script
url: !lambda return url;
- delay: 300ms
- script.execute:
id: single_script
url: !lambda return url;
- delay: 300ms
- script.stop: single_script
- lambda: 'ESP_LOGI("hrt", "CANCEL stopped running=%d", id(single_script).is_running());'
- delay: 4s
- lambda: 'ESP_LOGI("hrt", "CANCEL done maxgap=%u", (unsigned) std::max<uint32_t>(id(max_gap), millis() - id(last_tick)));'
- action: reboot_during
variables:
url: string
then:
- lambda: 'ESP_LOGI("hrt", "REBOOT_DURING start");'
- script.execute:
id: single_script
url: !lambda return url;
- script.execute:
id: burst_script
url: !lambda return url;
- delay: 500ms
- lambda: 'ESP_LOGI("hrt", "REBOOT_DURING rebooting"); App.safe_reboot();'
script:
- id: burst_script
mode: parallel
max_runs: 10
parameters:
url: string
then:
- http_request.get:
url: !lambda return url;
capture_response: true
max_response_buffer_size: 256
on_response:
then:
- lambda: 'ESP_LOGI("hrt", "BURSTRESP status=%d len=%u took=%u maxgap=%u", response->status_code, (unsigned) body.size(), (unsigned) (millis() - id(t_start)), (unsigned) std::max<uint32_t>(id(max_gap), millis() - id(last_tick)));'
on_error:
then:
- lambda: 'ESP_LOGI("hrt", "BURSTERR took=%u", (unsigned) (millis() - id(t_start)));'
- id: single_script
parameters:
url: string
then:
- lambda: 'ESP_LOGI("hrt", "SINGLE begin");'
- http_request.get:
url: !lambda return url;
capture_response: true
max_response_buffer_size: 256
on_response:
then:
- lambda: 'ESP_LOGI("hrt", "SINGLERESP status=%d took=%u", response->status_code, (unsigned) (millis() - id(t_start)));'
on_error:
then:
- lambda: 'ESP_LOGI("hrt", "SINGLEERR took=%u", (unsigned) (millis() - id(t_start)));'
- lambda: 'ESP_LOGI("hrt", "SINGLE end");'
http_request:
timeout: 8s
globals:
- id: max_gap
type: uint32_t
initial_value: "0"
- id: last_tick
type: uint32_t
initial_value: "0"
- id: t_start
type: uint32_t
initial_value: "0"
- id: tag
type: int
initial_value: "0"
interval:
- interval: 1ms
then:
- lambda: |-
const uint32_t now = millis();
if (id(last_tick) != 0 && now - id(last_tick) > id(max_gap))
id(max_gap) = now - id(last_tick);
id(last_tick) = now;
ota:
- platform: esphome
@@ -0,0 +1,7 @@
# Run file for the PR build; for the base build pass `-s name hr32base` to esphome (or copy it with that name).
substitutions: {name: hr32pr, bufsize: "32768"}
esp32:
board: esp32dev
framework: {type: esp-idf}
logger:
packages: {hr: !include hr-common.yaml}
@@ -0,0 +1,8 @@
# Run file for the PR build; for the base build pass `-s name hr66base` to esphome (or copy it with that name).
substitutions: {name: hr66pr, bufsize: "8192"}
esp8266:
board: d1_mini
logger:
packages: {hr: !include hr-common.yaml}
http_request:
verify_ssl: false
@@ -0,0 +1,22 @@
# Same as hr32.yaml plus the otaflash action otadrive.py calls; pass -s name hrota32base for the base build.
substitutions: {name: hrota32, bufsize: "32768"}
esp32:
board: esp32dev
framework: {type: esp-idf}
logger:
packages: {hr: !include hr-common.yaml}
ota:
- platform: http_request
id: ota_http
api:
actions:
- action: otaflash
variables:
url: string
sum: string
then:
- lambda: 'ESP_LOGI("hrt", "OTAFLASH start %s", url.c_str());'
- ota.http_request.flash:
url: !lambda return url;
md5: !lambda return sum;
- lambda: 'ESP_LOGI("hrt", "OTAFLASH returned (failed)");'
@@ -0,0 +1,10 @@
esphome:
name: upd32
esp32:
board: esp32dev
framework:
type: esp-idf
packages:
common: !include ../upd-common.yaml
@@ -0,0 +1,8 @@
esphome:
name: upd8266
esp8266:
board: d1_mini
packages:
common: !include ../upd-common.yaml
@@ -0,0 +1,52 @@
substitutions:
base: !secret manifest_base
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
api:
reboot_timeout: 0s
http_request:
verify_ssl: false
timeout: 10s
ota:
- platform: http_request
id: ota_http
update:
- platform: http_request
id: upd
name: Firmware
source: ${base}/stable.json
update_interval: 1h
button:
- platform: template
name: Check
on_press:
- lambda: id(upd).check();
- platform: template
name: URL Slow Cstr
on_press:
- lambda: |-
std::string url = "${base}/slow.json";
id(upd).set_source_url(url.c_str());
- platform: template
name: URL Stable Literal
on_press:
- lambda: id(upd).set_source_url("${base}/stable.json");
- platform: template
name: Heap
on_press:
- lambda: |-
#ifdef USE_ESP8266
ESP_LOGI("harness", "heap free %u", ESP.getFreeHeap());
#else
ESP_LOGI("harness", "heap free %u", (unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
#endif
@@ -0,0 +1,95 @@
substitutions:
base: !secret manifest_base
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
on_message:
- level: DEBUG
then:
- lambda: |-
// Inline check path: replace the URL from inside the check (ESP8266 runs the check on the main loop)
if (id(swap_in_check) && strstr(message, "Content-Length") != nullptr) {
id(swap_in_check) = false;
std::string url = "${base}/beta.json";
id(upd).set_source_url(url);
ESP_LOGI("harness", "URL replaced from inside the check");
}
api:
reboot_timeout: 0s
http_request:
verify_ssl: false
timeout: 10s
ota:
- platform: http_request
id: ota_http
update:
- platform: http_request
id: upd
name: Firmware
source: ${base}/stable.json
update_interval: 1h
globals:
- id: swap_in_check
type: bool
initial_value: "false"
button:
- platform: template
name: Check
on_press:
- lambda: id(upd).check();
- platform: template
name: URL Beta String
on_press:
- lambda: |-
std::string url = "${base}/beta.json";
id(upd).set_source_url(url);
- platform: template
name: URL Slow Cstr
on_press:
- lambda: |-
std::string url = "${base}/slow.json";
id(upd).set_source_url(url.c_str());
- platform: template
name: URL Stable Literal
on_press:
- lambda: id(upd).set_source_url("${base}/stable.json");
- platform: template
name: URL Temporary
on_press:
- lambda: id(upd).set_source_url(std::string("${base}/beta.json"));
- platform: template
name: Same URL Back
on_press:
- lambda: id(upd).set_source_url(id(upd).get_source_url());
- platform: template
name: Check Then Swap
on_press:
- lambda: |-
// ESP32: the check task reads slow.json while the URL is replaced under it
id(upd).check();
std::string url = "${base}/beta.json";
id(upd).set_source_url(url);
- platform: template
name: Arm Swap In Check
on_press:
- lambda: id(swap_in_check) = true;
- platform: template
name: Heap
on_press:
- lambda: |-
#ifdef USE_ESP8266
ESP_LOGI("harness", "heap free %u", ESP.getFreeHeap());
#else
ESP_LOGI("harness", "heap free %u", (unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
#endif
@@ -0,0 +1,57 @@
substitutions:
name: ld2412sel
esphome:
name: ${name}
esp32:
variant: esp32s3
board: esp32-s3-devkitc-1
flash_size: 8MB
framework:
type: esp-idf
logger:
level: VERBOSE
api:
reboot_timeout: 0s
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
uart:
- id: uart_ld2412
tx_pin: GPIO43
rx_pin: GPIO44
baud_rate: 115200
debug:
direction: BOTH
dummy_receiver: false
after:
timeout: 10ms
bytes: 120
ld2412:
id: ld2412_radar
uart_id: uart_ld2412
select:
- platform: ld2412
ld2412_id: ld2412_radar
out_pin_level:
name: "LD2412 Hardware output pin level"
binary_sensor:
- platform: gpio
name: "LD2412 Out Pin"
pin: GPIO38
- platform: ld2412
ld2412_id: ld2412_radar
has_target:
name: "LD2412 Presence"
@@ -0,0 +1,258 @@
esphome:
name: ${name}
on_boot:
priority: -100
then:
- lambda: |-
// UART0 TX looped back into its own RX so injected frames reach the hub
USC0(0) |= (1 << UCLBE);
ESP_LOGI("harness", "UART0 loopback on");
esp8266:
board: d1_mini
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
reboot_timeout: 0s
logger:
baud_rate: 0
level: VERBOSE
uart:
id: radar_uart
tx_pin: GPIO1
rx_pin: GPIO3
baud_rate: ${baud}
debug:
direction: TX
dummy_receiver: false
after:
delimiter: [0x04, 0x03, 0x02, 0x01]
ld2450:
id: radar
uart_id: radar_uart
select:
- platform: ld2450
baud_rate:
name: Baud Rate
zone_type:
name: Zone Type
text_sensor:
- platform: ld2450
version:
name: Radar Version
target_1:
direction:
name: T1 Direction
target_2:
direction:
name: T2 Direction
target_3:
direction:
name: T3 Direction
sensor:
- platform: ld2450
target_count:
name: Target Count
button:
# periodic frames: header, 3 x (x, y, speed, resolution), footer
- platform: template
name: Frame Approach
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
- platform: template
name: Frame Away
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x14, 0x80, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
- platform: template
name: Frame Stationary
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x00, 0x00, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
- platform: template
name: Frame None
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
- platform: template
name: Frame T2 T3
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xF4, 0x81, 0x20, 0x83, 0x1E, 0x00, 0x64, 0x00,
0x2C, 0x01, 0xB0, 0x84, 0x0A, 0x80, 0x64, 0x00,
0x55, 0xCC]
# query zone acks: header, length, command C1, status 01, 00 00, zone type, 00, 24 coordinate bytes, footer
- platform: template
name: Zone Ack Disabled
on_press:
- uart.write:
id: radar_uart
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x03, 0x02, 0x01]
- platform: template
name: Zone Ack Detection
on_press:
- uart.write:
id: radar_uart
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x03, 0x02, 0x01]
- platform: template
name: Zone Ack Filter
on_press:
- uart.write:
id: radar_uart
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x03, 0x02, 0x01]
# name based setters, the path lambdas use
- platform: template
name: Set Zone Filter By Name
on_press:
- lambda: id(radar).set_zone_type("Filter");
- platform: template
name: Set Zone Bogus By Name
on_press:
- lambda: id(radar).set_zone_type("Bogus");
- platform: template
name: Set Baud 57600 By Name
on_press:
- lambda: id(radar).set_baud_rate("57600");
- platform: template
name: Set Baud Bogus By Name
on_press:
- lambda: id(radar).set_baud_rate("Bogus");
# target 1 at x 772, y 258: bytes 04 03 02 01 are the command footer
- platform: template
name: Frame CmdFooter In Payload
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x04, 0x03, 0x02, 0x01, 0x00, 0x00, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
# zone ack, type Detection, zone 1 x1 = 0xCC55: bytes 55 CC are the periodic footer
- platform: template
name: Zone Ack DataFooter In Payload
on_press:
- uart.write:
id: radar_uart
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x01, 0x00,
0x55, 0xCC, 0xC8, 0x00, 0xDC, 0x05, 0xB8, 0x0B, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x03, 0x02, 0x01]
# zone ack, type Filter, zone 1 x1 = 0x0304 and y1 = 0x0102: bytes 04 03 02 01 are the command footer
- platform: template
name: Zone Ack CmdFooter In Payload
on_press:
- uart.write:
id: radar_uart
data: [0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x02, 0x00,
0x04, 0x03, 0x02, 0x01, 0xDC, 0x05, 0xB8, 0x0B, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x03, 0x02, 0x01]
# a periodic frame cut after 12 bytes, then a complete zone ack (type Disabled, zeros)
- platform: template
name: Cut Frame Then Zone Ack
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
0xFD, 0xFC, 0xFB, 0xFA, 0x1E, 0x00, 0xC1, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x03, 0x02, 0x01]
# a periodic frame missing its last byte, then a complete one (target 1 moving away)
- platform: template
name: Cut Frame Then Frame
on_press:
- uart.write:
id: radar_uart
data: [0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55,
0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x14, 0x80, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
# a command header claiming a 255 byte payload, then a complete periodic frame (target 1 stationary)
- platform: template
name: Bogus Length Then Frame
on_press:
- uart.write:
id: radar_uart
data: [0xFD, 0xFC, 0xFB, 0xFA, 0xFF, 0x00,
0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x00, 0x00, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
# five garbage bytes, then a complete periodic frame (target 1 approaching)
- platform: template
name: Garbage Then Frame
on_press:
- uart.write:
id: radar_uart
data: [0x01, 0x02, 0x03, 0x42, 0x99,
0xAA, 0xFF, 0x03, 0x00,
0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0xCC]
number:
- platform: ld2450
presence_timeout:
name: Presence Timeout
zone_1:
x1:
name: Z1 X1
y1:
name: Z1 Y1
x2:
name: Z1 X2
y2:
name: Z1 Y2
@@ -0,0 +1,65 @@
esp32:
board: esp32dev
framework:
type: esp-idf
watchdog_timeout: 30s
logger:
level: DEBUG
api:
reboot_timeout: 0s
modem:
id: atmodem
rx_pin: GPIO26
tx_pin: GPIO27
baud_rate: 115200
model: SIM7600
apn: internet
pin_code: "0000"
init_at:
- ATI
urc:
name: Modem URC
reboot_timeout: ${modem_reboot_timeout}
on_connect:
logger.log: "modem connected"
on_disconnect:
logger.log: "modem disconnected"
on_sync:
logger.log: "modem synced"
sensor:
- platform: uptime
name: Uptime
update_interval: 30s
- platform: debug
free:
name: Heap Free
loop_time:
name: Loop Time
debug:
update_interval: 30s
button:
- platform: template
name: Modem Enable
on_press:
- modem.enable
- platform: template
name: Modem Disable
on_press:
- modem.disable
- platform: template
name: Modem Send AT
on_press:
- modem.send_at: "AT+CGSN"
interval:
- interval: 20s
then:
- lambda: |-
ESP_LOGI("alive", "uptime %u s enabled=%d connected=%d", (unsigned) (millis() / 1000),
id(atmodem).is_enabled(), id(atmodem).is_connected());
@@ -0,0 +1,26 @@
substitutions:
modem_reboot_timeout: 2min
esphome:
name: modemo
on_boot:
priority: -100
then:
- modem.disable
- modem.enable
- if:
condition:
modem.enabled:
then:
modem.send_at: "AT+CGSN"
else:
modem.enable:
- if:
condition:
not:
modem.connected:
then:
logger.log: modem not connected at boot
packages:
common: !include modem-common.yaml
@@ -0,0 +1,12 @@
substitutions:
modem_reboot_timeout: 0s
esphome:
name: modemw
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
packages:
common: !include modem-common.yaml
@@ -0,0 +1,105 @@
esphome:
name: oi-s3eth
friendly_name: oi-s3eth
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
psram:
mode: octal
speed: 80MHz
logger:
level: DEBUG
hardware_uart: USB_SERIAL_JTAG
ethernet:
type: W5500
clk_pin: GPIO13
mosi_pin: GPIO11
miso_pin: GPIO12
cs_pin: GPIO14
interrupt_pin: GPIO10
reset_pin: GPIO9
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
actions:
- action: fetch
variables:
url: string
then:
- online_image.set_url:
id: img
url: !lambda 'return url;'
update: false
- component.update: img
- action: refetch
then:
- component.update: img
- action: fetch_small
variables:
url: string
then:
- online_image.set_url:
id: img_small
url: !lambda 'return url;'
update: false
- component.update: img_small
- action: refetch_small
then:
- component.update: img_small
http_request:
timeout: 4.5s
verify_ssl: false
display:
image:
- platform: online_image
id: img
url: http://127.0.0.1/none.bmp
format: AUTO
type: RGB565
update_interval: never
on_download_finished:
logger.log:
format: "download finished: img cached=%d"
args: [cached]
on_error:
logger.log: "download error: img"
- platform: online_image
id: img_small
url: http://127.0.0.1/none.bmp
format: AUTO
type: RGB565
buffer_size: 256
update_interval: never
on_download_finished:
logger.log:
format: "download finished: img_small cached=%d"
args: [cached]
on_error:
logger.log: "download error: img_small"
button:
- platform: restart
id: reboot_btn
name: Reboot
debug:
update_interval: 2s
sensor:
- platform: debug
loop_time:
name: Loop Time
free:
name: Heap Free
@@ -0,0 +1,33 @@
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
api:
reboot_timeout: 0s
actions:
- action: tx_all
then:
- script.execute: tx_all
ota:
- platform: esphome
esphome:
on_boot:
priority: -100
then:
- lambda: |-
// A dumper registered at runtime, the way external components do it from setup()
struct ExtDumper : remote_base::RemoteReceiverDumperBase {
bool dump(remote_base::RemoteReceiveData src) override {
ESP_LOGI("rbt", "ext dumper saw %d", (int) src.size());
return false;
}
};
static ExtDumper ext;
id(rx_all).register_dumper(&ext);
ESP_LOGI("rbt", "ext dumper register called");
@@ -0,0 +1,46 @@
script:
- id: tx_all
then:
- logger.log: "tx nec"
- remote_transmitter.transmit_nec: {address: 0x1234, command: 0x78}
- delay: 400ms
- logger.log: "tx samsung"
- remote_transmitter.transmit_samsung: {data: 0xE0E040BF}
- delay: 400ms
- logger.log: "tx sony"
- remote_transmitter.transmit_sony: {data: 0xA90, nbits: 12}
- delay: 400ms
- logger.log: "tx lg"
- remote_transmitter.transmit_lg: {data: 0x20DF10EF, nbits: 32}
- delay: 400ms
- logger.log: "tx jvc"
- remote_transmitter.transmit_jvc: {data: 0xC5E8}
- delay: 400ms
- logger.log: "tx panasonic"
- remote_transmitter.transmit_panasonic: {address: 0x4004, command: 0x0100BCBD}
- delay: 400ms
- logger.log: "tx rc5"
- remote_transmitter.transmit_rc5: {address: 0x1C, command: 0x05}
- delay: 400ms
- logger.log: "tx rc6"
- remote_transmitter.transmit_rc6: {address: 0x00, command: 0x0C}
- delay: 400ms
- logger.log: "tx dish"
- remote_transmitter.transmit_dish: {address: 1, command: 16}
- delay: 400ms
- logger.log: "tx coolix"
- remote_transmitter.transmit_coolix: {first: 0xB23FE4}
- delay: 400ms
- logger.log: "tx pioneer"
- remote_transmitter.transmit_pioneer: {rc_code_1: 0xA556}
- delay: 400ms
- logger.log: "tx samsung36"
- remote_transmitter.transmit_samsung36: {address: 0x0400, command: 0x000E00FF}
- delay: 400ms
- logger.log: "tx magiquest"
- remote_transmitter.transmit_magiquest: {wand_id: 0x01234567}
- delay: 400ms
- logger.log: "tx raw"
- remote_transmitter.transmit_raw: {code: [3000, -1000, 700, -2900, 1500, -400, 2200, -1700]}
- delay: 400ms
- logger.log: "tx done"
@@ -0,0 +1,32 @@
esphome:
name: rb32@V@
esp32:
board: esp32dev
framework:
type: esp-idf
packages:
common: !include ../rb-common.yaml
txscript: !include ../rb-txscript.yaml
remote_transmitter:
pin:
number: GPIO4
allow_other_uses: true
carrier_duty_percent: 100%
rmt_symbols: 64
remote_receiver:
- id: rx_all
pin:
number: GPIO4
allow_other_uses: true
dump: all
rmt_symbols: 192
- id: rx_nec_raw
pin:
number: GPIO4
allow_other_uses: true
dump: [nec, raw]
rmt_symbols: 192
@@ -0,0 +1,69 @@
esphome:
name: rb66@V@
includes:
- ../softtx.hpp
esp8266:
board: d1_mini
packages:
common: !include ../rb-common.yaml
remote_receiver:
- id: rx_all
pin: GPIO5
dump: all
# the receiver leaves its pin an input; drive it so the software sender reaches the pad
interval:
- interval: 1h
startup_delay: 0s
then:
- lambda: pinMode(5, OUTPUT);
script:
- id: tx_all
then:
- lambda: |-
remote_base::RemoteTransmitData d;
using namespace remote_base;
NECProtocol().encode(&d, {0x1234, 0x78, 1});
softtx::send(d, "nec");
- delay: 400ms
- lambda: |-
remote_base::RemoteTransmitData d;
using namespace remote_base;
SamsungProtocol().encode(&d, {0xE0E040BF, 32});
softtx::send(d, "samsung");
- delay: 400ms
- lambda: |-
remote_base::RemoteTransmitData d;
using namespace remote_base;
SonyProtocol().encode(&d, {0xA90, 12});
softtx::send(d, "sony");
- delay: 400ms
- lambda: |-
remote_base::RemoteTransmitData d;
using namespace remote_base;
LGProtocol().encode(&d, {0x20DF10EF, 32});
softtx::send(d, "lg");
- delay: 400ms
- lambda: |-
remote_base::RemoteTransmitData d;
using namespace remote_base;
JVCProtocol().encode(&d, {0xC5E8});
softtx::send(d, "jvc");
- delay: 400ms
- lambda: |-
remote_base::RemoteTransmitData d;
using namespace remote_base;
RC5Protocol().encode(&d, {0x1C, 0x05});
softtx::send(d, "rc5");
- delay: 400ms
- lambda: |-
remote_base::RemoteTransmitData d;
using namespace remote_base;
d.item(3000, 1000); d.item(700, 2900); d.item(1500, 400); d.mark(2200);
softtx::send(d, "raw");
- delay: 400ms
- logger.log: "tx done"
@@ -0,0 +1,15 @@
#pragma once
// Bit-bang a frame with interrupts left on, so a receiver on the same pin sees every edge.
// The ESP8266 remote_transmitter disables interrupts while it sends, which hides the edges.
#include "esphome/components/remote_base/remote_base.h"
namespace softtx {
inline void send(const esphome::remote_base::RemoteTransmitData &d, const char *name) {
ESP_LOGI("rbt", "tx %s", name);
for (int32_t v : d.get_data()) {
digitalWrite(5, v > 0 ? HIGH : LOW);
delayMicroseconds(v > 0 ? v : -v);
}
digitalWrite(5, LOW);
}
} // namespace softtx
@@ -0,0 +1,15 @@
esphome:
name: sy8266
esp8266:
board: d1_mini
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
packages:
common: !include ../sy-common.yaml
store_yaml:
@@ -0,0 +1,16 @@
esphome:
name: sy8266s
esp8266:
board: d1_mini
api:
encryption:
key: !secret api_key
reboot_timeout: 0s
packages:
common: !include ../sy-common.yaml
store_yaml:
include_secrets: true
@@ -0,0 +1,24 @@
# shared package for the store_yaml hardware test
substitutions:
greeting: "hello from the package"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
logger:
level: DEBUG
ota:
- platform: esphome
password: recoverme123
sensor:
- platform: template
name: Package Sensor
lambda: return 42.0;
update_interval: 60s
packages:
extra: !include sy-extra.yaml
@@ -0,0 +1,6 @@
# second level include
text_sensor:
- platform: template
name: Greeting
lambda: return std::string("${greeting}");
update_interval: 60s
@@ -0,0 +1,35 @@
esp32:
board: esp32dev
framework:
type: esp-idf
esphome:
includes:
- sim.hpp
logger:
level: VERBOSE
logs:
component: DEBUG
api.service: DEBUG
api.connection: DEBUG
scheduler: DEBUG
sensor: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
api:
reboot_timeout: 0s
debug:
update_interval: 10s
sensor:
- platform: debug
free:
name: Heap Free
loop_time:
name: Loop Time
Loaded 100 of 1825 files, more files were not shown because too many files have changed in this diff. Show more