Compare commits

...
367 Commits
Author SHA1 Message Date
1dc4006bfc [usb_audio] Add USB Audio Class (UAC) host support (#11728)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-18 17:44:59 -10:00
Clyde StubbsandClaude Opus 5.5 4d4ba5a1bc [core] Move CONF_MAX_PACKET_SIZE to components/const (#21102)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:37:35 -10:00
Jesse Hills 0e8e45c528 [mipi_spi] Fix GC9107 native size and add M5Stack AtomS3 model (#21087) 2026-10-19 16:35:15 +13:00
J. Nick Kostonandpre-commit-ci-lite[bot] 28e43235fa [gpio_expander][pcf8574][mpr121][ch422g][ch423][pcm5122][weikai] Move the GPIO expanders and pin providers onto the shared pin helpers (#20868)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-18 16:27:01 -10:00
J. Nick Koston ded5ad6cb1 [wifi] Build the wifi actions and conditions with the apply helpers (#20842) 2026-10-18 15:23:31 -10:00
J. Nick Koston ab656acbf1 [api] Build api.connected with register_apply_condition (#20836) 2026-10-18 15:23:19 -10:00
J. Nick Koston 52390ebbe4 [ethernet] Build the ethernet automations with the apply helpers (#20845) 2026-10-18 15:22:23 -10:00
J. Nick Koston 8d6b40f735 [components] Use Component::status_comm_result for communication failures (#20864) 2026-10-18 15:19:53 -10:00
J. Nick Koston 257a636099 [rx8025t][rx8130][rx8111][ufm01] Use the shared BCD helpers (#20861) 2026-10-18 15:17:24 -10:00
J. Nick Koston b53b3968b4 [light] Build light.toggle with register_apply_action (#20833) 2026-10-18 15:10:02 -10:00
J. Nick Koston 73ba726838 [ci] Extend the hardware-test skill with the flows learned since it was added (#21011) 2026-10-18 15:09:01 -10:00
J. Nick Koston 490d4723ca [ota] Log why the upload switched to UDP (#20996) 2026-10-18 15:08:46 -10:00
Jesse Hills a6dee800d1 Merge branch 'beta' into dev 2026-10-19 14:03:21 +13:00
Jesse Hills 2001dd2be8 Merge pull request #21097 from esphome/bump-2026.10.0b5
2026.10.0b5
2026-10-19 14:03:02 +13:00
J. Nick Koston c793ab30a7 [core][gpio_expander] Add a shared expander pin class and pin codegen helpers (#20866) 2026-10-18 14:48:35 -10:00
Clyde StubbsandClaude Opus 5.5 1753c45e53 [core] Move CONF_PROVIDER and CONF_REMOTE_ID to components/const (#21096)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:47:37 -10:00
J. Nick Koston 1767e4b0be [sensor] Add SensorWithDedup for child sensors that skip repeated values (#20925) 2026-10-18 14:47:19 -10:00
J. Nick Koston 5575dd8928 [core] Add final_validate.get_declaring_config (#20912) 2026-10-18 14:46:57 -10:00
J. Nick Koston 704dd4b445 [components] Report communication failures with status_comm_result in the remaining components (#21079) 2026-10-19 00:17:57 +00:00
e6ad995c8e [i2s_audio] Add ESP32-S31 support (#20115)
Co-authored-by: cosmicDustOfLightLength <cosmicDustOfLightLength@example.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-19 00:07:52 +00:00
J. Nick Koston 84ce39e493 [components] Use the codegen helpers in the remaining components (#21078) 2026-10-18 14:05:37 -10:00
J. Nick Koston d9242520e3 [components] Decode bytes with the shared helpers (#21080) 2026-10-18 20:00:48 -04:00
J. Nick Koston 03bf2de7df [components] Decode BLE fields with the shared helpers (#21082) 2026-10-18 19:59:19 -04:00
J. Nick Koston a70d75e469 [components] Read UART bytes with read_available and discard_available (#21084) 2026-10-18 13:59:09 -10:00
J. Nick Koston 2dcb97774d [tests] Add an esp32-s31-idf base file and shared i2c and i2s_audio packages (#21086) 2026-10-18 19:54:48 -04:00
Jacopo Labardi be9741c6de [const] Share CONF_VALUE_TYPE across components (#21062) 2026-10-18 13:02:22 -10:00
Jesse Hills a6a4b49e75 Bump version to 2026.10.0b5 2026-10-19 11:29:05 +13:00
Bonne EgglestonandJ. Nick Koston 649f2c91f5 [api] Poll the outgoing dial every loop (#21085)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-19 11:29:02 +13:00
Bonne Eggleston ffa20abe96 [api] Poll the outgoing dial every loop on deep sleep builds (#21077) 2026-10-19 11:29:02 +13:00
J. Nick Koston f979ddcf0d [bl0906] Read stale receive bytes instead of spinning on flush() (#21035) 2026-10-19 11:29:02 +13:00
J. Nick Koston 26064f1415 [uart_tcp] Expose the connection entities as platforms (#21008) 2026-10-19 11:29:02 +13:00
ab-tools b1c138c612 [audio] Compute the remaining AudioStreamInfo conversions in 64 bits (#21092) 2026-10-18 12:12:50 -10:00
ab-tools 9526233821 [audio] Compute frames_to_microseconds() in 64 bits (#21089) 2026-10-18 11:39:37 -10:00
J. Nick Koston c50ee215f2 [tcp_uart] Expose the connection entities as platforms (#21009) 2026-10-19 10:36:23 +13:00
Jonathan Swoboda 0fbba03522 [debug] Log uptime in dump_config (#20989) 2026-10-19 10:33:13 +13:00
Jonathan Swoboda 7ccaedfb9e [esp32] Bump recommended ESP-IDF to 5.5.6 (#20987) 2026-10-19 10:33:13 +13:00
esphome[bot] 0173127239 Bump bundled esphome-device-builder to 1.24.0 (#20851)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-19 10:33:13 +13:00
Clyde Stubbs 235c614e68 [epaper_spi] Ensure test card gets drawn (#20804) 2026-10-19 10:33:13 +13:00
Jonathan Swoboda 79823c89ab [mipi_rgb] Rotate pure RGB panels in software (#20757) 2026-10-19 10:33:13 +13:00
J. Nick Koston e09cb64756 [bluetooth_connection] Value-initialize the connection parameters for ESP-IDF 6.1 (#20774) 2026-10-19 10:33:13 +13:00
J. Nick Koston bd5b3fa641 [esp32_ble_client] Value-initialize the connection parameters for ESP-IDF 6.1 (#20776) 2026-10-19 10:33:13 +13:00
Kevin AhrendtandJ. Nick Koston f464994f85 [api][sendspin] Add the Sendspin client id to the capabilities message (#20775)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-19 10:33:13 +13:00
esphome[bot] 4552dbad7a Bump aioesphomeapi from 46.11.0 to 46.12.0 (#20781)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-19 10:33:13 +13:00
J. Nick Koston ad8cd0dd3e [noise] Initialize the protocol id's reserved field for ESP-IDF 6.1 (#20779) 2026-10-19 10:33:13 +13:00
puddlyandJ. Nick Koston dd2582bdfd [usb_uart] Expose interface_class and port_number (#20659)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-19 10:33:13 +13:00
J. Nick Koston ba575600dc [mqtt] Match subscription topics in a loop instead of recursing per character (#20767) 2026-10-19 10:33:13 +13:00
esphome[bot] cd6c0dd402 Bump aioesphomeapi from 46.10.0 to 46.11.0 (#20766)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-19 10:33:13 +13:00
4645d34e1a [web_server_idf] Keep active SSE sessions out of the LRU purge (#17383)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-19 10:33:13 +13:00
dff1422e36 [mdns] Advertise the connected interface in the network TXT record (#20673)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-19 10:33:13 +13:00
Bonne EgglestonandJ. Nick Koston 512e43793b [api] Poll the outgoing dial every loop (#21085)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-18 20:47:02 +00:00
603f16623f [usb_host] Size the FIFO split on ESP32-S2/S3 from periodic packet requests (#21081)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-18 20:44:05 +00:00
218e696654 [globals] Add RTC memory support (#13954)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-18 10:40:55 -10:00
Clyde Stubbs 3e68e0f5e1 [epaper_spi] Add the UC8151D controller and the Waveshare 2.13" B V3 (#21022) 2026-10-19 07:40:16 +11:00
dd456ea0b2 [homeassistant] Add the light platform (#11354)
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-18 10:02:31 -10:00
Bonne Eggleston e54d66e620 [api] Poll the outgoing dial every loop on deep sleep builds (#21077) 2026-10-18 09:36:35 -10:00
J. Nick Koston a3bb17d2c6 [components] Create BLE and modbus devices with the new helpers (#21026) 2026-10-18 15:09:58 -04:00
71c0f46e35 [bme68x_bsec2] disable loop (#20994)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-18 08:33:17 -10:00
J. Nick Koston a153fcc8ee [bl0906][kamstrup_kmp][ufm01] Drop stale receive bytes with discard_available (#21071) 2026-10-18 14:30:10 -04:00
85724892cf [light] Add power estimation and current limiting for addressable LED strips (#15348)
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-18 08:08:31 -10:00
J. Nick Koston deea006b39 [components] Set referenced components with cg.add_variable and register_parented (a-l) (#21021) 2026-10-18 17:43:05 +00:00
J. Nick Kostonandpre-commit-ci-lite[bot] 619728b686 [components] Drop stale UART receive bytes with discard_available (#21030)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-18 07:23:06 -10:00
J. Nick Koston c35d98b622 [core] Add progmem_string_table_find() (#20790) 2026-10-18 07:19:14 -10:00
J. Nick Koston b2d6c78499 [light] Add LightState::set_external_state() for outputs that mirror a light elsewhere (#20787) 2026-10-18 07:18:47 -10:00
J. Nick KostonandJonathan Swoboda a801e414a0 [components] Set referenced components with cg.add_variable and register_parented (m-z) (#21023)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-18 13:32:06 +00:00
J. Nick Koston 22a36de4ae [components] Decode little-endian I2C values with the new helpers (#20939) 2026-10-18 09:20:58 -04:00
J. Nick Koston eca95348ac [components] Assemble 24-bit readings with encode_uint24 (#21016) 2026-10-18 09:16:50 -04:00
J. Nick KostonandJonathan Swoboda ed9616d512 [components] Set pins with cg.add_pin (#21019)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-18 09:10:22 -04:00
J. Nick Koston 0cd1ae01f1 [components] Use cv.uint8_t and cv.uint16_t for full byte ranges (#21015) 2026-10-18 08:52:14 -04:00
J. Nick Koston 2a416fbce6 [core] Document that hub entities go in platforms in AGENTS.md (#21003) 2026-10-18 08:45:21 -04:00
J. Nick Koston 270d989481 [core] Add cg.add_pin and cg.add_variable for optional pins and ids (#21018) 2026-10-18 08:41:33 -04:00
J. Nick Koston 3665bcc4e3 [ble_device_base][ble_client][modbus] Add new_ble_device_component, new_ble_node_component and new_modbus_client_component (#21025) 2026-10-18 08:40:35 -04:00
J. Nick Koston 0922df9bb3 [uart] Add discard_available to drop stale receive bytes (#21029) 2026-10-18 08:39:02 -04:00
J. Nick Koston 54a28b3e3b [bl0906] Read stale receive bytes instead of spinning on flush() (#21035) 2026-10-18 08:37:48 -04:00
Michele PorelliandJ. Nick Koston 032a2b7200 [core] Let entities choose flash or RTC storage for their preferences (#21017)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-18 06:58:40 +00:00
J. Nick Koston 0bb78186f2 [components] Set display lambdas with display.add_writer (#21028) 2026-10-17 18:34:38 -10:00
J. Nick Koston 2fe1333cba [display] Add add_writer for display lambdas (#21027) 2026-10-17 18:34:38 -10:00
J. Nick Koston 265754c467 [components] Drop get_setup_priority overrides that return the default (#21014) 2026-10-17 17:43:16 -10:00
6580f4f314 [micro_wake_word] Allow configurable capture buffer duration (#20942)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 16:43:07 -10:00
J. Nick Koston 5ad63e3bb6 [http_request] Only treat a URL as secure when its scheme is https (#20867) 2026-10-18 02:38:07 +00:00
J. Nick Koston a8ab035b59 [lock] Filter on_lock and on_unlock with when (#20858) 2026-10-17 16:33:05 -10:00
J. Nick Koston 968d57c56b [core][time] Add BCD helpers and a shared RTC BCD calendar codec (#20860) 2026-10-17 16:32:13 -10:00
J. Nick Koston 1307faef06 [core] Add Component::status_comm_result (#20863) 2026-10-17 16:31:17 -10:00
ee117886b9 [vevor_dtx] Add Vevor weather station component (#17224)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 15:42:55 -10:00
1dbad7b951 [airton] Add support to climate for Airton IR A/C units (#7753)
Co-authored-by: Arjan <44190435+vingerha@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-18 01:37:58 +00:00
J. Nick Koston 5df6c65cc2 [api] Test api.connected without options and with a lambda (#20835) 2026-10-17 15:23:35 -10:00
J. Nick Koston af5efabdc6 [light] Test light.toggle with a transition length (#20832) 2026-10-17 15:23:22 -10:00
J. Nick Koston a2754f655c [dfrobot_sen0609][http_request] Use ESPHOME_strstr_P (#20825) 2026-10-17 15:23:06 -10:00
J. Nick Koston 34e6d3ec9c [pylontech] Restore the CONF_ID import (#21013) 2026-10-17 15:21:36 -10:00
J. Nick Koston 21362f8c92 [ethernet] Test the ethernet.connected condition (#20844) 2026-10-17 15:21:18 -10:00
J. Nick Koston 626d84bc64 [sensor][number] Test in_range with every bound combination (#20847) 2026-10-17 15:20:52 -10:00
J. Nick Koston 68fa7b9847 [wifi] Test the wifi.connected and wifi.enabled conditions (#20841) 2026-10-17 15:20:00 -10:00
J. Nick Koston 32dc3b8535 [http_request] Test ota.http_request.flash with every field (#20829) 2026-10-17 15:19:24 -10:00
J. Nick Koston 74bcec34ad [i2c] Add I2CDevice::update_register for register read-modify-write (#20854) 2026-10-17 15:18:36 -10:00
J. Nick Koston c1687aba4c [components] Let core report communication failures in dump_config (5/5) (#20874) 2026-10-17 15:16:21 -10:00
J. Nick Koston ec642f928f [i2c] Add little-endian and signed byte decoding helpers (#20938) 2026-10-17 15:15:36 -10:00
J. Nick Koston ec9a207f42 [components] Create UART devices with new_uart_component (#20919) 2026-10-17 15:15:21 -10:00
J. Nick Koston 3b7847d951 [components] Create SPI devices with new_spi_component (#20920) 2026-10-17 21:13:49 -04:00
J. Nick Koston 1197f22f00 [fusb302b] Expose the sensors as platforms (#21004) 2026-10-17 15:09:49 -10:00
J. Nick Koston e034662dc6 [uart_tcp] Expose the connection entities as platforms (#21008) 2026-10-17 15:09:32 -10:00
J. Nick Koston 42e849967a [bq76972] Expose the sensors as a sensor platform (#21006) 2026-10-17 15:09:14 -10:00
J. Nick Koston e6f1ed8d51 [tcp_uart] Expose the connection entities as platforms (#21009) 2026-10-17 15:07:42 -10:00
Igor StadnykandJ. Nick Koston bb9067acd7 [tfluna] Recover from transient runtime reconfiguration failures (#20992)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 15:07:16 -10:00
Michele Porelli 928cf7d696 [libretiny] Emulate rtc preference storage on the Beken BK7231T, BK7231N and BK7238 with flash and the reset reason (#21007) 2026-10-17 14:59:40 -10:00
89afb111ad [mt6701] Add MT6701 (MagnTek magnetic encoder) component (#17643)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 14:06:46 -10:00
Brandon HarveyandJ. Nick Koston d1ec52e95a [sen6x] Add device status binary sensors (#18784)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 23:34:15 +00:00
Bonne Egglestonandclydebarrow 818d43f742 [epaper_spi] Run the loop continuously between the steps of an update (#20554)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-10-18 10:25:36 +11:00
1a28a9220b [uart] Add a binary sensor that fires on a byte sequence (#14032)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 22:35:46 +00:00
Brandon HarveyandJ. Nick Koston 022733c46b [sen6x] Add PM number concentration sensors (#18782)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 12:16:45 -10:00
0f6fc503bd [sen6x] Add temperature compensation, acceleration and startup delay (#18781)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 11:01:14 -10:00
Bonne Egglestonandclydebarrow ae62960193 [epaper_spi] Add the UC8279 controller and the Xteink X4 Pro (#20545)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-10-18 07:51:07 +11:00
325c747dcd [sen6x] Add CO2 configuration options (#18780)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 10:41:13 -10:00
Brandon HarveyandJ. Nick Koston 5759c6b8e6 [deep_sleep] Honor nested wakeup_pin_mode on ESP32 single-pin configs (#16618)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 10:25:40 -10:00
c9ba6c355c [hoermann_hcp] Add an actuator error binary sensor (#20299)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 10:00:31 -10:00
Brandon HarveyandJ. Nick Koston d02da3c8de [wifi] Support hidden option in single-network shorthand (#18157)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 09:54:55 -10:00
Bascht74andJ. Nick Koston a576092f25 [remote_base] Send the 72-bit Jarolift TDEF group frame with transmit_keeloq (#20927)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 09:52:01 -10:00
Simonas Kazlauskas 792d650fb2 [remote_base] Add a protocol for 433MHz A-OK remotes (#20991) 2026-10-17 09:44:25 -10:00
f72857151a [pylontech] Add individual cell voltage and temperature sensors (#16852)
Co-authored-by: Michael <michael_e@gmx-topmail.com>
Co-authored-by: ritschys <117527311+ritschys@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-17 19:28:52 +00:00
5f8f075e76 [zephyr_coredump] Store a coredump in flash on nRF52 and print it to the next log reader (#13122)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 19:22:28 +00:00
Matias FernandezandJ. Nick Koston c2c8cb5eb3 [climate_ir_lg] Fix swing command to not include climate state (#17417)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 09:12:35 -10:00
J. Nick Koston 5c2b05d0eb [ci] Run the C++ unit tests on macOS without a sanitizer whose runtime aborts (#21000) 2026-10-17 09:00:51 -10:00
Igor StadnykandJ. Nick Koston e110613ef1 [core] Construct App once in the C++ unit test main (#21001)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 19:00:37 +00:00
luar123 4324b7d084 [zigbee] bump esp-zigbee-sdk to 2.0.5 (#20998) 2026-10-17 08:58:49 -10:00
37d6140193 [epaper_spi] Fix partial refresh and port waveshare 2.90inv2 (#18959)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: luar123 <49960470+luar123@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 08:55:37 -10:00
4f192130f8 [water_heater] Hand the component to the core team (#20999)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 18:51:06 +00:00
Hasan Khalilandpre-commit-ci-lite[bot] 4ef17cebdb [api] Pre-reserve Home Assistant state subscriptions vector (#20986)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-17 08:01:01 -10:00
35276a38bc [rtsp_server] Add an RTSP server that streams the esp32_camera picture (#17990)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 07:18:52 -10:00
J. Nick Koston 635df9c6ad [socket] Share the allow-list reject log and config lines through Ipv4Allow (#20983) 2026-10-17 06:51:29 -10:00
Jonathan Swoboda d16d8c111e [debug] Log uptime in dump_config (#20989) 2026-10-17 06:50:54 -10:00
Jonathan Swoboda 1ead61f11f [esp32] Remove trailing comment from ECDSA test signing key (#20988) 2026-10-17 06:50:11 -10:00
J. Nick Koston bd06d37d1a [components] Check I2C chip IDs with check_chip_id (#20943) 2026-10-17 06:49:46 -10:00
Jonathan Swoboda 7c6fac91ff [esp32] Bump recommended ESP-IDF to 5.5.6 (#20987) 2026-10-17 06:49:15 -10:00
J. Nick Koston 052a6096e6 [components] Log hex dumps with the ESP_LOG*_HEX macros (#20932) 2026-10-17 16:46:16 +00:00
J. Nick Koston 25b684f762 [pylontech] Parse each line as it arrives with one fixed buffer (#20959) 2026-10-17 06:44:41 -10:00
J. Nick Koston 62f8cc3436 [uart] Add read_available helpers to drain available bytes in chunks (#20947) 2026-10-17 06:41:51 -10:00
J. Nick Koston 34f9152d61 [i2c] Add write_registers for constant register tables (#20945) 2026-10-17 06:41:20 -10:00
luar123 73af6b0500 [core][scheduler] run process_to_add after component phase (#20993) 2026-10-17 06:40:39 -10:00
J. Nick Koston a384aae5b1 [button] Add factory reset, restart and query button schema presets (#20915) 2026-10-17 06:38:45 -10:00
J. Nick Koston 905267a9c3 [esp32_ble] Build ble.enabled, ble.enable and ble.disable with the apply helpers (#20839) 2026-10-17 06:38:25 -10:00
J. Nick Koston d08c845c65 [components] Create I2C devices with new_i2c_component (#20909) 2026-10-17 06:36:49 -10:00
J. Nick Koston 91dfb92c60 [esp32_ble] Test ble.enabled, ble.enable and ble.disable (#20838) 2026-10-17 09:22:29 -04:00
J. Nick Koston cef9418a0f [i2c][uart][spi] Add new_i2c_component, new_uart_component and new_spi_component (#20908) 2026-10-17 09:20:28 -04:00
J. Nick Koston c0a693dfce [components] Use xor_bytes and sum16_bytes for checksums (#20922) 2026-10-17 09:18:24 -04:00
Clyde Stubbs 1b4721d445 [core] Script improvements including choose (#20547) 2026-10-17 21:23:12 +11:00
f8d31c3e54 [dfrobot_sen0465] Add DFRobot SEN0465-SEN0476 electrochemical gas sensors (#20880)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-16 22:47:50 -10:00
eafcca8274 [mics_5524] Add datasheet model, diagnostics and advanced wiring (2/2) (#20521)
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-16 22:47:28 -10:00
Remco van Essen 06b585d373 [i2s_audio] Fix speaker restart loop with full_duplex (#20984) 2026-10-16 22:05:21 -10:00
HugoPeters1024andJ. Nick Koston 885f2fe5d8 [kamstrup_kmp] Fix Kamstrup KMP reads with echoing optical heads (#20897)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-16 22:04:01 -10:00
J. Nick Koston 4e27b2fc12 [components] Collect UART lines with LineReader (#20956) 2026-10-16 19:54:11 -10:00
J. Nick Koston 375900bfcd [core] Add LineReader to collect lines in a fixed buffer (#20952) 2026-10-16 19:54:11 -10:00
Zhaoqi Xu 6d664d3fef [web_server] Report numbers without a state as NA (#20973) 2026-10-16 19:15:04 -10:00
J. Nick Koston 4dc2b5911b [core] Move CONF_DRIVE_STRENGTH to the shared component constants (#20975) 2026-10-16 18:21:04 -10:00
tomaszduda23 687466b6cd [logger][usb_cdc_acm][core] Add CDC uart to NRF52 (#11634) 2026-10-16 18:14:51 -10:00
J. Nick Koston dcdc042a4b [host] Define a no-op InterruptLock (#20974) 2026-10-16 17:53:33 -10:00
d2a1f7ee31 [zephyr] Fix preference log levels and report settings read/save errors (#20810)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 03:02:54 +00:00
J. Nick Koston a5a883fd19 [core] Add xor_bytes and sum16_bytes checksum helpers (#20921) 2026-10-16 15:57:00 -10:00
J. Nick Koston 7671d4410c [i2c] Add check_chip_id helper (#20941) 2026-10-16 15:55:53 -10:00
J. Nick Koston 298bd5ec72 [core] Add ESP_LOGV_HEX, ESP_LOGVV_HEX and ESP_LOGD_HEX (#20931) 2026-10-16 15:55:28 -10:00
J. Nick Koston b8afcdfb3c [i2c] Write 16-bit registers without a heap allocation (#20907) 2026-10-16 15:54:53 -10:00
luar123andJ. Nick Koston 9031df757b [core][scheduler] add missing process_to_add call (#20961)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 01:33:29 +00:00
J. Nick Koston 107bebebad [components] Use sign_extend for raw sensor readings (#20934) 2026-10-16 15:24:45 -10:00
J. Nick Koston 1147b024d6 [core] Add sign_extend helper (#20933) 2026-10-16 21:17:59 -04:00
J. Nick Koston 36ba2f6106 [components] Build enum option dicts with options() (#20911) 2026-10-16 14:56:34 -10:00
J. Nick Koston bb0eabf241 [core] Add options() to C++ enums (#20910) 2026-10-16 14:56:34 -10:00
Frédéric MetrichandClaude d9969dd533 [teleinfo] Skip sensor values that are not numbers instead of publishing 0 (#20960)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 14:55:08 -10:00
Clyde Stubbs 51ae3bb642 [ci] Rename merge conflict label to has-conflicts (#20969) 2026-10-16 14:31:06 -10:00
b3ef5cf2f3 [online_image] Report an error when a download can never complete (#20594)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: guillempages <guillempages@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-16 23:58:12 +00:00
Hasan Khalil b3f5789dc6 [mipi_rgb] direct-to-PSRAM fast rotated blitting (#20905) 2026-10-17 10:39:44 +11:00
J. Nick Koston 1ddacb4931 [tests] Set i2c_id in the component tests that clash with tca9548a (#20963) 2026-10-16 13:24:34 -10:00
Igor StadnykandJ. Nick Koston 10d3ced897 [core] Parse unit values as one decimal so listed floats match, fixing at581x power consumption (#20889)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-16 10:11:56 -10:00
Bascht74 6fa7b87e6a [lp5562] Add the current per channel and logarithmic dimming (#20903) 2026-10-16 10:04:45 -10:00
c0b7c5e590 [dfrobot_sen0395] Forward trigger arguments in settings actions (#20885)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-16 10:03:07 -10:00
dependabot[bot] 57dd09018e Bump yamlrocks from 0.6.1 to 0.7.0 (#20904)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-16 10:00:58 -10:00
Clyde Stubbs 04aa59563d [display] Drop the color order from draw_pixels_at() and ColorUtil (#20884) 2026-10-17 06:45:03 +11:00
9440012413 [mics_5524] Add MiCS-5524 gas sensor platform (1/2) (#19721)
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-16 09:14:06 -10:00
Bascht74 38db8c1554 [ci] Fail a hung integration test instead of waiting for the job limit (#20902) 2026-10-16 09:00:55 -10:00
ab0c32c2aa [haier_water_heater] Initial water-heater implementation tested with Haier VH3W (#20765)
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-16 08:50:13 -10:00
cd6a34401b [web_server] Do not match internal entities by URL unless include_internal is set (#20883)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-16 08:22:46 -10:00
Igor StadnykandJ. Nick Koston 5ab92ed363 [ld2450] Handle footer bytes in zone query responses (#20852)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-16 08:15:17 -10:00
1368ce4342 [dfrobot_sen0609] Add restart, factory reset and query buttons (#20877)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-16 17:31:58 +00:00
Remco van Essen dc0d79635b [ld2410][ld2412] Move CONF_QUERY_PARAMS to the shared constants (#20878) 2026-10-16 07:12:49 -10:00
J. Nick Koston 1ff6b29b0d [components] Let core report communication failures in dump_config (4/5) (#20873) 2026-10-16 08:41:52 -04:00
J. Nick Koston 49caeae780 [components] Let core report communication failures in dump_config (3/5) (#20872) 2026-10-16 08:40:54 -04:00
J. Nick Koston e1a6c615b1 [core] Document passing the failure reason to mark_failed in AGENTS.md (#20879) 2026-10-16 08:40:06 -04:00
J. Nick Koston e95e56f100 [components] Let core report communication failures in dump_config (2/5) (#20871) 2026-10-16 08:39:20 -04:00
J. Nick Koston 126dc8b0b2 [components] Let core report communication failures in dump_config (1/5) (#20870) 2026-10-16 08:38:55 -04:00
esphome[bot] 3298988f5d Bump bundled esphome-device-builder to 1.24.0 (#20851)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-15 23:11:26 -10:00
802c2633ed [image] Draw rows in the outer loop and hand opaque RGB565 and RGB blocks to the display (#20653)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:38:58 +11:00
51b1fdcc8b [i2s_audio] Add full duplex support (#19959)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-15 21:35:46 -10:00
6555da2906 [display] Fall back per pixel when a bulk blit does not match the panel (#20652)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 16:57:11 +11:00
J. Nick Koston 6dbd15b903 [pca9505] Validate interrupt_pin with the shared expander validator (#20853) 2026-10-15 15:02:26 -10:00
Jonathan Swoboda 0399379c0d [lp5562] Pin the test fixture to the shared I2C bus (#20856) 2026-10-16 00:56:40 +00:00
Kevin Ahrendt 6817960365 [audio] Deprecate AudioSourceTransferBuffer (#20801) 2026-10-15 13:38:03 -10:00
Kevin Ahrendt cfdaf958e9 [audio][i2s_audio] Fix 16-bit mono playback at 44.1 kHz on the original ESP32 (#20806) 2026-10-15 13:32:35 -10:00
1e89abdb13 [http_request] Allow a per-request timeout (#20821)
Co-authored-by: p1ngb4ck <kleinecke.oliver@googlemail.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-15 13:17:46 -10:00
J. Nick Koston c838d4826e [core] Add ESPHOME_strstr_P (#20811) 2026-10-15 23:09:02 +00:00
J. Nick Koston 69215deb58 [core] Make the YAML load listener hook public (#20805) 2026-10-15 12:54:34 -10:00
J. Nick Koston 5b234bcc20 [wifi] Leave the cooldown from the loop after a credential save, not from the caller's context (#20802) 2026-10-15 12:53:51 -10:00
Panoplianandclydebarrow c101b3c101 [mipi_spi] Honour show_test_card when pages or a lambda are set (#20807)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-10-15 22:38:28 +00:00
Anton ViktorovandJ. Nick Koston c0d8a0dfe9 [as734x] [3/3] Add illuminance, photopic irradiance, PAR, PPFD, colour temperature and RGB (#18012)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-15 11:38:15 -10:00
J. Nick Koston 27c6b7caf5 [ci] Add hardware-test agent skill (#20817) 2026-10-15 21:36:36 +00:00
699e3bd037 [usb_uart] Runtime modem pin control for USB and host (#20330)
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-15 11:03:27 -10:00
Anton ViktorovandJ. Nick Koston a58ce2d641 [as734x] [2/3] Add basic counts, calibration and saturation level (#18011)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-15 10:48:18 -10:00
Remco van EssenandClaude Opus 5.5 27d8d49b17 [dfrobot_sen0609] Add number, switch and select settings (#20813)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 10:41:20 -10:00
Remco van Essen 304627efd3 [ld2410][ld2412] Move CONF_OUT_PIN_LEVEL to the shared constants (#20820) 2026-10-15 10:34:00 -10:00
7ed06e0ada [as734x] [1/3] Add AS7341/AS7343/TCS3448 spectral sensor hub with counts sensors (#17038)
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-15 10:19:56 -10:00
dependabot[bot] 7e564c12dd Bump py7zr from 1.1.3 to 1.1.4 (#20816)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-15 09:58:07 -10:00
dependabot[bot] 6f6fdd0f8a Bump actions/create-github-app-token from 3.2.0 to 3.3.0 (#20815)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-15 09:55:47 -10:00
45ea5f4220 [nrf52] nrf52840dongle support (#19009)
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-15 09:03:53 -10:00
techdregsandJ. Nick Koston 108ed67d31 [rx8111] Add basic RX8111CE real-time clock support (#20785)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-15 08:30:25 -10:00
Remco van EssenandClaude Opus 5.5 6356458081 [dfrobot_sen0609] Add DFRobot SEN0609/SEN0610 radar (#20809)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:27:57 +00:00
Panoplian bf67b49104 [i2c] Let host unit tests with I2C build on macOS (#20738) 2026-10-15 07:47:52 -10:00
Panoplian 1947c3d355 [improv_ble] Start Improv again after a session has stopped (#20808) 2026-10-15 07:47:14 -10:00
J. Nick Koston cf4e69273c [shelly_dimmer] Keep the STM32 device table in flash (#20643) 2026-10-15 07:28:55 -10:00
J. Nick Koston c433e96734 [homeassistant] Share the Python list parser between the platforms (#20793) 2026-10-15 07:28:27 -10:00
J. Nick Koston 93dc80d895 [esp8266] Refuse two components that drive hardware timer 1 (#20794) 2026-10-15 07:28:07 -10:00
J. Nick Koston 7f342054fe [wifi] Retry a failed fallback AP once per ap_timeout and start the captive portal once (#20784) 2026-10-15 07:27:19 -10:00
Jeroenandjeroen85 d924d2daf7 [core] Extend fast value formatting to four decimal places (#20800)
Co-authored-by: jeroen85 <26403565+jeroen85@users.noreply.github.com>
2026-10-15 07:15:11 -10:00
Clyde Stubbs 49b12daa58 [epaper_spi] Ensure test card gets drawn (#20804) 2026-10-15 10:55:19 +00:00
Clyde Stubbs d3faafc578 [display] Always treat 888 pixel blocks as b,g,r in memory (#20799) 2026-10-15 20:49:50 +11:00
Remco van Essen 7b08e738d3 [fusb302b] Add sensors, contract text sensor and automations (#20796) 2026-10-14 22:13:58 -10:00
J. Nick Koston 62e3c25c83 [lvgl] Fix the menu test after lambdas gained a line reset (#20798) 2026-10-14 21:50:47 -10:00
6a50f5c166 [apc_proteous] Cover component for APC Proteous gate motors (#13403)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-15 17:46:40 +11:00
Clyde Stubbs bb0837d814 [lvgl] Implement menu widget (#18022) 2026-10-15 17:44:23 +11:00
Bonne Egglestonandclydebarrow ecd5980c5d [epaper_spi] Pass bits per pixel and plane count to the base constructor (#20683)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-10-15 17:41:37 +11:00
8d963bde5b [openthread] Add ESP32 OpenThread Border Router support (#18104)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 20:02:33 -10:00
J. Nick Koston ea6d5499b3 [lsm6ds] Add the LSM6DSO and match all chips to their datasheets (#20733) 2026-10-14 19:24:00 -10:00
41e82b0295 [hdmi_cec] Add HDMI-CEC bus component (part 1: bus core) (#9189)
Co-authored-by: Stéphane Lepin <stephane.lepin@gmail.com>
Co-authored-by: Stéphane Lepin <Palakis@users.noreply.github.com>
Co-authored-by: Niko <nikokirste@hotmail.de>
Co-authored-by: Minims <github@minims.fr>
Co-authored-by: Nicholas Pace <segfly@users.noreply.github.com>
Co-authored-by: Nicholas Pace <3300476+segfly@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-15 04:16:09 +00:00
J. Nick Koston 39705a65e3 [bmi270] Keep the range name and scale tables in flash (#20715) 2026-10-14 17:44:46 -10:00
J. Nick Koston a522553e82 [lsm6ds] Keep the range and chip names in flash (#20718) 2026-10-14 17:43:54 -10:00
J. Nick Koston a07a1ab728 [qmi8658] Keep the range name tables in flash (#20714) 2026-10-14 17:43:34 -10:00
J. Nick Koston 9a5d2d9753 [api] Encode action strings straight from flash (#20425) 2026-10-14 17:38:39 -10:00
J. Nick Koston 3188e0d36f [ota] Fall back to a UDP data phase when the link is lossy (#20498) 2026-10-15 16:30:56 +13:00
Jonathan Swoboda f3eaf698d6 [mipi_rgb] Rotate pure RGB panels in software (#20757) 2026-10-14 22:05:01 -04:00
J. Nick Koston f7057e1270 [tests] Reset saved preferences before every C++ unit test (#20771) 2026-10-14 16:01:45 -10:00
J. Nick Koston 6edfbb594d [bluetooth_connection] Value-initialize the connection parameters for ESP-IDF 6.1 (#20774) 2026-10-14 22:00:07 -04:00
J. Nick Koston 22defcd704 [esp32_ble_client] Value-initialize the connection parameters for ESP-IDF 6.1 (#20776) 2026-10-14 21:59:56 -04:00
Jonathan Swoboda 0f408234d0 [core] Add ksz8851snl to the static analysis manifest for IDF 6 (#20788) 2026-10-14 21:53:17 -04:00
Jonathan Swoboda 92e0e6db6b [wifi] Call esp_supp_dpp_init without a callback on ESP-IDF 6 (#20789) 2026-10-15 01:40:07 +00:00
908c2f6778 [wifi] Add Wi-Fi Easy Connect (DPP) provisioning on the ESP32 (#17934)
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-14 14:29:47 -10:00
Keith BurzinskiandClaude Opus 5.5 af709ac9a5 [openthread] Fail instead of aborting when NVS is unavailable (#20717)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:30:36 -10:00
Panoplian fe57eef3f6 [core] Test that includes inside a package resolve next to the package (#20739) 2026-10-14 13:28:01 -10:00
d0b9f195b0 [wifi] Apply reboot_timeout with a fallback AP unless a station is connected to it (#9958)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 13:23:37 -10:00
eafeef7cb8 [fusb302b] Add FUSB302B driver (#20760)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 13:18:20 -10:00
471774cdbf [ecocomfort2] Fix the clock sync and read boost from the unit state (#20782)
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-14 13:14:04 -10:00
Kevin AhrendtandJ. Nick Koston 4df6291feb [api][sendspin] Add the Sendspin client id to the capabilities message (#20775)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 22:56:51 +00:00
d8ca4a9460 [audio_dac] Add ES9038Q2M Katana platform (#15017)
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-14 22:50:41 +00:00
48a0a41cce [wifi] Implement access point WISP mode (#13633)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 12:49:37 -10:00
esphome[bot] 0e2c4cf5c2 Bump aioesphomeapi from 46.11.0 to 46.12.0 (#20781)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-14 12:43:07 -10:00
ea76073d86 [micro_wake_word] Allow selecting the inference task core (#20126)
Co-authored-by: n-IA-hane <n-IA-hane@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-14 12:42:24 -10:00
J. Nick Koston dcb943ded3 [noise] Initialize the protocol id's reserved field for ESP-IDF 6.1 (#20779) 2026-10-14 11:45:54 -10:00
dependabot[bot] 2c6f59738c Bump filelock from 4.1.0 to 4.1.1 (#20778)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 21:41:28 +00:00
puddlyandJ. Nick Koston 04715fb32b [usb_uart] Expose interface_class and port_number (#20659)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 21:41:12 +00:00
nullable.ethandJ. Nick Koston 3df9360876 [i2s_audio] Add keep_alive option to speaker to prevent pop/click on audio start (#15565)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 11:39:30 -10:00
dependabot[bot] 61f3de0bc0 Bump pylint from 4.1.2 to 4.1.3 (#20773)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 21:30:26 +00:00
dependabot[bot] cd97c9a9cb Bump platformdirs from 4.12.4 to 4.13.0 (#20772)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 21:29:17 +00:00
J. Nick Koston d3668a7186 [mqtt] Match subscription topics in a loop instead of recursing per character (#20767) 2026-10-14 11:13:32 -10:00
J. Nick Koston ac74796263 [friedrich] Clear saved preferences in the test fixture (#20770) 2026-10-14 10:52:13 -10:00
c56bb14037 [mqtt] Print crash info over MQTT (#17935)
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-14 10:39:55 -10:00
J. Nick Koston a4278b8dec [ci] Run benchmarks when a component the Python import benchmark imports changes (#20763) 2026-10-14 10:35:33 -10:00
Panoplian 6d173c7371 [bq25186] Set i2c_id in the component tests (#20742) 2026-10-14 10:35:01 -10:00
esphome[bot] 1779b011c9 Bump aioesphomeapi from 46.10.0 to 46.11.0 (#20766)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-14 20:21:45 +00:00
PanoplianandJ. Nick Koston 3c4cf50cb6 [core] Point compile errors after a lambda at the right line (#20740)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 10:02:51 -10:00
J. Nick Koston e869d1f7ab [ci] Give the BK72xx clang-tidy environment the static allocation flag the real build has (#20764) 2026-10-14 09:56:09 -10:00
e1a996435d [ld2450] Add polygon zones evaluated on the device (#20154)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 19:29:16 +00:00
Jonathan Swoboda c5a624a1f2 [tests] Count OTA key probe retries on a fake clock (#20761) 2026-10-14 15:21:35 -04:00
Remco van Essen 15fb7f775e [tmc2209] Add chopper and PWM tuning actions (#20753) 2026-10-14 08:59:21 -10:00
J. Nick Koston 3fa72e61c6 [mqtt] Build the mqtt triggers with build_trigger_callback (#20433) 2026-10-14 08:55:39 -10:00
Kevin Ahrendt 163b09a473 [microphone] Speed up MicrophoneSource audio conversion (#20755) 2026-10-14 08:47:25 -10:00
J. Nick Koston 7afc2768f8 [esp8266] Check that every component's saved values fit together (#20686) 2026-10-14 08:45:51 -10:00
56f1a2bf46 [midea_portasplit] Add Midea PortaSplit climate component (#18033)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 08:45:21 -10:00
Panoplian fee47fe2f1 [pcf85063] Convert the weekday to and from the chip's 0-6 range (#20736) 2026-10-14 08:43:23 -10:00
Panoplian b994e081c3 [bm8563] Convert the weekday to and from the chip's 0-6 range (#20737) 2026-10-14 08:42:47 -10:00
Jonathan Swoboda a1f740b71b [lvgl] Keep LV_CONF_PATH one argument when the build path has a space (#20758) 2026-10-14 08:36:17 -10:00
Remco van EssenandClaude Opus 5.5 7f144365ba [fusb302b] Add USB PD sink protocol (#20632)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:35:29 -10:00
Marcus VoßandJ. Nick Koston 928459d16f [cse7766] Keep last power reading between measurements at low load (#20631)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 08:33:11 -10:00
Jonathan Swoboda 9163359445 [core] Analyze the ethernet component under ESP-IDF (#20745) 2026-10-14 08:31:39 -10:00
Jonathan Swoboda 67307f0eeb [ci] Select the feature-gated files the platform clang-tidy jobs were missing (#20743) 2026-10-14 08:31:06 -10:00
91e0edc11d [pn532] Fix NDEF parsing greater than 255 bytes (#18188)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 08:15:48 -10:00
Jonathan Swoboda 9fe75e262f [ci] Add a host clang-tidy environment and job (#20747) 2026-10-14 08:09:53 -10:00
Kevin Ahrendt 0132f722a3 [audio] Bump esp-audio-libs to v4.1.0 (#20754) 2026-10-14 13:24:36 -04:00
Jonathan Swoboda 7d7a78308d [ci] Test tinyusb_keyboard in isolation (#20749) 2026-10-14 10:05:02 -04:00
J. Nick Koston 9c568525b0 [core] Add CodSpeed benchmarks for importing entity components (#20648) 2026-10-14 09:08:41 -04:00
J. Nick Koston 10f42e3e7c [shelly_dimmer] Build the STM32 firmware update path in CI (#20677) 2026-10-14 09:03:34 -04:00
J. Nick Koston 25d4b88605 [nfc] Add unit tests for the URI record prefix codes (#20724) 2026-10-14 09:01:59 -04:00
J. Nick Koston d6abf35138 [sx126x][sx127x] Stop the watchdog reset loop when a packet is sent before setup (#20727) 2026-10-14 09:01:10 -04:00
J. Nick Koston 66ea99ce65 [seeed_mr60fda2] Compile without binary sensors or selects (#20726) 2026-10-14 09:00:08 -04:00
J. Nick Koston d9298cb7b9 [seeed_mr60bha2] Compile without sensors or binary sensors (#20729) 2026-10-14 08:59:27 -04:00
J. Nick Koston 34e1cf96d5 [bmi270] Build the component on ESP8266 in CI (#20731) 2026-10-14 08:58:56 -04:00
56cc03c4d4 [haier] Haier Select component added (#16148)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Pavlo Dudnytskyi <Pavlo.Dudnytskyi@addsecure.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 23:27:18 -10:00
35b416c6c0 [web_server_idf] Keep active SSE sessions out of the LRU purge (#17383)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 22:19:06 -10:00
Petar Petrov 7f2c3ba3f5 [display] Let other components draw into a display (#20622) 2026-10-14 06:11:47 +00:00
Keith BurzinskiandClaude Opus 5.5 a97604d574 [esp32_ble] Rely on preferences for NVS init (#20708)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:10:51 -10:00
Petar Petrov 1146b77836 [socket] Add TcpClientLink::connecting() (#20722) 2026-10-13 19:19:17 -10:00
J. Nick Koston 1338902ac5 [heatpumpir] Link only the configured protocol classes (#20707) 2026-10-14 17:06:22 +13:00
60a1d70805 [core] Move deferred include processing to parse_yaml (#16344)
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-13 17:46:54 -10:00
Ryan LudwigandCursor 887d4c3a83 [esp32_can] Add NO_ACK mode (#20706)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 17:36:15 -10:00
J. Nick Koston c1fa73470e [display] Walk the blit fallback in frame buffer order and resolve the bitness once (#20651) 2026-10-13 17:31:13 -10:00
J. Nick Koston e41d16ba7e [haier] Keep the alarm message table in flash (#20641) 2026-10-14 03:23:28 +00:00
J. Nick Koston 5171113c87 [core] Let PROGMEM_STRING_TABLE hold more than 255 bytes (#20646) 2026-10-13 17:17:48 -10:00
techdregsandJ. Nick Koston 5718e4b779 [tmp102] Add text sensor for diagnostics (#20629)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 17:06:56 -10:00
J. Nick Koston 40e12cd0d8 [image] Add CodSpeed benchmarks for image, runtime image and display block draws (#20649) 2026-10-13 17:01:47 -10:00
Brandon HarveyandJ. Nick Koston 07029d0077 [sen6x] Add measurement control and maintenance actions (#18783)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 15:47:31 +13:00
Christopher Reimer bb1119d83f [esp32_can] Support 20 kbit/s on ESP32 revision 2 or later (#20426) 2026-10-13 22:34:03 -04:00
7b591b26d6 [sy6970] Service the I2C watchdog so charge/LED settings persist (#19424)
Co-authored-by: Willem Oldemans <>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 16:19:58 -10:00
f45e6f0bf4 [midea] Add display_control and send the display toggle over UART without a transmitter (#18671)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 16:03:18 -10:00
Keith BurzinskiandClaude Opus 5.5 4b8af08155 [network] Notify listeners when the active interface changes (#20684)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:57:59 -05:00
613973b432 Bump ruff from 0.16.10 to 0.17.0 (#20694)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-13 21:53:44 -04:00
dependabot[bot] 9ee4314bc2 Bump esptool from 5.4.0 to 5.5.0 (#20693)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:41:30 -10:00
fredkotzeandJ. Nick Koston 59f55dbe9e [epaper_spi] Add Waveshare 2.13in-G (JD79660) display models (#18141)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 15:40:28 -10:00
dependabot[bot] 8fce95454a Bump github/codeql-action/init from 4.38.2 to 4.38.3 (#20701)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:39:49 -10:00
dependabot[bot] 16134ee993 Bump astral-sh/setup-uv from 10.2.0 to 10.3.0 (#20704)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:39:38 -10:00
dependabot[bot] bc39d56bb4 Bump CodSpeedHQ/action from 5.4.0 to 5.4.2 (#20705)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:39:29 -10:00
dependabot[bot] c5522abc71 Bump github/codeql-action/analyze from 4.38.2 to 4.38.3 (#20699)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 21:39:11 -04:00
dependabot[bot] 7cb5906d2a Bump actions/upload-artifact from 7.0.1 to 7.0.2 (#20702)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 01:38:42 +00:00
dependabot[bot] d452dbce30 Bump actions/download-artifact from 8.0.1 to 8.0.2 (#20703)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 01:38:39 +00:00
dependabot[bot] 81e5b50087 Bump backports-zstd from 1.7.0 to 1.8.0 (#20695)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:38:35 -10:00
dependabot[bot] 426a4a2dce Bump filelock from 4.0.12 to 4.1.0 (#20696)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:38:26 -10:00
dependabot[bot]andesphome[bot] c95c17c4e7 Bump pyupgrade from 3.21.2 to 3.22.0 (#20698)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-13 15:38:17 -10:00
dependabot[bot] 576d3325fe Bump astral-sh/setup-uv from 10.2.0 to 10.3.0 in /.github/actions/restore-python (#20700)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:37:47 -10:00
dependabot[bot] c9cf0ad92e Bump platformdirs from 4.12.3 to 4.12.4 (#20697)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 21:37:36 -04:00
Felipe Guedes f505212e64 [epaper_spi] Add Waveshare 2.66" e-Paper Module (B) support (#20546) 2026-10-13 15:03:05 -10:00
J. Nick Koston 52a5e6a007 [rpi_dpi_rgb] Set removal of the deprecated component for 2027.5.0 (#20691) 2026-10-14 13:32:18 +13:00
J. Nick Koston 3b261d2cf0 [st7789v] Set removal of the deprecated component for 2027.5.0 (#20689) 2026-10-14 13:30:52 +13:00
J. Nick Koston 9238fcc727 [st7735] Set removal of the deprecated component for 2027.5.0 (#20688) 2026-10-14 13:30:21 +13:00
J. Nick Koston 96902cc122 [qspi_dbi] Set removal of the deprecated component for 2027.5.0 (#20690) 2026-10-14 13:29:52 +13:00
J. Nick Koston 95711d83d1 [ili9xxx] Set removal of the deprecated component for 2027.5.0 (#20687) 2026-10-14 13:29:29 +13:00
Panoplian 3cdcf0417a [lvgl] Make the list_outside_block tests work offline (#20676) 2026-10-13 13:31:08 -10:00
J. Nick Koston c6e90de262 [ci] Catch out-of-class member log tags and keep midea's in flash (#20674) 2026-10-14 12:21:24 +13:00
J. Nick Koston 92f6ab8367 [ci] Lint log tags in headers and core (#20668) 2026-10-14 12:21:24 +13:00
J. Nick Koston f61955baa2 [core] Declare the remaining log tags with ESPHOME_LOG_TAG (#20666) 2026-10-14 12:21:23 +13:00
J. Nick Koston 36e1d831d3 [core] Add a decimal fast path to parse_number<float> (#20639) 2026-10-14 12:18:35 +13:00
J. Nick Koston 63616bf642 [rtttl] Play a configured song from static storage without copying it (#19569) 2026-10-14 12:06:27 +13:00
d275926f65 [mdns] Advertise the connected interface in the network TXT record (#20673)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 17:55:22 -05:00
Andrej Friesen f265c46c21 [PCA9505] Add PCA9505/PCA9506 40-channel GPIO expander (#20667) 2026-10-13 12:50:43 -10:00
Frédéric MetrichandClaude Opus 5.5 7d735f0677 [emontx] Lock the emonTx on reboot when its firmware supports it (#20621)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:37:09 -10:00
Remco van Essen 5c23a827b6 [tmc2209] Add current and diagnostic sensors (#20638) 2026-10-13 12:35:55 -10:00
Jonathan Swoboda e265c0c0fc [core] Sync the static analysis dependency pins with the code (#20681) 2026-10-13 12:30:42 -10:00
J. Nick Koston a0c7d754cb [syslog] Keep the log level to severity table in flash (#20662) 2026-10-13 12:22:35 -10:00
Jesse Hills a6dbd72c53 Merge branch 'beta' into dev 2026-10-14 11:15:09 +13:00
Jesse Hills 9d530f5c56 Merge pull request #20680 from esphome/bump-2026.10.0b4
2026.10.0b4
2026-10-14 11:14:49 +13:00
Panoplian 6562c49686 [thermostat] Allow a dry-only thermostat without cool_action (#20682) 2026-10-13 12:08:13 -10:00
guillempages a97ce3a201 [image] Fix pillow deprecation warning (#20679) 2026-10-13 11:58:21 -10:00
Jesse Hills 5a78f636b9 Bump version to 2026.10.0b4 2026-10-14 10:43:50 +13:00
tomaszduda23andClaude 53d504137b [nrf52] Accept S140 7.2.0 and 7.3.0 in the sd140_v7 DFU package (#20661)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-14 10:43:49 +13:00
bb0546fc0b [script] Keep the running script.wait item out of the queue while its chain runs (#20570)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 10:43:49 +13:00
emes30andJ. Nick Koston b43630cf8e [rp2_pio] Use pioasm 4.1.0 so PIO programs run on RP2350 (#19260)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 10:43:49 +13:00
Firat HajiyevandFirat17aze c9664d4e42 [wifi] Reject unsupported WPA3 on ESP8266 (#20592)
Co-authored-by: Firat17aze <187820330+Firat17aze@users.noreply.github.com>
2026-10-14 10:43:49 +13:00
Kevin Ahrendt 39ea6a3220 [audio] Bump microFLAC to v0.3.0 (#20591) 2026-10-14 10:43:49 +13:00
J. Nick Koston f4a6d6f18b [core] Create the PlatformIO HTTP cache dir before the prefetch resolve threads run (#20579) 2026-10-14 10:43:49 +13:00
esphome[bot] 6c24d4aa43 Bump aioesphomeapi from 46.9.0 to 46.10.0 (#20563) 2026-10-14 10:43:49 +13:00
J. Nick Koston 0dc1d3b5a1 [toshiba] Move the lookup tables to PROGMEM (#20636) 2026-10-13 11:25:18 -10:00
tomaszduda23andClaude 42341d05f9 [nrf52] Accept S140 7.2.0 and 7.3.0 in the sd140_v7 DFU package (#20661)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 11:12:08 -10:00
J. Nick Koston f941fee5ed [homeassistant] Add sensor platform benchmarks (#20637) 2026-10-13 11:10:14 -10:00
Bonne Eggleston 8721976066 [epaper_spi] Abandon an update when the busy pin never clears (#20555) 2026-10-14 07:47:09 +11:00
Bonne Eggleston 05bf7d3440 [epaper_spi] Write pixel blocks straight into mono buffers (#20552) 2026-10-14 07:40:56 +11:00
918b7d2553 [script] Keep the running script.wait item out of the queue while its chain runs (#20570)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 09:31:19 -10:00
rwrozelleandClaude Opus 5.5 417ab4d2e9 [openthread] Add core_options for OpenThread compile-time options (#20595)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:00:45 -04:00
Jonathan Swoboda 0821c84b7a [core] Let a nested repeat shadow the outer iteration variable (#20634) 2026-10-13 14:35:24 -04:00
Clyde Stubbs a827058928 [lvgl] Change deprecated calls to current version (#20620) 2026-10-13 14:27:50 -04:00
J. Nick Koston 3063b76210 [haier_tundra] Build the IR frame on the stack (#20635) 2026-10-13 14:25:36 -04:00
Remco van EssenandClaude Opus 5.5 55261ae7e5 [tmc2209] Add driver diagnostics and stall detection (#20618)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:17:25 -10:00
cociwebandJ. Nick Koston 1d8423edd8 [es8388] Add initial option configuration for DAC output and ADC input mic (#16342)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 08:14:43 -10:00
n-IA-haneandn-IA-hane d27ec8ec49 [voice_assistant] Make TTS playback start timeout configurable (#18707)
Co-authored-by: n-IA-hane <n-IA-hane@users.noreply.github.com>
2026-10-13 08:13:26 -10:00
Ivan KruglovandJ. Nick Koston 5acbd44067 [pcf8574] Add reset option to preserve outputs across MCU reboots (#20589)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 08:12:28 -10:00
40271fb597 [haier_tundra] New Climate IR component (#20469)
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-13 08:10:36 -10:00
95766ed3ad [icm20689] Add ICM-20689 6-axis accelerometer/gyroscope (SPI) (#19234)
Co-authored-by: Ludovic BOUÉ <938089+lboue@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 08:07:32 -10:00
4a80d785a8 [filter_lifetime] Add component with sensor/lambda support for is_on … (#13914)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 07:52:46 -10:00
2123 changed files with 66718 additions and 12086 deletions
+146
View File
@@ -0,0 +1,146 @@
---
name: hardware-test
description: Test an ESPHome change on real boards over USB. Use this whenever a task asks to test, verify, reproduce, bench, A/B or soak a pull request, a branch or a crash on hardware, or mentions flashing, serial logs, esphome upload, a captive portal or fallback AP test, or results to post on a PR. Covers finding the serial ports, building from the right checkout, flashing each chip family (including the LibreTiny kits that stock tools cannot flash), bounded log capture, proven test flows and how to report results.
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
---
# Hardware testing
The goal of a hardware test is evidence a reviewer can trust: which build ran on which board, what was
driven, what the logs show, and the same flow on a `dev` build when a quirk could be pre-existing.
`$SKILL` below is this directory (`.agents/skills/hardware-test`).
## Ground rules
1. **Serial ports cannot be shared.** One bounded capture per port, stopped before any flash on that
port, never an open-ended `esphome logs`. Stop a capture with `pkill -f "[s]erial_capture.py <port> "`,
naming the port so a capture on another board survives; the bracket keeps the pattern from matching
the shell that runs it. When more than one agent session shares the bench, run a copy of the script
under a session-specific name (`cap<topic>.py`) so another session's `pkill` cannot take your capture
down. Never run `esphome logs` in a `while true` loop: the loop respawns it and holds the port.
2. **Credentials stay local.** Put a `secrets.yaml` next to the scratch configs and reference it with
`!secret`. A copy of a device's own config keeps using the secrets file next to that config: two
checkouts can hold different networks, and the wrong one leaves the device unreachable after the OTA. Never paste a config with credentials into a PR, never commit scratch configs or logs.
3. **Build from the tree under test.** `python -m esphome` puts the current directory first on
`sys.path`, so run it with the checkout or worktree as the working directory and pass the yaml by
its absolute path. A venv `esphome` entry point may import an installed snapshot instead of the
checkout. Confirm with `python -c 'import esphome; print(esphome.__file__)'`, and for an A/B grep
the build's `src/` for the change before trusting any number. Tool calls sent together share one
shell, so a `cd` in one can land another's compile in the wrong tree: pass `PYTHONPATH=<worktree>` on
every compile and upload command and print `esphome.__file__` into the compile log.
4. **PR build and baseline build get different `name:` values.** Builds share
`.esphome/build/<name>`, so the same name means `esphome upload` can flash the wrong binary. Saved
Wi-Fi credentials are keyed by the config hash when the config has `networks:`, so a test that must
start unprovisioned gets a fresh name every run. A config with `ap:` only and no `networks:` uses one
fixed key shared by every such config on the board, so a new name does not help there: wipe once at
boot with an `on_boot` lambda that resets the preferences and saves a marker key
(`templates/dut-ap-only-wipe-once-bk72xx.yaml`).
5. **Flash and RAM deltas are measured against the PR's merge base** (`git merge-base origin/dev HEAD`
checked out in a throwaway worktree), with the baseline commit named in the table. When a delta
looks odd, diff symbol sizes (`nm -S -C --size-sort` from the matching toolchain) before quoting it.
6. **Prove a quirk is pre-existing before blaming the PR:** run the identical flow on a `dev` build in
the same session and say so in the results.
7. **Pipe safely.** Set `PYTHONUNBUFFERED=1` on any `esphome run` or `esphome logs` whose output is
piped, or lines land minutes late. Bound every long command with `timeout` (coreutils) or
`perl -e 'alarm shift; exec @ARGV' <seconds> <command>` on macOS without it.
## Workflow
1. **Find the boards:** `python $SKILL/scripts/ports.py` (any Python with pyserial, for example the
ESPHome venv). Port names follow the USB hub port and change on every replug, so never reuse a name
from an earlier session. CH340 adapters (D1 mini, BW15 kit, BK7238, CB3S) all look the same; tell
them apart with a 10 s capture using `serial_capture.py --reset` (ESP8266 prints its ROM banner at
74880 baud, LibreTiny boards print `[LT]` and ESPHome lines, BK7238 prints its bootloader version).
`ports.py --wait 2e8a` blocks until a Pico re-enumerates.
2. **Make a scratch directory** outside the checkout, with `secrets.yaml` and the templates you need.
One yaml per board per variant, file name equal to `name:`.
3. **Compile** in the right tree:
`(cd <worktree> && PYTHONUNBUFFERED=1 timeout 1500 python -m esphome compile <abs yaml> > <name>-compile.log 2>&1)`.
Keep the RAM and flash lines at the end. Two ESP-IDF or two LibreTiny builds at the same time race
over tool caches; run them one at a time.
4. **Flash** per chip family (table below). Stop the capture on that port first.
5. **Capture** with `serial_capture.py PORT SECONDS OUT` in the background, then poll the file with
`grep` in an `until ... sleep 2` loop with a cap. USB CDC boards (Pico, nRF52, native USB ESP32)
drop the port on reboot; the script reopens it until its deadline.
6. **Drive** the test: a second board running a client config, `curl`, an aioesphomeapi script that
presses a template button and subscribes to logs, MQTT publishes, or `interval:` and `on_boot:`
automations in the config itself.
7. **Analyze** with `grep -a -c` counts over the capture and keep an `-events.txt` extract of the lines
that matter. Decode crashes with the platform's `addr2line` (see `reference/boards.md`).
8. **Report** on the PR as a table: build SHA, board and framework, what was driven, counts, result,
plus the `dev` control row and the exact flow in a `<details>` block. No credentials, no port names,
no IP addresses.
## Flashing by chip family
| Board | Flash | Log |
|---|---|---|
| ESP8266 D1 mini (CH340) | `python -m esphome upload <yaml> --device <port>` | `serial_capture.py` at 115200; `esphome logs` on some CH340s emits garbage after the RTS reset. A failed esptool write can wedge the adapter until the board is replugged. `esptool erase_flash` before a test that needs an unprovisioned device: the preference sector survives a reflash |
| ESP32 devkit (CP2102 or CH340) | `esphome upload --device <port>`, ESP-IDF or Arduino | `serial_capture.py`. Default Wi-Fi light sleep delays API driven actions by seconds; judge timing from device logs. `power_save_mode: none` for OTA overlap tests |
| ESP32-S3, C3, C6, H2 native USB | `esphome upload --device /dev/cu.usbmodem*` | `logger: hardware_uart: USB_SERIAL_JTAG`. The USB console can wedge when the app takes over the console (0 bytes, esptool `No serial data received`); replug, or watch through `esphome logs --device <name>.local` |
| Pico W / Pico 2 W | `esphome upload --device <port>` (1200 baud touch); first flash ever via BOOTSEL and `--device /Volumes/RPI-RP2` or `RP2350` | port re-enumerates after upload: `ports.py --wait 2e8a` then capture |
| BW15 kit (RTL8720CF, rtl87xx) | `~/.platformio/penv/.libretiny/bin/python $SKILL/scripts/flash_rtl87xx_kit.py flash write -d <port> <build>/.pioenvs/<name>/*.uf2`. Plain `esphome upload` and plain ltchiptool time out because opening the port asserts DTR/RTS and holds the chip in reset | `serial_capture.py --released`, never `esphome logs` |
| BK7238 / CB3S (bk72xx, RTS on CEN) | `~/.platformio/penv/.libretiny/bin/python $SKILL/scripts/flash_bk72xx.py flash write -d <port> <uf2>` (about 75 s) | logs are on UART2 by default; `logger: hardware_uart: UART1` puts them on the flashing adapter. `serial_capture.py --reset` gives a chip-enable reset; a plain open may or may not reset it, check for the bootloader banner |
| LN882H | ltchiptool forces 1 Mbaud for writes; weak adapters need `LN882H_YM_BAUDRATE` patched to 115200 in the penv's `ln882htool.py`; short BOOT to GND while the tool connects | OTA after the first flash |
| nRF52840 Dongle (PCA10059) | Nordic `nrfutil nrf5sdk-tools dfu usb-serial` on the bootloader port, or `dfu: true` in the config so the 1200 baud touch pin-resets into the bootloader; adafruit-nrfutil is refused | app has two CDC ports, ESPHome log on the second |
| Adafruit-style nRF52 (UF2 bootloader) | `UF2BOOT` volume or adafruit-nrfutil; `dfu: true` works through GPREGRET | single CDC port |
| Any reachable device | `esphome upload <yaml> --device <ip or name.local>`; `--file <bin>` for a prebuilt image | `esphome logs --device <ip>` bounded with `timeout` |
ESP-IDF OTA image: `.esphome/build/<name>/build/firmware.ota.bin`. LibreTiny OTA port is 8892.
## Scripts in `$SKILL/scripts`
- `ports.py`: list ports with VID:PID, product, serial and a board guess; `--wait VID[:PID]`,
`--product NAME`, `--serial SN` (one specific board, whatever path it comes back under) and
`--serial-of PORT` (the serial number behind a path).
- `serial_capture.py PORT SECONDS OUT [--baud] [--released] [--reset] [--append]`: bounded timestamped
capture, ANSI stripped, `LOG_SECRET` text written as `<secret>`, reopens a vanished port, exits at the
deadline. OUT is truncated unless `--append` is given, so counts over it cover one run.
- `flash_rtl87xx_kit.py`: ltchiptool wrapper for the BW15 kit whose CH340 drives CEN (RTS) and the boot
strap (DTR). `flash read` needs the family `realtek-ambz2`; `flash write` does not.
- `flash_bk72xx.py`: ltchiptool wrapper for BK72xx boards whose RTS drives CEN. It misses the link now
and then; retry up to three times.
- `wifisave_hammer.sh KIND DUT_PORT DUT_YAML DUT_TREE CLIENT_PORT CLIENT_YAML CLIENT_TREE [SETTLE]`:
the fallback AP plus captive portal hammer end to end (compile, flash, capture, counts) with the
templates below. KIND is `esp`, `pico`, `bw15` or `bk`. It ends with `RESULT PASS` (exit 0),
`RESULT FAIL` (exit 3: a crash, a reboot after the AP came up, or the device never joined the network),
`RESULT INVALID`
(exit 2: a precondition was missed, the stimulus was insufficient, or a capture did not last the run)
or exits 1 when a build, flash or port step failed.
Per-topic scripts, each described in the matching `reference/recipes/<component>.md`: `online_image/`
(server, image generator, API driver), `zwave_proxy/` (checksum verifier, device info), `usb_cdc_acm/`
(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).
## Templates in `$SKILL/templates`
- `dut-fallback-ap.yaml` (package): a device that never finds its network, falls back to its AP and
serves the captive portal. `example-dut.yaml` is the per-run file with the platform block.
- `client-wifisave.yaml` (package): joins that AP, posts N bogus `/wifisave` requests, then the real
credentials from `!secret`. `example-client.yaml` is the per-run file. `bogus_saves: "1"` gives a
plain provisioning check.
- Per-topic device configs, each described in `reference/recipes/<component>.md`: `online-image-dut.yaml`,
`usb-cdc-acm-dongle-dut.yaml`, `nrf52-preferences-dut.yaml`, `rtsp-waveshare-s3-eth-dut.yaml`,
`waveshare-s3-eth-camdiag.yaml`, `s31-cse7766-dut.yaml`, `nrf52-coredump-crash-dut.yaml`,
`bk72xx-rtc-prefs-dut.yaml`, `bk72xx-crash-probe-dut.yaml`, `dut-ap-only-wipe-once-bk72xx.yaml`.
## When something looks wrong
- A capture stopped early or a line is missing: check the timestamps before trusting it.
- The client never joins the AP: another board may still run an AP with the same SSID, or the device
booted with saved credentials because the name was reused. One AP device at a time on the bench.
- Builds come out byte-identical between branches: the wrong tree was imported (rule 3). The reverse
too: two binaries that differ only in the build timestamp and in `__LINE__` values of log calls below
the changed code are the same build; map the offsets with `objdump -h` and `addr2line` first.
- A boot the host cannot capture (a power cut, an OTA-only board): keep the boot report in a flash
preference and repeat it in a periodic alive line, as `scripts/libretiny_prefs/ltprefs.hpp` does.
- Multicast from the host never arrives on some access points: test it device to device. Broadcast
is lossy under Wi-Fi power save, unicast is not. Throttle UDP floods to a fixed packet rate.
- `api:` reboots a board after 15 min without a client: `reboot_timeout: 0s` on `api:` and `mqtt:` for
soak tests.
`reference/boards.md` has per-chip facts and crash decoding; `reference/recipes/` has the proven
flows. Add new findings there.
@@ -0,0 +1,190 @@
# Board facts
Ports are not listed on purpose: they follow the USB hub port and change on every replug. Run
`scripts/ports.py`.
## ESP8266 (D1 mini, NodeMCU, CH340 or CP2102)
- 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`).
- `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.
- The preference sector survives a reflash: a provisioned API key or saved Wi-Fi credentials stay
active. `esptool erase_flash` gives a clean device.
- `Panic core_esp8266_main.cpp:191 __yield` with `ctx: sys` means a yield from lwIP context (a web
handler, an lwIP callback). Decode stacks with
`xtensa-lx106-elf-addr2line -e <build>/.pioenvs/<name>/firmware.elf -f -C -i <addr...>` from the
toolchain under `~/.platformio/packages/toolchain-xtensa/bin/` (PlatformIO builds) or the ESPHome
cache directory (native toolchain builds).
- Constant data lives in RAM unless marked `PROGMEM`; with 80 KB of RAM a few hundred bytes are worth a
row in the PR.
- AsyncMqttClient drops inbound topics over 128 characters; topics of 255 or more wrap a counter and
stall the link until the broker drops TCP.
## ESP32 classic devkit
- Takes ESP-IDF and Arduino. Default Wi-Fi light sleep delays API driven actions by up to 6 s; judge
timings from device logs, not from the host. `power_save_mode: none` for OTA overlap tests (an IDF web
upload of 739 KB took 107 s with power save and 45 s without).
- Receives only a few percent of IPv6 multicast on some access points with power save on; a raw lwIP
socket loses the same, so that is the AP or driver, not a PR.
- Decode with `xtensa-esp32-elf-addr2line` from the IDF tools directory against
`<build>/.pioenvs/<name>/firmware.elf` (PlatformIO) or `<build>/build/<name>.elf` (IDF native).
- Clean reboot between runs with the capture stopped: `esptool --after hard_reset chip_id`.
- Trigger a forced crash only after the app is marked valid (45 s after boot is plenty), never on an
image still pending OTA rollback.
- Signed OTA testing: build with `esp32.framework.advanced.signed_ota_verification.verification_keys`
under a different name, sign with `python -m espsecure sign_data --version 2 --keyfile
tests/components/esp32/dummy_signing_key.pem`, upload with `esphome upload --file`. The verifier
refuses unsigned images afterwards, so restore with a signed plain build.
## ESP32-S3, C3, C6, H2 with native USB
- `/dev/cu.usbmodem*`, esptool works, `logger: hardware_uart: USB_SERIAL_JTAG`.
- The USB console can die at the moment the app takes over the console during a reboot (log stops after
`Disabling RNG early entropy source`, then 0 bytes, esptool reports `No serial data received`). Only a
USB replug fixes it. Fallback: `esphome logs --device <name>.local` (API log stream, which drops lines
while Wi-Fi is off).
- The ESP32-P4 before silicon revision 3 is bricked by rev3-only firmware (boot loop at bootloader entry).
## RP2040 / RP2350 (Pico W, Pico 2 W)
- The CDC port re-enumerates after every reboot and after `esphome upload` (1200 baud touch). Re-detect
with `ports.py --wait 2e8a` before capturing; `serial_capture.py` reopens on its own if the port
vanishes mid-capture.
- First flash ever: BOOTSEL, then `esphome upload --device /Volumes/RPI-RP2` (or `RP2350`).
- arduino-pico creates the lwIP netif in `WiFi.begin()`, so any IGMP join during component setup fails;
joins must wait for `network::is_connected()`.
- Parallel Pico builds race on package extraction after a PlatformIO package wipe; one at a time.
- An RTS pulse from pyserial does not reset the board; reboot with `App.safe_reboot()` or the touch.
## BW15 kit (RTL8720CF, LibreTiny rtl87xx)
- The kit's CH340 wires RTS to CEN and DTR to PA00 (boot strap). A plain pyserial open asserts both and
holds the chip in reset, which is why `esphome upload`, plain `ltchiptool` and `esphome logs` all fail
("Timeout while linking", or silence). `scripts/flash_rtl87xx_kit.py` sets DTR high and RTS released
on every open, pulses RTS once so the ROM answers `ping`, and after the command drops DTR and pulses
RTS to boot the app. Run it with the PlatformIO LibreTiny penv Python.
- `flash write -d <port> <uf2>` takes no family argument; `flash read -d <port> -s 0 -l <len>
realtek-ambz2 <out>` does.
- Capture with `serial_capture.py --released`. The ESPHome log is on the flashing UART already.
- The macOS CH340 driver only pushes DTR/RTS to the wire once a session has driven them, so the first
plain open after a replug streams logs and the second is silent; always open with the lines released.
- Driver quirks seen on `dev`: `WiFi.begin` can refuse the real network on the first try after bogus
credentials (it connects after two cooldowns); right after connecting while the AP is still up the
driver can bus-fault in `rtw_is_adapter_up` (`R0 = 0xdeadbeef`) and reboot; the AP restarts on every
STA retry, so stations cannot stay joined; `WiFi.begin` fails synchronously for an absent network and
lands in a cooldown at once; connecting blocks the loop 2.5 to 4.1 s.
- Decode faults with `arm-none-eabi-addr2line -e <build>/.pioenvs/<name>/raw_firmware.elf -f -C -i <PC>`
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`.
## BK7238, WB3S, CB3S (BK7231N) (LibreTiny bk72xx, CH340, RTS on CEN)
- Stock ltchiptool closes and reopens the port after its reset, and pyserial raises both lines on open,
holding CEN low through the BootROM window. `scripts/flash_bk72xx.py` opens with both released and
pulses the reset right before the link checks. A write takes about 75 s. Only a CEN or power reset
reaches the downloader; no software reboot does.
- ESPHome logs on UART2 by default; `logger: hardware_uart: UART1` puts them on the flashing adapter.
- Opening the port may reset the chip (bootloader banner on open) or may not, depending on the adapter and
what pyserial does with DTR and RTS; `serial_capture.py --reset` pulses RTS on CEN and always does, and
`lt_get_reboot_reason()` reports that as Power-On. Check for the banner at the start of a capture before
assuming either.
- `flash_bk72xx.py` misses the link now and then (`Timed out attempting to link with chip`); retry up to 3
times before suspecting the board.
- Crashes: `__builtin_trap()` and a call into unmapped memory both end as an undefined-instruction trap
(`bk_misc_get_start_type()` 6, `lt_get_reboot_reason()` Crash); reads, unaligned loads and stores to
unmapped addresses never fault on the ARM968. Without Wi-Fi the SDK trap handler spins with interrupts off
for 10 s and the hardware watchdog resets the chip. With Wi-Fi connected a dev build hangs forever after a
crash, and never resets from a plain `portDISABLE_INTERRUPTS(); while (1) {}` either: the boot-time
watchdog is dead once Wi-Fi is up, for any lockup. The crash handler fix (#21012) re-arms the watchdog
from its wrapper, after which the reboot comes within a second (BK7238 and BK7231N).
- The SDK prints nothing on UART1 under `sdk_silent: all`; the bootloader then prints the banked
`cpsr/spsr/r13/r14` of SVC, IRQ, FIQ and SYS plus `ST:` (the retention register) before `go os_addr`.
A CEN or power reset wipes ITCM (reads `0xaaaaaaaa`), so only a watchdog reset keeps a record there. What
survives that reboot on the BK7238: ITCM (`0x3F0000..0x3F4000`) and the top 96 KB of SRAM
(`0x428000..0x440000`, the heap); the bootloader zeroes SRAM up to about `0x420000` and rewrites
`0x400024..0x4000FF` with its own register dump every boot, so neither a `.noinit` after `.bss` nor the
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.
- 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.
## BW16 kit (RTL8720DN, AmebaD)
- The kit's own USB-C (CH340) is on LOG_UART and flashes fine. Its two-transistor circuit resets while
RTS alone is asserted and straps BURN while DTR alone is; both asserted does nothing. ROM entry is
esptool's classic sequence (`dtr:false,rts:true` 100 ms, `dtr:true,rts:false` 400 ms, both false); the
first pulse after a replug can be lost, retry up to three times.
- Any reopen resets the chip, 460800 is flaky, 115200 is solid: one open per session, in-place baud
changes only. The ROM and the loader NAK continuously while idle, so ACK scans must drain first.
- The AmebaD core has no mDNS responder (`mdns: disabled: true`).
## LN882H
- ltchiptool forces 1 Mbaud for writes; adapters that cannot hold it fail with "Timeout while linking"
or die mid-write. Patch `LN882H_YM_BAUDRATE` in the penv's `ltchiptool/soc/ln882h/util/ln882htool.py`
to 115200 (lost on reprovision). Weak 3.3 V from the adapter: feed the module's 5 V pad instead. Short
BOOT to GND while the tool connects. OTA afterwards.
- `USE_WIFI_AP_EXCLUSIVE` (one radio mode at a time) exists only on LN882x; compile-check changes there.
## nRF52840
- Nordic PCA10059 dongle: the factory Open DFU bootloader enumerates as `1915:521F` on its own port
name; the app as `2FE3:0100` with two CDC ports, ESPHome log on the second. Only Nordic
`nrfutil nrf5sdk-tools` works (`pkg generate --hw-version 52 --sd-req 0x00 --application-version 1
--application zephyr.hex app.zip`, then `dfu usb-serial -pkg app.zip -p <port>`); ESPHome downloads
it on the first build. GPREGRET does not enter the bootloader; a pin reset through P0.19 does, which
`dfu: true` wires up on this board. A pulsing red LED means bootloader; the side RESET button
re-enters it.
- Makerdiary MDK USB dongle: UF2 bootloader without SoftDevice, app at 0x1000, `UF2BOOT` volume,
adafruit-nrfutil, GPREGRET 0x57 works. RGB LED active low (red P0.23, green P0.22, blue P0.24),
button P0.18.
- Adafruit ItsyBitsy nRF52840 Express (UF2 bootloader with S140, bootloader port `239a:0051` plus an
`ITSY840BOOT` volume): the board DTS already has `cdc_acm_uart0`, so the logger port is the first CDC
port and a `usb_cdc_acm` interface 1 the second. A `dfu: true` build enters the bootloader on the
1200 bps touch (an app without it needs a double tap on RESET); `adafruit-nrfutil` over the bootloader
serial port can fail, copying `.pioenvs/<name>/zephyr/zephyr.uf2` to the volume works (the copy reports
an I/O error as the board reboots, which is normal). Reboot a running `dfu: true` app with a 2001 bps touch.
- The PCA10059 board DTS already defines a `cdc_acm_uart` node, so every ESPHome CDC port is one USB port
later than its number (log on `cdc_acm_uart0` is the second port). A third CDC instance needs
`CONFIG_USB_MAX_NUM_TRANSFERS` above 4 or it never moves data (see `recipes/usb_cdc_acm.md`).
- USB CDC boot output: the Zephyr legacy stack's `cdc_acm` TX ring is 1 KB and `uart_poll_out` drops when it
is full, so anything printed in one burst beyond about 1 KB (a coredump, a long table) is lost even with
the port open, and the logger warns `logger took a long time`. Print one line per `loop()` instead.
- Zephyr settings survive a reflash: key a `restore_value` global by a `run` substitution when a test needs
a never-saved value.
## Waveshare ESP32-S3-ETH with the OV5640 camera module
- Native USB console (`303a:1001`), ESP32-S3R8, 16 MB flash, 8 MB octal PSRAM, W5500 on SPI (MOSI 11,
MISO 12, SCLK 13, CS 14, INT 10, RST 9), TF card on 4..7, WS2812 on 21.
- Camera header (vendor schematic): XCLK 3, SIOD 48, SIOC 47, VSYNC 1, HREF 2, PCLK 39, D0..D7 = 41, 45,
46, 42, 40, 38, 15, 18. PWDN is tied to GND and RESET is an RC to 3.3 V on the board, so no reset or
power-down GPIO exists.
- GPIO8 switches the camera 3.3 V rail through a P-channel MOSFET: LOW = camera powered, gate pulled up so
the camera is off by default. `power_down_pin: GPIO8` only pulls it low 10 ms before the probe, which is
not enough; use `switch: gpio GPIO8 restore_mode: ALWAYS_OFF setup_priority: 800` plus `on_boot:
priority: 700` with `delay(1200)` and `esp32_camera: setup_priority: 200`
(`templates/rtsp-waveshare-s3-eth-dut.yaml`). `esphome config` rejects a config that uses GPIO8 for both the switch
and `power_down_pin` (pin reuse), so it is one or the other.
- The sensor at SCCB 0x3C answers only with XCLK running, so the ESPHome i2c scan at boot (before XCLK)
reporting no devices on 48/47 is expected. If the SCCB ID register (0x3100 = 0x78) reads fine but every
0x30xx register reads 0x00 (`Detected camera not supported`, 0x106), reseat the ribbon before blaming
software; `templates/waveshare-s3-eth-camdiag.yaml` runs that probe with LEDC XCLK and raw SCCB reads.
## ESP Thread Border Router board v1.2 (S3 host + H2 RCP)
- S3 host on USB JTAG serial, H2 RCP on its own USB. Spinel UART1 from the host: `rx_pin: GPIO17`,
`tx_pin: GPIO18`, RCP reset `GPIO7` active low, 460800. The config needs `network: enable_ipv6: true`.
- The shipped H2 firmware fails `CheckRadioCapabilities` with IDF 5.5; build `examples/openthread/ot_rcp`
for esp32h2 with ESPHome's own IDF (`IDF_PATH` at the cached framework, its penv and tools,
`IDF_PYTHON_CHECK_CONSTRAINTS=no`) and flash with esptool at 0x0, 0x8000 and 0x10000.
- A crash-looping S3 pulses the H2 reset every boot, so the H2 cannot be flashed and the S3 will not
enter its bootloader over USB. Hold S3 BOOT, tap S3 RST, flash the H2, release, then replug both.
- `dns-sd -L <name> _meshcop._udp local.` shows the `omr` prefix; it disappears when routing is dead.
Keep one capture per chip; never reset the H2 over USB mid-test unless that is the test (the host
logs `Unexpected RCP reset: RESET_OTHER` and recovers).
@@ -0,0 +1,26 @@
# Proven test flows
Each flow names the pull requests it was used on, so their result tables show what to post.
One file per component, named after it:
- [`api.md`](api.md): noise handshake and OTA encryption on the ESP8266
- [`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
- [`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
- [`nrf52.md`](nrf52.md): dongle board support
- [`online_image.md`](online_image.md): stalled and malformed downloads
- [`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
- [`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)
- [`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
@@ -0,0 +1,7 @@
# api
## Noise handshake and OTA encryption on the ESP8266 (#19062, #19233)
Host side timing is useless on a weak link; use an on-device bench with the minimum of five runs over
two boots. Provision a key with `noise_encryption_set_key` through aioesphomeapi and upload with
`espota2.run_ota(host, 8266, None, bin, noise_psk=key)` when the key is not in the yaml.
@@ -0,0 +1,21 @@
# captive_portal
## Captive portal `/wifisave` hammer (#20802, #20784)
Reproduces a `/wifisave` arriving during the STA cooldown, and proves a fix on every board.
Device under test: `templates/example-dut.yaml` (bogus network, `ap:` with `ap_timeout: 15s`,
`captive_portal:`). Client: `templates/example-client.yaml` on an ESP32 or a D1 mini; it joins the
device's AP, posts 15 bogus saves 3 s apart, then the real credentials. With the portal up the cooldown
is 30 s, so nearly every save lands inside one. Driver: `scripts/wifisave_hammer.sh`. Settle 40 s on
ESP boards, 90 s on the BW15 (its first attempt on the real network fails, see boards.md).
Counts that matter: `Requested WiFi Settings Change` (saves received), `Exiting cooldown early`, crash
markers, boots (`Log initialized`), and the client's `bogus save status 200` and `real save status 200`.
A healthy run: every save answered 200, one cooldown exit per save (two on the BW15, whose retry enters
the adapter restart), no panic, and the real credentials connect the device within about 10 s.
Fault injection for AP start failures: wrap `this->ap_setup_ = this->wifi_start_ap_(this->ap_)` in
`setup_ap_config_()` with an `HWTEST_AP_FAIL_COUNT` build flag
(`esphome: platformio_options: build_flags: -DHWTEST_AP_FAIL_COUNT=2`), raise the portal's "Captive
portal started" from LOGV to LOGI, and after provisioning curl the device's STA address: connection
refused proves the portal's server is gone.
@@ -0,0 +1,11 @@
# cse7766
## cse7766 on a Sonoff S31 (#20948)
`templates/s31-cse7766-dut.yaml`: `logger: baud_rate: 0` because the CSE7766 owns the UART, VERBOSE logger
with everything but cse7766 quieted, 2 s throttles, and a `globals` frame counter fed by the voltage sensor's
`on_raw_value` and logged every 10 s (about 96 frames per 10 s). OTA only, every reflash reboots the relay.
`scripts/cse7766/run_obs.sh IP SECONDS LABEL [OUTDIR] [NAME_FILTER]` subscribes to states and logs over the
API with a concurrent ping and prints the frame counter, API gaps and warnings. API state gaps of 10 to 20 s
can appear on a weak Wi-Fi path while the device counter stays flat, so judge a UART parser by the
device-side counter, not by the API cadence. With nothing plugged in, current and power read 0.
@@ -0,0 +1,7 @@
# espnow
## ESP-NOW on the ESP8266 (#14407)
The NONOS SDK only delivers broadcast ESP-NOW frames while the station is associated or a soft-AP is
up; a hidden, open, client-less soft-AP (60 s beacon, DHCP stopped) makes broadcasts arrive. In AP-only
mode the device transmits from the soft-AP MAC.
@@ -0,0 +1,9 @@
# hdmi_cec
## HDMI-CEC two-board wire (#9189)
Two boards on one wire: promiscuous mode, a 0x8F request answered with 0x90 0x00, a `destination:`
filter on the reply handler, template buttons for each target plus lambdas calling
`send(source, dest, span)` as another initiator. Healthy numbers: device-side turnaround 15 to 22 ms,
round trip 144 to 156 ms. Press buttons over the API; judge timings from device logs (ESP32 light sleep
lags the press).
@@ -0,0 +1,7 @@
# ld2450
## LD2450 polygon zones (#20154)
To make a target "leave" without anyone moving, set the radar's Zone-1 numbers over the target and
switch the zone type to `Filter` (the count drops to 0 within 0.5 s); set it back to `Disabled` after.
Writing those numbers blocks the ld2450 loop about 50 ms.
@@ -0,0 +1,58 @@
# libretiny
## LibreTiny rtc preference emulation (#21007)
The PR keeps `storage: rtc` preferences in FlashDB and deletes them on the first load or save after a boot
whose `lt_get_reboot_reason()` is not software, watchdog, crash or sleep. Only the families listed in
`_libretiny_rtc_supported()` accept the config keys; on others force the backend in with
`platformio_options: build_flags: ["-DUSE_LIBRETINY_RTC_PREFERENCES"]` after confirming the rejection.
- Harness: `scripts/libretiny_prefs/ltprefs.hpp` plus `templates/bk72xx-rtc-prefs-dut.yaml`. `ltprefs_boot()`
loads one rtc and one flash preference, a second pair that shares one key as rtc and as flash, and a third
rtc entry (so a hard reset drops three entries in one pass), logs the reason and both results, saves `100 + step`, syncs; after
a templated delay `ltprefs_exit()` bumps the flash-backed step and runs the step's action (`lt_reboot()`,
`__builtin_trap()`, `lt_reboot_wdt()` or idle). The last 8 boot reports are kept in a flash preference and
repeated in the 10 s alive line, so a boot missed by the capture (a power cut, an OTA-only board) is still
readable. Bump `run:` for fresh keys.
- A store to an unmapped address does not fault on the ARM968, the board carries on: use `__builtin_trap()`
for the crash row. The SDK crash handler reboots about 10 s later. Works on BK7238 and BK7231N.
- Hard resets: `serial_capture.py ... --reset` pulses RTS on CEN (reported as Power-On). A real power cut
needs a hand or a smart plug; the history in the alive line covers the boot the host cannot see. On the
Beken SDK a chip-enable reset and a power cut both report Power-On, there is no separate reset-pin source.
- Step 0 and step 8 wait 30 s before acting so the host can open the port (and pulse a reset) after a flash
without losing the first boot; the flash tool's own reset starts the sequence immediately otherwise.
- OTA-only board: keep the device's wifi, api and ota blocks verbatim, keep safe_mode, read logs with
`esphome logs <yaml> --device <name>.local` (the API attaches late, hence the history), and restore the
device's own config afterwards.
- Plain-build comparison: `raw_firmware.bin` differs by the timestamp and by `__LINE__` in log calls of
functions below the added code; map offsets through `objdump -h` load addresses and `addr2line` before
calling a byte difference a code change.
## BK72xx crash report on the next boot (#21012)
Harness: `scripts/libretiny_crash/bkprobe.hpp` plus `templates/bk72xx-crash-probe-dut.yaml`. A flash
preference holds the step; `on_boot` (priority -100) logs the reset reason, the SDK start type, two canaries
and hex dumps of the low SRAM, a set of marker words and the record area after `.bss`, waits 30 s on step 0
(so a capture can be opened after the flash) or 5 s, saves `step + 1`, syncs, then acts: 0 `__builtin_trap()`,
1 call into unmapped memory, 2 unmapped and unaligned reads and a store (no fault on the ARM968), 3
`lt_reboot()`. A `globals: restore_value` step counter does not work here: it is only saved from the global's
loop, which a crash 50 ms later never reaches; save an `ESPPreferenceObject` and `global_preferences->sync()`
first. Bump `run:` for fresh keys. Keep the `wifi:` block: the bug the fix addresses only shows with Wi-Fi up.
Flow: `flash_bk72xx.py` (retry up to three times), one `serial_capture.py ... --reset` capture of 150 to 230 s,
then `grep -a "libretiny.crash\|probe"`. Decode with
`arm-none-eabi-addr2line -pfiaC -e .pioenvs/<name>/raw_firmware.elf <addr...>` (the ELF is `raw_firmware.elf`
on LibreTiny), or run `scripts/libretiny_crash/decode_capture.py <yaml> <capture>` from the config directory
to feed the capture through the CLI's `LogLineProcessor`.
Expected: `*** CRASH DETECTED ON PREVIOUS BOOT ***`, `Reason: Undefined instruction`, PC on the `udf`,
LR = PC + 1, BT0..BT7 walking the lambda, `StatelessLambdaAction`, `Action::play_complex`, `DelayAction`,
`Scheduler::call`, `Application::loop`. The wild call reports where the core ended up (it executes zeros as
Thumb NOPs until it wraps into real code), not the called address. Without the fix a dev build with Wi-Fi
connected never comes back from the crash; see the BK72xx section in `boards.md`.
OTA-only board: copy the device's own config, add the probe include and the step machine (step 0 crashes
45 s after boot, later steps idle), keep its wifi, api, ota and safe_mode blocks and use the secrets file
next to that config. A LibreTiny OTA is a 1.5 MB UF2 and takes minutes on a slow link, so do not wrap the
upload in a short timeout. Read the replayed report with `esphome logs ... --device <name>.local` (an API
client gets it on subscribe) or on the board's serial log port if it has one.
@@ -0,0 +1,12 @@
# mqtt
## MQTT (#20433, #17935, #20762, #20767, #20768)
A local mosquitto with `allow_anonymous true`, `log_type all` and `log_dest file`: the broker log gives
each subscription's QoS, the device address (`New client connected from <ip> as <name>-<mac>`) and exact
subscribe counts. Kill and restart mosquitto to exercise `on_disconnect` (reason 0) and resubscription.
Keep `reboot_timeout: 0s` on `api:` and `mqtt:`. The ESP-IDF outbox swings free heap about 2 kB right
after bulk subscribe and unsubscribe; read the heap seconds later over several rounds before calling a
leak. Spawned subscriptions need about 2.5 s to reach the broker before the next publish.
`esphome logs --device MQTT` only uses the MQTT transport when the config has no `api:`. Driver pattern: a
paho script publishes and polls the serial capture file.
@@ -0,0 +1,6 @@
# nrf52
## nRF52840 dongle boards (#19009, #20814)
Test config: the two LEDs as inverted GPIO outputs, the button as an inverted pull-up binary sensor, a
10 s alive interval. To retest the nrfutil download path, move the cached `sdk-nrf/nrfutil` aside first.
@@ -0,0 +1,20 @@
# online_image
## online_image stalled downloads (#20594)
One classic ESP32, both frameworks (the ESP32 Arduino build also uses `http_request_idf.cpp`). The host
serves the images: `scripts/online_image/make_images.py <www>` writes 16x16 BMP/PNG/JPEG/QOI plus an 8 bit
palette BMP with a 1078 byte header; `scripts/online_image/oi_server.py <www> 8765 <log>` answers one
misbehaviour per path name (`ok`, `truncated`, `large_header`, `empty`, `stall` holds the socket 40 s,
`closed_early`, `slow` pauses 2.5 s, `chunked`, `chunked_cut`, `noclen`, `huge` claims 20 MB) with an ETag
and a 304 on If-None-Match. `templates/online-image-dut.yaml` has two AUTO-format images (default and
256 byte buffer), API actions `fetch`/`fetch_small` (set_url with `update: false`, then component.update)
and `refetch`/`refetch_small` (component.update only, keeps the cache headers), and a restart button.
`scripts/online_image/drive.py <device ip> <api key> <capture> http://<host ip>:8765 pr|base` runs the
case list over aioesphomeapi and judges each from the serial capture. ESP-IDF facts: a stalled server
ends with `Error downloading image: -28679` (EAGAIN) after the transport timeout, an early close with
`-1`, both before and after the PR; a body without Content-Length and without chunking reads as complete
(Size: 0) and finishes. A build without the fix hangs on the truncated, oversized header, empty, chunked PNG
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.
@@ -0,0 +1,5 @@
# openthread
## Thread border router (#18104)
See boards.md for the ESP Thread BR board. Check that the H2 answers Spinel before blaming a PR.
@@ -0,0 +1,8 @@
# ota
## OTA overlap and lock (#20588), OTA timeouts (#19041), deflate (#19037)
All overlap orders (a web upload during a native OTA and the reverse) must be refused; the lock release
after an interrupted web upload waits about 14 s (ESP8266) or 7 s (ESP-IDF) for disconnect detection. A
refused web client on `web_server_idf` never gets the busy reply; the connection drops at reboot.
`pkill -f "a\|b"` does not alternate; use two pkills.
@@ -0,0 +1,18 @@
# rtsp_server
## RTSP camera streaming on the Waveshare ESP32-S3-ETH (#17990)
`templates/rtsp-waveshare-s3-eth-dut.yaml` (OV5640 pins, GPIO8 camera power switch with the 1.2 s boot delay,
`rtsp_server`, logger VERBOSE for `rtsp_server`); swap the `wifi:` block for the W5500 `ethernet:` block when
the cable is in, and add `network: enable_high_performance: true` for the big TCP buffers. Flash over native
USB, capture the log with `serial_capture.py`, take the IP from the last `IP Address` line that is not
0.0.0.0 (or `dns-sd -Q <name>.local`). `scripts/rtsp_server/rtsp_client.py IP [cases]` covers play, udp,
pipeline, badreq, replay, getparam, teardown, third, stall, timeout; `rawrate2.py IP quiet|chatty|rtcp|bigbuf`
measures the raw receive rate; `measure.sh IP LABEL LOGFILE [OUTDIR]` runs the fixed set (ping, ffmpeg 12 s
twice, two ffmpeg clients, raw reader) and appends to `results.txt`; `cam_api.py IP` pulls one image over the
API; `ffmpeg -rtsp_transport tcp -i rtsp://IP:554/ -frames:v 30 -f image2 f%03d.jpg` plus Pillow checks
decoding. The 5760 byte default TCP send buffer makes the stream stall over Wi-Fi (about 1 fps, dozens of
stall drops per run); `enable_high_performance` gives several fps on Wi-Fi and removes the stalls on Ethernet.
Frame counts vary a lot between runs (auto exposure changes the JPEG size), so always measure twice. Home
Assistant's stream pipeline does not accept MJPEG over RTSP; its ffmpeg camera opens one RTSP session per
snapshot, so `max_connections` needs headroom, and go2rtc transcoding to H.264 is the way to a live view.
@@ -0,0 +1,8 @@
# scheduler
## Scheduler and loop wake A/B (#20356)
An external component whose template button arms an SDK `os_timer` (50 ms, one shot) on the ESP8266;
the callback runs in SYS context, records `millis()` and calls `set_timeout` or `defer`; the timeout
logs the elapsed time and loop count delta. Drive with an aioesphomeapi script (`subscribe_logs` plus
`button_command`). Build the same yaml twice with different names, one per tree.
@@ -0,0 +1,11 @@
# udp
## UDP, multicast, sockets (#20556 to #20559, #20585, #20590)
A listener on port 9 needs no privileges on macOS. Test multicast device to device; host to device
multicast never arrives on some access points, and `239.255.x.x` groups are unreliable even to a stock
listener. Broadcast to boards is lossy under power save, unicast is not. Flood at a fixed packet rate
(the host hits `ENOBUFS` unthrottled); 1500 packets per second survived on every board. A forced Wi-Fi
reconnect on the Pico keeps IGMP membership. Set a long loop interval from `wifi.on_connect`
(`App.set_loop_interval(20000)`), not `on_boot`, or Wi-Fi connect crawls. The `logger.log` action
rejects `%.*s`; use a lambda.
@@ -0,0 +1,20 @@
# usb_cdc_acm
## usb_cdc_acm on the nRF52840 dongle (#11634)
`templates/usb-cdc-acm-dongle-dut.yaml`: logger on USB_CDC port 0, `usb_cdc_acm` interface 0 disabled,
interface 1 echoing from a 20 ms lambda, 0x01-prefixed commands for burst, stream, flush, peek, line coding
and counters. Build and flash with `dfu: true` through the app port; the dongle then shows three CDC ports:
the board DTS node `cdc_acm_uart` (unused), `cdc_acm_uart0` (log) and `cdc_acm_uart1` (data), in that order.
Capture the log port with `serial_capture.py`, drive the data port with `scripts/usb_cdc_acm/drive.py
DATA_PORT` (11 cases, prints PASS/FAIL and a summary) and grep the log for `Opened`, `Closed`, `buffer full`,
`flush result`, `line coding`. Boot lines are held in the driver's 1 KB ring until the host opens the port, so
a capture started later still shows the first 1 KB of the boot and then a truncated line.
With three CDC instances the Zephyr legacy stack's `CONFIG_USB_MAX_NUM_TRANSFERS=4` starves the last instance
(each instance holds an OUT read and an IN ZLP), so interface 1 shows DTR changes but moves no data.
`scripts/usb_cdc_acm/cdc_probe.hpp` (include it, call `cdc_probe()` from an interval) reads the Zephyr driver
directly to tell a dead endpoint from a component bug. Raising the transfer pool to 8, or disabling the
board's `cdc_acm_uart` node, fixes it. Design limit of the driver: `fifo_fill` holds `tx_ready` until the
write callback and `write_array` drops when the ring is full, so a 64 KiB full-duplex echo loses one 64 byte
tail on every buffer size.
@@ -0,0 +1,25 @@
# wifi
## `reboot_timeout` with a fallback AP (#9958)
Only one board runs the AP at a time, or stations join the wrong one. AP probe: bogus network,
`ap: {ssid, password, ap_timeout: 5s}`, `reboot_timeout: 60s`, `logger: level: VERBOSE` (ESP32 and
ESP8266 log `AP client connected MAC=` at verbose), `on_boot: logger.log: BOOT`. Station probes:
`reboot_timeout: 0s`, `interval: 150s` running `wifi.disable`, `delay: 20s`, `wifi.enable` (60 s cycles
are too short to cover the due moment). Pass pattern: "Can't connect; rebooting" exactly 60 s after boot
with no station joined, no reboot for minutes while one is, then a reboot within a second of the last
disconnect. The BW15 restarts its AP on every STA retry and cannot be tested this way.
## DHCP lease cache (#17767)
Cycle 20 s awake, `deep_sleep.enter 30s`, wake, 20 s, `App.safe_reboot()`, logging `millis()` at the
`wifi.connected` condition. An RTS hardware reset must fall back to DHCP; a software reboot and a deep
sleep wake must apply the cache. `esphome run --no-logs` silently skipped the upload on one board; use
`upload` then `logs`.
## Wi-Fi Easy Connect (DPP) enrollee (#17934)
Only an Android phone configures: Settings, network details, Add device, Share Wi-Fi (the Wi-Fi list's
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.
@@ -0,0 +1,10 @@
# zephyr
## Zephyr preferences on the nRF52840 dongle (#20810)
`templates/nrf52-preferences-dut.yaml`: logger on USB_CDC at VERBOSE, `flash_write_interval: 5s`, one
`globals` with `restore_value` incremented every 15 s. Give each build its own global id so each first boot
sees a never-saved key (settings survive a reflash). Flow per build: flash through the log port, 40 s capture
of the first boot, then start a second capture and reboot with a 2001 baud touch on the dongle's other CDC
port (the dfu rate callback is global, and the capture holds the log port); `serial_capture.py` reopens the
log port after the USB re-enumeration and gets the buffered boot lines (`load setting`, `Loaded N settings`).
@@ -0,0 +1,17 @@
# zephyr_coredump
## zephyr_coredump crash and decode on nRF52 boards (#13122)
`templates/nrf52-coredump-crash-dut.yaml` (dongle; change `board:` to `adafruit_itsybitsy_nrf52840` for the
ItsyBitsy): `zephyr_coredump:`, `dfu: true`, a `restore_value` global keyed by the `run` substitution
(settings survive a reflash, so bump `run` for every new crash), and an `on_boot` sequence that sets the
flag, yields 3 s so the globals loop saves it (a blocking lambda never gets there and the board crash-loops),
syncs preferences, then writes to 0xDEADBEEF. Flash, keep every port closed for 80 s, then
`scripts/zephyr_coredump/flow.sh LABEL PORT ELF [OUTDIR]` does the three opens (3 s cut, 100 s full print,
20 s reopen) and calls `decode.sh`, which keeps the first complete `#CD:BEGIN#..END#` block, runs Zephyr's
`coredump_serial_log_parser.py` and `coredump_gdbserver.py` from the sdk-nrf ESPHome installed and prints
`bt` and `info threads` with its `arm-zephyr-eabi-gdb`. The ELF is `.pioenvs/<name>/zephyr/zephyr/zephyr.elf`.
Expect about 9 KB dumps, 140 to 150 `#CD:` lines, a 673 ms loop warning for the erase, and `Has stored
dump: 0` on the reopen. Check `compiled on` in every capture: another session flashing the same board
mid-test looks exactly like a failed test otherwise. Boards without a network cannot exercise the API and
MQTT delivery paths of the component; say so in the results.
@@ -0,0 +1,23 @@
# zwave_proxy
## zwave_proxy frame checksums on a live proxy (#20922)
A Z-Wave proxy (ESP32-S3 with the module on `usb_uart`) is a live device: flash it over OTA and read logs
over the API, there is no serial console because the native USB is the host port for the module, so the
boot-time `GET_NETWORK_IDS` exchange is never captured. Home Assistant's Z-Wave JS holds the single API
subscription (it polls `0x3B` every 30 s and resubscribes after every reboot), so do not subscribe from a
script, and keep the device `name:` so it stays on Z-Wave JS; give each build its own scratch directory
instead.
1. `logger: level: VERY_VERBOSE` with `logs: zwave_proxy: VERY_VERBOSE` and DEBUG for the rest: the
`Sending:`, `Received frame:` and `CHECKSUM Received ... Calculated` lines are `ESP_LOGVV`, which needs
the compile-time level.
2. `md5` the two `firmware.ota.bin` files to be sure the A and B builds differ, then `esphome upload
--device <name>.local` and a `timeout 330 esphome logs --device <name>.local` capture started right after
the OTA to catch the resubscribe traffic.
3. `scripts/zwave_proxy/verify_checksums.py <captures>` recomputes the XOR (init 0xFF, LENGTH..payload) over
every logged frame and counts bad checksums and ACK/NAK sent and received; `scripts/zwave_proxy/devinfo.py
<name>.local` prints the firmware version, compile time and the Home ID from the API (a Home ID proves the
device-built `GET_NETWORK_IDS` frame was accepted at boot).
4. Pre-existing: one `zwave_proxy took a long time for an operation` warning on the first frame after the
log client connects, caused by VERY_VERBOSE logging; it appears on dev too.
@@ -0,0 +1,71 @@
"""Subscribe to states and logs of a device over the API for N seconds and summarize its float sensors.
usage: observe.py IP SECONDS OUTFILE [NAME_FILTER]
Every float sensor state whose entity name contains NAME_FILTER (default: all) is appended to OUTFILE
with a relative timestamp, log lines are appended with a LOG prefix, and a one-line summary per sensor
(count, min, max, last) plus the warning and error count is printed at the end.
"""
import asyncio
import pathlib
import re
import sys
import time
from aioesphomeapi import APIClient, LogLevel
IP, SECS, OUT = sys.argv[1], float(sys.argv[2]), sys.argv[3]
FILTER = sys.argv[4] if len(sys.argv) > 4 else ""
# ESPHome LOG_SECRET span; one cut off before its end marker is secret to the end of the line
SECRET = re.compile(r"\x1b\[8m.*?(?:\x1b\[28m|$)", re.DOTALL)
ANSI = re.compile(r"\x1b\[[0-9;]*m")
async def main() -> None:
c = APIClient(IP, 6053, None)
await c.connect(login=True)
di = await c.device_info()
ents, _ = await c.list_entities_services()
names = {e.key: e.name for e in ents}
readings: dict[str, list[float]] = {}
warnings: list[str] = []
t0 = time.monotonic()
with pathlib.Path(OUT).open("w", encoding="utf-8") as out:
out.write(
f"device {di.name} version {di.esphome_version} compiled {di.compilation_time}\n"
)
def on_state(st) -> None:
n = names.get(st.key, str(st.key))
if FILTER in n and hasattr(st, "state") and isinstance(st.state, float):
readings.setdefault(n, []).append(st.state)
out.write(f"[{time.monotonic() - t0:6.1f}] {n}: {st.state}\n")
out.flush()
def on_log(msg) -> None:
line = (
msg.message.decode(errors="replace")
if isinstance(msg.message, bytes)
else msg.message
)
line = ANSI.sub("", SECRET.sub("<secret>", line))
out.write(f"[{time.monotonic() - t0:6.1f}] LOG {line}\n")
out.flush()
if "[W]" in line or "[E]" in line or "Invalid" in line:
warnings.append(line)
c.subscribe_states(on_state)
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_VERBOSE)
await asyncio.sleep(SECS)
await c.disconnect()
summary = "; ".join(
f"{k.split()[-1]} n={len(v)} min={min(v):.3f} max={max(v):.3f} last={v[-1]:.3f}"
for k, v in sorted(readings.items())
)
print(f"{di.compilation_time}: {summary}; warnings/errors {len(warnings)}")
for w in warnings[:5]:
print(" ", w)
asyncio.run(main())
+27
View File
@@ -0,0 +1,27 @@
#!/bin/zsh
# usage: run_obs.sh IP SECONDS LABEL [OUTDIR] [NAME_FILTER]
# Observes states and logs over the API with a concurrent ping and prints a summary: the device-side
# "frames parsed" counter per 10 s, gaps over 5 s between voltage updates seen by the API, and warnings.
# Needs a python with aioesphomeapi (PYTHON, default python3).
D=${0:A:h}
IP=$1; SECS=$2; L=$3; S=${4:-.}; F=${5:-}
PY=${PYTHON:-python3}
setopt pipefail
ping -i 1 -c $SECS $IP > $S/ping-$L.txt 2>&1 &
P=$!
if ! timeout $((SECS + 40)) $PY $D/observe.py $IP $SECS $S/obs-$L.txt "$F" 2>&1 | sed "s/$IP/<ip>/g"; then
echo "observe.py failed or timed out, no summary"; wait $P; exit 1
fi
wait $P
echo "ping: $(tail -2 $S/ping-$L.txt | head -1 | sed "s/$IP/<ip>/g") $(tail -1 $S/ping-$L.txt)"
python3 - "$S/obs-$L.txt" <<'PY'
import re, sys
txt = open(sys.argv[1]).read()
fr = [int(m.group(1)) for m in re.finditer(r"frames parsed: (\d+) in 10 s", txt)]
print(f"device frame counter per 10 s: n={len(fr)} min={min(fr) if fr else None} max={max(fr) if fr else None} mean={sum(fr)/len(fr) if fr else 0:.1f}")
ts = [float(m.group(1)) for m in re.finditer(r"^\[\s*([0-9.]+)\] .*Voltage: ", txt, re.M)]
gaps = [round(b - a, 1) for a, b in zip(ts, ts[1:]) if b - a > 5]
print(f"API voltage updates {len(ts)}, gaps over 5 s: {gaps}")
warn = [l for l in txt.splitlines() if "LOG" in l and ("[W]" in l or "[E]" in l or "Invalid" in l)]
print(f"warnings/errors in log: {len(warn)}", *warn[:3], sep="\n ")
PY
@@ -0,0 +1,51 @@
"""ltchiptool for boards whose RTS drives CEN: reset and link on one open port.
Stock ltchiptool closes and reopens the port after its DTR/RTS reset, and
pyserial raises both lines on open, holding CEN low through the BootROM's
listen window. Open with both lines released and pulse the reset right
before the LinkChecks instead.
"""
import sys
import bk7231tools.serial.main as bk_main
from ltchiptool.__main__ import cli
from ltchiptool.soc.bk72xx.flash import BK72XXFlash
from serial import Serial
class ReleasedSerial(Serial):
def __init__(self, port=None, **kwargs):
super().__init__(None, **kwargs)
self.dtr = False
self.rts = False
self.port = port
self.open()
bk_main.Serial = ReleasedSerial
_connect = BK72XXFlash.flash_connect
def flash_hw_reset(self) -> None:
self._reset_pending = True
def flash_connect(self) -> None:
pending = getattr(self, "_reset_pending", False)
if self.bk and self.conn.linked and not pending:
return
self.flash_build_protocol()
if pending:
self._reset_pending = False
self.bk.hw_reset()
self.conn.linked = False # the chip rebooted, so link again below
_connect(self)
BK72XXFlash.flash_hw_reset = flash_hw_reset
BK72XXFlash.flash_connect = flash_connect
sys.argv = ["ltchiptool", *sys.argv[1:]]
cli()
@@ -0,0 +1,87 @@
"""ltchiptool for the BW15 kit whose CH340 drives CEN (RTS) and PA00 (DTR).
A plain open asserts both lines and holds the chip in reset. Here every open keeps DTR high and RTS
released; the first open pulses RTS so the ROM comes up; after the command DTR low plus an RTS pulse
boots the app. The exit status is ltchiptool's; 2 means the flash succeeded but the app was not
booted (boot pulse failed, or no -d PORT to send it to), so the board sits in the ROM until reset.
Usage: python flash_rtl87xx_kit.py flash write -d <port> firmware.uf2
"""
import sys
import time
import serial
_first = [True]
_orig_open = serial.Serial.open
def _open(self):
self.dtr = True # PA00 high: ROM download mode on the next reset
self.rts = False # CEN released
_orig_open(self)
if _first[0]:
_first[0] = False
self.rts = True
time.sleep(0.1)
self.rts = False
time.sleep(0.3) # reset into the ROM
self.reset_input_buffer()
serial.Serial.open = _open
from ltchiptool.__main__ import cli_entrypoint # noqa: E402
def port_argument(argv: list[str]) -> str | None:
for i, a in enumerate(argv):
if a in ("-d", "--device") and i + 1 < len(argv):
return argv[i + 1]
if a.startswith(("-d=", "--device=")):
return a.split("=", 1)[1]
if a.startswith("-d") and len(a) > 2:
return a[2:]
return None
def boot_app(port: str) -> None:
s = serial.Serial(port=None)
s.port = port
s.dtr = False
s.rts = False
_orig_open(s)
s.rts = True
time.sleep(0.1)
s.rts = False
s.close()
def main() -> int:
rc = 1
try:
result = cli_entrypoint.main(args=sys.argv[1:], standalone_mode=False)
rc = int(result) if isinstance(result, int) else 0
finally:
port = port_argument(sys.argv)
if not port:
print(
"[flash_rtl87xx_kit] no -d PORT given, the application was not booted",
file=sys.stderr,
)
if rc == 0:
rc = 2
else:
try:
boot_app(port)
print("[flash_rtl87xx_kit] application booted")
except (serial.SerialException, OSError) as err:
print(f"[flash_rtl87xx_kit] boot pulse failed: {err}", file=sys.stderr)
if rc == 0:
rc = 2 # flashed, but the board is still in the ROM: press reset
return rc
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,116 @@
// BK72xx crash probe: forces one crash per step and shows what the SDK and the bootloader leave behind.
// Steps: 0 undefined instruction, 1 call into unmapped memory, 2 unmapped and unaligned accesses (no fault),
// 3 lt_reboot(). The step lives in a flash preference saved and synced before the crash.
#pragma once
#include <Arduino.h>
#include <FreeRTOS.h>
#include <task.h>
#include <cstdint>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
extern "C" unsigned bk_misc_get_start_type(void);
extern "C" char _bss_end[];
static constexpr uint32_t PROBE_CANARY_LOW = 0x004000F8; // free word in the SDK-reserved low SRAM
static constexpr uint32_t PROBE_CANARY_ITCM = 0x003F3FF0; // top of ITCM
static char bkprobe_report[112];
static constexpr uint32_t PROBE_MARKS[] = {0x40A1B0, 0x40B000, 0x40C000, 0x40E000, 0x410000, 0x414000, 0x418000,
0x41C000, 0x420000, 0x428000, 0x430000, 0x438000, 0x43C000, 0x43E000,
0x43F000, 0x43F800, 0x43FC00, 0x43FE00, 0x43FFF0};
static constexpr size_t PROBE_MARK_COUNT = sizeof(PROBE_MARKS) / sizeof(PROBE_MARKS[0]);
static inline uint32_t rd(uint32_t a) { return *(volatile uint32_t *) a; }
inline esphome::ESPPreferenceObject bkprobe_pref(uint32_t run) {
return esphome::global_preferences->make_preference<int>(0xBC000000u + run);
}
inline int bkprobe_boot(uint32_t run) {
using namespace esphome;
int step = 0;
bkprobe_pref(run).load(&step);
unsigned st = bk_misc_get_start_type();
snprintf(bkprobe_report, sizeof(bkprobe_report), "step=%d reason=%s(%d) start_type=%u low=%08lx itcm=%08lx top=%08lx",
step, lt_get_reboot_reason_name(lt_get_reboot_reason()), (int) lt_get_reboot_reason(), st,
(unsigned long) rd(PROBE_CANARY_LOW), (unsigned long) rd(PROBE_CANARY_ITCM),
(unsigned long) rd(0x0043FFF0u));
ESP_LOGI("probe", "%s", bkprobe_report);
for (uint32_t a = 0x400000; a < 0x400100; a += 32) {
ESP_LOGI("probe", "%06lx: %08lx %08lx %08lx %08lx %08lx %08lx %08lx %08lx", (unsigned long) a,
(unsigned long) rd(a), (unsigned long) rd(a + 4), (unsigned long) rd(a + 8), (unsigned long) rd(a + 12),
(unsigned long) rd(a + 16), (unsigned long) rd(a + 20), (unsigned long) rd(a + 24),
(unsigned long) rd(a + 28));
}
for (size_t i = 0; i < PROBE_MARK_COUNT; i += 7) {
char line[120];
int n = 0;
for (size_t j = i; j < i + 7 && j < PROBE_MARK_COUNT; j++)
n += snprintf(line + n, sizeof(line) - n, " %06lx=%08lx", (unsigned long) PROBE_MARKS[j],
(unsigned long) rd(PROBE_MARKS[j]));
ESP_LOGI("probe", "marks%s", line);
}
uint32_t rec = ((uint32_t) _bss_end + 7) & ~7u;
for (uint32_t a = rec; a < rec + 64; a += 32) {
ESP_LOGI("probe", "rec %06lx: %08lx %08lx %08lx %08lx %08lx %08lx %08lx %08lx", (unsigned long) a,
(unsigned long) rd(a), (unsigned long) rd(a + 4), (unsigned long) rd(a + 8), (unsigned long) rd(a + 12),
(unsigned long) rd(a + 16), (unsigned long) rd(a + 20), (unsigned long) rd(a + 24),
(unsigned long) rd(a + 28));
}
*(volatile uint32_t *) PROBE_CANARY_LOW = 0xC0DE0000u | (uint32_t) step;
*(volatile uint32_t *) PROBE_CANARY_ITCM = 0x17C40000u | (uint32_t) step;
return step;
}
__attribute__((noinline)) void bkprobe_data_abort() {
using namespace esphome;
volatile uint32_t v = rd(0xFFFFFFF0);
ESP_LOGI("probe", "read 0xFFFFFFF0 -> %08lx (no fault)", (unsigned long) v);
uint32_t r;
__asm__ volatile("ldr %0, [%1]" : "=r"(r) : "r"(0x00400101) : "memory");
ESP_LOGI("probe", "unaligned ldr 0x400101 -> %08lx (no fault)", (unsigned long) r);
*(volatile uint32_t *) 0xFFFFFFF0 = 1;
ESP_LOGI("probe", "store 0xFFFFFFF0 (no fault)");
}
// act is the step number before it was bumped
inline void bkprobe_act(uint32_t run, int act) {
using namespace esphome;
int next = act + 1;
bkprobe_pref(run).save(&next);
global_preferences->sync();
ESP_LOGI("probe", "acting on step %d (saved %d)", act, next);
// mark candidate storage areas so the next boot shows what survives
uint32_t rec = ((uint32_t) _bss_end + 7) & ~7u;
for (uint32_t i = 0; i < 8; i++)
*(volatile uint32_t *) (rec + 32 + i * 4) = 0xA5A50000u | i; // second half of the record area
for (size_t i = 0; i < PROBE_MARK_COUNT; i++)
*(volatile uint32_t *) PROBE_MARKS[i] = 0x70700000u | (uint32_t) i;
ESP_LOGI("probe", "ramtop before: %08lx", (unsigned long) rd(0x0043FFF0u));
vTaskDelay(pdMS_TO_TICKS(50));
switch (act) {
case 0:
__builtin_trap(); // undefined instruction
break;
case 1: {
void (*fn)() = (void (*)()) 0xFFFFFFF1; // prefetch abort (unmapped, Thumb)
fn();
ESP_LOGI("probe", "call 0xFFFFFFF1 returned (no fault)");
break;
}
case 2:
bkprobe_data_abort();
break;
case 3:
lt_reboot();
break;
default:
break;
}
}
inline void bkprobe_alive(int step) {
using namespace esphome;
ESP_LOGI("probe", "alive step=%d uptime=%lus boot: %s", step, (unsigned long) (millis() / 1000), bkprobe_report);
}
@@ -0,0 +1,25 @@
"""Feed a serial capture through the CLI's stacktrace decoder for a build.
usage: decode_capture.py CONFIG_YAML CAPTURE
Run from the directory that holds the config (its .esphome build is used), with the esphome checkout that
built it on PYTHONPATH. Lines of the crash report are echoed, decoded frames follow them.
"""
import logging
from pathlib import Path
import sys
from esphome.config import read_config
from esphome.core import CORE
from esphome.stacktrace import LogLineProcessor
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
yaml, capture = Path(sys.argv[1]), Path(sys.argv[2])
CORE.config_path = yaml
config = read_config({})
proc = LogLineProcessor(config, CORE.target_platform)
for line in capture.read_text(encoding="utf-8", errors="replace").splitlines():
if "libretiny.crash" in line:
print(line[:120])
proc.process_line(line)
@@ -0,0 +1,155 @@
#pragma once
// BK7238 bench harness for esphome#21007: one rtc and one flash preference, a step counter in flash,
// and an exit action per step so the board drives its own soft reboot, crash and watchdog reboot.
#include <libretiny.h>
#include <cstring>
#include <FreeRTOS.h>
#include <task.h>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
using namespace esphome;
// Boot report kept for the alive line, so a boot the host could not capture is still visible
static char ltprefs_report[128];
// Last boots' reports, kept in a flash preference so boots the host never saw stay readable
struct LtprefsHistory {
char line[8][128];
};
static LtprefsHistory ltprefs_hist;
inline ESPPreferenceObject ltprefs_step_pref(uint32_t run) {
return global_preferences->make_preference<int>(0x3B210070u ^ run, true);
}
// Loads the step and both preferences, logs them, saves this step's value.
// Returns the exit action: 0 idle, 1 lt_reboot, 2 crash, 3 watchdog.
inline int ltprefs_boot(int &step, uint32_t run) {
auto reason = lt_get_reboot_reason();
step = 0;
ltprefs_step_pref(run).load(&step);
auto rtcp = global_preferences->make_preference<uint32_t>(0x1B210070u ^ run, false);
auto flp = global_preferences->make_preference<uint32_t>(0x2B210070u ^ run, true);
uint32_t rv = 0, fv = 0;
// a second pair sharing one key as rtc and as flash, and a third rtc entry, so a hard reset drops three
auto rtcp2 = global_preferences->make_preference<uint32_t>(0x5B210070u ^ run, false);
auto flp2 = global_preferences->make_preference<uint32_t>(0x5B210070u ^ run, true);
auto rtcp3 = global_preferences->make_preference<uint32_t>(0x6B210070u ^ run, false);
uint32_t rv2 = 0, fv2 = 0, rv3 = 0;
bool rok = rtcp.load(&rv);
bool fok = flp.load(&fv);
bool rok2 = rtcp2.load(&rv2);
bool fok2 = flp2.load(&fv2);
bool rok3 = rtcp3.load(&rv3);
snprintf(ltprefs_report, sizeof(ltprefs_report),
"step=%d reason=%s(%d) rtc=%s%u flash=%s%u sharedkey rtc=%s%u flash=%s%u rtc3=%s%u", step,
lt_get_reboot_reason_name(reason), (int) reason, rok ? "" : "none ", rv, fok ? "" : "none ", fv,
rok2 ? "" : "none ", rv2, fok2 ? "" : "none ", fv2, rok3 ? "" : "none ", rv3);
ESP_LOGI("lttest", "%s", ltprefs_report);
auto histp = global_preferences->make_preference<LtprefsHistory>(0x4B210070u ^ run, true);
if (!histp.load(&ltprefs_hist))
memset(&ltprefs_hist, 0, sizeof(ltprefs_hist));
memmove(&ltprefs_hist.line[1], &ltprefs_hist.line[0], sizeof(ltprefs_hist.line) - sizeof(ltprefs_hist.line[0]));
strncpy(ltprefs_hist.line[0], ltprefs_report, sizeof(ltprefs_hist.line[0]) - 1);
ltprefs_hist.line[0][sizeof(ltprefs_hist.line[0]) - 1] = 0;
if (!histp.save(&ltprefs_hist)) {
ESP_LOGW("lttest", "history save failed");
}
int act = 0;
bool save = true;
switch (step) {
case 0:
act = 1;
break; // soft reboot
case 1:
act = 2;
break; // crash
case 2:
act = 3;
break; // watchdog
case 3:
act = 0;
break; // idle: chip-enable reset from the host
case 4:
act = 1;
break; // soft reboot after the hard reset
case 5:
act = 0;
break; // idle: chip-enable reset from the host
case 6:
act = 1;
save = false;
break; // no save, soft reboot: deleted, not skipped?
case 7:
act = 0;
break; // idle
case 8:
act = 2;
break; // crash
case 9:
act = 0;
break; // idle
case 10:
act = 0;
break; // idle: power cut by the user
default:
act = 0;
save = false;
break;
}
if (save) {
uint32_t v = 100 + step, v2r = 200 + step, v2f = 300 + step, v3 = 400 + step;
bool srtc = rtcp.save(&v);
bool sfl = flp.save(&v);
bool srtc2 = rtcp2.save(&v2r);
bool sfl2 = flp2.save(&v2f);
bool srtc3 = rtcp3.save(&v3);
bool synced = global_preferences->sync();
ESP_LOGI("lttest", "save %u/%u/%u/%u: rtc=%d flash=%d sharedkey rtc=%d flash=%d rtc3=%d sync=%d", v, v2r, v2f, v3,
srtc, sfl, srtc2, sfl2, srtc3, synced);
}
ESP_LOGI("lttest", "exit action=%d", act);
return act;
}
inline void ltprefs_exit(int &step, uint32_t run, int act) {
step++;
bool saved = ltprefs_step_pref(run).save(&step);
bool synced = global_preferences->sync();
ESP_LOGI("lttest", "step %d save=%d sync=%d", step, saved, synced);
if (!saved || !synced) {
ESP_LOGE("lttest", "step not stored, staying idle instead of repeating the step");
return;
}
vTaskDelay(pdMS_TO_TICKS(200));
switch (act) {
case 1:
ESP_LOGW("lttest", "soft reboot now");
vTaskDelay(pdMS_TO_TICKS(50));
lt_reboot();
break;
case 2:
ESP_LOGW("lttest", "crashing now");
vTaskDelay(pdMS_TO_TICKS(50));
__builtin_trap(); // undefined instruction: a plain bad store does not fault on the ARM968
break;
case 3:
ESP_LOGW("lttest", "watchdog reboot now");
vTaskDelay(pdMS_TO_TICKS(50));
lt_reboot_wdt();
break;
default:
ESP_LOGW("lttest", "idle, waiting for an external reset");
break;
}
}
inline void ltprefs_alive(int step, int act) {
ESP_LOGI("lttest", "alive step=%d act=%d boot: %s", step, act, ltprefs_report);
for (int i = 7; i >= 0; i--) {
if (ltprefs_hist.line[i][0] != 0) {
ESP_LOGI("lttest", " history[-%d]: %s", i, ltprefs_hist.line[i]);
}
}
}
@@ -0,0 +1,179 @@
"""Drive the online_image cases over the native API and judge each from the serial capture."""
import asyncio
import pathlib
import re
import sys
import time
from aioesphomeapi import (
APIClient,
APIConnectionError,
InvalidEncryptionKeyAPIError,
RequiresEncryptionAPIError,
)
HOST, KEY, CAPTURE, BASE, MODE = sys.argv[1:6]
CAP_S = 45
CASES = [
# (label, action, path, expected outcome)
("ok bmp", "fetch", "ok.bmp", "finished"),
("ok png", "fetch", "ok.png", "finished"),
("ok jpg", "fetch", "ok.jpg", "finished"),
("ok qoi", "fetch", "ok.qoi", "finished"),
("truncated bmp", "fetch", "truncated.bmp", "error"),
("ok bmp after error", "fetch", "ok.bmp", "finished"),
("large header, 256 byte buffer", "fetch_small", "large_header.bmp", "error"),
("ok bmp, 256 byte buffer", "fetch_small", "ok.bmp", "finished"),
("empty body", "fetch", "empty.bmp", "error"),
("stalled connection", "fetch", "stall.bmp", "error"),
("closed early", "fetch", "closed_early.bmp", "error"),
("slow but alive", "fetch", "slow.bmp", "finished"),
("chunked bmp", "fetch", "chunked.bmp", "finished"),
("chunked png", "fetch", "chunked.png", "finished"),
("chunked qoi", "fetch", "chunked.qoi", "finished"),
("chunked bmp cut", "fetch", "chunked_cut.bmp", "error"),
("chunked png cut", "fetch", "chunked_cut.png", "any"),
("no content-length", "fetch", "noclen.bmp", "any"),
("huge jpeg claim", "fetch", "huge.jpg", "error"),
("ok jpg after huge", "fetch", "ok.jpg", "finished"),
("ok bmp again", "fetch", "ok.bmp", "finished"),
("ok bmp refetch: 304", "refetch", "", "finished"),
("ok png", "fetch", "ok.png", "finished"),
("ok png refetch: 304", "refetch", "", "finished"),
("chunked bmp", "fetch", "chunked.bmp", "finished"),
("chunked bmp refetch: 304", "refetch", "", "finished"),
("chunked png", "fetch", "chunked.png", "finished"),
("chunked png refetch: not cached", "refetch", "", "finished"),
("chunked png cut", "fetch", "chunked_cut.png", "any"),
("chunked png cut refetch: not cached", "refetch", "", "any"),
]
if MODE == "base":
# Each stuck case is followed by a fetch that shows the symptom, then a reboot.
CASES = [
("ok bmp", "fetch", "ok.bmp", "finished"),
("truncated bmp", "fetch", "truncated.bmp", "error"),
("ok bmp after truncated", "fetch", "ok.bmp", "finished"),
("REBOOT", "", "", ""),
("large header, 256 byte buffer", "fetch_small", "large_header.bmp", "error"),
("ok bmp, 256 byte buffer", "fetch_small", "ok.bmp", "finished"),
("REBOOT", "", "", ""),
("empty body", "fetch", "empty.bmp", "error"),
("ok bmp after empty", "fetch", "ok.bmp", "finished"),
("REBOOT", "", "", ""),
("stalled connection", "fetch", "stall.bmp", "error"),
("ok bmp after stall", "fetch", "ok.bmp", "finished"),
("REBOOT", "", "", ""),
("closed early", "fetch", "closed_early.bmp", "error"),
("ok bmp after closed early", "fetch", "ok.bmp", "finished"),
("REBOOT", "", "", ""),
("chunked png", "fetch", "chunked.png", "finished"),
("ok bmp after chunked png", "fetch", "ok.bmp", "finished"),
("REBOOT", "", "", ""),
("chunked bmp cut", "fetch", "chunked_cut.bmp", "error"),
("ok bmp after chunked cut", "fetch", "ok.bmp", "finished"),
("REBOOT", "", "", ""),
("huge jpeg claim", "fetch", "huge.jpg", "error"),
("ok jpg after huge", "fetch", "ok.jpg", "finished"),
]
OUTCOME = re.compile(r"download (finished|error): (img_small|img)")
INTEREST = re.compile(
r"\[(E|W)\]\[online_image|Image already being updated|download (finished|error)|Downloading image|Updating image|Image fully downloaded|HTTP 304|allocat|Panic|abort\(\)|Guru|Backtrace|rst:0x",
re.IGNORECASE,
)
def tail(path: str, offset: int) -> tuple[list[str], int]:
with pathlib.Path(path).open("rb") as f:
f.seek(offset)
data = f.read()
return data.decode("utf-8", "replace").splitlines(), offset + len(data)
async def connect() -> tuple[APIClient, dict, int]:
for _ in range(60):
try:
c = APIClient(HOST, 6053, None, noise_psk=KEY)
await c.connect(login=True)
ents, services = await c.list_entities_services()
except (InvalidEncryptionKeyAPIError, RequiresEncryptionAPIError) as exc:
raise SystemExit(f"API key problem, not retrying: {exc}") from exc
except (APIConnectionError, OSError) as exc:
# only connection trouble is retried; a wrong key or a missing button must fail at once
print(f"connect: {exc}", flush=True)
await asyncio.sleep(3)
continue
btn = next(
(e.key for e in ents if getattr(e, "object_id", "") == "reboot"), None
)
if btn is None:
raise SystemExit("the config has no reboot button")
return c, {s.name: s for s in services}, btn
raise SystemExit("API never connected")
async def main() -> None:
client, actions, btn = await connect()
with pathlib.Path(CAPTURE).open("rb") as f:
f.seek(0, 2)
offset = f.tell()
rows = []
for label, action, path, expect in CASES:
if label == "REBOOT":
client.button_command(btn)
await asyncio.sleep(1)
await client.disconnect()
await asyncio.sleep(8)
client, actions, btn = await connect()
_, offset = tail(CAPTURE, offset)
print("--- rebooted", flush=True)
continue
img = "img_small" if action.endswith("_small") else "img"
t0 = time.monotonic()
args = {} if action.startswith("refetch") else {"url": f"{BASE}/{path}"}
await client.execute_service(actions[action], args)
outcome, notes = "no outcome", []
while time.monotonic() - t0 < CAP_S:
await asyncio.sleep(0.5)
lines, offset = tail(CAPTURE, offset)
done = False
for line in lines:
if INTEREST.search(line):
notes.append(line.strip())
m = OUTCOME.search(line)
if m and m.group(2) == img:
outcome = m.group(1)
done = True
if done:
break
took = time.monotonic() - t0
if outcome == "no outcome":
took = CAP_S
# drain a little more so the "already being updated" line lands
await asyncio.sleep(1)
lines, offset = tail(CAPTURE, offset)
notes += [line.strip() for line in lines if INTEREST.search(line)]
ok = outcome == expect or (expect == "any" and outcome != "no outcome")
rows.append((label, path, expect, outcome, took, ok, notes))
print(
f"{'PASS' if ok else 'FAIL'} {label:32s} {path:18s} expected {expect:9s} got {outcome:10s} {took:5.1f}s",
flush=True,
)
for n in notes:
print(f" {n}", flush=True)
await asyncio.sleep(1.5)
await client.disconnect()
print(
"\nSUMMARY",
"all expectations met"
if all(r[5] for r in rows)
else "MISMATCHES: " + ", ".join(r[0] for r in rows if not r[5]),
flush=True,
)
if not all(r[5] for r in rows):
sys.exit(1)
asyncio.run(main())
@@ -0,0 +1,38 @@
"""Build the small test images the HTTP server serves."""
from pathlib import Path
import struct
import sys
from PIL import Image
out = Path(sys.argv[1])
out.mkdir(exist_ok=True)
w = h = 16
im = Image.new("RGB", (w, h))
px = im.load()
for y in range(h):
for x in range(w):
px[x, y] = (x * 16, y * 16, (x + y) * 8)
im.save(out / "ok.bmp", "BMP")
im.save(out / "ok.png", "PNG")
im.save(out / "ok.jpg", "JPEG", quality=90)
# QOI with only QOI_OP_RGB codes: valid and trivially encoded.
qoi = b"qoif" + struct.pack(">IIBB", w, h, 3, 0)
for y in range(h):
for x in range(w):
qoi += bytes([0xFE, *px[x, y]])
qoi += bytes(7) + b"\x01"
(out / "ok.qoi").write_bytes(qoi)
# 8x8 8-bit palette BMP: 1078 byte header, pixels start beyond a 256 byte buffer.
pw = ph = 8
data_offset = 14 + 40 + 256 * 4
file_size = data_offset + pw * ph
hdr = b"BM" + struct.pack("<IHHI", file_size, 0, 0, data_offset)
dib = struct.pack("<IiiHHIIiiII", 40, pw, ph, 1, 8, 0, pw * ph, 0, 0, 256, 0)
(out / "large_header.bmp").write_bytes(hdr + dib + bytes(256 * 4) + bytes(pw * ph))
for p in sorted(out.iterdir()):
print(p.name, p.stat().st_size)
@@ -0,0 +1,142 @@
"""HTTP server with one misbehaviour per path, for the online_image stalled-download test."""
import asyncio
from pathlib import Path
import sys
import time
WWW = Path(sys.argv[1])
PORT = int(sys.argv[2])
LOG = Path(sys.argv[3]).open("a", encoding="utf-8", buffering=1) # noqa: SIM115
MIME = {"bmp": "image/bmp", "png": "image/png", "jpg": "image/jpeg", "qoi": "image/qoi"}
IMG = {n: (WWW / f"ok.{n}").read_bytes() for n in MIME}
BMP = IMG["bmp"]
BMP_HDR = 54 # 16x16 24-bit: pixel rows follow directly
LARGE_HEADER = (WWW / "large_header.bmp").read_bytes()
def log(msg: str) -> None:
LOG.write(f"{time.strftime('%H:%M:%S')} {msg}\n")
def headers(ctype: str, extra: list[bytes]) -> bytes:
lines = [
b"HTTP/1.1 200 OK",
b"Content-Type: " + ctype.encode(),
b"Connection: close",
*extra,
b"",
b"",
]
return b"\r\n".join(lines)
def etag(path: str) -> bytes:
return b'"' + path.strip("/").encode() + b'-v1"'
def chunked(body: bytes, size: int) -> bytes:
out = b""
for i in range(0, len(body), size):
part = body[i : i + size]
out += b"%x\r\n" % len(part) + part + b"\r\n"
return out + b"0\r\n\r\n"
async def respond(path: str, req: bytes, w: asyncio.StreamWriter) -> str:
name, _, ext = path.lstrip("/").rpartition(".")
ctype = MIME.get(ext, "application/octet-stream")
body = IMG.get(ext, BMP)
tag = [b"ETag: " + etag(path)]
if b"if-none-match: " + etag(path).lower() in req.lower():
w.write(
b"HTTP/1.1 304 Not Modified\r\nETag: "
+ etag(path)
+ b"\r\nConnection: close\r\n\r\n"
)
return "304 not modified"
if name == "ok":
w.write(headers(ctype, [b"Content-Length: %d" % len(body), *tag]) + body)
return f"full {len(body)} bytes"
if name == "truncated": # cut one byte into the sixth pixel, Content-Length matches
cut = BMP[: BMP_HDR + 5 * 3 + 1]
w.write(headers(ctype, [b"Content-Length: %d" % len(cut)]) + cut)
return f"truncated {len(cut)} bytes"
if name == "large_header":
w.write(
headers(ctype, [b"Content-Length: %d" % len(LARGE_HEADER)]) + LARGE_HEADER
)
return f"large header {len(LARGE_HEADER)} bytes"
if name == "empty":
w.write(headers(ctype, [b"Content-Length: 0"]))
return "empty body"
if (
name == "stall"
): # claims the full length, sends part, then holds the socket open
part = body[: BMP_HDR + 15]
w.write(headers(ctype, [b"Content-Length: %d" % len(body)]) + part)
await w.drain()
await asyncio.sleep(40)
return f"stalled after {len(part)} bytes, closed after 40 s"
if name == "closed_early": # claims the full length, sends part, closes at once
part = body[: BMP_HDR + 15]
w.write(headers(ctype, [b"Content-Length: %d" % len(body)]) + part)
return f"closed after {len(part)} of {len(body)} bytes"
if name == "slow": # pauses under the timeout, then completes
w.write(headers(ctype, [b"Content-Length: %d" % len(body)]) + body[:BMP_HDR])
await w.drain()
await asyncio.sleep(2.5)
w.write(body[BMP_HDR:])
return f"full {len(body)} bytes with a 2.5 s pause"
if name == "chunked":
w.write(
headers(ctype, [b"Transfer-Encoding: chunked", *tag]) + chunked(body, 64)
)
return f"chunked full {len(body)} bytes"
if (
name == "chunked_cut"
): # chunked, ends cleanly at a pixel boundary (or half a png)
cut = body[: BMP_HDR + 5 * 3] if ext == "bmp" else body[: len(body) // 2]
w.write(
headers(ctype, [b"Transfer-Encoding: chunked", *tag]) + chunked(cut, 64)
)
return f"chunked cut {len(cut)} of {len(body)} bytes"
if name == "noclen": # body delimited by connection close
w.write(headers(ctype, []) + body)
return f"no content-length, full {len(body)} bytes"
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"
async def handle(r: asyncio.StreamReader, w: asyncio.StreamWriter) -> None:
peer = w.get_extra_info("peername")
try:
async with asyncio.timeout(5):
line = await r.readuntil(b"\r\n")
req = await r.readuntil(b"\r\n\r\n")
path = line.split(b" ")[1].decode()
log(
f"GET {path} from {peer}"
+ (" (If-None-Match)" if b"if-none-match" in req.lower() else "")
)
what = await respond(path, req, w)
await w.drain()
log(f" -> {path}: {what}")
except Exception as exc: # noqa: BLE001
log(f" !! {peer}: {exc!r}")
finally:
w.close()
async def main() -> None:
server = await asyncio.start_server(handle, "0.0.0.0", PORT)
log(f"listening on {PORT}")
async with server:
await server.serve_forever()
asyncio.run(main())
@@ -0,0 +1,123 @@
"""List the boards on USB, or wait for one to enumerate.
ports.py table: port, vid:pid, product, serial number, guess
ports.py --wait 2e8a print the first port with VID 0x2e8a once it appears (default 60 s)
ports.py --wait 1a86:7523 --timeout 30
ports.py --product "USB Serial" first port whose USB product string matches (case-insensitive)
ports.py --wait 2e8a --serial SN the port of one specific board, whatever name it comes back under
ports.py --serial-of PORT print the USB serial number behind a port name
Run with a Python that has pyserial (any ESPHome venv). Port names follow the hub port on macOS, so
re-run this after every replug instead of trusting a name from an earlier session.
"""
import argparse
import sys
import time
from serial.tools import list_ports
GUESS = [
(
(0x1A86, 0x7523),
"CH340: D1 mini, BW15 kit, BK7238/CB3S or LN882H adapter; identify by the boot banner",
),
((0x1A86, 0x55D3), "CH343: ESP32-P4 devkit console"),
((0x1A86, 0x55D4), "CH343: ESP32 devkit"),
((0x1A86, None), "WCH CH34x USB serial bridge"),
((0x10C4, 0xEA60), "CP2102: classic ESP32 devkit (esp32dev)"),
(
(0x303A, 0x1001),
"Espressif USB JTAG serial: S3/C3/C6/H2 native USB (logger hardware_uart USB_SERIAL_JTAG)",
),
(
(0x2E8A, None),
"Raspberry Pi Pico W / Pico 2 W CDC; the port vanishes and comes back on every reboot",
),
((0x1915, 0x521F), "nRF52840 Dongle (PCA10059) in the Open DFU bootloader"),
(
(0x2FE3, 0x0100),
"nRF52840 Dongle running a Zephyr app; two CDC ports, ESPHome log on the second",
),
((0x1915, 0xCAFE), "Makerdiary nRF52840 MDK dongle running a Zephyr app"),
((0x239A, None), "Adafruit style nRF52 board (UF2 bootloader family)"),
]
def guess(vid: int, pid: int) -> str:
for (gv, gp), text in GUESS:
if vid == gv and (gp is None or pid == gp):
return text
return ""
def parse_id(text: str) -> tuple[int, int | None]:
vid, _, pid = text.partition(":")
return int(vid, 16), (int(pid, 16) if pid else None)
def matches(
port, vid: int | None, pid: int | None, product: str | None, serial: str | None
) -> bool:
if vid is not None and (port.vid != vid or (pid is not None and port.pid != pid)):
return False
if product and (port.product or "").lower() != product.lower():
return False
return not serial or (port.serial_number or "") == serial
def main() -> int:
ap = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
ap.add_argument(
"--wait",
metavar="VID[:PID]",
help="block until a matching port appears, print it",
)
ap.add_argument("--product", help="USB product string to match")
ap.add_argument("--serial", help="USB serial number to match")
ap.add_argument(
"--serial-of", metavar="PORT", help="print the USB serial number of PORT"
)
ap.add_argument("--timeout", type=float, default=60)
args = ap.parse_args()
if args.serial_of:
for p in list_ports.comports():
if p.device == args.serial_of and p.serial_number:
print(p.serial_number)
return 0
print(f"no USB serial number for {args.serial_of}", file=sys.stderr)
return 1
vid = pid = None
if args.wait:
vid, pid = parse_id(args.wait)
if args.wait or args.product or args.serial:
deadline = time.time() + args.timeout
while True:
for p in sorted(list_ports.comports(), key=lambda p: p.device):
if p.vid and matches(p, vid, pid, args.product, args.serial):
print(p.device)
return 0
if time.time() >= deadline:
print("no matching port", file=sys.stderr)
return 1
time.sleep(1)
rows = [p for p in list_ports.comports() if p.vid]
if not rows:
print("no USB serial ports")
return 1
for p in sorted(rows, key=lambda p: p.device):
print(
f"{p.device:32} {p.vid:04x}:{p.pid:04x} {p.product or '?':34} "
f"{p.serial_number or '':18} {guess(p.vid, p.pid)}"
)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,33 @@
"""Request one camera image over the native API and report its size (direct esp32_camera check)."""
import asyncio
import sys
from aioesphomeapi import APIClient
async def main():
c = APIClient(sys.argv[1], 6053, None)
await c.connect(login=True)
ents, _ = await c.list_entities_services()
cams = [e for e in ents if type(e).__name__ == "CameraInfo"]
print("camera entities:", [e.name for e in cams])
got = asyncio.get_event_loop().create_future()
def on_state(state):
if type(state).__name__ == "CameraState" and not got.done():
got.set_result(len(state.data))
c.subscribe_states(on_state)
c.request_single_image()
try:
n = await asyncio.wait_for(got, 10)
print("single image bytes:", n)
except TimeoutError:
print("no image within 10 s")
await c.disconnect()
sys.exit(1)
await c.disconnect()
asyncio.run(main())
@@ -0,0 +1,32 @@
#!/bin/zsh
# usage: measure.sh IP LABEL LOGFILE [OUTDIR]
# Runs the fixed RTSP measurement set against one build and appends a result line to OUTDIR/results.txt:
# ping RTT, two 12 s ffmpeg runs, two concurrent ffmpeg clients, and the raw receive rate with the default
# and a 1 MiB socket buffer. LOGFILE is the device capture, used to count "stalled" drops during the run.
# A measurement that produced nothing is written as ERR and the script exits 1, so a broken run never looks
# like a measured one. Needs ffmpeg and a python with the rtsp scripts' dependencies (PYTHON, default python3).
D=${0:A:h}
IP=$1; LABEL=$2; LOG=$3; S=${4:-.}
PY=${PYTHON:-python3}
[ -f "$LOG" ] || { echo "device capture not found: $LOG"; exit 1; }
rtt=$(ping -c 10 -i 0.2 $IP 2>&1 | tail -1 | awk -F/ '{print $5}')
stall0=$(grep -a -c "stalled" $LOG)
frames() { # OUTFILE RC: the frame count of a run that finished on its own, nothing for a failed or killed one
[ "$2" = 0 ] && grep -o 'frame= *[0-9]*' $1 | tail -1 | tr -dc 0-9
}
timeout 40 ffmpeg -hide_banner -rtsp_transport tcp -i rtsp://$IP:554/ -t 12 -f null - > $S/m-f1.txt 2>&1; f1=$(frames $S/m-f1.txt $?); sleep 2
timeout 40 ffmpeg -hide_banner -rtsp_transport tcp -i rtsp://$IP:554/ -t 12 -f null - > $S/m-f2.txt 2>&1; f2=$(frames $S/m-f2.txt $?); sleep 2
timeout 40 ffmpeg -hide_banner -rtsp_transport tcp -i rtsp://$IP:554/ -t 12 -f null - > $S/m-twoA.txt 2>&1 & A=$!; sleep 1
timeout 40 ffmpeg -hide_banner -rtsp_transport tcp -i rtsp://$IP:554/ -t 12 -f null - > $S/m-twoB.txt 2>&1 & B=$!
wait $A; ra=$?; wait $B; rb=$?
fa=$(frames $S/m-twoA.txt $ra); fb=$(frames $S/m-twoB.txt $rb); sleep 2
timeout 40 $PY $D/rawrate2.py $IP quiet > $S/m-raw.txt 2>&1 && raw=$(grep -o "[0-9.]* KiB/s" $S/m-raw.txt | head -1); sleep 2
timeout 40 $PY $D/rawrate2.py $IP bigbuf > $S/m-raw2.txt 2>&1 && raw2=$(grep -o "[0-9.]* KiB/s" $S/m-raw2.txt | head -1); sleep 1
stall1=$(grep -a -c "stalled" $LOG)
bad=0
for v in rtt f1 f2 fa fb raw raw2; do
if [ -z "${(P)v}" ]; then eval "$v=ERR"; bad=1; fi
done
line="$LABEL | rtt avg ${rtt} ms | ffmpeg single 12 s: ${f1}, ${f2} frames | two clients: ${fa} + ${fb} | python raw: ${raw} (1 MiB rcvbuf ${raw2}) | stalls logged: $((stall1 - stall0))"
echo "$line" | tee -a $S/results.txt
if [ $bad = 1 ]; then echo "one or more measurements failed (ERR above)"; exit 1; fi
@@ -0,0 +1,83 @@
import contextlib
import socket
import sys
import time
IP = sys.argv[1]
mode = sys.argv[2]
secs = 8
s = socket.create_connection((IP, 554), timeout=5)
if mode == "bigbuf":
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1 << 20)
cseq = 0
buf = b""
def read_response() -> bytes:
"""One complete RTSP response: headers through the blank line plus a Content-Length body."""
global buf # noqa: PLW0603
while (end := buf.find(b"\r\n\r\n")) < 0:
chunk = s.recv(4096)
if not chunk:
raise ConnectionError("closed")
buf += chunk
head = buf[:end].decode(errors="replace")
body_len = 0
for line in head.split("\r\n"):
if line.lower().startswith("content-length:"):
body_len = int(line.split(":", 1)[1])
while len(buf) < end + 4 + body_len:
chunk = s.recv(4096)
if not chunk:
raise ConnectionError("closed")
buf += chunk
resp, buf = buf[: end + 4 + body_len], buf[end + 4 + body_len :]
return resp
def req(m, extra="", wait=True):
global cseq # noqa: PLW0603
cseq += 1
url = f"rtsp://{IP}:554/trackID=0" if m == "SETUP" else f"rtsp://{IP}:554/"
s.sendall(f"{m} {url} RTSP/1.0\r\nCSeq: {cseq}\r\n{extra}\r\n".encode())
return read_response() if wait else b""
req("OPTIONS")
req("DESCRIBE", "Accept: application/sdp\r\n")
r = req("SETUP", "Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n")
sess = (
[x for x in r.decode(errors="replace").split("\r\n") if x.startswith("Session")][0]
.split()[1]
.split(";")[0]
)
req("PLAY", f"Session: {sess}\r\n")
t0 = time.monotonic()
total = 0
chunks = 0
last_chat = t0
s.settimeout(2)
while time.monotonic() - t0 < secs:
now = time.monotonic()
if mode == "chatty" and now - last_chat > 0.2:
req("GET_PARAMETER", f"Session: {sess}\r\n", wait=False)
last_chat = now
if mode == "rtcp" and now - last_chat > 0.2:
s.sendall(b"$\x01\x00\x08" + b"\x80\xc9\x00\x01\x00\x00\x00\x01")
last_chat = now
try:
d = s.recv(65536)
except TimeoutError:
continue
if not d:
break
total += len(d)
chunks += 1
print(
f"{mode:7s}: {total / 1024 / secs:5.1f} KiB/s, {chunks} recvs, avg {total // max(chunks, 1)} B/recv"
)
with contextlib.suppress(OSError):
req("TEARDOWN", f"Session: {sess}\r\n")
s.close()
@@ -0,0 +1,345 @@
"""Raw RTSP/TCP client for the esphome rtsp_server hardware test.
usage: rtsp_client.py HOST [case ...]
cases: play, udp, pipeline, third, stall, teardown, getparam, badreq, replay, timeout
"""
import socket
import struct
import sys
import time
HOST = sys.argv[1]
PORT = 554
cases = sys.argv[2:] or [
"play",
"udp",
"pipeline",
"badreq",
"replay",
"getparam",
"teardown",
"third",
"stall",
]
results = []
def say(case, ok, detail):
results.append((case, ok, detail))
print(f"{'PASS' if ok else 'FAIL'} {case}: {detail}", flush=True)
class Client:
def __init__(self, timeout=5):
self.s = socket.create_connection((HOST, PORT), timeout=timeout)
self.cseq = 0
self.session = None
self.buf = b""
def send(self, method, extra="", url=None):
self.cseq += 1
url = (
url or f"rtsp://{HOST}:{PORT}/trackID=0"
if method == "SETUP"
else (url or f"rtsp://{HOST}:{PORT}/")
)
req = f"{method} {url} RTSP/1.0\r\nCSeq: {self.cseq}\r\n"
if self.session:
req += f"Session: {self.session}\r\n"
req += extra + "\r\n"
self.s.sendall(req.encode())
return self.cseq
def _recv(self):
chunk = self.s.recv(4096)
if not chunk:
raise ConnectionError("closed")
self.buf += chunk
def read_response(self):
# Read until a full RTSP response (headers + optional body); skip any interleaved data in between
while True:
if self.buf.startswith(b"$"):
if len(self.buf) < 4:
self._recv()
continue
ln = struct.unpack(">H", self.buf[2:4])[0]
if len(self.buf) < 4 + ln:
self._recv()
continue
self.buf = self.buf[4 + ln :]
continue
end = self.buf.find(b"\r\n\r\n")
if end < 0:
self._recv()
continue
head = self.buf[:end].decode(errors="replace")
body_len = 0
for line in head.split("\r\n"):
if line.lower().startswith("content-length:"):
body_len = int(line.split(":", 1)[1])
while len(self.buf) < end + 4 + body_len:
self._recv()
body = self.buf[end + 4 : end + 4 + body_len]
self.buf = self.buf[end + 4 + body_len :]
status = int(head.split(" ", 2)[1])
for line in head.split("\r\n"):
if line.lower().startswith("session:"):
self.session = line.split(":", 1)[1].strip().split(";")[0]
return status, head, body
def read_frames(self, seconds):
"""Collect interleaved RTP packets on channel 0 for `seconds`; return (frames, packets, seq_gaps, sizes)."""
deadline = time.monotonic() + seconds
frames = 0
packets = 0
gaps = 0
last_seq = None
cur = 0
sizes = []
self.s.settimeout(1.0)
while time.monotonic() < deadline:
try:
chunk = self.s.recv(65536)
except TimeoutError:
continue
if not chunk:
break
self.buf += chunk
while True:
if len(self.buf) < 4 or self.buf[0:1] != b"$":
if self.buf and self.buf[0:1] != b"$":
end = self.buf.find(b"\r\n\r\n")
if end < 0:
break
self.buf = self.buf[end + 4 :]
continue
break
ch = self.buf[1]
ln = struct.unpack(">H", self.buf[2:4])[0]
if len(self.buf) < 4 + ln:
break
pkt = self.buf[4 : 4 + ln]
self.buf = self.buf[4 + ln :]
if ch != 0 or len(pkt) < 12:
continue
packets += 1
seq = struct.unpack(">H", pkt[2:4])[0]
marker = pkt[1] & 0x80
if last_seq is not None and seq != ((last_seq + 1) & 0xFFFF):
gaps += 1
last_seq = seq
cur += len(pkt) - 12
if marker:
frames += 1
sizes.append(cur)
cur = 0
return frames, packets, gaps, sizes
def close(self):
self.s.close()
def setup_play(c):
c.send("OPTIONS")
st, head, _ = c.read_response()
assert st == 200, head
c.send("DESCRIBE", "Accept: application/sdp\r\n")
st, head, body = c.read_response()
assert st == 200, head
c.send("SETUP", "Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n")
st, head, _ = c.read_response()
assert st == 200, head
c.send("PLAY")
st, head, _ = c.read_response()
assert st == 200, head
return body
for case in cases:
try:
if case == "play":
c = Client()
sdp = setup_play(c)
frames, packets, gaps, sizes = c.read_frames(10)
c.send("TEARDOWN")
c.read_response()
c.close()
ok = frames >= 50 and gaps == 0
say(
case,
ok,
f"{frames} frames in 10 s ({frames / 10:.1f} fps), {packets} RTP packets, {gaps} seq gaps, frame size min {min(sizes) if sizes else 0} max {max(sizes) if sizes else 0}; sdp has m=video {'m=video' in sdp.decode()}",
)
elif case == "udp":
c = Client()
c.send("OPTIONS")
c.read_response()
c.send("SETUP", "Transport: RTP/AVP;unicast;client_port=5000-5001\r\n")
st, head, _ = c.read_response()
c.close()
say(case, st == 461, f"UDP SETUP answered {st}")
elif case == "pipeline":
c = Client()
c.s.sendall(
f"OPTIONS rtsp://{HOST}/ RTSP/1.0\r\nCSeq: 1\r\n\r\nDESCRIBE rtsp://{HOST}/ RTSP/1.0\r\nCSeq: 2\r\nAccept: application/sdp\r\n\r\n".encode()
)
c.cseq = 2
st1, h1, _ = c.read_response()
st2, h2, b2 = c.read_response()
c.close()
cs = [x for x in (h1 + h2).split("\r\n") if x.startswith("CSeq")]
say(
case,
st1 == 200 and st2 == 200 and cs == ["CSeq: 1", "CSeq: 2"],
f"two pipelined requests answered {st1}, {st2} with {cs}",
)
elif case == "badreq":
c = Client()
c.s.sendall(b"BOGUS rtsp://x/ RTSP/1.0\r\nCSeq: 7\r\n\r\n")
st, head, _ = c.read_response()
c.close()
say(
case,
st in (400, 405, 501) and "CSeq: 7" in head,
f"unknown method answered {st}, CSeq echoed {'CSeq: 7' in head}",
)
elif case == "replay":
c = Client()
setup_play(c)
f1, _, _, _ = c.read_frames(3)
c.send("PLAY")
st, head, _ = c.read_response()
f2, _, gaps, _ = c.read_frames(3)
c.send("TEARDOWN")
c.read_response()
c.close()
say(
case,
st == 200 and f2 >= 10 and gaps == 0,
f"second PLAY answered {st}, {f1} then {f2} frames, {gaps} seq gaps after repeat PLAY",
)
elif case == "getparam":
c = Client()
setup_play(c)
c.read_frames(1)
c.send("GET_PARAMETER")
st, head, _ = c.read_response()
f, _, _, _ = c.read_frames(2)
c.send("TEARDOWN")
c.read_response()
c.close()
say(
case,
st == 200 and f > 5,
f"GET_PARAMETER while playing answered {st}, stream continued with {f} frames in 2 s",
)
elif case == "teardown":
c = Client()
setup_play(c)
c.read_frames(2)
c.send("TEARDOWN")
st, head, _ = c.read_response()
c.s.settimeout(3)
time.sleep(1.5)
try:
rest = c.s.recv(65536)
closed = rest == b""
except TimeoutError:
rest = b"x"
closed = False
except ConnectionError:
closed = True
c.close()
say(
case,
st == 200,
f"TEARDOWN answered {st}; connection closed by server afterwards: {closed}",
)
elif case == "third":
a = Client()
setup_play(a)
b = Client()
setup_play(b)
fa, _, ga, _ = a.read_frames(4)
fb, _, gb, _ = b.read_frames(4)
try:
d = Client(timeout=3)
d.send("OPTIONS")
st, _, _ = d.read_response()
third = f"third got {st}"
d.close()
except ConnectionError as e:
# refused, reset or closed by the server; a timeout or a bug propagates and fails the case
third = f"third rejected ({type(e).__name__})"
a.send("TEARDOWN")
a.read_response()
a.close()
b.send("TEARDOWN")
b.read_response()
b.close()
say(
case,
fa > 10 and fb > 10 and ga == 0 and gb == 0 and "rejected" in third,
f"two clients: {fa} and {fb} frames in 4 s each, gaps {ga}/{gb}; {third}",
)
elif case == "stall":
a = Client()
setup_play(a)
b = Client()
setup_play(b)
# stop reading on a for 4 s while b keeps playing
a.s.settimeout(None)
fb1, _, gb1, _ = b.read_frames(4)
a.s.settimeout(1.0)
fa, _, ga, _ = a.read_frames(4)
fb2, _, gb2, _ = b.read_frames(3)
a.send("TEARDOWN")
a.read_response()
a.close()
b.send("TEARDOWN")
b.read_response()
b.close()
say(
case,
fb1 > 10 and fb2 > 10 and fa > 5,
f"b kept {fb1} then {fb2} frames while a stalled 4 s; a recovered {fa} frames after resuming (gaps {ga}); b gaps {gb1}/{gb2}; device log must show stalled/dropping frame",
)
elif case == "timeout":
c = Client(timeout=70)
setup_play(c)
c.s.settimeout(
2
) # a server that stops sending but never closes must not hang the run
t0 = time.monotonic()
dead = False
try:
while time.monotonic() - t0 < 68:
try:
d = c.s.recv(65536)
except TimeoutError:
continue
if not d:
dead = True
break
except ConnectionError:
dead = True
elapsed = time.monotonic() - t0
say(
case,
dead and 50 <= elapsed <= 68,
f"connection closed by server after idle: {dead} at {elapsed:.0f} s (expect the 60 s session timeout when the client sends nothing)",
)
except Exception as e: # noqa: BLE001
say(case, False, f"exception {type(e).__name__}: {e}")
time.sleep(1)
print(
"SUMMARY",
sum(1 for r in results if r[1]),
"pass",
sum(1 for r in results if not r[1]),
"fail",
)
sys.exit(1 if any(not r[1] for r in results) else 0)
@@ -0,0 +1,157 @@
"""Bounded, timestamped serial capture that always exits and closes the port.
serial_capture.py PORT SECONDS OUTFILE [--baud 115200] [--released] [--reset] [--append]
default open with DTR/RTS as pyserial leaves them (asserted); fine for ESP devkits and the Pico.
--released drive DTR and RTS low before opening: the BW15 and BK7238 CH340 kits strap the boot pin
(DTR) and reset (RTS), so a plain open holds the chip in reset.
--reset pulse reset after opening (DTR low, RTS high 100 ms, RTS low) to capture a fresh boot.
--baud 115200 by default; 74880 shows the ESP8266 ROM boot messages.
--append keep what OUTFILE already holds; by default it is truncated so counts cover this run only.
Exit status 1 when nothing was received (wrong port name, or a port another capture still holds).
Text ESPHome marks as secret (LOG_SECRET, the Wi-Fi password) is written as <secret>.
Lines are written as [HH:MM:SS]text with ANSI colour stripped and flushed one by one, so a shell
loop can grep the file while the capture runs. If the port is missing or disappears (USB CDC boards
re-enumerate on reboot) the script retries until the deadline, and follows the board by its USB serial
number when it comes back under another device path.
Never run an open-ended serial session: every capture has a deadline, and a flash on the same port
needs the capture stopped first (pkill -f "[s]erial_capture.py" does not match the killing shell).
"""
import argparse
import contextlib
import pathlib
import re
import sys
import time
import serial
from serial.tools import list_ports
ANSI = re.compile(rb"\x1b\[[0-9;]*[A-Za-z]")
# ESPHome's LOG_SECRET span (a Wi-Fi password); a span cut off before its end marker is secret to the end
SECRET = re.compile(rb"\x1b\[8m.*?(?:\x1b\[28m|$)", re.DOTALL)
PORT_ERRORS = (serial.SerialException, OSError)
def identify(port: str) -> tuple[str | None, int]:
"""USB serial number of the port and its position among that device's ports (a dongle has two)."""
ports = sorted(list_ports.comports(), key=lambda p: p.device)
serial_number = next((p.serial_number for p in ports if p.device == port), None)
siblings = [
p.device for p in ports if serial_number and p.serial_number == serial_number
]
return serial_number, siblings.index(port) if port in siblings else 0
def current_path(port: str, serial_number: str | None, index: int) -> str:
"""The port's current device path: a USB CDC board can come back under a new name after a reboot."""
if serial_number:
ports = sorted(list_ports.comports(), key=lambda p: p.device)
siblings = [p.device for p in ports if p.serial_number == serial_number]
if index < len(siblings):
return siblings[index]
return port
def open_port(args: argparse.Namespace, path: str) -> serial.Serial:
s = serial.Serial(port=None, baudrate=args.baud, timeout=0.2)
s.port = path
if args.released:
s.dtr = False
s.rts = False
s.open()
if args.reset:
try:
s.dtr = False
s.rts = True
time.sleep(0.1)
s.rts = False
s.reset_input_buffer()
except PORT_ERRORS:
s.close()
raise
return s
def write_line(out, tag: bytes, data: bytes) -> None:
stamp = time.strftime("[%H:%M:%S]").encode()
out.write(stamp + tag + ANSI.sub(b"", SECRET.sub(b"<secret>", data)) + b"\n")
out.flush()
def note(out, text: str) -> None:
write_line(out, b"[capture] ", text.encode())
def capture(args: argparse.Namespace, out) -> int:
"""Returns the number of bytes received."""
deadline = time.time() + args.seconds
buf = b""
s = None
received = 0
serial_number, index = None, 0
while time.time() < deadline:
if s is None:
path = current_path(args.port, serial_number, index)
try:
s = open_port(args, path)
args.reset = False # only reset on the first open
if serial_number is None:
serial_number, index = identify(path)
if path != args.port:
note(out, f"reopened as {path}")
except PORT_ERRORS as err:
note(out, f"open failed: {err}")
time.sleep(1)
continue
try:
data = s.read(4096)
except PORT_ERRORS as err:
if buf: # keep the cut-off line apart from the next boot's first line
write_line(out, b"[partial]", buf)
buf = b""
note(out, f"port lost: {err}")
with contextlib.suppress(*PORT_ERRORS):
s.close()
s = None
time.sleep(1)
continue
if not data:
continue
received += len(data)
buf += data
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
write_line(out, b"", line.rstrip(b"\r"))
if buf:
write_line(out, b"[partial]", buf)
if s is not None:
s.close()
return received
def main() -> int:
ap = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
ap.add_argument("port")
ap.add_argument("seconds", type=float)
ap.add_argument("outfile")
ap.add_argument("--baud", type=int, default=115200)
ap.add_argument("--released", action="store_true")
ap.add_argument("--reset", action="store_true")
ap.add_argument("--append", action="store_true")
args = ap.parse_args()
with pathlib.Path(args.outfile).open("ab" if args.append else "wb") as out:
received = capture(args, out)
if received == 0:
print(f"[capture] nothing received from {args.port}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,13 @@
#pragma once
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include "esphome/core/log.h"
// Direct look at the Zephyr cdc_acm_uart1 driver state, bypassing the usb_cdc_acm component
inline void cdc_probe() {
const device *d = DEVICE_DT_GET(DT_NODELABEL(cdc_acm_uart1));
uint8_t tmp[32];
int rr = uart_irq_rx_ready(d);
int n = uart_fifo_read(d, tmp, sizeof(tmp));
ESP_LOGI("probe", "driver rx_ready=%d fifo_read=%d first=0x%02X", rr, n, n > 0 ? tmp[0] : 0);
}
@@ -0,0 +1,203 @@
"""Drive the usb_cdc_acm echo firmware on the nRF52840 dongle from the Mac.
usage: drive.py DATA_PORT [case ...]
Cases: short, bulk1k, bulk16k, bulk64k, burst, flush, peek, linecoding, dtr, stream, counters
"""
import random
import sys
import time
import serial
port = sys.argv[1]
cases = sys.argv[2:] or [
"short",
"bulk1k",
"bulk16k",
"bulk64k",
"burst",
"flush",
"peek",
"linecoding",
"dtr",
"stream",
"counters",
]
results = []
def say(case, ok, detail):
results.append((case, ok, detail))
print(f"{'PASS' if ok else 'FAIL'} {case}: {detail}", flush=True)
def info(case, detail):
"""A case whose result lives in the device log; printed, not counted."""
print(f"INFO {case}: {detail}", flush=True)
def open_port(baud=115200):
s = serial.Serial(port, baudrate=baud, timeout=1)
time.sleep(0.3) # let the 100 ms line-state poll see DTR and the echo loop settle
return s
def read_exact(s, n, timeout):
buf = bytearray()
deadline = time.monotonic() + timeout
while len(buf) < n and time.monotonic() < deadline:
chunk = s.read(min(4096, n - len(buf)))
if chunk:
buf += chunk
return bytes(buf)
def payload(n):
# No 0x01 anywhere: the device treats a chunk starting with 0x01 as a command
rnd = random.Random(n)
return bytes(rnd.choice(range(2, 256)) for _ in range(n))
def cmd(s, c, extra=b""):
s.write(b"\x01" + c + extra)
s.flush()
def echo_case(name, n, chunk):
s = open_port()
s.reset_input_buffer()
data = payload(n)
t0 = time.monotonic()
for i in range(0, n, chunk):
s.write(data[i : i + chunk])
got = read_exact(s, n, timeout=max(5, n / 2000))
dt = time.monotonic() - t0
s.close()
ok = got == data
first_bad = next((i for i in range(min(len(got), n)) if got[i] != data[i]), None)
say(
name,
ok,
f"{len(got)}/{n} bytes echoed in {dt:.2f}s ({n / dt / 1024:.1f} KiB/s), first mismatch {first_bad}",
)
for case in cases:
if case == "short":
s = open_port()
s.reset_input_buffer()
ok = True
lat = []
for i in range(20):
msg = f"hello {i}\n".encode()
t0 = time.monotonic()
s.write(msg)
got = read_exact(s, len(msg), 2)
lat.append(time.monotonic() - t0)
ok = ok and got == msg
s.close()
say(
case,
ok,
f"20 lines, latency min {min(lat) * 1000:.0f} ms max {max(lat) * 1000:.0f} ms",
)
elif case == "bulk1k":
echo_case(case, 1024, 1024)
elif case == "bulk16k":
echo_case(case, 16 * 1024, 4096)
elif case == "bulk64k":
echo_case(case, 64 * 1024, 16384)
elif case == "burst":
s = open_port()
s.reset_input_buffer()
cmd(s, b"B")
got = read_exact(s, 1000, 3)
s.close()
expect = bytes((ord("A") + (i % 26)) for i in range(1000))
prefix_ok = got == expect[: len(got)]
say(
case,
prefix_ok and 0 < len(got) < 1000,
f"{len(got)} of 1000 bytes arrived (256 byte TX ring, rest must be dropped and warned), prefix intact {prefix_ok}",
)
elif case == "flush":
s = open_port()
s.reset_input_buffer()
cmd(s, b"F")
got = read_exact(s, 200, 3)
s.close()
expect = bytes((ord("a") + (i % 26)) for i in range(200))
say(case, got == expect, f"{len(got)}/200 bytes, flush result in device log")
time.sleep(0.5)
elif case == "peek":
s = open_port()
s.reset_input_buffer()
cmd(s, b"K", b"Z")
time.sleep(0.5)
s.close()
info(
case,
"peek/read result is in the device log: expect `peek ok=1 0x5A read ok=1 0x5A`",
)
elif case == "linecoding":
for baud in (9600, 115200, 921600):
s = open_port(baud)
s.reset_input_buffer()
cmd(s, b"L")
time.sleep(0.4)
s.close()
time.sleep(0.3)
info(case, "opened at 9600, 115200, 921600; the device log must show each baud")
elif case == "dtr":
for _i in range(5):
s = open_port()
time.sleep(0.3)
s.close()
time.sleep(0.3)
info(
case, "5 open/close cycles; the device log must show 5 Opened and 5 Closed"
)
elif case == "stream":
# device streams 100 x 100 byte lines over 10 s; close the port for 3 s in the middle
s = open_port()
s.reset_input_buffer()
cmd(s, b"S")
part1 = bytearray()
t0 = time.monotonic()
while time.monotonic() - t0 < 2.5:
part1 += s.read(4096)
s.close()
time.sleep(3.0)
s = open_port()
part2 = bytearray()
t0 = time.monotonic()
while time.monotonic() - t0 < 6.0:
part2 += s.read(4096)
s.close()
lines1 = [x for x in bytes(part1).split(b"\n") if x.startswith(b"STREAM")]
lines2 = [x for x in bytes(part2).split(b"\n") if x.startswith(b"STREAM")]
n1 = [int(x[7:10]) for x in lines1]
n2 = [int(x[7:10]) for x in lines2]
resumed = bool(n2) and max(n2) >= 90
say(
case,
resumed,
f"before close: lines {min(n1) if n1 else None}-{max(n1) if n1 else None} ({len(n1)}), after reopen: {min(n2) if n2 else None}-{max(n2) if n2 else None} ({len(n2)}); streaming resumed after reopen {resumed}",
)
elif case == "counters":
s = open_port()
cmd(s, b"C")
time.sleep(0.3)
s.close()
info(case, "counters are in the device log")
time.sleep(0.5)
print(
"SUMMARY",
sum(1 for r in results if r[1]),
"pass",
sum(1 for r in results if not r[1]),
"fail",
)
sys.exit(1 if any(not r[1] for r in results) else 0)
+133
View File
@@ -0,0 +1,133 @@
#!/bin/bash
# wifisave_hammer.sh: the device under test (DUT) falls back to its AP; a client board joins it and posts
# /wifisave repeatedly, then the real credentials. Compiles, flashes, captures both serial logs and
# prints counts. Configs come from templates/example-dut.yaml and templates/example-client.yaml.
#
# Usage: wifisave_hammer.sh DUT_KIND DUT_PORT DUT_YAML DUT_TREE CLIENT_PORT CLIENT_YAML CLIENT_TREE [SETTLE_S]
# DUT_KIND esp (esphome upload), pico (upload, then re-find the CDC port by USB serial number),
# bw15 (flash_rtl87xx_kit.py) or bk (flash_bk72xx.py)
# *_YAML absolute path; the file name without .yaml must equal the config's `name`
# *_TREE checkout or worktree whose esphome code must build that yaml (cwd for python -m esphome)
# SETTLE_S seconds to keep capturing after the client reports the real save (default 40; BW15 wants 90)
# Env: PY (python with esphome; default DUT_TREE/venv/bin/python, else python3 on PATH; an explicit PY must exist), LTPY
# (LibreTiny penv python with ltchiptool; default ~/.platformio/penv/.libretiny/bin/python)
# Output next to each yaml: NAME-compile.log, NAME-upload.log, NAME-serial.log, NAME-capture.err, and DUT NAME-events.txt
# (addresses redacted so the extract can go on a PR; the serial logs themselves stay local).
# Exit status: 0 RESULT PASS (no crash marker or reboot after the AP came up, the device joined the network,
# and real stimulus: saves answered and a cooldown exit); 3 RESULT FAIL (the counts show
# a failure); 2 RESULT INVALID (a precondition or the stimulus was missed, or a capture did not last the run);
# 1 when a build, flash or port step fails (nothing is reported).
set -u
SKILL=$(cd "$(dirname "$0")/.." && pwd)
KIND=$1; DPORT=$2; DYAML=$3; DTREE=$4; CPORT=$5; CYAML=$6; CTREE=$7; SETTLE=${8:-40}
if [ -n "${PY:-}" ]; then [ -x "$PY" ] || { echo "[hammer] FAILED: PY=$PY is not executable"; exit 1; }
else PY=$DTREE/venv/bin/python; [ -x "$PY" ] || PY=$(command -v python3) || { echo "[hammer] FAILED: no python found, set PY"; exit 1; }; fi
LTPY=${LTPY:-$HOME/.platformio/penv/.libretiny/bin/python}
DDIR=$(dirname "$DYAML"); DNAME=$(basename "$DYAML" .yaml)
CDIR=$(dirname "$CYAML"); CNAME=$(basename "$CYAML" .yaml)
DLOG=$DDIR/$DNAME-serial.log; CLOG=$CDIR/$CNAME-serial.log
INVALID=""
AP_WAIT=120; UPLOAD_WAIT=300; SAVE_WAIT=240
# Crash signatures (extended regex), not bare words: "aborting connection attempt" is a normal Wi-Fi retry line
CRASH_RE='Panic |abort\(\)|Fatal exception|Exception \(|Guru Meditation|HardFault|Bus Fault|Backtrace:|Soft WDT reset|wdt reset|Brownout|rst cause:[24]'
# Boot banners and port losses are counted apart: a Pico reboot shows as a port loss (and maybe a banner too),
# while on a UART board a port loss comes from the adapter or the hub, not the chip
BANNER_RE='Log initialized'; LOSS_RE='\[capture\] port lost'
DUT_CAPTURE=$((AP_WAIT + UPLOAD_WAIT + SAVE_WAIT + SETTLE + 60)); CLIENT_CAPTURE=$((SAVE_WAIT + SETTLE + 60))
bound() { local t=$1; shift; if command -v timeout >/dev/null; then timeout "$t" "$@"; else perl -e 'alarm shift; exec @ARGV' "$t" "$@"; fi; }
count() { LC_ALL=C grep -a -c "$1" "$2"; }
wait_for() { # wait_for FILE PATTERN SECONDS -> prints elapsed, status 1 on timeout
local n=0; until LC_ALL=C grep -a -q "$2" "$1"; do sleep 2; n=$((n+2)); [ $n -ge "$3" ] && { echo "$n"; return 1; }; done; echo "$n"; }
# Only this run's captures: another capture on the host (a soak on a third board) must survive
stop_tails() { pkill -f "[s]erial_capture.py $DPORT " 2>/dev/null; pkill -f "[s]erial_capture.py $CPORT " 2>/dev/null; sleep 1; }
fail() { echo "[hammer] FAILED: $1"; stop_tails; exit 1; }
port_never_opened() { # an open failure and no line that is not a capture note: the port, not the firmware
LC_ALL=C grep -a -q '\[capture\] open failed' "$1" && ! LC_ALL=C grep -a -q -v '^\[[0-9:]*\]\[capture\]' "$1"; }
compile() { # compile YAML TREE
(cd "$2" && PYTHONUNBUFFERED=1 bound 1500 "$PY" -m esphome compile "$1" > "${1%.yaml}-compile.log" 2>&1); local rc=$?
echo "[hammer] compile $(basename "$1"): rc=$rc"; return $rc; }
esp_upload() { # esp_upload YAML PORT TREE
(cd "$3" && bound $UPLOAD_WAIT "$PY" -m esphome upload "$1" --device "$2" > "${1%.yaml}-upload.log" 2>&1)
local ok; ok=$(count 'Successfully uploaded' "${1%.yaml}-upload.log"); echo "[hammer] upload $(basename "$1"): ok=$ok"; [ "$ok" -ge 1 ]; }
stop_tails
compile "$DYAML" "$DTREE" || fail "DUT build failed, see $DDIR/$DNAME-compile.log"
compile "$CYAML" "$CTREE" || fail "client build failed, see $CDIR/$CNAME-compile.log"
TAILOPT=""
case $KIND in
esp) esp_upload "$DYAML" "$DPORT" "$DTREE" || fail "DUT upload failed" ;;
pico)
SN=$("$PY" "$SKILL/scripts/ports.py" --serial-of "$DPORT") || fail "no USB serial number for $DPORT"
esp_upload "$DYAML" "$DPORT" "$DTREE" || fail "DUT upload failed"
# The Pico drops its CDC port on reboot and may already be back: take whatever path this board has now;
# the capture follows the serial number if the path changes again
DPORT=$("$PY" "$SKILL/scripts/ports.py" --wait 2e8a --serial "$SN" --timeout 60) || fail "pico did not re-enumerate"
echo "[hammer] pico port after upload: $DPORT"; sleep 2 ;;
bw15|bk)
UF2=$(ls "$DDIR/.esphome/build/$DNAME/.pioenvs/$DNAME/"*.uf2 2>/dev/null | head -1)
[ -n "$UF2" ] || fail "no uf2 under $DDIR/.esphome/build/$DNAME"
echo "[hammer] uf2: ${UF2##*/}"
W=$SKILL/scripts/flash_rtl87xx_kit.py; [ "$KIND" = bk ] && W=$SKILL/scripts/flash_bk72xx.py
(cd "$DDIR" && bound 400 "$LTPY" "$W" flash write -d "$DPORT" "$UF2" > "$DDIR/$DNAME-upload.log" 2>&1); rc=$?
echo "[hammer] $KIND flash: rc=$rc"; [ $rc -eq 0 ] || fail "DUT flash failed, see $DDIR/$DNAME-upload.log"
TAILOPT="--released" ;;
*) echo "unknown DUT_KIND $KIND"; exit 1 ;;
esac
: > "$DLOG"; ("$PY" "$SKILL/scripts/serial_capture.py" "$DPORT" $DUT_CAPTURE "$DLOG" $TAILOPT 2> "${DLOG%-serial.log}-capture.err" &)
if ! n=$(wait_for "$DLOG" "Captive portal\|Starting fallback AP" $AP_WAIT); then
[ -s "$DLOG" ] || fail "the DUT capture produced nothing (the capture itself failed), see ${DLOG%-serial.log}-capture.err"
port_never_opened "$DLOG" && fail "could not open $DPORT (held by another process, or the wrong port), see $DLOG"
fail "DUT never raised its AP within ${n}s, see $DLOG"
fi
echo "[hammer] DUT fallback AP after ${n}s"; sleep 2
# reboots are counted from here; the first banner may predate the capture
BANNER_BASE=$(count "$BANNER_RE" "$DLOG"); LOSS_BASE=$(count "$LOSS_RE" "$DLOG")
esp_upload "$CYAML" "$CPORT" "$CTREE" || fail "client upload failed"
: > "$CLOG"; ("$PY" "$SKILL/scripts/serial_capture.py" "$CPORT" $CLIENT_CAPTURE "$CLOG" 2> "${CLOG%-serial.log}-capture.err" &)
if n=$(wait_for "$CLOG" "real save status\|posting the real" $SAVE_WAIT); then
echo "[hammer] client posted the real credentials after ${n}s; settling ${SETTLE}s"; sleep "$SETTLE"
else
[ -s "$CLOG" ] || fail "the client capture produced nothing (the capture itself failed), see ${CLOG%-serial.log}-capture.err"
port_never_opened "$CLOG" && fail "could not open $CPORT (held by another process, or the wrong port), see $CLOG"
INVALID="client never reached the real save within ${n}s"; echo "[hammer] WARNING: $INVALID"
fi
# A verdict needs complete evidence: both captures must have lasted the run and logged no error of their own
for spec in "$DPORT:${DLOG%-serial.log}-capture.err" "$CPORT:${CLOG%-serial.log}-capture.err"; do
port=${spec%%:*}; err=${spec#*:}
pgrep -f "[s]erial_capture.py $port " >/dev/null || INVALID="${INVALID:+$INVALID; }the capture on $port ended before the run did, see $err"
[ -s "$err" ] && INVALID="${INVALID:+$INVALID; }the capture on $port reported an error, see $err"
done
CRASHES=$(LC_ALL=C grep -a -c -E "$CRASH_RE" "$DLOG")
BANNERS=$(( $(count "$BANNER_RE" "$DLOG") - BANNER_BASE )); LOSSES=$(( $(count "$LOSS_RE" "$DLOG") - LOSS_BASE ))
if [ "$KIND" = pico ]; then
REBOOTS=$BANNERS; [ "$LOSSES" -gt "$REBOOTS" ] && REBOOTS=$LOSSES # one reboot can show as both, so take the larger
else
REBOOTS=$BANNERS; [ "$LOSSES" -gt 0 ] && INVALID="${INVALID:+$INVALID; }the DUT port dropped $LOSSES time(s) mid-run (adapter or hub, not the chip)"
fi
STA=$(LC_ALL=C grep -a 'IP Address:' "$DLOG" | grep -v -c '192\.168\.4\.'); REAL=$(LC_ALL=C grep -a -o 'real save status [-0-9]*' "$CLOG" | tail -1 | awk '{print $4}')
SAVES=$(count 'Requested WiFi Settings Change' "$DLOG"); EXITS=$(count 'cooldown early' "$DLOG"); ANSWERED=$(count 'bogus save status 200' "$CLOG")
echo "[hammer] $DNAME: saves requested $SAVES, cooldown exits $EXITS, crash markers $CRASHES, after the AP came up: boot banners $BANNERS, port losses $LOSSES, reboots $REBOOTS; STA addresses $STA"
FAILED=$(count 'bogus save status -1' "$CLOG")
echo "[hammer] $CNAME: bogus saves answered $ANSWERED, failed $FAILED, real save status ${REAL:-none}"
# Only whole messages, matched where the message starts, so a credential line or an odd SSID never gets in
LC_ALL=C grep -a -E "^\[[0-9:]+\](\[[A-Z]\]\[[a-z_]+:[0-9]+\]: *(Exiting cooldown early|Starting fallback AP|Captive portal|Requested WiFi Settings Change|Retry phase|Connected\$|IP Address: )|\[capture\] port lost|($CRASH_RE))" "$DLOG" | LC_ALL=C tr -cd '\11\12\15\40-\176' | sed -E 's/(IP Address: )[^ ]+/\1<redacted>/' | cut -c1-110 | head -80 > "$DDIR/$DNAME-events.txt"
echo "[hammer] events: $DDIR/$DNAME-events.txt"
stop_tails
[ -z "$INVALID" ] || { echo "[hammer] RESULT INVALID: $INVALID"; exit 2; }
# Provisioning is proven by the device joining the network; the client's answer to the real save can time
# out because the device leaves the AP before replying, so its status is information only
if [ "$CRASHES" -gt 0 ] || [ "$REBOOTS" -gt 0 ] || [ "$STA" -lt 1 ]; then
echo "[hammer] RESULT FAIL: crash markers $CRASHES, reboots $REBOOTS, STA addresses $STA, real save status ${REAL:-none}"; exit 3
fi
# A pass needs the stimulus the flow is about: at least half the configured bogus saves answered and one cooldown exit
CONFIGURED=$(LC_ALL=C grep -a -E '^[[:space:]]*bogus_saves:' "$CYAML" | head -1 | tr -dc '0-9'); CONFIGURED=${CONFIGURED:-15}
if [ "$EXITS" -lt 1 ] || [ $((ANSWERED * 2)) -lt "$CONFIGURED" ]; then
echo "[hammer] RESULT INVALID: insufficient stimulus (bogus saves answered $ANSWERED of $CONFIGURED, cooldown exits $EXITS)"; exit 2
fi
[ "$FAILED" -eq 0 ] || echo "[hammer] WARNING: $FAILED of $CONFIGURED bogus saves got no answer"
echo "[hammer] RESULT PASS"; exit 0
@@ -0,0 +1,39 @@
#!/bin/zsh
# usage: decode.sh CAPTURE ELF OUTPREFIX
# Parses the #CD: lines of a serial capture with Zephyr's coredump tools from the sdk-nrf that ESPHome
# installed (SDK_NRF, default: the sdk-nrf directory under the ESPHome cache), serves the dump to GDB on a
# random port and prints the backtrace and the thread list. The ELF is .pioenvs/<name>/zephyr/zephyr/zephyr.elf.
# Every step is checked: a missing tool, a capture without a complete dump, an empty .bin or a gdbserver
# that died all stop the script with exit 1, and the exit status is GDB's otherwise.
CAP=$1; ELF=$2; OUT=$3
SDK_NRF=${SDK_NRF:-$(ls -d ~/Library/Caches/esphome/sdk-nrf ~/.cache/esphome/sdk-nrf 2>/dev/null | head -1)}
[ -n "$SDK_NRF" ] || { echo "sdk-nrf not found, set SDK_NRF"; exit 1; }
F=$(ls -d $SDK_NRF/frameworks/v*/zephyr/scripts/coredump 2>/dev/null | tail -1)
PY=${PYTHON:-$(ls $SDK_NRF/penvs/*/bin/python 2>/dev/null | head -1)}
# SDK 0.x keeps the tools under arm-zephyr-eabi/, SDK 1.0+ under gnu/arm-zephyr-eabi/
GDB=$(ls $SDK_NRF/toolchains/*/arm-zephyr-eabi/bin/arm-zephyr-eabi-gdb $SDK_NRF/toolchains/*/gnu/arm-zephyr-eabi/bin/arm-zephyr-eabi-gdb 2>/dev/null | tail -1)
for v in F PY GDB; do
[ -n "${(P)v}" ] || { echo "$v not found under $SDK_NRF"; exit 1; }
done
[ -f "$ELF" ] || { echo "ELF not found: $ELF"; exit 1; }
# strip capture timestamps and ANSI so the parser sees the raw log lines
sed -E 's/^\[[0-9:.]+\]//; s/\x1b\[[0-9;]*m//g' "$CAP" > $OUT-all.log
# keep only the first complete #CD:BEGIN# .. #CD:END# block
awk '/#CD:BEGIN#/{if(!done){p=1;buf=""}} p{buf=buf $0 "\n"} /#CD:END#/{if(p){printf "%s", buf; done=1; p=0}}' $OUT-all.log > $OUT-raw.log
echo "CD lines in capture: $(grep -c '#CD:' $OUT-all.log) (BEGIN $(grep -c '#CD:BEGIN#' $OUT-all.log), END $(grep -c '#CD:END#' $OUT-all.log)); first complete block: $(grep -c '#CD:' $OUT-raw.log) lines"
[ -s $OUT-raw.log ] || { echo "no complete #CD:BEGIN# .. #CD:END# block in the capture"; exit 1; }
if ! $PY $F/coredump_serial_log_parser.py $OUT-raw.log $OUT.bin > $OUT-parser.log 2>&1; then
echo "parser failed:"; tail -5 $OUT-parser.log; exit 1
fi
tail -3 $OUT-parser.log
[ -s $OUT.bin ] || { echo "parser wrote an empty $OUT.bin"; exit 1; }
ls -la $OUT.bin | awk '{print "bin bytes", $5}'
PORT=$((20000 + RANDOM % 10000))
$PY $F/coredump_gdbserver.py --port $PORT "$ELF" $OUT.bin > $OUT-gdbserver.log 2>&1 &
G=$!
sleep 3
if ! kill -0 $G 2>/dev/null; then echo "gdbserver exited:"; tail -5 $OUT-gdbserver.log; exit 1; fi
$GDB -q -batch -ex "target remote :$PORT" -ex "bt" -ex "info threads" "$ELF" 2>&1 | grep -v "^warning\|Reading symbols\|^$" | head -40
rc=$pipestatus[1]
kill $G 2>/dev/null
exit $rc
@@ -0,0 +1,29 @@
#!/bin/zsh
# usage: flow.sh LABEL PORT ELF [OUTDIR]
# Run at least 60 s after the board rebooted from its crash, with the port closed. Proves the stored
# coredump waits for a reader and survives a reader that goes away:
# 1) open the port for 3 s and close it (a reader that leaves mid-print)
# 2) open it for 100 s: the whole dump must print again, then be erased
# 3) open it again for 20 s: dump_config must report no stored dump
# then decode.sh parses the first complete dump and prints the GDB backtrace.
# Needs a python with pyserial (PYTHON, default python3).
D=${0:A:h}
L=$1; P=$2; ELF=$3; S=${4:-.}
PY=${PYTHON:-python3}
CAP=$D/../serial_capture.py
capture() { # SECONDS OUTFILE: stop the flow when the capture fails (busy or missing port)
if ! $PY $CAP $P $1 $2 > $2.err 2>&1; then
echo "capture of $2 failed:"; cat $2.err; exit 1
fi
}
capture 3 $S/$L-brief.txt
echo "$L brief 3 s open: $(wc -l < $S/$L-brief.txt) lines, CD lines $(grep -a -c '#CD:' $S/$L-brief.txt), END $(grep -a -c '#CD:END#' $S/$L-brief.txt)"
sleep 5
capture 100 $S/$L-full.txt
echo "$L full 100 s open: $(wc -l < $S/$L-full.txt) lines, CD lines $(grep -a -c '#CD:' $S/$L-full.txt), BEGIN $(grep -a -c '#CD:BEGIN#' $S/$L-full.txt), END $(grep -a -c '#CD:END#' $S/$L-full.txt)"
grep -a "crashtest\|zephyr_coredump\|Reset reason\|Has stored\| Size:\| Error:\|took a long\|compiled on" $S/$L-full.txt | grep -a -v "#CD:[0-9A-F]" | head -16
sleep 3
capture 20 $S/$L-after.txt
echo "$L reopen 20 s: CD lines $(grep -a -c '#CD:' $S/$L-after.txt); $(grep -a 'Has stored\| Size:' $S/$L-after.txt | sed -E 's/^\[[^]]*\]//' | tr '\n' ' ')"
echo "== decode"
$D/decode.sh $S/$L-full.txt $ELF $S/$L
@@ -0,0 +1,24 @@
import asyncio
import sys
from aioesphomeapi import APIClient
async def main():
c = APIClient(sys.argv[1], 6053, None)
await c.connect(login=True)
di = await c.device_info()
print(
"version",
di.esphome_version,
"compiled",
di.compilation_time,
"home_id",
hex(di.zwave_home_id),
"flags",
di.zwave_proxy_feature_flags,
)
await c.disconnect()
asyncio.run(main())
@@ -0,0 +1,45 @@
"""Recompute the Z-Wave checksum (XOR of LENGTH..payload, initial 0xFF) over every frame the proxy logged."""
from functools import reduce
import pathlib
import re
import sys
def zw_checksum(frame: bytes) -> int:
return reduce(lambda a, b: a ^ b, frame[1:-1], 0xFF)
def complete(frame: bytes) -> bool:
"""A data frame is SOF, LENGTH, then LENGTH bytes; the proxy logs at most 168 bytes, so longer ones are cut."""
return len(frame) >= 3 and frame[0] == 0x01 and len(frame) == frame[1] + 2
for path in sys.argv[1:]:
text = pathlib.Path(path).read_text(encoding="utf-8", errors="replace")
sent = re.findall(r"Sending: ((?:[0-9A-F]{2}:)+[0-9A-F]{2})", text)
recv = re.findall(r"Received frame: ((?:[0-9A-F]{2}:)+[0-9A-F]{2})", text)
calc = re.findall(
r"CHECKSUM Received: 0x([0-9A-F]{2}) - Calculated: 0x([0-9A-F]{2})", text
)
bad = len(re.findall(r"Bad checksum", text))
acks_sent = len(re.findall(r"Sending ACK", text))
naks_sent = len(re.findall(r"Sending NAK/CAN", text))
acks_recv = len(re.findall(r"Received ACK", text))
naks_recv = len(re.findall(r"Received NAK", text))
reboots = len(re.findall(r"Boot count|Reset reason|rst:0x", text))
long_ops = len(re.findall(r"zwave_proxy took a long time", text))
rx_frames = [bytes.fromhex(f.replace(":", "")) for f in recv]
tx_frames = [bytes.fromhex(f.replace(":", "")) for f in sent]
rx_cut = sum(1 for b in rx_frames if not complete(b))
tx_cut = sum(1 for b in tx_frames if len(b) > 1 and not complete(b))
rx_ok = sum(1 for b in rx_frames if complete(b) and zw_checksum(b) == b[-1])
tx_ok = sum(1 for b in tx_frames if complete(b) and zw_checksum(b) == b[-1])
calc_ok = sum(1 for r, c in calc if r == c)
cmds = sorted({f.split(":")[3] for f in recv})
print(
f"{path.rsplit('/', 1)[-1]}: tx frames {len(sent)} (checksum ok {tx_ok}, truncated in log {tx_cut}), "
f"rx frames {len(recv)} (recomputed ok {rx_ok}, truncated in log {rx_cut}), "
f"device calc==received {calc_ok}/{len(calc)}, bad checksum warnings {bad}, ACK sent {acks_sent}, NAK sent {naks_sent}, "
f"ACK received {acks_recv}, NAK received {naks_recv}, long ops {long_ops}, rx cmd ids {cmds}"
)
@@ -0,0 +1,44 @@
substitutions:
run: "1"
esphome:
name: bk72xx-crash-probe
includes: [../scripts/libretiny_crash/bkprobe.hpp]
# step 0 waits 30 s so a capture can be opened (and a reset pulsed) after the flash; later steps 5 s
on_boot:
priority: -100
then:
- lambda: id(step) = bkprobe_boot(${run});
- delay: !lambda 'return id(step) == 0 ? 30000 : 5000;'
- lambda: bkprobe_act(${run}, id(step));
bk72xx:
board: generic-bk7238-tuya
logger:
hardware_uart: UART1
level: DEBUG
preferences:
flash_write_interval: 1s
globals:
- id: step
type: int
initial_value: "0"
interval:
- interval: 10s
then:
- lambda: bkprobe_alive(id(step));
# Wi-Fi matters: a crashed BK72xx with Wi-Fi up never reboots on its own without the crash handler fix
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
reboot_timeout: 0s
ota:
- platform: esphome
@@ -0,0 +1,38 @@
substitutions:
run: "1"
esphome:
name: lt21007e
includes: [../scripts/libretiny_prefs/ltprefs.hpp]
platformio_options:
build_flags: ["-DUSE_LIBRETINY_RTC_PREFERENCES"]
on_boot:
priority: -100
then:
- lambda: id(act) = ltprefs_boot(id(step), ${run});
# the first boot waits so the host can open the port and pulse a reset before the sequence starts
- delay: !lambda 'return (id(step) == 0 || id(step) == 8) ? 30000 : 3000;'
- lambda: ltprefs_exit(id(step), ${run}, id(act));
bk72xx:
board: generic-bk7238-tuya
logger:
hardware_uart: UART1
level: DEBUG
preferences:
flash_write_interval: 1s
globals:
- id: step
type: int
initial_value: "0"
- id: act
type: int
initial_value: "0"
interval:
- interval: 10s
then:
- lambda: ltprefs_alive(id(step), id(act));
@@ -0,0 +1,77 @@
# Package for the client board: it joins the device under test's AP (priority 10), posts
# `bogus_saves` bogus /wifisave requests `save_interval` apart, then posts the real credentials once.
# Include from a per-run yaml that holds the platform block and the substitutions (example-client.yaml);
# real_ssid and real_psk come from the scratch directory's secrets.yaml and must be URL safe.
# Set bogus_saves to 1 for a plain provisioning check. Log lines the driver script counts:
# "bogus save N", "bogus save status 200", "posting the real credentials", "real save status 200"
# The hammer is a single-mode script, so a reconnect to the AP (the device under test rebooted) does not
# start a second series on top of the first.
substitutions:
ap_password: hwtestpass
bogus_saves: "15"
save_interval: 3s
bogus_ssid: hwtest-no-such-network
bogus_psk: nosuchpassword
esphome:
name: ${name}
logger:
level: DEBUG
http_request:
id: http
verify_ssl: false
timeout: 10s
wifi:
id: wifi_id
networks:
- ssid: ${ap_ssid}
password: ${ap_password}
priority: 10
- ssid: ${real_ssid}
password: ${real_psk}
on_connect:
- lambda: |-
char b[40];
ESP_LOGI("test", "connected, ip %s", id(wifi_id).get_ip_addresses()[0].str_to(b));
- if:
condition:
lambda: |-
char b[40];
return strncmp(id(wifi_id).get_ip_addresses()[0].str_to(b), "192.168.4.", 10) == 0;
then:
- script.execute: hammer
script:
- id: hammer
mode: single
then:
- delay: 3s
- logger.log: "hammering /wifisave with bogus credentials"
- repeat:
count: ${bogus_saves}
then:
- lambda: ESP_LOGI("test", "bogus save %u", (unsigned) iteration + 1);
- http_request.get:
url: "http://192.168.4.1/wifisave?ssid=${bogus_ssid}&psk=${bogus_psk}"
capture_response: true
on_response:
- lambda: ESP_LOGI("test", "bogus save status %d", response->status_code);
on_error:
- lambda: ESP_LOGW("test", "bogus save status -1");
- delay: ${save_interval}
- logger.log: "posting the real credentials"
- http_request.get:
url: "http://192.168.4.1/wifisave?ssid=${real_ssid}&psk=${real_psk}"
capture_response: true
on_response:
- lambda: ESP_LOGI("test", "real save status %d", response->status_code);
on_error:
- lambda: ESP_LOGW("test", "real save status -1");
interval:
- interval: 5s
then:
- lambda: ESP_LOGI("test", "alive %u s", (unsigned) (millis() / 1000));
@@ -0,0 +1,34 @@
esphome:
name: i19430-dut3
on_boot:
priority: 800
then:
- lambda: |-
auto marker = global_preferences->make_preference<uint32_t>(0x19430001, true);
uint32_t v = 0;
if (!marker.load(&v)) {
ESP_LOGW("test", "first boot: wiping preferences");
global_preferences->reset();
static_cast<esphome::libretiny::LibreTinyPreferences *>(global_preferences)->open();
v = 1;
marker.save(&v);
global_preferences->sync();
}
bk72xx:
board: generic-bk7238
logger:
hardware_uart: UART1
level: VERBOSE
wifi:
ap:
ssid: i19430-ap
captive_portal:
interval:
- interval: 5s
then:
- lambda: ESP_LOGI("test", "alive %u s", (unsigned) (millis() / 1000));
@@ -0,0 +1,31 @@
# Package for a device under test that never finds its network, falls back to its AP and serves the
# captive portal. Include it from a per-run yaml that holds the platform block and the substitutions
# (see example-dut.yaml). Bump `name` on every run: saved credentials are keyed by it, and a reused
# name boots straight onto the real network and never raises the AP.
substitutions:
ap_password: hwtestpass
ap_timeout: 15s
bogus_ssid: hwtest-no-such-network
bogus_psk: nosuchpassword
esphome:
name: ${name}
logger:
level: DEBUG
wifi:
networks:
- ssid: ${bogus_ssid}
password: ${bogus_psk}
ap:
ssid: ${ap_ssid}
password: ${ap_password}
ap_timeout: ${ap_timeout}
captive_portal:
interval:
- interval: 5s
then:
- lambda: ESP_LOGI("test", "alive %u s", (unsigned) (millis() / 1000));
@@ -0,0 +1,18 @@
# Client board for the /wifisave hammer. Copy into a scratch directory next to a secrets.yaml;
# ap_ssid must match the device under test, real_ssid and real_psk must be URL safe.
substitutions:
name: hwtest-client-1
ap_ssid: hwtest-ap
real_ssid: !secret wifi_ssid
real_psk: !secret wifi_password
bogus_saves: "15"
esp32:
board: esp32dev
framework:
type: esp-idf
# esp8266: {board: d1_mini}
packages:
hwtest: !include client-wifisave.yaml
@@ -0,0 +1,16 @@
# Device under test for the /wifisave hammer. Copy into a scratch directory, keep the file name equal
# to `name`, pick one platform block, bump `name` every run.
substitutions:
name: hwtest-dut-1
ap_ssid: hwtest-ap
esp8266:
board: d1_mini
# esp32: {board: esp32dev, framework: {type: esp-idf}}
# rtl87xx: {board: bw15}
# bk72xx: {board: generic-bk7238} # plus logger: {hardware_uart: UART1}, BK72xx logs on UART2 by default
# rp2040: {board: rpipico2w}
packages:
hwtest: !include dut-fallback-ap.yaml
@@ -0,0 +1,50 @@
substitutions:
run: "3"
esphome:
name: cd13122d
on_boot:
priority: -100
then:
- lambda: |-
ESP_LOGI("crashtest", "boot, crashed_before=%d run=%s", id(crashed_${run}), "${run}");
- delay: 20s
- if:
condition:
lambda: 'return id(crashed_${run}) == 0;'
then:
- lambda: |-
id(crashed_${run}) = 1;
ESP_LOGW("crashtest", "flag set, letting the loop save it");
- delay: 3s
- lambda: |-
global_preferences->sync();
delay(500);
ESP_LOGW("crashtest", "writing to 0xDEADBEEF now");
delay(100);
*reinterpret_cast<volatile uint32_t *>(0xDEADBEEF) = 1;
else:
- lambda: |-
ESP_LOGI("crashtest", "already crashed once, running normally");
nrf52:
board: nrf52840dongle
dfu: true
logger:
hardware_uart: USB_CDC
level: DEBUG
zephyr_coredump:
globals:
- id: crashed_${run}
type: int
restore_value: true
initial_value: "0"
interval:
- interval: 10s
then:
- lambda: |-
ESP_LOGI("crashtest", "alive");
@@ -0,0 +1,31 @@
esphome:
name: pref20810
on_boot:
priority: -100
then:
- lambda: |-
ESP_LOGI("test", "boot: restored counter_pr=%d", id(counter_pr));
nrf52:
board: nrf52840dongle
dfu: true
logger:
hardware_uart: USB_CDC
level: VERBOSE
preferences:
flash_write_interval: 5s
globals:
- id: counter_pr
type: int
restore_value: true
initial_value: "0"
interval:
- interval: 15s
then:
- lambda: |-
id(counter_pr) += 1;
ESP_LOGI("test", "counter_pr now %d", id(counter_pr));
@@ -0,0 +1,84 @@
esphome:
name: online-image-dut
friendly_name: online-image-dut
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
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
@@ -0,0 +1,105 @@
esphome:
name: rtsp17990
on_boot:
# GPIO8 low enables the camera's power supply on the Waveshare ESP32-S3-ETH; give the sensor
# time to come up before esp32_camera (setup_priority 200) probes it
priority: 700
then:
- switch.turn_off: cam_power
- lambda: |-
delay(1200);
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
psram:
mode: octal
speed: 80MHz
logger:
level: VERBOSE
hardware_uart: USB_SERIAL_JTAG
logs:
rtsp_server: VERBOSE
esp32_camera: DEBUG
camera: DEBUG
wifi: DEBUG
api: DEBUG
api.connection: DEBUG
api.socket: DEBUG
socket: DEBUG
scheduler: DEBUG
component: DEBUG
app: DEBUG
mdns: DEBUG
ota: DEBUG
logger: DEBUG
psram: DEBUG
esp32: DEBUG
network: DEBUG
i2c: DEBUG
i2c.idf: DEBUG
preferences: DEBUG
debug: DEBUG
sensor: DEBUG
wifi_esp32: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
api:
reboot_timeout: 0s
ota:
- platform: esphome
debug:
update_interval: 10s
sensor:
- platform: debug
free:
name: "Heap Free"
block:
name: "Heap Largest Block"
psram:
name: "PSRAM Free"
# Waveshare ESP32-S3-ETH camera header (Waveshare wiki, WIFI_Web_CAM table)
i2c:
- id: cam_i2c
sda: GPIO48
scl: GPIO47
frequency: 400kHz
switch:
- platform: gpio
id: cam_power
name: Camera Power
pin: GPIO8
restore_mode: ALWAYS_OFF
setup_priority: 800
esp32_camera:
name: cam
setup_priority: 200
data_pins: [GPIO41, GPIO45, GPIO46, GPIO42, GPIO40, GPIO38, GPIO15, GPIO18]
vsync_pin: GPIO1
href_pin: GPIO2
pixel_clock_pin: GPIO39
external_clock:
pin: GPIO3
frequency: 20MHz
i2c_id: cam_i2c
resolution: 640X480
max_framerate: 10fps
rtsp_server:
port: 554
max_connections: 2
@@ -0,0 +1,107 @@
# Basic Config
esphome:
name: s31-cse7766
esp8266:
board: esp01_1m
toolchain: arduino
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
logger:
baud_rate: 0 # (UART logging interferes with cse7766)
level: VERBOSE
logs:
cse7766: VERBOSE
api: DEBUG
api.connection: DEBUG
api.socket: DEBUG
socket: DEBUG
scheduler: DEBUG
component: DEBUG
app: DEBUG
wifi: DEBUG
mdns: DEBUG
ota: DEBUG
logger: DEBUG
sensor: DEBUG
switch: DEBUG
binary_sensor: DEBUG
status_led: DEBUG
uart: DEBUG
gpio: DEBUG
preferences: DEBUG
esp8266: DEBUG
wifi_esp8266: DEBUG
api:
ota:
platform: esphome
# Device Specific Config
uart:
rx_pin: RX
baud_rate: 4800
parity: EVEN
binary_sensor:
- platform: gpio
pin:
number: GPIO0
mode: INPUT_PULLUP
inverted: True
name: "S31 Button"
on_press:
- switch.toggle: relay
- platform: status
name: "S31 Status"
sensor:
- platform: wifi_signal
name: "S31 WiFi Signal"
update_interval: 60s
- platform: cse7766
current:
accuracy_decimals: 3
name: "S31 Current"
filters:
- throttle: 2s
voltage:
accuracy_decimals: 1
name: "S31 Voltage"
filters:
- throttle: 2s
on_raw_value:
then:
- lambda: |-
id(frames) += 1;
power:
accuracy_decimals: 1
name: "S31 Power"
filters:
- throttle: 2s
switch:
- platform: gpio
name: "S31 Relay"
pin: GPIO12
id: relay
restore_mode: RESTORE_DEFAULT_ON
status_led:
pin: GPIO13
globals:
- id: frames
type: uint32_t
initial_value: "0"
interval:
- interval: 10s
then:
- lambda: |-
static uint32_t last = 0;
ESP_LOGI("frames", "cse7766 frames parsed: %u in 10 s (total %u)", (unsigned) (id(frames) - last), (unsigned) id(frames));
last = id(frames);
@@ -0,0 +1,112 @@
esphome:
name: cdc11634
nrf52:
board: nrf52840dongle
dfu: true
logger:
hardware_uart: USB_CDC
level: VERBOSE
logs:
usb_cdc_acm: VERBOSE
scheduler: DEBUG
component: DEBUG
app: DEBUG
preferences: DEBUG
logger: DEBUG
nrf52: DEBUG
dfu: DEBUG
usb_cdc_acm:
interfaces:
- disabled: true
- id: cdc1
globals:
- id: rx_total
type: uint32_t
initial_value: "0"
- id: tx_attempted
type: uint32_t
initial_value: "0"
- id: stream_left
type: int32_t
initial_value: "0"
interval:
- interval: 20ms
then:
- lambda: |-
uint8_t buf[64];
while (id(cdc1).available() > 0) {
// a command is exactly two bytes, so what the host sends after it stays in the driver for peek/read
uint8_t first = 0;
id(cdc1).peek_byte(&first);
size_t n = std::min<size_t>(id(cdc1).available(), sizeof(buf));
if (first == 0x01 && n >= 2)
n = 2;
if (!id(cdc1).read_array(buf, n)) {
ESP_LOGE("test", "read_array(%u) failed", (unsigned) n);
break;
}
id(rx_total) += n;
if (buf[0] == 0x01 && n >= 2) {
const char cmd = buf[1];
if (cmd == 'B') {
// one oversized write: the 256 byte TX ring must drop the rest and warn once
static uint8_t big[1000];
for (size_t i = 0; i < sizeof(big); i++) big[i] = 'A' + (i % 26);
id(cdc1).write_array(big, sizeof(big));
id(tx_attempted) += sizeof(big);
ESP_LOGI("test", "burst 1000 written");
} else if (cmd == 'S') {
id(stream_left) = 100;
ESP_LOGI("test", "stream started");
} else if (cmd == 'L') {
ESP_LOGI("test", "line coding baud=%u data=%u stop=%u parity=%u", (unsigned) id(cdc1).get_baud_rate(),
id(cdc1).get_data_bits(), id(cdc1).get_stop_bits(), (unsigned) id(cdc1).get_parity());
} else if (cmd == 'K') {
// the host sends "\x01KZ" in one write; the Z must still be in the driver here
uint8_t pb = 0, rb = 0;
bool okp = id(cdc1).peek_byte(&pb);
bool okr = id(cdc1).read_byte(&rb);
ESP_LOGI("test", "peek ok=%d 0x%02X read ok=%d 0x%02X (expect ok=1 0x5A for both)", okp, pb, okr, rb);
if (okr) id(rx_total) += 1;
} else if (cmd == 'F') {
static uint8_t pat[200];
for (size_t i = 0; i < sizeof(pat); i++) pat[i] = 'a' + (i % 26);
id(cdc1).write_array(pat, sizeof(pat));
id(tx_attempted) += sizeof(pat);
const uint32_t t0 = millis();
auto res = id(cdc1).flush();
ESP_LOGI("test", "flush result=%d took %u ms", (int) res, (unsigned) (millis() - t0));
} else if (cmd == 'C') {
ESP_LOGI("test", "counters rx=%u tx_attempted=%u (write_array drops silently when the ring is full)", (unsigned) id(rx_total), (unsigned) id(tx_attempted));
} else {
ESP_LOGW("test", "unknown cmd 0x%02X", cmd);
}
continue;
}
id(cdc1).write_array(buf, n);
id(tx_attempted) += n;
}
- interval: 100ms
then:
- lambda: |-
if (id(stream_left) > 0) {
char line[101];
int len = snprintf(line, sizeof(line), "STREAM %03d ", 100 - id(stream_left));
for (int i = len; i < 99; i++) line[i] = 'x';
line[99] = '\n';
id(cdc1).write_array(reinterpret_cast<const uint8_t *>(line), 100);
id(tx_attempted) += 100;
id(stream_left) -= 1;
if (id(stream_left) == 0) {
ESP_LOGI("test", "stream done");
}
}
- interval: 10s
then:
- lambda: |-
ESP_LOGI("test", "alive rx=%u tx_attempted=%u", (unsigned) id(rx_total), (unsigned) id(tx_attempted));
@@ -0,0 +1,83 @@
esphome:
name: camdiag
on_boot:
priority: -100
then:
- output.set_level:
id: xclk
level: 50%
- delay: 200ms
- lambda: |-
auto *bus = id(cam_i2c);
auto scan = [&](const char *what) {
int found = 0;
for (uint8_t a = 0x08; a < 0x78; a++) {
if (bus->write(a, nullptr, 0) == i2c::ERROR_OK) {
ESP_LOGI("camdiag", " ACK at 0x%02X", a);
found++;
}
}
ESP_LOGI("camdiag", "scan %s: %d devices", what, found);
};
auto rd16 = [&](uint16_t reg) {
uint8_t r[2] = {(uint8_t) (reg >> 8), (uint8_t) reg};
uint8_t v = 0xEE;
auto err = bus->write_readv(0x3C, r, 2, &v, 1);
ESP_LOGI("camdiag", " 0x3C [0x%04X] err=%d -> 0x%02X", reg, (int) err, v);
return v;
};
auto rd8 = [&](uint8_t addr, uint8_t reg) {
uint8_t v = 0xEE;
auto err = bus->write_readv(addr, &reg, 1, &v, 1);
ESP_LOGI("camdiag", " 0x%02X [0x%02X] (8 bit) err=%d -> 0x%02X", addr, reg, (int) err, v);
};
id(g8).turn_off(); delay(1500);
id(xclk).set_level(0.0f); delay(300);
scan("GPIO8 LOW, XCLK OFF");
id(xclk).set_level(0.5f); delay(300);
scan("GPIO8 LOW, XCLK ON");
ESP_LOGI("camdiag", "OV5640 style 16 bit reads (expect 300A=0x56 300B=0x40, 302A=0xB0..B2):");
rd16(0x300A); rd16(0x300B); rd16(0x302A); rd16(0x3008); rd16(0x3100); rd16(0x3103); rd16(0x4300);
ESP_LOGI("camdiag", "8 bit reads at 0x3C (GC2145 expects F0=0x21 F1=0x45):");
rd8(0x3C, 0xF0); rd8(0x3C, 0xF1); rd8(0x3C, 0x0A); rd8(0x3C, 0x0B);
ESP_LOGI("camdiag", "write 0x3103=0x11 then read back, then soft reset 0x3008=0x82 and re-read IDs:");
uint8_t w[3] = {0x31, 0x03, 0x11};
auto werr = bus->write(0x3C, w, 3);
ESP_LOGI("camdiag", " write err=%d", (int) werr);
rd16(0x3103);
uint8_t rst[3] = {0x30, 0x08, 0x82};
bus->write(0x3C, rst, 3); delay(100);
rd16(0x300A); rd16(0x300B);
id(g8).turn_on(); delay(500); scan("GPIO8 HIGH (camera off)");
id(g8).turn_off(); delay(1500); scan("GPIO8 LOW again");
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
i2c:
- id: cam_i2c
sda: GPIO48
scl: GPIO47
frequency: 100kHz
scan: true
output:
- platform: gpio
id: g8
pin: GPIO8
- platform: ledc
pin: GPIO3
id: xclk
frequency: 20000000Hz
+1 -1
View File
@@ -32,7 +32,7 @@ runs:
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
# downstream jobs rely on is preserved.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
@@ -97,7 +97,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: generated-proto-files
path: |
+2 -2
View File
@@ -128,7 +128,7 @@ jobs:
- name: Upload compile-test image artifact
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
# The tar is already compressed, so upload it as-is. archive: false
# skips the redundant zip and makes the file name the artifact name
@@ -210,7 +210,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download image artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: compile-test-image.tar.zst
- name: Load image
+40 -23
View File
@@ -49,7 +49,7 @@ jobs:
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
# that ``. venv/bin/activate`` see an identical layout.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -81,8 +81,8 @@ jobs:
- name: Install apt packages (cached)
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
determine-jobs:
name: Determine which jobs to run
@@ -395,6 +395,8 @@ jobs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.integration-tests == 'true'
# A hung test is failed by pytest's --timeout below; this bounds the rest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -415,8 +417,8 @@ jobs:
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Set up Python 3.13
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
@@ -438,7 +440,7 @@ jobs:
- name: Set up uv
# Only needed on cache miss to populate the venv.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -471,13 +473,14 @@ jobs:
exit 1
fi
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \
# A hung test fails after 30 min: the 15 min shared build lock wait plus a cold compile
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal --timeout=1800 \
--junitxml=junit-integration.xml "${test_files[@]}"
- name: Upload test artifacts
# Tests that compare rendered output write the image they actually got here, so a
# failure can be looked at without reproducing the whole build locally.
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: integration-test-artifacts-${{ matrix.bucket.name }}
path: test_artifacts/
@@ -488,7 +491,7 @@ jobs:
# script/update_integration_test_durations.py; only full matrix dev
# runs produce usable data.
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: junit-integration-${{ strategy.job-index }}
path: junit-integration.xml
@@ -528,7 +531,7 @@ jobs:
script/check_import_time.py --check --har importtime.har
- name: Upload waterfall HAR
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: import-time-waterfall
path: importtime.har
@@ -626,7 +629,7 @@ jobs:
apt-get install -y libc6-dbg
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@c4fd08a3a159bd0cc208da1e0edf32b8c47d75e5 # v5.4.0
uses: CodSpeedHQ/action@f842b9a3ffe213dbb81768fd7f7eeb4dfcf1c369 # v5.4.2
with:
run: |
. venv/bin/activate
@@ -680,7 +683,7 @@ jobs:
include:
- id: clang-tidy
name: Run script/clang-tidy for ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266 --grep USE_SOCKET_IMPL_LWIP_TCP --grep USE_SHD_FIRMWARE_DATA
pio_cache_key: tidyesp8266
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino
@@ -693,12 +696,16 @@ jobs:
ignore_errors: false
- id: clang-tidy
name: Run script/clang-tidy for RP2
options: --environment rp2-tidy --grep USE_RP2
options: --environment rp2-tidy --grep USE_RP2 --grep USE_BLE_SCAN_RESPONSE_MERGER
pio_cache_key: tidyrp2
- id: clang-tidy
name: Run script/clang-tidy for host
options: --environment host-tidy --grep USE_HOST
pio_cache_key: tidyhost
- id: clang-tidy
name: Run script/clang-tidy for LibreTiny
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X --grep USE_LN882H_BLE --grep USE_SOCKET_IMPL_LWIP_SOCKETS
pio_cache_key: tidylibretiny
steps:
@@ -707,6 +714,16 @@ jobs:
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Install apt packages (cached)
if: matrix.pio_cache_key == 'tidyhost'
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
# by dsmr and dlms_meter. Packages and version must match seed-apt-cache
# exactly.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Restore Python
id: restore-python
@@ -989,14 +1006,14 @@ jobs:
- id: clang-tidy
name: Run script/clang-tidy for ESP32 S3
# yamllint disable-line rule:line-length
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC --grep SOC_I2S_SUPPORTS_TDM --grep USE_OPENTHREAD
tidy_environment: esp32s3-idf-tidy
- id: clang-tidy
name: Run script/clang-tidy for ESP32 P4
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
# so their code paths differ -- lint them under the P4 build too.
# yamllint disable-line rule:line-length
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep CONFIG_IDF_TARGET_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
tidy_environment: esp32p4-idf-tidy
- id: clang-tidy
name: Run script/clang-tidy for ESP32 C6
@@ -1102,8 +1119,8 @@ jobs:
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -1448,7 +1465,7 @@ jobs:
# install step (order-of-magnitude faster on cold boots,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -1647,7 +1664,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -1710,7 +1727,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -1740,13 +1757,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: memory-analysis-pr
path: ./memory-analysis
@@ -35,7 +35,7 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+3 -3
View File
@@ -162,7 +162,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -195,7 +195,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: digests-*
path: /tmp/digests
@@ -245,7 +245,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
days-before-close: 7
stale-label: stale
exempt-label: not-stale
conflict-label: needs-rebase
conflict-label: has-conflicts
ignored-users: esphbot,codecov-commenter
stale-pr-message: >
There hasn't been any activity on this pull request recently. This
@@ -43,7 +43,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``prek`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
@@ -20,7 +20,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+2 -2
View File
@@ -10,7 +10,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.16.10
rev: v0.17.0
hooks:
# Run the linter.
- id: ruff
@@ -36,7 +36,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
rev: v3.22.0
hooks:
- id: pyupgrade
args: [--py312-plus]
+46 -6
View File
@@ -225,6 +225,15 @@ file does, and it is the authority when they disagree. The most useful starting
stays inside the `#ifdef`.
* **Failures after `setup()`:** `mark_failed()` is for `setup()` only. A runtime read or write failure
sets `status_set_warning()` and the next success clears it; writes skip work when `is_failed()`.
* **`dump_config()` already prints the failure reason:** for a failed component, core prints
`<component> is marked FAILED: <reason>` right after the component's `dump_config()`, using the reason
passed to `mark_failed()`. So `dump_config()` never checks `is_failed()` to log a failure, and no
`error_code_` member exists only to pick that message. Pass the reason where setup fails instead:
`this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL))` for a failed bus transfer, a short specific reason
otherwise (`LOG_STR("Wrong chip ID")`). Core also logs it at that moment, so no `ESP_LOGE` repeats it
before `mark_failed()`; keep a separate line only when it prints a value read at run time, such as the
chip ID that did not match. Only the first reason is kept, so do not call `status_set_error()` before
`mark_failed()` and do not mark the component failed again after a helper already did.
* **Fixed platform facts compile to nothing:** gate on `#ifdef USE_ESP8266` and friends, not on a
runtime check of something the build already knows.
* **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they
@@ -278,7 +287,8 @@ file does, and it is the authority when they disagree. The most useful starting
`from esphome.components.my_component import DOMAIN`. Keep it in alphabetical order with the other
metadata constants. CI (`lint_component_domain`) fails without it.
- `DEPENDENCIES`: List of required components
- `AUTO_LOAD`: Components to automatically load
- `AUTO_LOAD`: Components to automatically load. Not a way to put entities under the component's
own key; see "Entities of a hub" below.
- `CONFLICTS_WITH`: Incompatible components
- `CODEOWNERS`: GitHub usernames responsible for maintenance
- `MULTI_CONF`: Whether multiple instances are allowed
@@ -370,6 +380,26 @@ file does, and it is the authority when they disagree. The most useful starting
var = await switch.new_switch(config)
```
- **Entities of a hub:** a new component's entities go in platform files (`sensor.py`,
`binary_sensor.py`, ...) that take the hub's id, never under the hub's own key with their platforms
added to `AUTO_LOAD` to make that work. Auto loading a platform because the C++ uses its type is
fine. See https://developers.esphome.io/architecture/components/#exposing-multiple-entities.
```yaml
# Bad - entities under the hub's key
my_hub:
connected:
name: Connected
# Good - the hub, then one platform entry that holds its entities
my_hub:
id: hub1
binary_sensor:
- platform: my_hub
my_hub_id: hub1 # optional with a single hub
connected:
name: Connected
```
- **Optional child entities of a hub:** bind the config once with `sensor.sub_sensors(config)` (or
`sub_binary_sensors`, `sub_text_sensors`, `sub_buttons`, `sub_switches`, `sub_numbers`,
`sub_selects` in their domains), adding `parent=hub` for entities that derive from `Parented<T>`,
@@ -380,15 +410,25 @@ file does, and it is the authority when they disagree. The most useful starting
itself, `hub` for one fetched with `cg.get_variable`) so the calls fit on one line. Loops whose
setter also takes an index, such as `set_gate_threshold(x, n)`, stay as they are.
```python
# my_hub/sensor.py
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
hub = await cg.get_variable(config[CONF_MY_HUB_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
buttons = button.sub_buttons(config, parent=var)
await buttons(CONF_RESTART, var.set_restart_button)
await sensors(CONF_TEMPERATURE, hub.set_temperature_sensor)
await sensors(CONF_HUMIDITY, hub.set_humidity_sensor)
# my_hub/button.py
async def to_code(config):
hub = await cg.get_variable(config[CONF_MY_HUB_ID])
buttons = button.sub_buttons(config, parent=hub)
await buttons(CONF_RESTART, hub.set_restart_button)
```
- **Pins and referenced components:** set them with `await cg.add_pin(var.set_cs_pin, config[CONF_CS_PIN])`
and `await cg.add_variable(var.set_bus, config[CONF_BUS_ID])`; both skip a `None` value, so pass
`config.get(KEY)` for an optional key, and return what they created. Use `cg.register_parented` for `set_parent`.
* **Automations (Triggers, Actions, Conditions):**
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true).
+32 -1
View File
@@ -30,6 +30,9 @@ esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_base/* @jeromelaban @ncareau
esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
esphome/components/airton/* @Procsiab
esphome/components/airton/select/* @Procsiab
esphome/components/airton/switch/* @Procsiab
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/alpha3/* @jan-hofmeier
esphome/components/am2315c/* @swoboda1337
@@ -40,6 +43,7 @@ esphome/components/analog_threshold/* @ianchi
esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix
esphome/components/apc1/* @ademuri
esphome/components/apc_proteous/* @clydebarrow
esphome/components/apds9306/* @aodrenah
esphome/components/apds9930/* @iyesin
esphome/components/api/* @esphome/core
@@ -48,6 +52,7 @@ esphome/components/aranet/* @patrickod
esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr
esphome/components/as734x/* @latonita @mrgnr
esphome/components/async_tcp/* @esphome/core
esphome/components/at581x/* @X-Ryl669
esphome/components/atc_mithermometer/* @ahpohl
@@ -155,6 +160,10 @@ esphome/components/delonghi/* @grob6000
esphome/components/dew_point/* @CFlix
esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber
esphome/components/dfrobot_sen0465_base/* @tzabbi
esphome/components/dfrobot_sen0465_i2c/* @tzabbi
esphome/components/dfrobot_sen0465_uart/* @tzabbi
esphome/components/dfrobot_sen0609/* @remcom
esphome/components/dht/* @OttoWinter
esphome/components/display_menu_base/* @numo68
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
@@ -183,6 +192,7 @@ esphome/components/es7243e/* @kbx81
esphome/components/es8156/* @kbx81
esphome/components/es8311/* @kahrendt @kroimon
esphome/components/es8388/* @P4uLT
esphome/components/es9038q2m_katana/* @GioMez
esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito
esphome/components/esp32_ble_client/* @bdraco @jesserockz
@@ -212,12 +222,14 @@ esphome/components/factory_reset/* @anatoly-savchenkov
esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi
esphome/components/file/* @esphome/core
esphome/components/filter_lifetime/* @tuct
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
esphome/components/font/* @clydebarrow @esphome/core
esphome/components/friedrich/* @rwrozelle
esphome/components/fs3000/* @kahrendt
esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio
esphome/components/fusb302b/* @remcom
esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval
@@ -239,9 +251,14 @@ esphome/components/gt911/* @clydebarrow @jesserockz
esphome/components/haier/* @paveldn
esphome/components/haier/binary_sensor/* @paveldn
esphome/components/haier/button/* @paveldn
esphome/components/haier/select/* @paveldn
esphome/components/haier/sensor/* @paveldn
esphome/components/haier/switch/* @paveldn
esphome/components/haier/text_sensor/* @paveldn
esphome/components/haier_tundra/* @fauxpark
esphome/components/haier_tundra/button/* @fauxpark
esphome/components/haier_tundra/switch/* @fauxpark
esphome/components/haier_water_heater/* @TheLexus
esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann
@@ -250,6 +267,7 @@ esphome/components/hc8/* @omartijn
esphome/components/hdc2010/* @optimusprimespace @ssieb
esphome/components/hdc2080/* @G-Pereira @jesserockz
esphome/components/hdc302x/* @joshuasing
esphome/components/hdmi_cec/* @JosVanEijndhoven
esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
@@ -261,6 +279,7 @@ esphome/components/hmac_sha256/* @dwmw2
esphome/components/hoermann_hcp/* @zweckj
esphome/components/homeassistant/* @esphome/core @OttoWinter
esphome/components/homeassistant/button/* @jesserockz
esphome/components/homeassistant/light/* @egormanga
esphome/components/homeassistant/number/* @landonr
esphome/components/homeassistant/select/* @jesserockz
esphome/components/homeassistant/switch/* @Links2004
@@ -286,6 +305,7 @@ esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/i2s_clockless_led_strip/* @j9brown
esphome/components/iaqcore/* @yozik04
esphome/components/icm20689/* @lboue
esphome/components/icnt86/* @danepowell
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
esphome/components/improv_base/* @esphome/core
@@ -378,8 +398,10 @@ esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @edenhaus @jorre05
esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz
esphome/components/mics_5524/* @nliaudat
esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov
esphome/components/midea_portasplit/* @Fexiven
esphome/components/mipi_dsi/* @clydebarrow
esphome/components/mipi_rgb/* @clydebarrow
esphome/components/mipi_spi/* @clydebarrow
@@ -410,6 +432,9 @@ esphome/components/mpl3115a2/* @kbickar
esphome/components/mpu6886/* @fabaff
esphome/components/ms8607/* @e28eta
esphome/components/msa3xx/* @latonita
esphome/components/mt6701/* @slimcdk
esphome/components/mt6701_i2c/* @slimcdk
esphome/components/mt6701_spi/* @slimcdk
esphome/components/nau7802/* @cujomalainey
esphome/components/network/* @esphome/core
esphome/components/nextion/* @edwardtfn @senexcrenshaw
@@ -434,6 +459,7 @@ esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931
esphome/components/pca9505/* @ajfriesen @ssieb
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon
esphome/components/pcf8563/* @KoenBreeman
@@ -493,10 +519,12 @@ esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rp2_ble_tracker/* @bdraco
esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtsp_server/* @max246
esphome/components/rtttl/* @glmnet @ximex
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
esphome/components/runtime_stats/* @bdraco
esphome/components/rx8025t/* @remcom
esphome/components/rx8111/* @techdregs
esphome/components/rx8130/* @beormund
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/samsung/* @jorofi
@@ -656,6 +684,7 @@ esphome/components/ultrasonic/* @ssieb @swoboda1337
esphome/components/unicode/* @esphome/core
esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon
esphome/components/usb_audio/* @ab-tools
esphome/components/usb_cdc_acm/* @kbx81
esphome/components/usb_host/* @clydebarrow
esphome/components/usb_uart/* @clydebarrow
@@ -664,10 +693,11 @@ esphome/components/vbus/* @ssieb
esphome/components/veml3235/* @kbx81
esphome/components/veml7700/* @latonita
esphome/components/version/* @esphome/core
esphome/components/vevor_dtx/* @Bofr5555
esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
esphome/components/water_heater/* @dhoeben
esphome/components/water_heater/* @esphome/core
esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/waveshare_io_ch32v003/* @latonita
esphome/components/web_server/ota/* @esphome/core
@@ -706,6 +736,7 @@ esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23
esphome/components/zephyr_coredump/* @tomaszduda23
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
esphome/components/zephyr_pwm/* @wiomoc
esphome/components/zhlt01/* @cfeenstra1024
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.1
RUN uv pip install --no-cache-dir esphome-device-builder==1.24.0
RUN \
platformio settings set enable_telemetry No \
+142 -4
View File
@@ -14,6 +14,7 @@ from esphome.const import (
CONF_COUNT,
CONF_ELSE,
CONF_ID,
CONF_IF,
CONF_THEN,
CONF_TIME,
CONF_TIMEOUT,
@@ -34,6 +35,9 @@ from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType, SafeExpType
from esphome.util import Registry
CONF_CHOOSE = "choose"
CONF_CHOICE_ID = "choice_id"
def progmem_bytes(name: str, data: bytes | list[int]) -> MockObj:
"""Shared PROGMEM table for constant bytes; equal payloads share one, empty is nullptr."""
@@ -840,10 +844,39 @@ async def delay_action_to_code(
return var
def _wrap_bare_if_condition(value: Any) -> Any:
"""Allow a single condition directly under `if:` in place of `condition: {...}`."""
if not isinstance(value, dict):
return value
# `any` and `all` are also registered conditions, but here they are the if action's own keys
bare = [
key
for key in value
if key in CONDITION_REGISTRY and key not in (CONF_ANY, CONF_ALL)
]
if not bare:
return value
if len(bare) > 1:
raise cv.Invalid(
f"Only one condition may be given directly under 'if', found {', '.join(bare)}. "
f"Use '{CONF_CONDITION}', '{CONF_ALL}' or '{CONF_ANY}' to combine conditions"
)
if other := [k for k in (CONF_CONDITION, CONF_ANY, CONF_ALL) if k in value]:
raise cv.Invalid(
f"'{bare[0]}' given directly under 'if' cannot be used with '{other[0]}'. "
f"Move it into '{other[0]}'"
)
key = bare[0]
result = {k: v for k, v in value.items() if k != key}
result[CONF_CONDITION] = {key: value[key]}
return result
@register_action(
"if",
IfAction,
cv.All(
_wrap_bare_if_condition,
{
cv.Exclusive(
CONF_CONDITION, CONF_CONDITION
@@ -870,14 +903,104 @@ async def if_action_to_code(
cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION))
condition = await build_condition(config[cond_conf], template_arg, args)
var = cg.new_Pvariable(action_id, if_template_arg, condition)
then_actions = else_actions = None
if CONF_THEN in config:
for action in await build_action_list(config[CONF_THEN], template_arg, args):
then_actions = await build_action_list(config[CONF_THEN], template_arg, args)
if has_else:
else_actions = await build_action_list(config[CONF_ELSE], template_arg, args)
_add_if_branches(var, then_actions, else_actions)
return var
def _add_if_branches(
var: MockObj,
then_actions: list[MockObj] | None,
else_actions: list[MockObj] | None,
) -> None:
"""Add the branches of an IfAction one action at a time; None leaves a branch out."""
if then_actions is not None:
for action in then_actions:
cg.add(var.add_then_action(action))
cg.add(var.finish_then())
if has_else:
for action in await build_action_list(config[CONF_ELSE], template_arg, args):
if else_actions is not None:
for action in else_actions:
cg.add(var.add_else_action(action))
cg.add(var.finish_else())
def _validate_choice(choice: ConfigType) -> ConfigType:
"""A choice is either `if` with `then`, or `else` on its own."""
if CONF_ELSE in choice:
for key in (CONF_IF, CONF_THEN):
if key in choice:
raise cv.Invalid(f"'{key}' cannot be used with 'else'", path=[key])
return choice
for key in (CONF_IF, CONF_THEN):
if key not in choice:
raise cv.Invalid(f"'{key}' is required", path=[key])
return choice
def _validate_choose_else(choices: list[ConfigType]) -> list[ConfigType]:
"""`else` may only be the last choice, and not the only one."""
for index, choice in enumerate(choices[:-1]):
if CONF_ELSE in choice:
raise cv.Invalid(
"Only the last choice may be 'else'", path=[index, CONF_ELSE]
)
if len(choices) == 1 and CONF_ELSE in choices[0]:
raise cv.Invalid(
"At least one choice must have 'if' and 'then'", path=[0, CONF_ELSE]
)
return choices
@register_action(
CONF_CHOOSE,
IfAction,
cv.All(
cv.ensure_list(
cv.All(
cv.Schema(
{
cv.GenerateID(CONF_CHOICE_ID): cv.declare_id(IfAction),
cv.Optional(CONF_IF): validate_potentially_and_condition,
cv.Optional(CONF_THEN): validate_action_list,
cv.Optional(CONF_ELSE): validate_action_list,
}
),
_validate_choice,
)
),
cv.Length(min=1),
_validate_choose_else,
),
synchronous=True,
)
async def choose_action_to_code(
config: list[ConfigType],
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
# Built as a chain of if/else actions: each choice's else branch holds the next choice,
# and the last choice's else branch holds the `else` actions, if any.
else_actions: list[MockObj] | None = None
if CONF_ELSE in config[-1]:
else_actions = await build_action_list(
config[-1][CONF_ELSE], template_arg, args
)
config = config[:-1]
var: MockObj | None = None
for index in reversed(range(len(config))):
choice = config[index]
if_template_arg = cg.TemplateArguments(else_actions is not None, *template_arg)
condition = await build_condition(choice[CONF_IF], template_arg, args)
choice_id = action_id if index == 0 else choice[CONF_CHOICE_ID]
var = cg.new_Pvariable(choice_id, if_template_arg, condition)
then_actions = await build_action_list(choice[CONF_THEN], template_arg, args)
_add_if_branches(var, then_actions, else_actions)
else_actions = [var]
return var
@@ -906,6 +1029,21 @@ async def while_action_to_code(
return var
def _shadow_iteration(args: TemplateArgsType) -> TemplateArgsType:
"""Rename an inherited `iteration` so the inner repeat's own counter can shadow it."""
names = {name for _, name in args}
renamed: TemplateArgsType = []
for type_, name in args:
if name == "iteration":
depth = 1
while f"iteration_{depth}" in names:
depth += 1
name = f"iteration_{depth}"
names.add(name)
renamed.append((type_, name))
return renamed
@register_action(
"repeat",
RepeatAction,
@@ -929,7 +1067,7 @@ async def repeat_action_to_code(
actions = await build_action_list(
config[CONF_THEN],
cg.TemplateArguments(cg.uint32, *template_arg.args),
[(cg.uint32, "iteration"), *args],
[(cg.uint32, "iteration"), *_shadow_iteration(args)],
)
for action in actions:
cg.add(var.add_then_action(action))
+3
View File
@@ -53,11 +53,14 @@ from esphome.cpp_generator import ( # noqa: F401
with_local_variable,
)
from esphome.cpp_helpers import ( # noqa: F401
add_pin,
add_variable,
build_registry_entry,
build_registry_list,
extract_registry_entry_config,
get_slot_count,
gpio_pin_expression,
new_bus_component,
past_safe_mode,
register_component,
register_parented,
+3 -5
View File
@@ -9,15 +9,13 @@ namespace esphome::a01nyub {
ESPHOME_LOG_TAG(TAG, "a01nyub.sensor");
void A01nyubComponent::loop() {
uint8_t data;
while (this->available() > 0) {
this->read_byte(&data);
this->read_available([this](uint8_t data) {
if (this->buffer_.empty() && (data != 0xff))
continue;
return;
buffer_.push_back(data);
if (this->buffer_.size() == 4)
this->check_buffer_();
}
});
}
void A01nyubComponent::check_buffer_() {
+3 -5
View File
@@ -9,15 +9,13 @@ namespace esphome::a02yyuw {
ESPHOME_LOG_TAG(TAG, "a02yyuw.sensor");
void A02yyuwComponent::loop() {
uint8_t data;
while (this->available() > 0) {
this->read_byte(&data);
this->read_available([this](uint8_t data) {
if (this->buffer_.empty() && (data != 0xff))
continue;
return;
buffer_.push_back(data);
if (this->buffer_.size() == 4)
this->check_buffer_();
}
});
}
void A02yyuwComponent::check_buffer_() {
+3 -8
View File
@@ -23,11 +23,6 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await stepper.register_stepper(var, config)
step_pin = await cg.gpio_pin_expression(config[CONF_STEP_PIN])
cg.add(var.set_step_pin(step_pin))
dir_pin = await cg.gpio_pin_expression(config[CONF_DIR_PIN])
cg.add(var.set_dir_pin(dir_pin))
if sleep_pin_config := config.get(CONF_SLEEP_PIN):
sleep_pin = await cg.gpio_pin_expression(sleep_pin_config)
cg.add(var.set_sleep_pin(sleep_pin))
await cg.add_pin(var.set_step_pin, config[CONF_STEP_PIN])
await cg.add_pin(var.set_dir_pin, config[CONF_DIR_PIN])
await cg.add_pin(var.set_sleep_pin, config.get(CONF_SLEEP_PIN))
+3 -10
View File
@@ -19,11 +19,7 @@ AbsoluteHumidityComponent = absolute_humidity_ns.class_(
SaturationVaporPressureEquation = absolute_humidity_ns.enum(
"SaturationVaporPressureEquation"
)
EQUATION = {
"BUCK": SaturationVaporPressureEquation.BUCK,
"TETENS": SaturationVaporPressureEquation.TETENS,
"WOBUS": SaturationVaporPressureEquation.WOBUS,
}
EQUATION = SaturationVaporPressureEquation.options("BUCK", "TETENS", "WOBUS")
CONFIG_SCHEMA = (
sensor.sensor_schema(
@@ -48,10 +44,7 @@ async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
temperature_sensor = await cg.get_variable(config[CONF_TEMPERATURE])
cg.add(var.set_temperature_sensor(temperature_sensor))
humidity_sensor = await cg.get_variable(config[CONF_HUMIDITY])
cg.add(var.set_humidity_sensor(humidity_sensor))
await cg.add_variable(var.set_temperature_sensor, config[CONF_TEMPERATURE])
await cg.add_variable(var.set_humidity_sensor, config[CONF_HUMIDITY])
cg.add(var.set_equation(config[CONF_EQUATION]))
+5 -9
View File
@@ -14,11 +14,9 @@ ac_dimmer_ns = cg.esphome_ns.namespace("ac_dimmer")
AcDimmer = ac_dimmer_ns.class_("AcDimmer", output.FloatOutput, cg.Component)
DimMethod = ac_dimmer_ns.enum("DimMethod")
DIM_METHODS = {
"LEADING_PULSE": DimMethod.DIM_METHOD_LEADING_PULSE,
"LEADING": DimMethod.DIM_METHOD_LEADING,
"TRAILING": DimMethod.DIM_METHOD_TRAILING,
}
DIM_METHODS = DimMethod.options(
"LEADING_PULSE", "LEADING", "TRAILING", prefix="DIM_METHOD_"
)
ZC_INTERRUPT_TYPES = {
"RISING": gpio_ns.INTERRUPT_RISING_EDGE,
@@ -70,10 +68,8 @@ async def to_code(config: ConfigType) -> None:
config[CONF_MIN_POWER] = 0.1
await output.register_output(var, config)
pin = await cg.gpio_pin_expression(config[CONF_GATE_PIN])
cg.add(var.set_gate_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN])
cg.add(var.set_zero_cross_pin(pin))
await cg.add_pin(var.set_gate_pin, config[CONF_GATE_PIN])
await cg.add_pin(var.set_zero_cross_pin, config[CONF_ZERO_CROSS_PIN])
cg.add(var.set_zero_cross_interrupt_type(config[CONF_ZERO_CROSS_INTERRUPT_TYPE]))
cg.add(var.set_init_with_half_cycle(config[CONF_INIT_WITH_HALF_CYCLE]))
cg.add(var.set_method(config[CONF_METHOD]))
@@ -68,32 +68,32 @@ void AdalightLightEffect::apply(light::AddressableLight &it, const Color &curren
blank_all_leds_(it);
}
if (this->available() > 0) {
ESP_LOGV(TAG, "Frame: Available (size=%d).", this->available());
const size_t avail = this->available();
if (avail > 0) {
ESP_LOGV(TAG, "Frame: Available (size=%zu).", avail);
}
while (this->available() != 0) {
uint8_t data;
if (!this->read_byte(&data))
break;
this->frame_.push_back(data);
this->last_byte_ = now;
this->read_available(
[this, &it, now](uint8_t data) {
this->frame_.push_back(data);
this->last_byte_ = now;
switch (this->parse_frame_(it)) {
case INVALID:
ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=%d).", this->frame_.size(), this->frame_[0]);
reset_frame_(it);
break;
switch (this->parse_frame_(it)) {
case INVALID:
ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=%d).", this->frame_.size(), this->frame_[0]);
reset_frame_(it);
break;
case PARTIAL:
break;
case PARTIAL:
break;
case CONSUMED:
ESP_LOGV(TAG, "Frame: Consumed (size=%zu).", this->frame_.size());
reset_frame_(it);
break;
}
}
case CONSUMED:
ESP_LOGV(TAG, "Frame: Consumed (size=%zu).", this->frame_.size());
reset_frame_(it);
break;
}
},
avail);
}
AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableLight &it) {
+1 -5
View File
@@ -46,11 +46,7 @@ ATTENUATION_MODES = {
sampling_mode = adc_ns.enum("SamplingMode", is_class=True)
SAMPLING_MODES = {
"avg": sampling_mode.AVG,
"min": sampling_mode.MIN,
"max": sampling_mode.MAX,
}
SAMPLING_MODES = sampling_mode.options("avg", "min", "max")
adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True)
+1 -2
View File
@@ -164,8 +164,7 @@ async def to_code(config: ConfigType) -> None:
# Remove before 2027.2.0
cg.add(var.set_is_temperature())
elif not CORE.is_nrf52 or config[CONF_PIN][CONF_NUMBER] not in EXTRA_ADC:
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
await cg.add_pin(var.set_pin, config[CONF_PIN])
# Skip the setters when the config matches the C++ initializers.
if config[CONF_RAW]:
+1 -4
View File
@@ -1,7 +1,6 @@
import esphome.codegen as cg
from esphome.components import spi
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = ["spi"]
@@ -20,6 +19,4 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
await spi.new_spi_component(config)
@@ -56,8 +56,4 @@ async def to_code(config: ConfigType) -> None:
)
cg.add(var.set_pixel_mapper(pixel_mapper_template_))
if lambda_config := config.get(CONF_LAMBDA):
lambda_ = await cg.process_lambda(
lambda_config, [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
await display.add_writer(var, config)
+1 -7
View File
@@ -11,12 +11,6 @@
namespace esphome::ade7880 {
// adapted from https://stackoverflow.com/a/55912127/1886371
template<size_t Bits, typename T> inline T sign_extend(const T &v) noexcept {
using S = struct { signed Val : Bits; };
return reinterpret_cast<const S *>(&v)->Val;
}
// Register types
// unsigned 8-bit (uint8_t)
// signed 10-bit - 16-bit ZP on wire (int16_t, needs sign extension)
@@ -49,7 +43,7 @@ uint16_t ADE7880::read_u16_register16_(uint16_t a_register) {
int32_t ADE7880::read_s24zp_register16_(uint16_t a_register) {
// s24zp means 24 bit signed value in the lower 24 bits of a 32-bit register
int32_t in;
uint32_t in;
this->read_register16(a_register, reinterpret_cast<uint8_t *>(&in), sizeof(in));
return sign_extend<24>(convert_big_endian(in));
}
+4 -13
View File
@@ -282,20 +282,11 @@ async def power_channel(config: ConfigType) -> MockObj:
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
var = await i2c.new_i2c_component(config)
if irq0_pin := config.get(CONF_IRQ0_PIN):
pin = await cg.gpio_pin_expression(irq0_pin)
cg.add(var.set_irq0_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_IRQ1_PIN])
cg.add(var.set_irq1_pin(pin))
if reset_pin := config.get(CONF_RESET_PIN):
pin = await cg.gpio_pin_expression(reset_pin)
cg.add(var.set_reset_pin(pin))
await cg.add_pin(var.set_irq0_pin, config.get(CONF_IRQ0_PIN))
await cg.add_pin(var.set_irq1_pin, config[CONF_IRQ1_PIN])
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
if frequency := config.get(CONF_FREQUENCY):
cg.add(var.set_frequency(frequency))
+1 -3
View File
@@ -169,9 +169,7 @@ ADE7953_CONFIG_SCHEMA = cv.Schema(
async def register_ade7953(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
if irq_pin_config := config.get(CONF_IRQ_PIN):
irq_pin = await cg.gpio_pin_expression(irq_pin_config)
cg.add(var.set_irq_pin(irq_pin))
await cg.add_pin(var.set_irq_pin, config.get(CONF_IRQ_PIN))
cg.add(var.set_pga_v(PGA_GAINS[config.get(CONF_VOLTAGE_PGA_GAIN)]))
cg.add(var.set_pga_ia(PGA_GAINS[config.get(CONF_CURRENT_PGA_GAIN_A)]))
+1 -4
View File
@@ -1,7 +1,6 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"]
@@ -27,8 +26,6 @@ CONFIG_SCHEMA = (
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
var = await i2c.new_i2c_component(config)
cg.add(var.set_continuous_mode(config[CONF_CONTINUOUS_MODE]))
+2 -5
View File
@@ -11,7 +11,7 @@ static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
void ADS1115Component::setup() {
uint16_t value;
if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) {
this->mark_failed();
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
@@ -58,7 +58,7 @@ void ADS1115Component::setup() {
config |= 0b0000000000000011;
if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) {
this->mark_failed();
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->prev_config_ = config;
@@ -66,9 +66,6 @@ void ADS1115Component::setup() {
void ADS1115Component::dump_config() {
ESP_LOGCONFIG(TAG, "ADS1115:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
}
float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain,
ADS1115Resolution resolution, ADS1115Samplerate samplerate) {
+1 -4
View File
@@ -1,7 +1,6 @@
import esphome.codegen as cg
from esphome.components import spi
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
CODEOWNERS = ["@solomondg1"]
@@ -26,6 +25,4 @@ CONFIG_SCHEMA = (
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
await spi.new_spi_component(config)
+4 -9
View File
@@ -4,7 +4,6 @@ from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_ID,
CONF_MODE,
CONF_TVOC,
CONF_VALUE,
@@ -58,9 +57,7 @@ FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema("ags10", max_frequency=
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
var = await i2c.new_i2c_component(config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_TVOC, var.set_tvoc)
@@ -84,11 +81,9 @@ automation.register_apply_action(
)
AGS10SetZeroPointActionMode = ags10_ns.enum("AGS10SetZeroPointActionMode")
AGS10_SET_ZERO_POINT_ACTION_MODE = {
"FACTORY_DEFAULT": AGS10SetZeroPointActionMode.FACTORY_DEFAULT,
"CURRENT_VALUE": AGS10SetZeroPointActionMode.CURRENT_VALUE,
"CUSTOM_VALUE": AGS10SetZeroPointActionMode.CUSTOM_VALUE,
}
AGS10_SET_ZERO_POINT_ACTION_MODE = AGS10SetZeroPointActionMode.options(
"FACTORY_DEFAULT", "CURRENT_VALUE", "CUSTOM_VALUE"
)
AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
{
+4 -11
View File
@@ -52,8 +52,7 @@ void AHT10Component::setup() {
break;
}
if (error_code != i2c::ERROR_OK) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
this->mark_failed();
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
uint8_t cal_attempts = 0;
@@ -61,20 +60,17 @@ void AHT10Component::setup() {
while (data & AHT10_STATUS_BUSY) {
delay(AHT10_DEFAULT_DELAY);
if (this->read(&data, 1) != i2c::ERROR_OK) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
this->mark_failed();
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
++cal_attempts;
if (cal_attempts > AHT10_INIT_ATTEMPTS) {
ESP_LOGE(TAG, "Initialization timed out");
this->mark_failed();
this->mark_failed(LOG_STR("Initialization timed out"));
return;
}
}
if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
ESP_LOGE(TAG, "Initialization failed");
this->mark_failed();
this->mark_failed(LOG_STR("Initialization failed"));
return;
}
}
@@ -152,9 +148,6 @@ void AHT10Component::update() {
void AHT10Component::dump_config() {
ESP_LOGCONFIG(TAG, "AHT10:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
}
+2 -8
View File
@@ -3,7 +3,6 @@ from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_HUMIDITY,
CONF_ID,
CONF_TEMPERATURE,
CONF_VARIANT,
DEVICE_CLASS_HUMIDITY,
@@ -20,10 +19,7 @@ aht10_ns = cg.esphome_ns.namespace("aht10")
AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice)
AHT10Variant = aht10_ns.enum("AHT10Variant")
AHT10_VARIANTS = {
"AHT10": AHT10Variant.AHT10,
"AHT20": AHT10Variant.AHT20,
}
AHT10_VARIANTS = AHT10Variant.options("AHT10", "AHT20")
CONFIG_SCHEMA = (
cv.Schema(
@@ -52,9 +48,7 @@ CONFIG_SCHEMA = (
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
var = await i2c.new_i2c_component(config)
cg.add(var.set_variant(config[CONF_VARIANT]))
sensors = sensor.sub_sensors(config)
+1 -6
View File
@@ -10,8 +10,7 @@ ESPHOME_LOG_TAG(TAG, "aic3204");
#define ERROR_CHECK(err, msg) \
if (!(err)) { \
ESP_LOGE(TAG, msg); \
this->mark_failed(); \
this->mark_failed(LOG_STR(msg)); \
return; \
}
@@ -108,10 +107,6 @@ void AIC3204::setup() {
void AIC3204::dump_config() {
ESP_LOGCONFIG(TAG, "AIC3204:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
}
bool AIC3204::set_mute_off() {
+2 -4
View File
@@ -3,7 +3,7 @@ import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.audio_dac import AudioDac
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODE
from esphome.const import CONF_MODE
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
@@ -41,6 +41,4 @@ automation.register_apply_action(
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
await i2c.new_i2c_component(config)

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