Compare commits

...

237 Commits

Author SHA1 Message Date
Clyde Stubbs dd82a91d8f [lvgl] Don't animate page change when not requested (#15069) 2026-03-22 11:13:17 +11:00
J. Nick Koston 86ec218f75 [benchmark] Add plaintext API frame write benchmarks (#15036) 2026-03-21 13:15:35 -10:00
Samuel Sieb 2a6ec597b4 [analog_threshhold] add missing header (#15058) 2026-03-21 18:13:08 +00:00
dependabot[bot] 8dd69207ea Bump aioesphomeapi from 44.6.2 to 44.7.0 (#15052)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 10:24:56 +00:00
J. Nick Koston d203a46ef8 [api] Enable HAVE_WEAK_SYMBOLS and HAVE_INLINE_ASM for libsodium (#15038) 2026-03-21 04:17:37 +00:00
J. Nick Koston 1920d8a887 [benchmark] Add noise encryption benchmarks (#15037) 2026-03-20 17:35:17 -10:00
J. Nick Koston 95dea59382 [core] Use SplitMix32 PRNG for random_uint32() (#14984) 2026-03-20 15:25:54 -10:00
J. Nick Koston f3cddcee21 [core] Store parent pointers as members to enable inline Callback storage (#14923) 2026-03-20 15:25:40 -10:00
J. Nick Koston 21e384cafd [esp32] Disable PicolibC Newlib compatibility shim on IDF 6.0+ (#15008)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 15:10:18 -10:00
J. Nick Koston 32db055b98 [number] Clean up NumberCall::perform() increment/decrement logic (#15000) 2026-03-20 15:09:28 -10:00
J. Nick Koston 2c87260046 [core] Optimize Component::is_ready() with bitmask check (#15005) 2026-03-20 15:09:13 -10:00
J. Nick Koston 51ccad8461 [preferences] Shorten TAG strings across all platforms (#15004) 2026-03-20 15:09:01 -10:00
J. Nick Koston 7f500c4b6e [modbus] Fix size_t format warning in clear_rx_buffer_ (#15002) 2026-03-20 15:08:46 -10:00
J. Nick Koston 564d155cb6 [wifi] Use LOG_STR_LITERAL for scan complete log on ESP8266 (#15001) 2026-03-20 15:08:33 -10:00
J. Nick Koston edf5542559 [analyze-memory] Attribute extern C symbols to components via source file mapping (#15006) 2026-03-20 15:05:17 -10:00
J. Nick Koston 51335e8830 [ledc] Fix deprecated intr_type warning on ESP-IDF 6.0+ (#15009) 2026-03-20 15:01:30 -10:00
J. Nick Koston 391ffe34f8 [rp2040] Fix get_mac_address_raw to use ethernet MAC when WiFi unavailable (#15033) 2026-03-20 15:01:11 -10:00
J. Nick Koston 12ead0408a [gpio] Use constexpr uint32_t timer ID for interlock timeout (#15010) 2026-03-20 15:00:56 -10:00
Daniel Kent 2d39cc2540 [spa06_i2c] Add SPA06-003 Temperature and Pressure Sensor - I2C support (Part 2 of 3) (#14522)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-21 00:38:04 +00:00
J. Nick Koston a9a8f4cb3b [time] Fix timezone_offset() and recalc_timestamp_local() always returning UTC (#14996) 2026-03-20 13:58:14 -10:00
J. Nick Koston 8fa2e75afa [core] Add copy() method to StringRef for std::string compatibility (#15028) 2026-03-20 13:58:02 -10:00
J. Nick Koston 0b01f9fc42 [web_server] Increase httpd task stack size to prevent stack overflow (#14997)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 13:57:51 -10:00
Jonathan Swoboda ed8c062d9f [esp32_touch] Fix initial state never published when sensor untouched (#15032) 2026-03-20 19:53:02 -04:00
J. Nick Koston 5e516e78e4 [wifi] Fix ESP8266 power_save_mode mapping (LIGHT/HIGH were swapped) (#15029) 2026-03-20 12:13:49 -10:00
J. Nick Koston 896b6ec8c9 [api] Increase noise handshake timeout to 60s for slow WiFi environments (#15022) 2026-03-20 17:06:23 -05:00
dependabot[bot] 9e7cdaf475 Bump aioesphomeapi from 44.6.1 to 44.6.2 (#15027)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 11:10:40 -10:00
dependabot[bot] a3fd1d5d00 Bump github/codeql-action from 4.33.0 to 4.34.1 (#15023)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 10:41:45 -10:00
dependabot[bot] 7257bed1e9 Bump CodSpeedHQ/action from 4.11.1 to 4.12.1 (#15024)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 10:41:20 -10:00
Javier Peletier 5a9977cf5c [lvgl] Fix arc indicator widget not registered in widget_map (#14986) 2026-03-21 07:35:41 +11:00
Keith Roehrenbeck 12b3aec567 [ld2450] Fix zone target counts including untracked ghost targets (#15026) 2026-03-20 10:11:57 -10:00
J. Nick Koston d59c006ff9 [uart] Fix UART0 default pin IOMUX loopback on ESP32 (#14978) 2026-03-19 20:56:51 -10:00
J. Nick Koston 02ada93ea5 [wifi] Reject WiFi config on RP2040/RP2350 boards without CYW43 chip (#14990) 2026-03-19 18:40:33 -10:00
Kent Gibson 6e87f8eb4e [template] alarm_control_panel collapse SensorDataStore and bypassed_sensor_indicies into SensorInfo (#14852)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-20 02:06:58 +00:00
Clyde Stubbs 7df550f2a9 Ensure lvgl libs available when editing for host (#14987) 2026-03-20 01:52:52 +00:00
Clyde Stubbs b02f0e3c5f [sdl] Fix get_width()/height() when rotation used (#14950) 2026-03-20 12:39:10 +11:00
J. Nick Koston 151f71e033 [ci] Add libretiny and zephyr to memory impact platform filter (#14985) 2026-03-19 14:12:15 -10:00
J. Nick Koston 7ac001e994 [mhz19] Fix unused function warning for detection_range_to_log_string (#14981) 2026-03-19 14:12:03 -10:00
J. Nick Koston de177d2445 [logger] Fix ESP8266 crash with VERY_VERBOSE log level (#14980) 2026-03-19 14:11:49 -10:00
J. Nick Koston a9cb7143dc [core] Inline calculate_looping_components_ into header (#14944) 2026-03-19 14:11:17 -10:00
J. Nick Koston 902258b56e [preferences] Compile out loop() when flash_write_interval is non-zero (#14943) 2026-03-19 14:11:06 -10:00
dependabot[bot] c2a96ea293 Bump ruff from 0.15.6 to 0.15.7 (#14977)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 10:54:53 -10:00
J. Nick Koston 37a3c3ab3a [core] Replace std::bind with placeholders to lambdas (#14962) 2026-03-19 08:50:38 -10:00
J. Nick Koston a8ed781f3e [time] Fix lookup of top-level IANA timezone keys like UTC and GMT (#14952) 2026-03-19 08:44:35 -10:00
J. Nick Koston 63f0d054b7 [core] Replace std::bind with lambda in DelayAction (#14968) 2026-03-19 08:44:16 -10:00
J. Nick Koston e7dcf54a77 [http_request] Replace std::bind with lambdas in HttpRequestSendAction (#14966) 2026-03-19 08:44:02 -10:00
J. Nick Koston 1ba5504944 [mqtt] Replace std::bind with lambda in MQTTPublishJsonAction (#14965) 2026-03-19 08:43:47 -10:00
J. Nick Koston 5637116378 [mqtt] Replace std::bind with lambdas in CustomMQTTDevice (#14964) 2026-03-19 08:43:22 -10:00
J. Nick Koston cdc4ba6295 [api] Replace std::bind with lambdas in CustomAPIDevice (#14963) 2026-03-19 08:43:06 -10:00
J. Nick Koston d1aa1881bb [core] Replace std::bind with lambdas across 13 components (#14961) 2026-03-19 08:42:26 -10:00
J. Nick Koston 14107ec452 [bme68x_bsec2] Store trigger time as member to avoid std::function SBO overflow (#14960)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-19 08:42:08 -10:00
J. Nick Koston 2ca6681896 [xiaomi_rtcgq02lm] Drop unused capture from timeout lambdas (#14959) 2026-03-19 08:41:15 -10:00
J. Nick Koston 40a65d36b4 [nau7802] Replace std::bind with lambda to fit std::function SBO (#14958) 2026-03-19 08:41:01 -10:00
J. Nick Koston 16ec237ac6 [wireguard] Replace std::bind with inline lambdas (#14957) 2026-03-19 08:40:32 -10:00
J. Nick Koston 0afcdbfe73 [binary_sensor] Replace std::bind with inline lambda in MultiClickTrigger (#14956) 2026-03-19 08:40:18 -10:00
aanban b9439036d4 [remote_base] add support for brennenstuhl comfort-line switches (#9407)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-19 09:19:24 -04:00
CFlix cb23f9453f [absolute_humidity] loop() improvement (#14684)
Co-authored-by: DAVe3283 <DAVe3283+GitHub@gmail.com>
2026-03-19 09:09:00 -04:00
Daniel Kent 0858ecbb8e [spa06_base] Add SPA06-003 Temperature and Pressure Sensor (Part 1 of 3) (#14521)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-19 08:05:12 -04:00
luar123 96da6dd075 [esp32] Add custom partitions and refactor partition table generation (#7682)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 07:41:11 -04:00
dependabot[bot] 2c31bdc6a2 Bump aioesphomeapi from 44.6.0 to 44.6.1 (#14954)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 23:43:50 -10:00
Javier Peletier 0a3393bed3 [core] Disable LeakSanitizer in C++ unit tests (#14712) 2026-03-18 23:13:36 -10:00
Javier Peletier c2c50ceea7 [substitutions] substitutions pass and !include redesign (package refactor part 2a) (#14917)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-18 23:10:01 -10:00
Clyde Stubbs 2341d510d3 [lvgl] Migrate to library v9.5.0 (#12312)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-19 20:31:33 +13:00
Kevin Ahrendt 9d6f2f71e8 [speaker_source] Reshuffle playlist on repeat all restart (#14773)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 19:51:17 -10:00
Nate Clark e1334cf57f [mqtt] Support JSON payload with code for alarm control panel commands (#14731)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-19 05:40:48 +00:00
J. Nick Koston a1aff7cadf [preferences] Devirtualize preference backend and manager classes (#14825) 2026-03-18 18:42:05 -10:00
J. Nick Koston 2271ac6470 [api] Extract overflow buffer from frame helper into APIOverflowBuffer (#14871) 2026-03-18 18:41:45 -10:00
J. Nick Koston 8fe36cde23 [core] Replace std::function with lightweight Callback in CallbackManager (#14853) 2026-03-18 18:41:05 -10:00
Jesse Hills fdd5956c1e Merge branch 'release' into dev 2026-03-19 17:34:17 +13:00
Jonathan Swoboda 403ba262c6 [openthread] Guard InstanceLock against uninitialized semaphore (#14940)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 21:23:47 -04:00
Jonathan Swoboda f8be27ce6d [ble_client] Fix RSSI sensor reporting same value for all clients (#14939)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:10:42 -04:00
J. Nick Koston a50d70c8d3 [core] Remove call_loop_ wrapper and call loop() directly (#14931) 2026-03-18 14:08:03 -10:00
J. Nick Koston 4d86049c21 [ota] Pack deferred state args into uint32 to avoid heap allocation (#14922)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 14:06:55 -10:00
J. Nick Koston 44037c4f9b [http_request] Prevent double update task launch (#14910) 2026-03-18 14:06:41 -10:00
J. Nick Koston 5856d05701 [core] Devirtualize PollingComponent::set_update_interval (#14938) 2026-03-18 14:05:57 -10:00
J. Nick Koston a2a048e3bf [ld2412] Inline trivial gate threshold number setters (#14937) 2026-03-18 13:02:07 -10:00
Kevin Ahrendt 9f4c773963 [media_source] Add request helpers for smart sources (#14936) 2026-03-18 12:34:16 -10:00
Kevin Ahrendt ef0eef8117 [const] Move shared volume constants to components/const (#14935) 2026-03-18 21:57:33 +00:00
Jonathan Swoboda 097e6eb41f [i2s_audio] Remove legacy I2S driver support (#14932) 2026-03-18 11:42:56 -10:00
Jonathan Swoboda 73a49493a2 [vbus][shelly_dimmer][st7789v][modbus_controller] Fix integer overflows, off-by-one, and coordinate swap (#14916) 2026-03-18 16:43:42 -04:00
Jonathan Swoboda 4a93d5b544 [vl53l0x][ld2420][ble_client][inkplate] Fix state corruption, crash, OOB read, and shift UB (#14919) 2026-03-18 16:42:53 -04:00
Jonathan Swoboda cc0655a904 [bedjet][light][i2s_audio][ld2412] Fix uninitialized pointers, div-by-zero, and buffer validation (#14925) 2026-03-18 16:42:13 -04:00
Jesse Hills a859cb3cce Merge branch 'beta' into dev 2026-03-19 09:20:26 +13:00
Jonathan Swoboda 47909d5299 [hub75] Bump esp-hub75 to 0.3.5 (#14915) 2026-03-18 09:47:14 -10:00
dependabot[bot] 16667bf5be Bump aioesphomeapi from 44.5.2 to 44.6.0 (#14927)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 09:39:26 -10:00
dependabot[bot] ef3afe3e21 Bump codecov/codecov-action from 5.5.2 to 5.5.3 (#14928)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 09:33:29 -10:00
dependabot[bot] 3a47317fc8 Bump actions/cache from 5.0.3 to 5.0.4 in /.github/actions/restore-python (#14930)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 09:33:15 -10:00
dependabot[bot] 89066e3e20 Bump actions/cache from 5.0.3 to 5.0.4 (#14929)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 09:33:00 -10:00
J. Nick Koston 9a80c980cb [scheduler] Early exit cancel path after first match (#14902) 2026-03-18 07:48:26 -10:00
J. Nick Koston c9e6c85e6a [scheduler] Inline fast-path checks into header (#14905) 2026-03-18 07:48:11 -10:00
J. Nick Koston e88c9ba066 [core] Inline progmem_read functions on non-ESP8266 platforms (#14913) 2026-03-18 07:47:42 -10:00
J. Nick Koston 45be290392 [ci] Bump Python to 3.14 in sync-device-classes workflow (#14912) 2026-03-18 07:47:17 -10:00
J. Nick Koston 3f28ab88ca [http_request] Fix data race on update_info_ strings in update task (#14909) 2026-03-18 07:46:18 -10:00
Jonathan Swoboda 1d07f37d62 [opentherm] Migrate from legacy timer API to GPTimer API (#14859) 2026-03-18 09:22:28 -04:00
Jonathan Swoboda 16c5224341 [tc74][apds9960] Fix signed temperature and FIFO register address (#14907) 2026-03-18 07:48:43 -04:00
Jesse Hills e83372e2f3 Merge branch 'beta' into dev 2026-03-18 16:22:02 +13:00
Jonathan Swoboda 2531fb1a02 [voice_assistant][micro_wake_word] Fix null deref and missing error return (#14906) 2026-03-17 23:12:13 -04:00
J. Nick Koston 3e845d387a [tests] Fix test_show_logs_serial taking 30s due to unmocked serial port wait (#14903) 2026-03-17 14:44:17 -10:00
J. Nick Koston b9e8da92c7 [scheduler] Fix UB in cross-thread counter/vector reads, add atomic fast-path (#14880)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-18 00:19:31 +00:00
Javier Peletier 0c5f055d45 [core] cpp tests: Allow customizing code generation during tests (#14681)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-18 00:16:01 +00:00
J. Nick Koston 342020e1d3 [mqtt] Fix data race on inbound event queue (#14891)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-03-17 13:49:24 -10:00
J. Nick Koston 62f9bc79c4 [ci] Add CodSpeed badge to README (#14901) 2026-03-17 13:48:21 -10:00
Jonathan Swoboda 53bfb02a21 [sensor][ee895][hdc2010] Fix misc bugs found during component scan (#14890) 2026-03-17 19:46:26 -04:00
J. Nick Koston 83484a8828 [esp32_ble_server] Remove vestigial semaphore from BLECharacteristic (#14900) 2026-03-17 13:38:41 -10:00
J. Nick Koston ece235218f [debug][bme680_bsec] Use fnv1_hash_extend to avoid temporary string allocations (#14876) 2026-03-17 13:27:46 -10:00
J. Nick Koston f3409acfa8 [core] Document EventPool sizing requirement with LockFreeQueue (#14897) 2026-03-17 13:08:58 -10:00
J. Nick Koston 77b7201eb8 [ci] Run CodSpeed benchmarks on push to dev for baseline (#14899)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 13:08:45 -10:00
J. Nick Koston 6b91df8d75 [esp32_ble][esp32_ble_server] Inline is_active/is_running and remove STL bloat (#14875) 2026-03-17 13:05:16 -10:00
J. Nick Koston 1670f04a87 [core] Add CodSpeed C++ benchmarks for protobuf, main loop, and helpers (#14878) 2026-03-17 12:29:38 -10:00
J. Nick Koston 1adf05e2d5 [esp32_ble] Fix EventPool/LockFreeQueue sizing off-by-one (#14892) 2026-03-17 22:24:02 +00:00
J. Nick Koston a94bb74d04 [usb_uart] Fix EventPool/LockFreeQueue sizing off-by-one (#14895) 2026-03-18 11:18:31 +13:00
J. Nick Koston c19c75220b [usb_host] Fix EventPool/LockFreeQueue sizing off-by-one (#14896) 2026-03-18 11:17:59 +13:00
J. Nick Koston 97382ed814 [usb_cdc_acm] Fix EventPool/LockFreeQueue sizing off-by-one (#14894) 2026-03-18 11:17:43 +13:00
J. Nick Koston 5f06679d78 [espnow] Fix EventPool/LockFreeQueue sizing off-by-one (#14893) 2026-03-18 11:16:44 +13:00
Jonathan Swoboda 851e8b6c0d [gree] Fix IR checksum for YAA/YAC/YAC1FB9/GENERIC models (#14888) 2026-03-17 16:28:13 -04:00
J. Nick Koston 9a729608d5 [core] Add back deprecated set_internal() for external projects (#14887) 2026-03-17 19:58:05 +00:00
Jonathan Swoboda 53fa346ddc [speaker] Fix media playlist using announcement delay (#14889) 2026-03-17 19:18:49 +00:00
J. Nick Koston b3210de374 [core] Extract shared C++ build helpers from cpp_unit_test.py (#14883) 2026-03-17 08:53:36 -10:00
J. Nick Koston 82ccc37ba1 [ethernet] Mark EthernetComponent as final (#14842) 2026-03-17 08:14:52 -10:00
J. Nick Koston 3826e95506 [api] Fix ProtoMessage protected destructor compile error on host platform (#14882) 2026-03-17 08:14:36 -10:00
Jonathan Swoboda b083491e74 [microphone] Switch IDF test to new I2S driver (#14886) 2026-03-17 13:46:32 -04:00
Diorcet Yann 73ca0ff106 [core] Small improvements (#14884) 2026-03-17 09:22:31 -04:00
Jesse Hills bba11b3b1e Merge branch 'beta' into dev 2026-03-17 14:14:03 +13:00
Fabrice 2142bc1b76 [mipi_rgb] Make h- and v-sync pins optional (#14870) 2026-03-17 09:25:11 +11:00
KamilCuk f81e04b036 [web_server] Fix wrong printf format specifier (#14836) 2026-03-16 11:30:31 -10:00
Jonathan Swoboda c3327d0b43 [i2s_audio] Fix ESP-IDF 6.0 compatibility for I2S port types (#14818)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-03-16 16:04:20 -04:00
Jonathan Swoboda 8577c26358 [i2c] Handle ESP_ERR_INVALID_RESPONSE as NACK for IDF 6.0 (#14867) 2026-03-16 20:03:09 +00:00
Jonathan Swoboda 80730fd012 [seeed_mr24hpc1] Fix frame parser length handling bugs (#14863) 2026-03-16 09:57:53 -10:00
dependabot[bot] 5ee3e94ca1 Bump actions/create-github-app-token from 2.2.1 to 3.0.0 (#14868)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 09:57:33 -10:00
dependabot[bot] 037f75e0ff Bump github/codeql-action from 4.32.6 to 4.33.0 (#14869)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 09:57:17 -10:00
Jonathan Swoboda c47f4fbc1c [core] Support both dot and dash separators in Version.parse (#14858) 2026-03-16 09:45:16 -10:00
Jonathan Swoboda 2f86e48a83 [as3935] Fix ENERGY_MASK dropping bit 4 of lightning energy MMSB (#14861) 2026-03-16 09:44:55 -10:00
Jonathan Swoboda 0bbba75757 [am43] Fix battery update throttle using wrong type (#14864) 2026-03-16 09:42:13 -10:00
Jonathan Swoboda 9362d9745e [ci] Fix clang-tidy hash check 403 error on fork PRs (#14860) 2026-03-16 09:41:21 -10:00
Jonathan Swoboda c8f708c13c [lilygo_t5_47] Fix Y coordinate mapping and clamp touch point count (#14865) 2026-03-16 09:40:24 -10:00
Jonathan Swoboda 05590a3a21 [gpio][dallas_temp] Fix one_wire read64() and DS18S20 division by zero (#14866) 2026-03-16 09:39:26 -10:00
Jonathan Swoboda cdf2867baf [hub75] Bump esp-hub75 to 0.3.4 (#14862) 2026-03-16 15:05:56 -04:00
J. Nick Koston b142557979 [ethernet] Add RP2040 W5500 Ethernet support (#14820) 2026-03-16 18:26:06 +00:00
J. Nick Koston db405c483e [core] Cache errno to avoid duplicate __errno() calls (#14751)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 07:35:34 -10:00
J. Nick Koston 808c7b67b3 [core] Inline WarnIfComponentBlockingGuard::finish() into header (#14798)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 07:35:19 -10:00
J. Nick Koston 7131eafc09 [logger] Reduce per-message overhead by inlining hot path helpers (#14851) 2026-03-16 07:35:04 -10:00
J. Nick Koston 7b4af76a61 [core] Inline Mutex on all embedded platforms (#14756)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 07:33:10 -10:00
J. Nick Koston 2cd93daa5e [api] Optimize plaintext varint encoding and devirtualize write_protobuf_packet (#14758) 2026-03-16 07:32:58 -10:00
J. Nick Koston f86bb2bdb0 [ethernet] Add IDF 6.0 registry component dependencies (#14847)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 08:02:59 -04:00
tomaszduda23 414182fe6d [ble_nus] fix uart debug (#14850) 2026-03-15 21:08:05 -10:00
dependabot[bot] 2ee0df1da3 Bump aioesphomeapi from 44.5.1 to 44.5.2 (#14849)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 05:43:38 +00:00
Jonathan Swoboda e1252e32d1 [deep_sleep] Fix ESP-IDF 6.0 GPIO wakeup API rename (#14846) 2026-03-15 19:10:30 -10:00
Jonathan Swoboda 1183ef825b [usb_host] Fix ESP-IDF 6.0 compatibility for external USB host component (#14844) 2026-03-15 19:09:55 -10:00
Keith Burzinski c09edb94c1 [tinyusb] Fix regression from bump to 2.x in #14796 (#14848) 2026-03-16 00:04:07 -05:00
J. Nick Koston 9948adc6a0 [runtime_image] Add esp-dsp dependency for JPEGDEC SIMD on ESP32 (#14840)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-15 18:15:01 -10:00
J. Nick Koston ccb467b219 [fastled] Include esp_lcd IDF component for ESP32-S3 compatibility (#14839) 2026-03-15 18:14:41 -10:00
J. Nick Koston 1377776d21 [ethernet] Restructure for multi-platform support (#14819) 2026-03-15 15:17:21 -10:00
J. Nick Koston 29501ef4f8 [core] Mark leaf Component subclasses as final (#14833)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 15:13:34 -10:00
J. Nick Koston d97c23b8e3 [core] Add no-arg status_set_warning() to allow linker GC of const char* overload (#14821) 2026-03-15 15:13:10 -10:00
Bonne Eggleston 92d5e7b18c [tests] Fix integration helper to match entities exactly (#14837)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-15 13:02:23 -10:00
Jesse Hills 15ce4b3616 Merge branch 'beta' into dev 2026-03-16 11:46:15 +13:00
Jonathan Swoboda 33f9ad9cee [esp32] Support non-numeric version extras in IDF version string (#14826) 2026-03-15 14:58:12 -04:00
Jonathan Swoboda 18a082de30 [ci] Support URL and version extras in generate-esp32-boards.py (#14828)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-15 14:58:01 -04:00
Jonathan Swoboda 7f418d969e [multiple] Fix implicit int-to-gpio_num_t conversions for GCC 15 (#14830) 2026-03-15 14:57:52 -04:00
Jonathan Swoboda fe9f19d9ed [mqtt] Fix ESP-IDF 6.0 compatibility for external MQTT component (#14822) 2026-03-15 09:30:12 -04:00
J. Nick Koston d37f8876d7 [bthome_mithermometer][xiaomi_ble] Migrate CCM to PSA AEAD API for ESP-IDF 6.0 (#14816) 2026-03-14 13:39:07 -10:00
J. Nick Koston d7c42bc9ec [debug] Fix ESP-IDF 6.0 compatibility for wakeup cause API (#14812) 2026-03-14 13:38:51 -10:00
J. Nick Koston efc508a82b [dlms_meter] Migrate GCM to PSA AEAD API for ESP-IDF 6.0 (#14817) 2026-03-14 13:18:40 -10:00
J. Nick Koston 0edc0fd9c8 [esp32_ble_tracker] Migrate to PSA Crypto API for ESP-IDF 6.0 (#14811) 2026-03-14 13:17:56 -10:00
J. Nick Koston cc4c13930f [hmac_sha256] Migrate to PSA Crypto MAC API for ESP-IDF 6.0 (#14814) 2026-03-14 13:17:43 -10:00
J. Nick Koston 234ca7c951 [debug] Fix shared buffer between reset reason and wakeup cause (#14813) 2026-03-14 13:17:32 -10:00
J. Nick Koston 447c4669b1 [esp32] Disable SHA-512 in mbedTLS on IDF 6.0+ and add idf_version() helper (#14810) 2026-03-14 11:26:20 -10:00
J. Nick Koston 27942f1973 [helpers] Replace deprecated std::is_trivial in FixedRingBuffer (#14808) 2026-03-14 11:05:39 -10:00
J. Nick Koston 158a119a5a [sha256] Migrate to PSA Crypto API for ESP-IDF 6.0 (#14809) 2026-03-14 10:43:04 -10:00
Jonathan Swoboda b126f3af3b [ledc] Fix ESP-IDF 6.0 compatibility for peripheral reset (#14790)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:02:13 -04:00
Jonathan Swoboda d4e1e32a30 [mipi_dsi] Fix ESP-IDF 6.0 compatibility for use_dma2d flag (#14792)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:02:06 -04:00
Jonathan Swoboda 271b423b22 [psram] Fix ESP-IDF 6.0 compatibility for PSRAM sdkconfig options (#14794)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:01:58 -04:00
Jonathan Swoboda 417858f098 [psram] Add ESP32-C61 PSRAM support (#14795)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:01:49 -04:00
Jonathan Swoboda c52042e023 [tinyusb][usb_cdc_acm] Bump esp_tinyusb to 2.1.1 (#14796)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:01:29 -04:00
Jonathan Swoboda f12531e7e0 [esp32_camera] Bump esp32-camera to 2.1.5 (#14806)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 08:32:17 -10:00
J. Nick Koston ca279110c9 [output] Inline trivial FloatOutput accessors (#14786)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 08:31:50 -10:00
J. Nick Koston f2968e0449 [api] Reduce API code size with buffer and nodelay optimizations (#14797) 2026-03-14 08:13:50 -10:00
J. Nick Koston 0043be6165 [core] Inline trivial EntityBase accessors (#14782)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 08:13:01 -10:00
J. Nick Koston 0716c9f722 [core] Inline LwIPLock as no-op on platforms without lwIP core locking (#14787) 2026-03-14 08:12:04 -10:00
leccelecce fcf5637aa5 [online_image] Log download duration in milliseconds instead of seconds (#14803) 2026-03-14 09:15:54 -04:00
J. Nick Koston 5e3c44d48f [rp2040] Add CI check for boards.py freshness (#14754)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-13 13:28:55 -10:00
J. Nick Koston d6d3bbbad8 [scheduler] Use integer math for interval offset calculation (#14755) 2026-03-13 13:28:34 -10:00
Jonathan Swoboda 86b7933081 [esp32_rmt_led_strip][remote_transmitter][remote_receiver] Fix ESP-IDF 6.0 RMT compatibility (#14783)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 19:24:41 -04:00
J. Nick Koston 7cceb72cc3 [api] Inline force-variant ProtoSize calc methods (#14781) 2026-03-13 13:23:41 -10:00
J. Nick Koston 56f7b3e61b [ci] Only run integration tests for changed components (#14776) 2026-03-13 13:20:35 -10:00
J. Nick Koston 22062d79a2 [analyze-memory] Add function call frequency analysis (#14779) 2026-03-13 13:20:17 -10:00
Jonathan Swoboda ab3b677113 [adc] Fix ESP-IDF 6.0 compatibility for ADC_ATTEN_DB_12 (#14784)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:11:18 +00:00
Jonathan Swoboda cdb445f69d [mipi_dsi] Fix ESP-IDF 6.0 compatibility for LCD color format (#14785)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:00:28 +00:00
Thomas SAMTER 1eed1adfa0 [pid] Replace std::deque with FixedRingBuffer (#14733)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-13 11:38:45 -10:00
J. Nick Koston a6c08576be [sensor] Use FixedRingBuffer in SlidingWindowFilter, add window_size limit (#14736) 2026-03-13 10:17:40 -10:00
dependabot[bot] f41aa8b18c Bump ruff from 0.15.5 to 0.15.6 (#14774)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-13 19:35:10 +00:00
Jonathan Swoboda 6700347a48 [wifi] Fix ESP-IDF 6.0 compatibility (#14766)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:47:12 -04:00
Jonathan Swoboda b147830ef9 [core] Fix std::isnan conflict with picolibc on ESP-IDF 6.0 (#14768)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:24:39 -04:00
J. Nick Koston bd844fcd0a [template] Fix misleading 'Text value too long to save' warning (#14753) 2026-03-13 07:37:44 -10:00
J. Nick Koston 8936be628f [api] Increase log Nagle coalescing on all platforms except ESP8266 (#14752) 2026-03-13 07:37:30 -10:00
J. Nick Koston 5920fa97e4 [select] Fix -Wmaybe-uninitialized warnings on ESP8266 (#14759) 2026-03-13 09:20:50 -04:00
Kjell Braden 326769e43c [runtime_image] fix BMP parsing (#14762) 2026-03-13 09:18:42 -04:00
Thomas SAMTER 7524590bcf [const] Add CONF_CLIMATE_ID for climate component sub-entities (#14764) 2026-03-13 09:17:11 -04:00
Michael Kerscher 15ec46abfe [vbus] add DeltaSol CS4 (Citrin Solar 1.3) (#12477) 2026-03-12 22:31:16 -07:00
J. Nick Koston 920af91db6 [rp2040] Fix compiler warnings in crash_handler and mdns (#14739) 2026-03-13 01:37:46 +00:00
J. Nick Koston a744261934 [mdns] Fix RP2040 mDNS not restarting after WiFi reconnect (#14737)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-13 01:12:22 +00:00
J. Nick Koston 59c1368440 [i2c] Fix RP2040 I2C bus selection based on pin assignment (#14745) 2026-03-12 14:53:46 -10:00
J. Nick Koston 7e8e085a04 [light] Fix binary light spamming 'brightness not supported' warning with strobe effect (#14735) 2026-03-12 14:49:07 -10:00
J. Nick Koston 22b25724ae [wifi] Reject EAP/WPA2 Enterprise config on unsupported platforms (#14746) 2026-03-12 14:48:55 -10:00
J. Nick Koston 89719cf4b2 [water_heater] Set OPERATION_MODE feature flag when modes are configured (#14748) 2026-03-12 14:48:41 -10:00
J. Nick Koston e15b19b223 [captive_portal] Fix captive portal inaccessible when web_server auth is configured (#14734)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 14:48:29 -10:00
J. Nick Koston 2ca13972b9 [debug] Fix missing reset reason for RP2040/RP2350 (#14740) 2026-03-12 14:48:06 -10:00
J. Nick Koston 7bb4e75459 [rp2040] Use full flash for sketch in testing mode (#14747)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:47:16 -10:00
J. Nick Koston fd8e510745 [light] Fix ambiguous set_effect overload for const char* (#14732) 2026-03-12 18:28:25 -05:00
Brian Kaufman 25c74c8f99 [OTA] Stage exact uploaded size for ESP8266 web OTA (gzip fix) (#14741) 2026-03-12 13:23:29 -10:00
J. Nick Koston 05d285ba86 [api] Fix heap-buffer-overflow in protobuf message dump for StringRef (#14721) 2026-03-12 07:16:53 -10:00
J. Nick Koston 186ca4e458 [uart] Allow hardware UART with single pin on RP2040 (#14725) 2026-03-12 07:16:38 -10:00
J. Nick Koston 618312f0ee [api] Fix undefined behavior in noise handshake with empty rx buffer (#14722) 2026-03-12 07:16:23 -10:00
J. Nick Koston 70d188202a [adc] Fix PICO_VSYS_PIN compile error on RP2350 boards (#14724) 2026-03-12 07:16:08 -10:00
J. Nick Koston 4a21afe7ce [ota][socket] Fix ESP8266/RP2040 OTA timeout by using SO_RCVTIMEO instead of polling (#14675) 2026-03-12 07:15:48 -10:00
J. Nick Koston fd1d016795 [time] Fix settimeofday() failure on ESP8266 (#14707)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 07:15:34 -10:00
J. Nick Koston 03c091adfc [esp32_ble_client] Fix disconnect race that causes stuck connections (#14211)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:15:21 -10:00
J. Nick Koston a3a88acfcf [socket] Fast path for TCP_NODELAY bypasses lwip_setsockopt overhead (#14693) 2026-03-12 07:15:04 -10:00
J. Nick Koston 07f8ae6c82 [socket] Fix use-after-free in LWIP PCB close/abort path (#14706) 2026-03-12 07:14:49 -10:00
Matthias König 25c30ac5bb [mqtt] Fixed permission denied error for client certificates on Windows (#13525)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-12 12:00:08 -04:00
guillempages a76767a0ab [runtime_image] Update jpegdec lib version (#14726)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-12 10:15:20 -04:00
Kevin Ahrendt 511d185772 [audio] Bump microOpus to v0.3.5 (#14727) 2026-03-12 08:56:01 -04:00
Brian Kaufman c4c19c8a6c [web_server] use DETAIL_ALL in update_all_json_generator (#14711) 2026-03-11 23:07:26 -10:00
Massimo Antonello fe2d60ccec [one_wire] allow changing address at runtime (#12150) 2026-03-12 01:52:58 -07:00
Keith Burzinski 657890695f [ledc] Fix high-pressure crash & recovery (#14720) 2026-03-12 03:16:02 -05:00
Adam DeMuri 8a5f008aee [modbus] Fix buffer overflow in modbus (#14719)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-11 22:00:26 -10:00
J. Nick Koston f8a22b87b8 [rp2040] Fix crash handler design flaws (#14716) 2026-03-12 18:23:01 +13:00
Keith Burzinski 7f38d95424 [ethernet] ESP32-S3 Ethernet compilation fix (#14717) 2026-03-11 23:48:27 -05:00
Javier Peletier bb7d96b954 [const] Add UNIT_METER_PER_SECOND, UNIT_MILLILITRE, UNIT_POUND to const.py (#14713) 2026-03-11 16:31:17 -10:00
J. Nick Koston 8daa946afa [esp32] Add crash handler to capture and report backtrace across reboots (#14709) 2026-03-12 14:00:20 +13:00
Keith Burzinski ddc40f44fa [ethernet] ESP32-P4 Ethernet compilation fix (#14714)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-11 19:56:25 -05:00
Jonathan Swoboda 409640c0ee [esp32_hosted] Bump esp_hosted to 2.12.1 (#14708)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:30:44 -04:00
Jesse Hills 822c9161c6 Merge branch 'beta' into dev 2026-03-12 09:15:50 +13:00
dependabot[bot] a060f175ad Bump actions/download-artifact from 8.0.0 to 8.0.1 (#14705)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 09:28:46 -10:00
dependabot[bot] 73f305ff9c Bump tornado from 6.5.4 to 6.5.5 (#14704)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 09:28:19 -10:00
Jesse Hills b6ff7185e7 [ci] Dont run codeowners workflows on release or beta PRs (#14703) 2026-03-12 08:04:07 +13:00
J. Nick Koston 928f6f1866 [ci] Add PR title check for unescaped angle brackets (#14701) 2026-03-12 07:57:43 +13:00
Jesse Hills e7c3277eeb Bump version to 2026.4.0-dev 2026-03-12 07:34:53 +13:00
491 changed files with 13535 additions and 6119 deletions
+1 -1
View File
@@ -1 +1 @@
8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a
9f5d763f95ff720024f3fdddba2fad3801e2bfe00b7cc2124e6d68c17d3504c6
+1 -1
View File
@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
# yamllint disable-line rule:line-length
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+2 -2
View File
@@ -40,7 +40,7 @@ jobs:
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
- if: failure()
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
name: Request changes
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
@@ -53,7 +53,7 @@ jobs:
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
})
- if: success()
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
name: Dismiss review
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
+71 -19
View File
@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
# yamllint disable-line rule:line-length
@@ -106,6 +106,7 @@ jobs:
script/build_codeowners.py --check
script/build_language_schema.py --check
script/generate-esp32-boards.py --check
script/generate-rp2040-boards.py --check
pytest:
name: Run pytest
@@ -153,12 +154,12 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -170,6 +171,8 @@ jobs:
- common
outputs:
integration-tests: ${{ steps.determine.outputs.integration-tests }}
integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }}
integration-test-files: ${{ steps.determine.outputs.integration-test-files }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
python-linters: ${{ steps.determine.outputs.python-linters }}
@@ -182,6 +185,7 @@ jobs:
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
benchmarks: ${{ steps.determine.outputs.benchmarks }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -194,7 +198,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -210,6 +214,8 @@ jobs:
# Extract individual fields
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT
echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
@@ -222,9 +228,10 @@ jobs:
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -246,7 +253,7 @@ jobs:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -261,9 +268,20 @@ jobs:
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run integration tests
env:
INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }}
INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }}
run: |
. venv/bin/activate
pytest -vv --no-cov --tb=native -n auto tests/integration/
if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then
echo "Running all integration tests"
pytest -vv --no-cov --tb=native -n auto tests/integration/
else
# Parse JSON array into bash array to avoid shell expansion issues
mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]')
echo "Running ${#test_files[@]} specific integration tests"
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
fi
cpp-unit-tests:
name: Run C++ unit tests
@@ -292,6 +310,40 @@ jobs:
script/cpp_unit_test.py $ARGS
fi
benchmarks:
name: Run CodSpeed benchmarks
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: >-
(github.event_name == 'push' && github.ref_name == 'dev') ||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Build benchmarks
id: build
run: |
. venv/bin/activate
export BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py)
# --build-only prints BUILD_BINARY=<path> to stdout
BINARY=$(script/cpp_benchmark.py --all --build-only | grep '^BUILD_BINARY=' | tail -1 | cut -d= -f2-)
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4
with:
run: ${{ steps.build.outputs.binary }}
mode: simulation
clang-tidy-single:
name: ${{ matrix.name }}
runs-on: ubuntu-24.04
@@ -335,14 +387,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -414,14 +466,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -503,14 +555,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -765,7 +817,7 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -789,7 +841,7 @@ jobs:
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -830,7 +882,7 @@ jobs:
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -877,7 +929,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -945,13 +997,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: memory-analysis-pr
path: ./memory-analysis
@@ -10,6 +10,9 @@ name: Codeowner Approved Label
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
branches-ignore:
- release
- beta
permissions:
issues: write
@@ -13,6 +13,9 @@ on:
# Needs to be pull_request_target to get write permissions
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
branches-ignore:
- release
- beta
permissions:
pull-requests: write
+2 -2
View File
@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
with:
category: "/language:${{matrix.language}}"
+12
View File
@@ -65,6 +65,18 @@ jobs:
return;
}
// Check for angle brackets not wrapped in backticks.
// Astro docs MDX treats bare < as JSX component opening tags.
const stripped = title.replace(/`[^`]*`/g, '');
if (/[<>]/.test(stripped)) {
core.setFailed(
'PR title contains `<` or `>` not wrapped in backticks.\n' +
'Astro docs MDX interprets bare `<` as JSX components.\n' +
'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support'
);
return;
}
// Check title starts with [tag] prefix
const bracketPattern = /^\[\w+\]/;
if (!bracketPattern.test(title)) {
+4 -4
View File
@@ -171,7 +171,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download digests
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: digests-*
path: /tmp/digests
@@ -221,7 +221,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -256,7 +256,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -287,7 +287,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.13
python-version: "3.14"
- name: Install Home Assistant
run: |
+1 -1
View File
@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.5
rev: v0.15.6
hooks:
# Run the linter.
- id: ruff
+2 -1
View File
@@ -244,7 +244,6 @@ esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core
esphome/components/i2c_device/* @gabest11
esphome/components/i2s_audio/* @jesserockz
esphome/components/i2s_audio/media_player/* @jesserockz
esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/iaqcore/* @yozik04
@@ -458,6 +457,8 @@ esphome/components/sn74hc165/* @jesserockz
esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt
esphome/components/spa06_base/* @danielkent-net
esphome/components/spa06_i2c/* @danielkent-net
esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/speaker_source/* @kahrendt
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.3.0
PROJECT_NUMBER = 2026.4.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -1,4 +1,4 @@
# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/)
# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) [![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/esphome/esphome)
<a href="https://esphome.io/">
<picture>
+178 -3
View File
@@ -1,6 +1,6 @@
"""Memory usage analyzer for ESPHome compiled binaries."""
from collections import defaultdict
from collections import Counter, defaultdict
from dataclasses import dataclass, field
import logging
from pathlib import Path
@@ -40,6 +40,15 @@ _READELF_SECTION_PATTERN = re.compile(
r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)"
)
# Regex for extracting call targets from objdump disassembly
# Matches direct call instructions across architectures:
# Xtensa: call0/call4/call8/call12/callx0/callx4/callx8/callx12 <addr> <symbol>
# ARM: bl/blx <addr> <symbol>
# Captures the mangled symbol name inside angle brackets.
_CALL_TARGET_PATTERN = re.compile(
r"\t(?:call(?:0|4|8|12)|callx(?:0|4|8|12)|blx?)\s+[\da-fA-F]+ <([^>]+)>"
)
# Component category prefixes
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
_COMPONENT_PREFIX_EXTERNAL = "[external]"
@@ -192,20 +201,27 @@ class MemoryAnalyzer:
self._cswtch_symbols: list[tuple[str, int, str, str]] = []
# Library symbol mapping: symbol_name -> library_name
self._lib_symbol_map: dict[str, str] = {}
# Source file symbol mapping: symbol_name -> component_name
# Used for extern "C" and other symbols without C++ namespace
self._source_symbol_map: dict[str, str] = {}
# Library dir to name mapping: "lib641" -> "espsoftwareserial",
# "espressif__mdns" -> "mdns"
self._lib_hash_to_name: dict[str, str] = {}
# Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns"
self._heuristic_to_lib: dict[str, str] = {}
# Function call counts: mangled_name -> call_count
self._function_call_counts: Counter[str] = Counter()
def analyze(self) -> dict[str, ComponentMemory]:
"""Analyze the ELF file and return component memory usage."""
self._parse_sections()
self._parse_symbols()
self._scan_libraries()
self._scan_source_symbols()
self._categorize_symbols()
self._analyze_cswtch_symbols()
self._analyze_sdk_libraries()
self._analyze_function_calls()
return dict(self.components)
def _parse_sections(self) -> None:
@@ -351,6 +367,11 @@ class MemoryAnalyzer:
if lib_name := self._lib_symbol_map.get(symbol_name):
return f"{_COMPONENT_PREFIX_LIB}{lib_name}"
# Check source file mapping (catches extern "C" functions in ESPHome sources)
# Must be before heuristic patterns since source attribution is authoritative
if component := self._source_symbol_map.get(symbol_name):
return component
# Check against symbol patterns
for component, patterns in SYMBOL_PATTERNS.items():
if any(pattern in symbol_name for pattern in patterns):
@@ -384,8 +405,9 @@ class MemoryAnalyzer:
return
_LOGGER.info("Demangling %d symbols", len(symbols))
self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path)
_LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache))
demangled = batch_demangle(symbols, objdump_path=self.objdump_path)
self._demangle_cache.update(demangled)
_LOGGER.info("Successfully demangled %d symbols", len(demangled))
def _demangle_symbol(self, symbol: str) -> str:
"""Get demangled C++ symbol name from cache."""
@@ -640,6 +662,7 @@ class MemoryAnalyzer:
return None
symbol_map: dict[str, str] = {}
source_symbol_map: dict[str, str] = {}
current_symbol: str | None = None
section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.")
@@ -675,9 +698,18 @@ class MemoryAnalyzer:
if dir_key in source_path:
symbol_map[current_symbol] = lib_name
break
else:
# Map ESPHome source files to components for extern "C"
# and other symbols without C++ namespace
component = self._source_file_to_component(source_path)
if component.startswith(
(_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL)
):
source_symbol_map[current_symbol] = component
current_symbol = None
self._source_symbol_map = source_symbol_map
return symbol_map or None
def _scan_libraries(self) -> None:
@@ -728,6 +760,112 @@ class MemoryAnalyzer:
len(libraries),
)
def _scan_source_symbols(self) -> None:
"""Scan ESPHome source object files to map extern "C" symbols to components.
When no linker map file is available, this uses ``nm`` to scan ``.o`` files
under ``src/esphome/`` and build a symbol-to-component mapping. This catches
``extern "C"`` functions and other symbols that lack C++ namespace prefixes.
Skips scanning if ``_source_symbol_map`` was already populated by
``_parse_map_file()``.
"""
if self._source_symbol_map or not self.nm_path:
return
obj_dir = self._find_object_files_dir()
if obj_dir is None:
return
# Find ESPHome source object files
esphome_src_dir = obj_dir / "src" / "esphome"
if not esphome_src_dir.is_dir():
return
obj_files = sorted(esphome_src_dir.rglob("*.o"))
if not obj_files:
return
# Run nm with --print-file-name to get file:symbol mapping
result = run_tool(
[self.nm_path, "--print-file-name", "-g", "--defined-only"]
+ [str(f) for f in obj_files],
)
if result is None or result.returncode != 0:
_LOGGER.debug("nm scan of source objects failed")
return
self._source_symbol_map = self._parse_nm_source_output(result.stdout, obj_dir)
if self._source_symbol_map:
_LOGGER.info(
"Built source symbol map from nm: %d symbols",
len(self._source_symbol_map),
)
def _parse_nm_source_output(self, output: str, base_dir: Path) -> dict[str, str]:
"""Parse nm output to map non-namespaced symbols to ESPHome components.
Extracts global defined symbols from ESPHome source object files that
don't use C++ namespacing (e.g. ``extern "C"`` functions).
Args:
output: Raw stdout from ``nm --print-file-name -g --defined-only``
or ``nm --print-file-name -S``.
base_dir: Build directory for computing relative paths.
Returns:
Dict mapping symbol names to component names.
"""
source_map: dict[str, str] = {}
for line in output.splitlines():
# Format: /path/to/file.o: addr type name
# or: /path/to/file.o: addr size type name (with -S)
colon_idx = line.rfind(".o:")
if colon_idx == -1:
continue
file_path = line[: colon_idx + 2]
fields = line[colon_idx + 3 :].split()
if len(fields) < 3:
continue
# With -S flag, format is: addr size type name
# Without -S flag: addr type name
# type is a single char; size is hex digits
# Detect by checking if fields[1] is a single uppercase letter (type)
if len(fields[1]) == 1 and fields[1].isalpha():
# addr type name
sym_type = fields[1]
symbol_name = fields[2]
elif len(fields) >= 4:
# addr size type name
sym_type = fields[2]
symbol_name = fields[3]
else:
continue
# Only global defined symbols (uppercase type)
if not sym_type.isupper() or sym_type == "U":
continue
# Skip symbols already in esphome:: namespace
if symbol_name.startswith("_ZN7esphome"):
continue
# Make path relative to base_dir for _source_file_to_component
try:
rel_path = str(Path(file_path).relative_to(base_dir))
except ValueError:
continue
component = self._source_file_to_component(rel_path)
if component.startswith(
(_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL)
):
source_map[symbol_name] = component
return source_map
def _find_object_files_dir(self) -> Path | None:
"""Find the directory containing object files for this build.
@@ -1011,6 +1149,43 @@ class MemoryAnalyzer:
total_size,
)
def _analyze_function_calls(self) -> None:
"""Count function call sites by parsing disassembly output.
Parses direct call instructions (call0/call8/bl/blx) from objdump -d
to count how many times each function is called. This helps identify
inlining candidates — frequently called small functions benefit most
from inlining.
"""
result = run_tool(
[self.objdump_path, "-d", str(self.elf_path)],
timeout=60,
)
if result is None or result.returncode != 0:
_LOGGER.debug("Failed to disassemble ELF for function call analysis")
return
self._function_call_counts = Counter(
match.group(1)
for line in result.stdout.splitlines()
if (match := _CALL_TARGET_PATTERN.search(line))
)
# Demangle any call targets not already in the cache
missing = [
name
for name in self._function_call_counts
if name not in self._demangle_cache
]
if missing:
self._batch_demangle_symbols(missing)
_LOGGER.debug(
"Function call analysis: %d unique targets, %d total calls",
len(self._function_call_counts),
sum(self._function_call_counts.values()),
)
def get_unattributed_ram(self) -> tuple[int, int, int]:
"""Get unattributed RAM sizes (SDK/framework overhead).
+109
View File
@@ -231,6 +231,110 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(f" {size:>6,} B {sym_name}")
lines.append("")
# Number of top called functions to show
TOP_CALLS_LIMIT: int = 50
# Number of inlining candidates to show
INLINE_CANDIDATES_LIMIT: int = 25
# Maximum function size in bytes to consider for inlining
INLINE_SIZE_THRESHOLD: int = 16
def _build_symbol_sizes(self) -> dict[str, int]:
"""Build a size lookup from all component symbols: mangled_name -> size."""
return {
symbol: size
for symbols in self._component_symbols.values()
for symbol, _, size, _ in symbols
}
def _format_call_row(
self, index: int, mangled: str, count: int, symbol_sizes: dict[str, int]
) -> str:
"""Format a single row for call frequency tables."""
demangled = self._demangle_cache.get(mangled, mangled)
if len(demangled) > 80:
demangled = f"{demangled[:77]}..."
size = symbol_sizes.get(mangled)
size_str = f"{size:>5,} B" if size is not None else " ?"
return f"{index:>3} {count:>5} {size_str} {demangled}"
def _add_call_table_header(self, lines: list[str]) -> None:
"""Add the header row for call frequency tables."""
lines.append(f"{'#':>3} {'Calls':>5} {'Size':>7} Function")
lines.append(f"{'---':>3} {'-----':>5} {'-------':>7} {'-' * 60}")
def _add_function_call_analysis(self, lines: list[str]) -> None:
"""Add function call frequency analysis section.
Shows the most frequently called functions by call site count.
"""
self._add_section_header(lines, "Top Called Functions")
symbol_sizes = self._build_symbol_sizes()
# Sort by call count descending
sorted_calls = sorted(
self._function_call_counts.items(), key=lambda x: x[1], reverse=True
)
self._add_call_table_header(lines)
for i, (mangled, count) in enumerate(sorted_calls[: self.TOP_CALLS_LIMIT]):
lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes))
total_calls = sum(self._function_call_counts.values())
lines.append("")
lines.append(
f"Total: {len(self._function_call_counts)} unique targets, "
f"{total_calls:,} call sites"
)
lines.append("")
def _add_inline_candidates(self, lines: list[str]) -> None:
"""Add inlining candidates section.
Shows frequently called functions that are small enough to benefit
from inlining (< 16 bytes). These are the best candidates for
reducing call overhead.
"""
self._add_section_header(
lines,
f"Inlining Candidates (<{self.INLINE_SIZE_THRESHOLD} B, by call count)",
)
symbol_sizes = self._build_symbol_sizes()
# Filter to small functions with known size, sort by call count
candidates = sorted(
(
(mangled, count)
for mangled, count in self._function_call_counts.items()
if mangled in symbol_sizes
and symbol_sizes[mangled] < self.INLINE_SIZE_THRESHOLD
),
key=lambda x: x[1],
reverse=True,
)
if not candidates:
lines.append("No candidates found.")
lines.append("")
return
self._add_call_table_header(lines)
for i, (mangled, count) in enumerate(
candidates[: self.INLINE_CANDIDATES_LIMIT]
):
lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes))
lines.append("")
lines.append(
f"Showing top {min(len(candidates), self.INLINE_CANDIDATES_LIMIT)} "
f"of {len(candidates)} functions under "
f"{self.INLINE_SIZE_THRESHOLD} B"
)
lines.append("")
def generate_report(self, detailed: bool = False) -> str:
"""Generate a formatted memory report."""
components = sorted(
@@ -533,6 +637,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
if self._cswtch_symbols:
self._add_cswtch_analysis(lines)
# Function call frequency analysis
if self._function_call_counts:
self._add_function_call_analysis(lines)
self._add_inline_candidates(lines)
lines.append(
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
)
-1
View File
@@ -408,7 +408,6 @@ SYMBOL_PATTERNS = {
],
"arduino_core": [
"pinMode",
"resetPins",
"millis",
"micros",
"delay(", # More specific - Arduino delay function with parenthesis
@@ -1,22 +1,29 @@
#include "esphome/core/log.h"
#include "absolute_humidity.h"
namespace esphome {
namespace absolute_humidity {
namespace esphome::absolute_humidity {
static const char *const TAG = "absolute_humidity.sensor";
static const char *const TAG{"absolute_humidity.sensor"};
void AbsoluteHumidityComponent::setup() {
this->temperature_sensor_->add_on_state_callback([this](float state) {
this->temperature_ = state;
this->enable_loop();
});
ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str());
this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); });
// Get initial value
if (this->temperature_sensor_->has_state()) {
this->temperature_callback_(this->temperature_sensor_->get_state());
this->temperature_ = this->temperature_sensor_->get_state();
}
this->humidity_sensor_->add_on_state_callback([this](float state) {
this->humidity_ = state;
this->enable_loop();
});
ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str());
this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); });
// Get initial value
if (this->humidity_sensor_->has_state()) {
this->humidity_callback_(this->humidity_sensor_->get_state());
this->humidity_ = this->humidity_sensor_->get_state();
}
}
@@ -46,14 +53,12 @@ void AbsoluteHumidityComponent::dump_config() {
}
void AbsoluteHumidityComponent::loop() {
if (!this->next_update_) {
return;
}
this->next_update_ = false;
// Only run once
this->disable_loop();
// Ensure we have source data
const bool no_temperature = std::isnan(this->temperature_);
const bool no_humidity = std::isnan(this->humidity_);
const bool no_temperature{std::isnan(this->temperature_)};
const bool no_humidity{std::isnan(this->humidity_)};
if (no_temperature || no_humidity) {
if (no_temperature) {
ESP_LOGW(TAG, "No valid state from temperature sensor!");
@@ -67,9 +72,9 @@ void AbsoluteHumidityComponent::loop() {
}
// Convert to desired units
const float temperature_c = this->temperature_;
const float temperature_k = temperature_c + 273.15;
const float hr = this->humidity_ / 100;
const float temperature_c{this->temperature_};
const float temperature_k{temperature_c + 273.15f};
const float hr{this->humidity_ / 100.0f};
// Calculate saturation vapor pressure
float es;
@@ -90,7 +95,7 @@ void AbsoluteHumidityComponent::loop() {
}
// Calculate absolute humidity
const float absolute_humidity = vapor_density(es, hr, temperature_k);
const float absolute_humidity{vapor_density(es, hr, temperature_k)};
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity);
@@ -103,16 +108,16 @@ void AbsoluteHumidityComponent::loop() {
// More accurate than Tetens in normal meteorologic conditions
float AbsoluteHumidityComponent::es_buck(float temperature_c) {
float a, b, c, d;
if (temperature_c >= 0) {
a = 0.61121;
b = 18.678;
c = 234.5;
d = 257.14;
if (temperature_c >= 0.0f) {
a = 0.61121f;
b = 18.678f;
c = 234.5f;
d = 257.14f;
} else {
a = 0.61115;
b = 18.678;
c = 233.7;
d = 279.82;
a = 0.61115f;
b = 18.678f;
c = 233.7f;
d = 279.82f;
}
return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c)));
}
@@ -120,14 +125,14 @@ float AbsoluteHumidityComponent::es_buck(float temperature_c) {
// Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation)
float AbsoluteHumidityComponent::es_tetens(float temperature_c) {
float a, b;
if (temperature_c >= 0) {
a = 17.27;
b = 237.3;
if (temperature_c >= 0.0f) {
a = 17.27f;
b = 237.3f;
} else {
a = 21.875;
b = 265.5;
a = 21.875f;
b = 265.5f;
}
return 0.61078 * expf((a * temperature_c) / (temperature_c + b));
return 0.61078f * expf((a * temperature_c) / (temperature_c + b));
}
// Wobus equation
@@ -146,18 +151,18 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
//
// Baker, Schlatter 17-MAY-1982 Original version.
const float c0 = +0.99999683e00;
const float c1 = -0.90826951e-02;
const float c2 = +0.78736169e-04;
const float c3 = -0.61117958e-06;
const float c4 = +0.43884187e-08;
const float c5 = -0.29883885e-10;
const float c6 = +0.21874425e-12;
const float c7 = -0.17892321e-14;
const float c8 = +0.11112018e-16;
const float c9 = -0.30994571e-19;
const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))));
return 0.61078 / pow(p, 8);
constexpr float c0{+0.99999683e+00f};
constexpr float c1{-0.90826951e-02f};
constexpr float c2{+0.78736169e-04f};
constexpr float c3{-0.61117958e-06f};
constexpr float c4{+0.43884187e-08f};
constexpr float c5{-0.29883885e-10f};
constexpr float c6{+0.21874425e-12f};
constexpr float c7{-0.17892321e-14f};
constexpr float c8{+0.11112018e-16f};
constexpr float c9{-0.30994571e-19f};
const float p{c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))))};
return 0.61078f / powf(p, 8.0f);
}
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
@@ -168,11 +173,10 @@ float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
// hr = relative humidity [0-1]
// ta = absolute temperature (K)
const float ea = hr * es * 1000; // vapor pressure of the air (Pa)
const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹)
const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹)
const float ea{hr * es * 1000.0f}; // vapor pressure of the air (Pa)
const float mw{18.01528f}; // molar mass of water (g⋅mol⁻¹)
const float r{8.31446261815324f}; // molar gas constant (J⋅K⁻¹)
return (ea * mw) / (r * ta);
}
} // namespace absolute_humidity
} // namespace esphome
} // namespace esphome::absolute_humidity
@@ -3,8 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace absolute_humidity {
namespace esphome::absolute_humidity {
/// Enum listing all implemented saturation vapor pressure equations.
enum SaturationVaporPressureEquation {
@@ -16,8 +15,6 @@ enum SaturationVaporPressureEquation {
/// This class implements calculation of absolute humidity from temperature and relative humidity.
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
public:
AbsoluteHumidityComponent() = default;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; }
@@ -27,15 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
void loop() override;
protected:
void temperature_callback_(float state) {
this->next_update_ = true;
this->temperature_ = state;
}
void humidity_callback_(float state) {
this->next_update_ = true;
this->humidity_ = state;
}
/** Buck equation for saturation vapor pressure in kPa.
*
* @param temperature_c Air temperature in °C.
@@ -57,19 +45,15 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
* @param es Saturation vapor pressure in kPa.
* @param hr Relative humidity 0 to 1.
* @param ta Absolute temperature in K.
* @param heater_duration The duration in ms that the heater should turn on for when measuring.
*/
static float vapor_density(float es, float hr, float ta);
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
bool next_update_{false};
float temperature_{NAN};
float humidity_{NAN};
SaturationVaporPressureEquation equation_;
};
} // namespace absolute_humidity
} // namespace esphome
} // namespace esphome::absolute_humidity
+2 -1
View File
@@ -22,7 +22,8 @@ namespace adc {
#ifdef USE_ESP32
// clang-format off
#if (ESP_IDF_VERSION_MAJOR == 5 && \
#if ESP_IDF_VERSION_MAJOR >= 6 || \
(ESP_IDF_VERSION_MAJOR == 5 && \
((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \
(ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \
(ESP_IDF_VERSION_MINOR >= 2)) \
@@ -51,22 +51,6 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
}
}
void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback) {
this->state_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
this->cleared_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) {
this->chime_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) {
this->ready_callback_.add(std::move(callback));
}
void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
const char *code) {
auto call = this->make_call();
@@ -37,25 +37,24 @@ class AlarmControlPanel : public EntityBase {
*
* @param callback The callback function
*/
void add_on_state_callback(std::function<void()> &&callback);
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
/** Add a callback for when the state of the alarm_control_panel clears from triggered
*
* @param callback The callback function
*/
void add_on_cleared_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel clears from triggered. */
template<typename F> void add_on_cleared_callback(F &&callback) {
this->cleared_callback_.add(std::forward<F>(callback));
}
/** Add a callback for when a chime zone goes from closed to open
*
* @param callback The callback function
*/
void add_on_chime_callback(std::function<void()> &&callback);
/** Add a callback for when a chime zone goes from closed to open. */
template<typename F> void add_on_chime_callback(F &&callback) {
this->chime_callback_.add(std::forward<F>(callback));
}
/** Add a callback for when a ready state changes
*
* @param callback The callback function
*/
void add_on_ready_callback(std::function<void()> &&callback);
/** Add a callback for when a ready state changes. */
template<typename F> void add_on_ready_callback(F &&callback) {
this->ready_callback_.add(std::forward<F>(callback));
}
/** A numeric representation of the supported features as per HomeAssistant
*
@@ -1,5 +1,6 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/sensor/sensor.h"
+6 -2
View File
@@ -301,11 +301,12 @@ CONFIG_SCHEMA = cv.All(
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
# Platform defaults based on available RAM and typical message rates:
# CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo
cv.SplitDefault(
CONF_MAX_SEND_QUEUE,
esp8266=5, # Limited RAM, need to fail fast
esp8266=4, # Limited RAM, need to fail fast
esp32=8, # More RAM, can buffer more
rp2040=5, # Limited RAM
rp2040=8, # Moderate RAM
bk72xx=8, # Moderate RAM
nrf52=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
@@ -454,6 +455,9 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.11")
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
else:
cg.add_define("USE_API_PLAINTEXT")
+6
View File
@@ -44,6 +44,12 @@ class APIBuffer {
this->reserve(n);
this->size_ = n; // no zero-fill
}
/// Reserve capacity for max(reserve_size, new_size) bytes, then set size to new_size.
/// Single grow_ check regardless of argument order.
inline void reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
this->reserve(std::max(reserve_size, new_size));
this->size_ = new_size;
}
uint8_t *data() { return this->data_.get(); }
const uint8_t *data() const { return this->data_.get(); }
size_t size() const { return this->size_; }
+6 -3
View File
@@ -64,7 +64,11 @@ static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS *
// A stalled handshake from a buggy client or network glitch holds a connection
// slot, which can prevent legitimate clients from reconnecting. Also hardens
// against the less likely case of intentional connection slot exhaustion.
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000;
//
// 60s is intentionally high: on ESP8266 with power_save_mode: LIGHT and weak
// WiFi (-70 dBm+), TCP retransmissions push real-world handshake times to
// 28-30s. See https://github.com/esphome/esphome/issues/14999
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000;
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
@@ -2025,8 +2029,7 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode
// Batch message second or later
// Add padding for previous message footer + this message header
size_t current_size = shared_buf.size();
shared_buf.reserve(current_size + total_calculated_size);
shared_buf.resize(current_size + footer_size + header_padding);
shared_buf.reserve_and_resize(current_size + total_calculated_size, current_size + footer_size + header_padding);
}
// Pre-resize buffer to include payload, then encode through raw pointer
+3 -3
View File
@@ -305,9 +305,9 @@ class APIConnection final : public APIServerConnectionBase {
// Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
shared_buf.reserve(total_size);
// Resize to add header padding so message encoding starts at the correct position
shared_buf.resize(header_padding);
// Reserve full size but only set initial size to header padding
// so message encoding starts at the correct position
shared_buf.reserve_and_resize(total_size, header_padding);
}
// Convenience overload - computes frame overhead internally
+39 -126
View File
@@ -100,149 +100,61 @@ const LogString *api_error_to_logstr(APIError err) {
return LOG_STR("UNKNOWN");
}
// Default implementation for loop - handles sending buffered data
APIError APIFrameHelper::loop() {
if (this->tx_buf_count_ > 0) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
return APIError::OK;
}
// Common socket write error handling
APIError APIFrameHelper::handle_socket_write_error_() {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
}
HELPER_LOG("Socket write failed with errno %d", errno);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
// Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
uint16_t offset) {
// Check if queue is full
if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) {
HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_);
this->state_ = State::FAILED;
return;
}
uint16_t buffer_size = total_write_len - offset;
auto &buffer = this->tx_buf_[this->tx_buf_tail_];
buffer = std::make_unique<SendBuffer>(SendBuffer{
.data = std::make_unique<uint8_t[]>(buffer_size),
.size = buffer_size,
.offset = 0,
});
uint16_t to_skip = offset;
uint16_t write_pos = 0;
for (int i = 0; i < iovcnt; i++) {
if (to_skip >= iov[i].iov_len) {
// Skip this entire segment
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
} else {
// Include this segment (partially or fully)
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(buffer->data.get() + write_pos, src, len);
write_pos += len;
to_skip = 0;
}
}
// Update circular buffer tracking
this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE;
this->tx_buf_count_++;
}
// This method writes data to socket or buffers it
// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full.
// Returns OK if all data was sent or successfully queued.
// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED).
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
// Returns APIError::OK if successful (or would block, but data has been buffered)
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
if (iovcnt == 0)
return APIError::OK; // Nothing to do, success
#ifdef HELPER_LOG_PACKETS
for (int i = 0; i < iovcnt; i++) {
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
}
#endif
// Try to send any existing buffered data first if there is any
if (this->tx_buf_count_ > 0) {
APIError send_result = try_send_tx_buf_();
// If real error occurred (not just WOULD_BLOCK), return it
if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
return send_result;
}
uint16_t skip = 0;
// If there is still data in the buffer, we can't send, buffer
// the new data and return
if (this->tx_buf_count_ > 0) {
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered
}
// Drain any existing backlog first
if (!this->overflow_buf_.empty()) [[unlikely]] {
APIError err = this->drain_overflow_and_handle_errors_();
if (err != APIError::OK)
return err;
}
// Try to send directly if no buffered data
// Optimize for single iovec case (common for plaintext API)
ssize_t sent =
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
// If backlog is clear, try direct send
if (this->overflow_buf_.empty()) [[likely]] {
ssize_t sent =
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
if (sent == -1) {
APIError err = this->handle_socket_write_error_();
if (err == APIError::WOULD_BLOCK) {
// Socket would block, buffer the data
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered
}
return err; // Socket write failed
} else if (static_cast<uint16_t>(sent) < total_write_len) {
// Partially sent, buffer the remaining data
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent));
}
return APIError::OK; // Success, all data sent or buffered
}
// Common implementation for trying to send buffered data
// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method
APIError APIFrameHelper::try_send_tx_buf_() {
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
while (this->tx_buf_count_ > 0) {
// Get the first buffer in the queue
SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get();
// Try to send the remaining data in this buffer
ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining());
if (sent == -1) {
return this->handle_socket_write_error_();
} else if (sent == 0) {
// Nothing sent but not an error
return APIError::WOULD_BLOCK;
} else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) {
// Partially sent, update offset
// Cast to ensure no overflow issues with uint16_t
front_buffer->offset += static_cast<uint16_t>(sent);
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
if (sent == -1) [[unlikely]] {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
} else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] {
return APIError::OK;
} else {
// Buffer completely sent, remove it from the queue
this->tx_buf_[this->tx_buf_head_].reset();
this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE;
this->tx_buf_count_--;
// Continue loop to try sending the next buffer
skip = static_cast<uint16_t>(sent);
}
}
return APIError::OK; // All buffers sent successfully
// Queue unsent data into overflow buffer
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) {
HELPER_LOG("Overflow buffer full, dropping connection");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
return APIError::OK;
}
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
@@ -278,11 +190,12 @@ APIError APIFrameHelper::init_common_() {
APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) {
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
const int err = errno;
if (err == EWOULDBLOCK || err == EAGAIN) {
return APIError::WOULD_BLOCK;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
HELPER_LOG("Socket read failed with errno %d", err);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;
+39 -45
View File
@@ -9,9 +9,11 @@
#include "esphome/core/defines.h"
#ifdef USE_API
#include "esphome/components/api/api_buffer.h"
#include "esphome/components/api/api_overflow_buffer.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "proto.h"
namespace esphome::api {
@@ -37,8 +39,6 @@ static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
class ProtoWriteBuffer;
// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32;
@@ -105,9 +105,9 @@ class APIFrameHelper {
}
virtual ~APIFrameHelper() = default;
virtual APIError init() = 0;
virtual APIError loop();
virtual APIError loop() = 0;
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
bool can_write_without_blocking() { return this->state_ == State::DATA && this->overflow_buf_.empty(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
if (state_ == State::CLOSED)
@@ -147,25 +147,28 @@ class APIFrameHelper {
//
void set_nodelay_for_message(bool is_log_message) {
if (!is_log_message) {
if (this->nodelay_state_ != NODELAY_ON) {
if (this->nodelay_counter_) {
this->set_nodelay_raw_(true);
this->nodelay_state_ = NODELAY_ON;
this->nodelay_counter_ = 0;
}
return;
}
// Log messages: state transitions -1 -> 1 -> ... -> LOG_NAGLE_COUNT -> -1 (flush)
if (this->nodelay_state_ == NODELAY_ON) {
// Log message: enable Nagle on first, flush after LOG_NAGLE_COUNT
if (!this->nodelay_counter_)
this->set_nodelay_raw_(false);
this->nodelay_state_ = 1;
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
if (++this->nodelay_counter_ > LOG_NAGLE_COUNT) {
this->set_nodelay_raw_(true);
this->nodelay_state_ = NODELAY_ON;
} else {
this->nodelay_state_++;
this->nodelay_counter_ = 0;
}
}
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize buffer to include footer space if needed (e.g. Noise MAC)
if (frame_footer_size_)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
MessageInfo msg{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
}
// Write multiple protobuf messages in a single operation
// messages contains (message_type, offset, length) for each message in the buffer
// The buffer contains all messages with appropriate padding before each
@@ -187,28 +190,23 @@ class APIFrameHelper {
}
protected:
// Buffer containing data to be sent
struct SendBuffer {
std::unique_ptr<uint8_t[]> data;
uint16_t size{0}; // Total size of the buffer
uint16_t offset{0}; // Current offset within the buffer
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
uint16_t remaining() const { return size - offset; }
const uint8_t *current_data() const { return data.get() + offset; }
};
// Drain backlogged overflow data to the socket and handle errors.
// Called when overflow_buf_.empty() is false. Out-of-line to keep the
// fast path (empty check) inline at call sites.
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
APIError drain_overflow_and_handle_errors_();
// Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
// Try to send data from the tx buffer
APIError try_send_tx_buf_();
// Helper method to buffer data from IOVs
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset);
// Common socket write error handling
APIError handle_socket_write_error_();
// Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN).
// Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors.
APIError check_socket_write_err_(int err) {
if (err == EWOULDBLOCK || err == EAGAIN)
return APIError::WOULD_BLOCK;
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;
@@ -243,8 +241,8 @@ class APIFrameHelper {
return APIError::WOULD_BLOCK;
}
// Containers (size varies, but typically 12+ bytes on 32-bit)
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
// Backlog for unsent data when TCP send buffer is full (rarely used in production)
APIOverflowBuffer overflow_buf_;
APIBuffer rx_buf_;
// Client name buffer - stores name from Hello message or initial peername
@@ -255,21 +253,17 @@ class APIFrameHelper {
State state_{State::INITIALIZE};
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
// (immediate send). Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
// Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send).
// Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
// After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0.
// ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching.
// ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more.
static constexpr int8_t NODELAY_ON = -1;
#ifdef USE_ESP8266
static constexpr int8_t LOG_NAGLE_COUNT = 2;
static constexpr uint8_t LOG_NAGLE_COUNT = 2;
#else
static constexpr int8_t LOG_NAGLE_COUNT = 3;
static constexpr uint8_t LOG_NAGLE_COUNT = 3;
#endif
int8_t nodelay_state_{NODELAY_ON};
uint8_t nodelay_counter_{0};
// Internal helper to set TCP_NODELAY socket option
void set_nodelay_raw_(bool enable) {
@@ -153,8 +153,10 @@ APIError APINoiseFrameHelper::loop() {
}
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
if (!this->overflow_buf_.empty()) [[unlikely]] {
return this->drain_overflow_and_handle_errors_();
}
return APIError::OK;
}
/** Read a packet into the rx_buf_.
@@ -450,14 +452,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = type;
return APIError::OK;
}
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize to include MAC space (required for Noise encryption)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
MessageInfo msg{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
@@ -22,7 +22,6 @@ class APINoiseFrameHelper final : public APIFrameHelper {
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
@@ -64,8 +64,10 @@ APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
if (!this->overflow_buf_.empty()) [[unlikely]] {
return this->drain_overflow_and_handle_errors_();
}
return APIError::OK;
}
/** Read a packet into the rx_buf_.
@@ -235,11 +237,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = this->rx_header_parsed_type_;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
}
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
std::span<const MessageInfo> messages) {
APIError aerr = this->check_data_state_();
@@ -257,9 +254,11 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
uint16_t total_write_len = 0;
for (const auto &msg : messages) {
// Calculate varint sizes for header layout
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.payload_size));
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.message_type));
// Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead
uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE
? 1
: (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3);
uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2;
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// Calculate where to start writing the header
@@ -281,8 +280,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
//
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
// [0] - 0x00 indicator byte
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
// [4-5] - Message type varint (2 bytes, for types 128-32767)
// [1-3] - Payload size varint (3 bytes, for sizes 16384-65535)
// [4-5] - Message type varint (2 bytes, for types 128-16383)
// [6...] - Actual payload data
//
// The message starts at offset + frame_header_padding_
@@ -19,7 +19,6 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
@@ -0,0 +1,73 @@
#include "api_overflow_buffer.h"
#ifdef USE_API
#include <cstring>
namespace esphome::api {
APIOverflowBuffer::~APIOverflowBuffer() {
for (auto *entry : this->queue_) {
if (entry != nullptr)
Entry::destroy(entry);
}
}
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
while (this->count_ > 0) {
Entry *front = this->queue_[this->head_];
ssize_t sent = socket->write(front->current_data(), front->remaining());
if (sent <= 0) {
// -1 = error (caller checks errno for EWOULDBLOCK vs hard error)
// 0 = nothing sent (treat as no progress)
return sent;
}
if (static_cast<uint16_t>(sent) < front->remaining()) {
// Partially sent, update offset and stop
front->offset += static_cast<uint16_t>(sent);
return sent;
}
// Entry fully sent — free it and advance
Entry::destroy(front);
this->queue_[this->head_] = nullptr;
this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE;
this->count_--;
}
return 0; // All drained
}
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) {
if (this->count_ >= API_MAX_SEND_QUEUE)
return false;
uint16_t buffer_size = total_len - skip;
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
auto *entry = new Entry{new uint8_t[buffer_size], buffer_size, 0};
this->queue_[this->tail_] = entry;
uint16_t to_skip = skip;
uint16_t write_pos = 0;
for (int i = 0; i < iovcnt; i++) {
if (to_skip >= iov[i].iov_len) {
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
} else {
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(entry->data + write_pos, src, len);
write_pos += len;
to_skip = 0;
}
}
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
this->count_++;
return true;
}
} // namespace esphome::api
#endif // USE_API
@@ -0,0 +1,76 @@
#pragma once
#include <array>
#include <cstdint>
#include <sys/types.h>
#include "esphome/core/defines.h"
#ifdef USE_API
#include "esphome/components/socket/headers.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/helpers.h"
namespace esphome::api {
/// Circular queue of heap-allocated byte buffers used as a TCP send backlog.
///
/// Under normal operation this buffer is **never used** — data goes straight
/// from the frame helper to the socket. It only fills when the LWIP TCP
/// send buffer is full (slow client, congested network, heavy logging).
/// The queue drains automatically on subsequent write/loop calls once the
/// socket becomes writable again.
///
/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python
/// config). If the queue fills completely the connection is marked failed.
class APIOverflowBuffer {
public:
/// A single heap-allocated send-backlog entry.
/// Lifetime is manually managed — see destroy().
struct Entry {
uint8_t *data;
uint16_t size; // Total size of the buffer
uint16_t offset; // Current send offset within the buffer
uint16_t remaining() const { return this->size - this->offset; }
const uint8_t *current_data() const { return this->data + this->offset; }
/// Free this entry and its data buffer.
static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) {
delete[] entry->data;
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
}
};
~APIOverflowBuffer();
/// True when no backlogged data is waiting.
bool empty() const { return this->count_ == 0; }
/// True when the queue has no room for another entry.
bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; }
/// Number of entries currently queued.
uint8_t count() const { return this->count_; }
/// Try to drain queued data to the socket.
/// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress,
/// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors).
/// Callers only need to act on -1; 0 and positive values both mean "no error".
/// Frees entries as they are fully sent.
ssize_t try_drain(socket::Socket *socket);
/// Enqueue unsent IOV data into the backlog.
/// Copies iov data starting at byte offset `skip` into a new entry.
/// Returns false if the queue is full (caller should fail the connection).
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
protected:
std::array<Entry *, API_MAX_SEND_QUEUE> queue_{};
uint8_t head_{0};
uint8_t tail_{0};
uint8_t count_{0};
};
} // namespace esphome::api
#endif // USE_API
+3 -3
View File
@@ -36,11 +36,11 @@ struct SavedNoisePsk {
} PACKED; // NOLINT
#endif
class APIServer : public Component,
public Controller
class APIServer final : public Component,
public Controller
#ifdef USE_CAMERA
,
public camera::CameraListener
public camera::CameraListener
#endif
{
public:
+17 -10
View File
@@ -136,8 +136,9 @@ class CustomAPIDevice {
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
auto *obj = static_cast<T *>(this);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
[obj, callback](StringRef state) { (obj->*callback)(state); });
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
@@ -148,10 +149,12 @@ class CustomAPIDevice {
ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
auto *obj = static_cast<T *>(this);
// Explicit type to disambiguate overload resolution
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(f));
global_api_server->subscribe_home_assistant_state(
entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(
[obj, callback](const std::string &state) { (obj->*callback)(state); }));
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
@@ -176,8 +179,10 @@ class CustomAPIDevice {
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
auto *obj = static_cast<T *>(this);
global_api_server->subscribe_home_assistant_state(
entity_id, optional<std::string>(attribute),
[obj, callback, entity_id](StringRef state) { (obj->*callback)(entity_id, state); });
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
@@ -188,10 +193,12 @@ class CustomAPIDevice {
ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
auto *obj = static_cast<T *>(this);
// Explicit type to disambiguate overload resolution
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(f));
global_api_server->subscribe_home_assistant_state(
entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(
[obj, callback, entity_id](const std::string &state) { (obj->*callback)(entity_id, state); }));
}
#else
template<typename T>
+15 -8
View File
@@ -477,6 +477,12 @@ class ProtoDecodableMessage : public ProtoMessage {
class ProtoSize {
public:
// Varint encoding thresholds: values below each threshold fit in N bytes
static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = 1 << 7; // 128
static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = 1 << 14; // 16384
static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152
static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456
/**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
*
@@ -484,7 +490,7 @@ class ProtoSize {
* @return The number of bytes needed to encode the value
*/
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) {
if (value < 128) [[likely]]
if (value < VARINT_THRESHOLD_1_BYTE) [[likely]]
return 1; // Fast path: 7 bits, most common case
if (__builtin_is_constant_evaluated())
return varint_wide(value);
@@ -496,11 +502,11 @@ class ProtoSize {
static uint32_t varint_slow(uint32_t value) __attribute__((noinline));
// Shared cascade for values >= 128 (used by both constexpr and noinline paths)
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) {
if (value < 16384)
if (value < VARINT_THRESHOLD_2_BYTE)
return 2;
if (value < 2097152)
if (value < VARINT_THRESHOLD_3_BYTE)
return 3;
if (value < 268435456)
if (value < VARINT_THRESHOLD_4_BYTE)
return 4;
return 5;
}
@@ -606,7 +612,7 @@ class ProtoSize {
static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) {
return value ? field_id_size + varint(encode_zigzag32(value)) : 0;
}
static constexpr uint32_t calc_sint32_force(uint32_t field_id_size, int32_t value) {
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) {
return field_id_size + varint(encode_zigzag32(value));
}
static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) {
@@ -618,13 +624,13 @@ class ProtoSize {
static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) {
return value ? field_id_size + varint(value) : 0;
}
static constexpr uint32_t calc_uint64_force(uint32_t field_id_size, uint64_t value) {
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) {
return field_id_size + varint(value);
}
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
}
static constexpr uint32_t calc_length_force(uint32_t field_id_size, size_t len) {
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_length_force(uint32_t field_id_size, size_t len) {
return field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
}
static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) {
@@ -642,7 +648,8 @@ class ProtoSize {
static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) {
return nested_size ? field_id_size + varint(nested_size) + nested_size : 0;
}
static constexpr uint32_t calc_message_force(uint32_t field_id_size, uint32_t nested_size) {
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_message_force(uint32_t field_id_size,
uint32_t nested_size) {
return field_id_size + varint(nested_size) + nested_size;
}
};
@@ -52,11 +52,12 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
connect_cb_(connect_arg_, this);
return true;
}
if (errno != EINPROGRESS) {
ESP_LOGE(TAG, "Connect failed: %d", errno);
const int saved_errno = errno;
if (saved_errno != EINPROGRESS) {
ESP_LOGE(TAG, "Connect failed: %d", saved_errno);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
error_cb_(error_arg_, this, saved_errno);
return false;
}
@@ -79,11 +80,12 @@ size_t AsyncClient::write(const char *data, size_t len) {
ssize_t sent = socket_->write(data, len);
if (sent < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
ESP_LOGE(TAG, "Write error: %d", errno);
const int err = errno;
if (err != EAGAIN && err != EWOULDBLOCK) {
ESP_LOGE(TAG, "Write error: %d", err);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
error_cb_(error_arg_, this, err);
}
return 0;
}
@@ -129,10 +131,11 @@ void AsyncClient::loop() {
error_cb_(error_arg_, this, error);
}
} else if (ret < 0) {
ESP_LOGE(TAG, "Select error: %d", errno);
const int err = errno;
ESP_LOGE(TAG, "Select error: %d", err);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
error_cb_(error_arg_, this, err);
}
} else if (connected_) {
// For connected sockets, use the Application's select() results
@@ -148,11 +151,14 @@ void AsyncClient::loop() {
} else if (len > 0) {
if (data_cb_)
data_cb_(data_arg_, this, buf, len);
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
ESP_LOGW(TAG, "Read error: %d", errno);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
} else {
const int err = errno;
if (err != EAGAIN && err != EWOULDBLOCK) {
ESP_LOGW(TAG, "Read error: %d", err);
close();
if (error_cb_)
error_cb_(error_arg_, this, err);
}
}
}
}
+1 -1
View File
@@ -183,7 +183,7 @@ class BedjetCodec {
BedjetPacket packet_;
BedjetStatusPacket *status_packet_;
BedjetStatusPacket *status_packet_{nullptr};
BedjetStatusPacket buf_;
};
@@ -96,8 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
void setup() override {
this->last_state_ = this->parent_->get_state_default(false);
auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1);
this->parent_->add_on_state_callback(f);
this->parent_->add_on_state_callback([this](bool state) { this->on_state_(state); });
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
+1 -1
View File
@@ -37,7 +37,7 @@ class TimeoutFilter : public Filter, public Component {
TemplatableValue<uint32_t> timeout_delay_{};
};
class DelayedOnOffFilter : public Filter, public Component {
class DelayedOnOffFilter final : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;
@@ -47,6 +47,8 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
switch (event) {
// server response on RSSI request:
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
if (!this->parent()->check_addr(param->read_rssi_cmpl.remote_addr))
return;
if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) {
int8_t rssi = param->read_rssi_cmpl.rssi;
ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi);
@@ -102,6 +102,10 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.value_len == 0) {
ESP_LOGW(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: empty value", this->get_name().c_str());
break;
}
ESP_LOGD(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
param->notify.handle, param->notify.value[0]);
if (param->notify.handle != this->handle)
@@ -131,8 +135,10 @@ float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
if (this->has_data_to_value_) {
std::vector<uint8_t> data(value, value + value_len);
return this->data_to_value_func_(data);
} else {
} else if (value_len > 0) {
return value[0];
} else {
return NAN;
}
}
@@ -104,6 +104,10 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.handle != this->handle)
break;
if (param->notify.value_len == 0) {
ESP_LOGW(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: empty value", this->get_name().c_str());
break;
}
ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
param->notify.handle, param->notify.value[0]);
this->publish_state(reinterpret_cast<const char *>(param->notify.value), param->notify.value_len);
@@ -521,7 +521,7 @@ int BME680BSECComponent::reinit_bsec_lib_() {
}
void BME680BSECComponent::load_state_() {
uint32_t hash = fnv1_hash("bme680_bsec_state_" + this->device_id_);
uint32_t hash = fnv1_hash_extend(fnv1_hash("bme680_bsec_state_"), this->device_id_);
this->bsec_state_ = global_preferences->make_preference<uint8_t[BSEC_MAX_STATE_BLOB_SIZE]>(hash, true);
if (!this->bsec_state_.load(&this->bsec_state_data_)) {
@@ -279,7 +279,8 @@ void BME68xBSEC2Component::run_() {
uint32_t meas_dur = 0;
meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_);
ESP_LOGV(TAG, "Queueing read in %uus", meas_dur);
this->set_timeout("read", meas_dur / 1000, [this, curr_time_ns]() { this->read_(curr_time_ns); });
this->trigger_time_ns_ = curr_time_ns;
this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); });
} else {
ESP_LOGV(TAG, "Measurement not required");
this->read_(curr_time_ns);
@@ -116,6 +116,8 @@ class BME68xBSEC2Component : public Component {
int8_t bme68x_status_{BME68X_OK};
int64_t last_time_ms_{0};
int64_t trigger_time_ns_{0}; // Stored for set_timeout lambda to help avoid heap allocation on supported 32-bit
// toolchains with small std::function SBO
uint32_t millis_overflow_counter_{0};
std::queue<std::function<void()>> queue_;
@@ -10,7 +10,12 @@
#ifdef USE_ESP32
#include <esp_idf_version.h>
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
#include <psa/crypto.h>
#else
#include "mbedtls/ccm.h"
#endif
namespace esphome {
namespace bthome_mithermometer {
@@ -196,6 +201,37 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &da
const uint8_t *ciphertext = data.data() + 1;
const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
// PSA AEAD expects ciphertext + tag concatenated
// BLE advertisement max payload is 31 bytes, so this is always sufficient
static constexpr size_t MAX_CT_WITH_TAG = 32;
uint8_t ct_with_tag[MAX_CT_WITH_TAG];
size_t ct_with_tag_size = ciphertext_size + BTHOME_MIC_SIZE;
memcpy(ct_with_tag, ciphertext, ciphertext_size);
memcpy(ct_with_tag + ciphertext_size, mic, BTHOME_MIC_SIZE);
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attributes, BTHOME_BINDKEY_SIZE * 8);
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT);
psa_set_key_algorithm(&attributes, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_MIC_SIZE));
mbedtls_svc_key_id_t key_id;
if (psa_import_key(&attributes, this->bindkey_, BTHOME_BINDKEY_SIZE, &key_id) != PSA_SUCCESS) {
ESP_LOGVV(TAG, "psa_import_key() failed.");
return false;
}
size_t plaintext_length;
psa_status_t status = psa_aead_decrypt(key_id, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_MIC_SIZE),
nonce.data(), nonce.size(), nullptr, 0, ct_with_tag, ct_with_tag_size,
payload.data(), ciphertext_size, &plaintext_length);
psa_destroy_key(key_id);
if (status != PSA_SUCCESS || plaintext_length != ciphertext_size) {
ESP_LOGVV(TAG, "BTHome decryption failed.");
return false;
}
#else
mbedtls_ccm_context ctx;
mbedtls_ccm_init(&ctx);
@@ -213,6 +249,7 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &da
ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret);
return false;
}
#endif
return true;
}
-1
View File
@@ -20,6 +20,5 @@ void Button::press() {
this->press_action();
this->press_callback_.call();
}
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
} // namespace esphome::button
+3 -1
View File
@@ -34,7 +34,9 @@ class Button : public EntityBase {
*
* @param callback The void() callback.
*/
void add_on_press_callback(std::function<void()> &&callback);
template<typename F> void add_on_press_callback(F &&callback) {
this->press_callback_.add(std::forward<F>(callback));
}
protected:
/** You should implement this virtual method if you want to create your own button.
@@ -50,7 +50,7 @@ async def to_code(config: ConfigType) -> None:
buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID])
cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE]))
if config[CONF_TYPE] == ESP32_CAMERA_ENCODER:
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
add_idf_component(name="espressif/esp32-camera", ref="2.1.5")
cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER")
var = cg.new_Pvariable(
config[CONF_ID],
+1 -4
View File
@@ -91,10 +91,7 @@ class Canbus : public Component {
* - rtr If this is a remote transmission request
* - data The message data
*/
void add_callback(
std::function<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)> callback) {
this->callback_manager_.add(std::move(callback));
}
template<typename F> void add_callback(F &&callback) { this->callback_manager_.add(std::forward<F>(callback)); }
protected:
template<typename... Ts> friend class CanbusSendAction;
@@ -100,8 +100,9 @@ void DNSServer::process_next_request() {
&client_addr_len);
if (len < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
ESP_LOGE(TAG, "recvfrom failed: %d", errno);
const int err = errno;
if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR) {
ESP_LOGE(TAG, "recvfrom failed: %d", err);
}
return;
}
-8
View File
@@ -356,14 +356,6 @@ ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode)
return *this;
}
void Climate::add_on_state_callback(std::function<void(Climate &)> &&callback) {
this->state_callback_.add(std::move(callback));
}
void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callback) {
this->control_callback_.add(std::move(callback));
}
// Random 32bit value; If this changes existing restore preferences are invalidated
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
+6 -2
View File
@@ -192,7 +192,9 @@ class Climate : public EntityBase {
*
* @param callback The callback to call.
*/
void add_on_state_callback(std::function<void(Climate &)> &&callback);
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
/**
* Add a callback for the climate device configuration; each time the configuration parameters of a climate device
@@ -200,7 +202,9 @@ class Climate : public EntityBase {
*
* @param callback The callback to call.
*/
void add_on_control_callback(std::function<void(ClimateCall &)> &&callback);
template<typename F> void add_on_control_callback(F &&callback) {
this->control_callback_.add(std::forward<F>(callback));
}
/** Make a climate device control call, this is used to control the climate device, see the ClimateCall description
* for more info.
+5
View File
@@ -3,6 +3,7 @@
CODEOWNERS = ["@esphome/core"]
CONF_BYTE_ORDER = "byte_order"
CONF_CLIMATE_ID = "climate_id"
BYTE_ORDER_LITTLE = "little_endian"
BYTE_ORDER_BIG = "big_endian"
@@ -20,6 +21,10 @@ CONF_REQUEST_HEADERS = "request_headers"
CONF_ROWS = "rows"
CONF_STOP_BITS = "stop_bits"
CONF_USE_PSRAM = "use_psram"
CONF_VOLUME_INCREMENT = "volume_increment"
CONF_VOLUME_INITIAL = "volume_initial"
CONF_VOLUME_MAX = "volume_max"
CONF_VOLUME_MIN = "volume_min"
ICON_CURRENT_DC = "mdi:current-dc"
ICON_SOLAR_PANEL = "mdi:solar-panel"
+10 -8
View File
@@ -105,17 +105,18 @@ template<typename... Ts> using CoverIsClosedCondition = CoverPositionCondition<f
template<bool OPEN> class CoverPositionTrigger : public Trigger<> {
public:
CoverPositionTrigger(Cover *a_cover) {
a_cover->add_on_state_callback([this, a_cover]() {
if (a_cover->position != this->last_position_) {
this->last_position_ = a_cover->position;
if (a_cover->position == (OPEN ? COVER_OPEN : COVER_CLOSED))
CoverPositionTrigger(Cover *a_cover) : cover_(a_cover) {
a_cover->add_on_state_callback([this]() {
if (this->cover_->position != this->last_position_) {
this->last_position_ = this->cover_->position;
if (this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED))
this->trigger();
}
});
}
protected:
Cover *cover_;
float last_position_{NAN};
};
@@ -124,9 +125,9 @@ using CoverClosedTrigger = CoverPositionTrigger<false>;
template<CoverOperation OP> class CoverTrigger : public Trigger<> {
public:
CoverTrigger(Cover *a_cover) {
a_cover->add_on_state_callback([this, a_cover]() {
auto current_op = a_cover->current_operation;
CoverTrigger(Cover *a_cover) : cover_(a_cover) {
a_cover->add_on_state_callback([this]() {
auto current_op = this->cover_->current_operation;
if (current_op == OP) {
if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) {
this->trigger();
@@ -137,6 +138,7 @@ template<CoverOperation OP> class CoverTrigger : public Trigger<> {
}
protected:
Cover *cover_;
optional<CoverOperation> last_operation_{};
};
} // namespace esphome::cover
-1
View File
@@ -139,7 +139,6 @@ bool CoverCall::get_stop() const { return this->stop_; }
CoverCall Cover::make_call() { return {this}; }
void Cover::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); }
void Cover::publish_state(bool save) {
this->position = clamp(this->position, 0.0f, 1.0f);
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
+1 -1
View File
@@ -125,7 +125,7 @@ class Cover : public EntityBase {
/// Construct a new cover call used to control the cover.
CoverCall make_call();
void add_on_state_callback(std::function<void()> &&f);
template<typename F> void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward<F>(f)); }
/** Publish the current state of the cover.
*
+8 -3
View File
@@ -14,7 +14,9 @@ class DateTimeBase : public EntityBase {
public:
virtual ESPTime state_as_esptime() const = 0;
void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
#ifdef USE_TIME
void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; }
@@ -31,9 +33,12 @@ class DateTimeBase : public EntityBase {
class DateTimeStateTrigger : public Trigger<ESPTime> {
public:
explicit DateTimeStateTrigger(DateTimeBase *parent) {
parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); });
explicit DateTimeStateTrigger(DateTimeBase *parent) : parent_(parent) {
parent->add_on_state_callback([this]() { this->trigger(this->parent_->state_as_esptime()); });
}
protected:
DateTimeBase *parent_;
};
} // namespace esphome::datetime
+68 -23
View File
@@ -5,6 +5,7 @@
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <esp_sleep.h>
#include <esp_idf_version.h>
#include <esp_heap_caps.h>
#include <esp_system.h>
@@ -48,7 +49,8 @@ static const size_t REBOOT_MAX_LEN = 24;
void DebugComponent::on_shutdown() {
auto *component = App.get_current_component();
char buffer[REBOOT_MAX_LEN]{};
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN,
fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str()));
if (component != nullptr) {
strncpy(buffer, LOG_STR_ARG(component->get_component_log_str()), REBOOT_MAX_LEN - 1);
buffer[REBOOT_MAX_LEN - 1] = '\0';
@@ -65,7 +67,8 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
unsigned reason = esp_reset_reason();
if (reason < sizeof(RESET_REASONS) / sizeof(RESET_REASONS[0])) {
if (reason == ESP_RST_SW) {
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN,
fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str()));
char reboot_source[REBOOT_MAX_LEN]{};
if (pref.load(&reboot_source)) {
reboot_source[REBOOT_MAX_LEN - 1] = '\0';
@@ -82,32 +85,74 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
return buf;
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
static const char *const WAKEUP_CAUSES[] = {
"undefined",
"undefined",
"external signal using RTC_IO",
"external signal using RTC_CNTL",
"timer",
"touchpad",
"ULP program",
"GPIO",
"UART",
"WIFI",
"COCPU int",
"COCPU crash",
"BT",
"undefined", // ESP_SLEEP_WAKEUP_UNDEFINED (0)
"undefined", // ESP_SLEEP_WAKEUP_ALL (1)
"external signal using RTC_IO", // ESP_SLEEP_WAKEUP_EXT0 (2)
"external signal using RTC_CNTL", // ESP_SLEEP_WAKEUP_EXT1 (3)
"timer", // ESP_SLEEP_WAKEUP_TIMER (4)
"touchpad", // ESP_SLEEP_WAKEUP_TOUCHPAD (5)
"ULP program", // ESP_SLEEP_WAKEUP_ULP (6)
"GPIO", // ESP_SLEEP_WAKEUP_GPIO (7)
"UART", // ESP_SLEEP_WAKEUP_UART (8)
"UART1", // ESP_SLEEP_WAKEUP_UART1 (9)
"UART2", // ESP_SLEEP_WAKEUP_UART2 (10)
"WIFI", // ESP_SLEEP_WAKEUP_WIFI (11)
"COCPU int", // ESP_SLEEP_WAKEUP_COCPU (12)
"COCPU crash", // ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG (13)
"BT", // ESP_SLEEP_WAKEUP_BT (14)
"VAD", // ESP_SLEEP_WAKEUP_VAD (15)
"VBAT under voltage", // ESP_SLEEP_WAKEUP_VBAT_UNDER_VOLT (16)
};
#else
static const char *const WAKEUP_CAUSES[] = {
"undefined", // ESP_SLEEP_WAKEUP_UNDEFINED (0)
"undefined", // ESP_SLEEP_WAKEUP_ALL (1)
"external signal using RTC_IO", // ESP_SLEEP_WAKEUP_EXT0 (2)
"external signal using RTC_CNTL", // ESP_SLEEP_WAKEUP_EXT1 (3)
"timer", // ESP_SLEEP_WAKEUP_TIMER (4)
"touchpad", // ESP_SLEEP_WAKEUP_TOUCHPAD (5)
"ULP program", // ESP_SLEEP_WAKEUP_ULP (6)
"GPIO", // ESP_SLEEP_WAKEUP_GPIO (7)
"UART", // ESP_SLEEP_WAKEUP_UART (8)
"WIFI", // ESP_SLEEP_WAKEUP_WIFI (9)
"COCPU int", // ESP_SLEEP_WAKEUP_COCPU (10)
"COCPU crash", // ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG (11)
"BT", // ESP_SLEEP_WAKEUP_BT (12)
};
#endif
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
const char *wake_reason;
unsigned reason = esp_sleep_get_wakeup_cause();
if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) {
wake_reason = WAKEUP_CAUSES[reason];
} else {
wake_reason = "unknown source";
static constexpr auto NUM_CAUSES = sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0]);
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
// IDF 6.0+ returns a bitmap of all wakeup sources
uint32_t causes = esp_sleep_get_wakeup_causes();
if (causes == 0) {
return WAKEUP_CAUSES[0]; // "undefined"
}
// Return the static string directly - no need to copy to buffer
return wake_reason;
char *p = buffer.data();
char *end = p + buffer.size();
*p = '\0';
const char *sep = "";
for (unsigned i = 0; i < NUM_CAUSES && p < end; i++) {
if (causes & (1U << i)) {
size_t needed = strlen(sep) + strlen(WAKEUP_CAUSES[i]);
if (p + needed >= end) {
break;
}
p += snprintf(p, end - p, "%s%s", sep, WAKEUP_CAUSES[i]);
sep = ", ";
}
}
return buffer.data();
#else
unsigned reason = esp_sleep_get_wakeup_cause();
if (reason < NUM_CAUSES) {
return WAKEUP_CAUSES[reason];
}
return "unknown source";
#endif
}
void DebugComponent::log_partition_info_() {
@@ -3,6 +3,7 @@
#include "driver/gpio.h"
#include "deep_sleep_component.h"
#include "esphome/core/log.h"
#include <esp_idf_version.h>
namespace esphome {
namespace deep_sleep {
@@ -26,7 +27,7 @@ namespace deep_sleep {
// - ext0: Single pin wakeup using RTC GPIO (esp_sleep_enable_ext0_wakeup)
// - ext1: Multiple pin wakeup (esp_sleep_enable_ext1_wakeup)
// - Touch: Touch pad wakeup (esp_sleep_enable_touchpad_wakeup)
// - GPIO wakeup: GPIO wakeup for RTC pins (esp_deep_sleep_enable_gpio_wakeup)
// - GPIO wakeup: GPIO wakeup for RTC pins
static const char *const TAG = "deep_sleep";
@@ -135,8 +136,13 @@ void DeepSleepComponent::deep_sleep_() {
}
// Internal pullup/pulldown resistors are enabled automatically, when
// ESP_SLEEP_GPIO_ENABLE_INTERNAL_RESISTORS is set (by default it is)
esp_deep_sleep_enable_gpio_wakeup(1 << this->wakeup_pin_->get_pin(),
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << this->wakeup_pin_->get_pin(),
static_cast<esp_sleep_gpio_wake_up_mode_t>(level));
#else
esp_deep_sleep_enable_gpio_wakeup(1ULL << this->wakeup_pin_->get_pin(),
static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level));
#endif
}
#endif
+2 -2
View File
@@ -51,8 +51,8 @@ class DFPlayer : public uart::UARTDevice, public Component {
bool is_playing() { return is_playing_; }
void dump_config() override;
void add_on_finished_playback_callback(std::function<void()> callback) {
this->on_finished_playback_callback_.add(std::move(callback));
template<typename F> void add_on_finished_playback_callback(F &&callback) {
this->on_finished_playback_callback_.add(std::forward<F>(callback));
}
protected:
@@ -96,37 +96,52 @@ template<typename... Ts> class IsActiveCondition : public Condition<Ts...> {
class DisplayMenuOnEnterTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnEnterTrigger(MenuItem *parent) {
parent->add_on_enter_callback([this, parent]() { this->trigger(parent); });
explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) {
parent->add_on_enter_callback([this]() { this->trigger(this->parent_); });
}
protected:
MenuItem *parent_;
};
class DisplayMenuOnLeaveTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) {
parent->add_on_leave_callback([this, parent]() { this->trigger(parent); });
explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) : parent_(parent) {
parent->add_on_leave_callback([this]() { this->trigger(this->parent_); });
}
protected:
MenuItem *parent_;
};
class DisplayMenuOnValueTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnValueTrigger(MenuItem *parent) {
parent->add_on_value_callback([this, parent]() { this->trigger(parent); });
explicit DisplayMenuOnValueTrigger(MenuItem *parent) : parent_(parent) {
parent->add_on_value_callback([this]() { this->trigger(this->parent_); });
}
protected:
MenuItem *parent_;
};
class DisplayMenuOnNextTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) {
parent->add_on_next_callback([this, parent]() { this->trigger(parent); });
explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) : parent_(parent) {
parent->add_on_next_callback([this]() { this->trigger(this->parent_); });
}
protected:
MenuItemCustom *parent_;
};
class DisplayMenuOnPrevTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) {
parent->add_on_prev_callback([this, parent]() { this->trigger(parent); });
explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) : parent_(parent) {
parent->add_on_prev_callback([this]() { this->trigger(this->parent_); });
}
protected:
MenuItemCustom *parent_;
};
} // namespace display_menu_base
@@ -44,9 +44,9 @@ class MenuItem {
MenuItemMenu *get_parent() { return this->parent_; }
MenuItemType get_type() const { return this->item_type_; }
template<typename V> void set_text(V val) { this->text_ = val; }
void add_on_enter_callback(std::function<void()> &&cb) { this->on_enter_callbacks_.add(std::move(cb)); }
void add_on_leave_callback(std::function<void()> &&cb) { this->on_leave_callbacks_.add(std::move(cb)); }
void add_on_value_callback(std::function<void()> &&cb) { this->on_value_callbacks_.add(std::move(cb)); }
template<typename F> void add_on_enter_callback(F &&cb) { this->on_enter_callbacks_.add(std::forward<F>(cb)); }
template<typename F> void add_on_leave_callback(F &&cb) { this->on_leave_callbacks_.add(std::forward<F>(cb)); }
template<typename F> void add_on_value_callback(F &&cb) { this->on_value_callbacks_.add(std::forward<F>(cb)); }
std::string get_text() const { return const_cast<MenuItem *>(this)->text_.value(this); }
virtual bool get_immediate_edit() const { return false; }
@@ -170,8 +170,8 @@ class MenuItemCommand : public MenuItem {
class MenuItemCustom : public MenuItemEditable {
public:
explicit MenuItemCustom() : MenuItemEditable(MENU_ITEM_CUSTOM) {}
void add_on_next_callback(std::function<void()> &&cb) { this->on_next_callbacks_.add(std::move(cb)); }
void add_on_prev_callback(std::function<void()> &&cb) { this->on_prev_callbacks_.add(std::move(cb)); }
template<typename F> void add_on_next_callback(F &&cb) { this->on_next_callbacks_.add(std::forward<F>(cb)); }
template<typename F> void add_on_prev_callback(F &&cb) { this->on_prev_callbacks_.add(std::forward<F>(cb)); }
bool has_value() const override { return this->value_getter_.has_value(); }
std::string get_value_text() const override;
@@ -3,9 +3,14 @@
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
#include <bearssl/bearssl.h>
#elif defined(USE_ESP32)
#include <esp_idf_version.h>
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
#include <psa/crypto.h>
#else
#include "mbedtls/esp_config.h"
#include "mbedtls/gcm.h"
#endif
#endif
namespace esphome::dlms_meter {
@@ -240,6 +245,35 @@ bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t m
br_gcm_flip(&gcm_ctx);
br_gcm_run(&gcm_ctx, 0, payload_ptr, message_length);
#elif defined(USE_ESP32)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
// PSA Crypto multipart AEAD (no tag verification, matching legacy behavior)
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attributes, this->decryption_key_.size() * 8);
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT);
psa_set_key_algorithm(&attributes, PSA_ALG_GCM);
mbedtls_svc_key_id_t key_id;
bool decrypt_failed = true;
if (psa_import_key(&attributes, this->decryption_key_.data(), this->decryption_key_.size(), &key_id) == PSA_SUCCESS) {
psa_aead_operation_t op = PSA_AEAD_OPERATION_INIT;
if (psa_aead_decrypt_setup(&op, key_id, PSA_ALG_GCM) == PSA_SUCCESS &&
psa_aead_set_nonce(&op, iv, sizeof(iv)) == PSA_SUCCESS) {
size_t outlen = 0;
if (psa_aead_update(&op, payload_ptr, message_length, payload_ptr, message_length, &outlen) == PSA_SUCCESS &&
outlen == message_length) {
decrypt_failed = false;
}
}
psa_aead_abort(&op);
psa_destroy_key(key_id);
}
if (decrypt_failed) {
ESP_LOGE(TAG, "Decryption failed");
this->receive_buffer_.clear();
return false;
}
#else
size_t outlen = 0;
mbedtls_gcm_context gcm_ctx;
mbedtls_gcm_init(&gcm_ctx);
@@ -252,6 +286,7 @@ bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t m
this->receive_buffer_.clear();
return false;
}
#endif
#else
#error "Invalid Platform"
#endif
+1 -2
View File
@@ -127,8 +127,7 @@ void DPS310Component::read_() {
this->update_in_progress_ = false;
this->status_clear_warning();
} else {
auto f = std::bind(&DPS310Component::read_, this);
this->set_timeout("dps310", 10, f);
this->set_timeout("dps310", 10, [this]() { this->read_(); });
}
}
+306 -52
View File
@@ -28,6 +28,7 @@ from esphome.const import (
CONF_PLATFORMIO_OPTIONS,
CONF_REF,
CONF_SAFE_MODE,
CONF_SIZE,
CONF_SOURCE,
CONF_TYPE,
CONF_VARIANT,
@@ -59,6 +60,7 @@ from .const import ( # noqa
KEY_EXTRA_BUILD_FILES,
KEY_FLASH_SIZE,
KEY_FULL_CERT_BUNDLE,
KEY_IDF_VERSION,
KEY_PATH,
KEY_REF,
KEY_REPO,
@@ -95,6 +97,7 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
CONF_RELEASE = "release"
CONF_SUBTYPE = "subtype"
ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32"
ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}"
@@ -420,9 +423,20 @@ def set_core_data(config):
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded
# Initialize Arduino library tracking - cg.add_library() auto-enables libraries
CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set()
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION]
)
framework_ver = cv.Version.parse(config[CONF_FRAMEWORK][CONF_VERSION])
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = framework_ver
# Store the underlying IDF version for framework-agnostic checks
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = framework_ver
elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = idf_ver
else:
raise cv.Invalid(
f"Arduino version {framework_ver} has no known ESP-IDF version mapping. "
"Please update ARDUINO_IDF_VERSION_LOOKUP.",
path=[CONF_FRAMEWORK, CONF_VERSION],
)
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
@@ -600,10 +614,12 @@ def _format_framework_espidf_version(
ext = "tar.xz"
else:
ext = "zip"
# Build version string with dot-separated extra (e.g., "5.5.3.1" not "5.5.3-1")
# Build version string with extra separator based on type:
# numeric extra uses dot (e.g., "5.5.3.1"), string extra uses dash (e.g., "6.0.0-rc1")
ver_str = f"{ver.major}.{ver.minor}.{ver.patch}"
if ver.extra:
ver_str += f".{ver.extra}"
sep = "." if str(ver.extra).isdigit() else "-"
ver_str += f"{sep}{ver.extra}"
if release:
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}.{release}/esp-idf-v{ver_str}.{ext}"
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}/esp-idf-v{ver_str}.{ext}"
@@ -974,6 +990,7 @@ KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
KEY_FATFS_REQUIRED = "fatfs_required"
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
def require_vfs_select() -> None:
@@ -1043,6 +1060,25 @@ def require_mbedtls_pkcs7() -> None:
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
def require_mbedtls_sha512() -> None:
"""Mark that mbedTLS SHA-384/SHA-512 support is required by a component.
Call this from components that need to verify TLS certificates or signatures
using SHA-384 or SHA-512 algorithms. This prevents CONFIG_MBEDTLS_SHA384_C
and CONFIG_MBEDTLS_SHA512_C from being disabled.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_SHA512_REQUIRED] = True
def idf_version() -> cv.Version:
"""Return the underlying ESP-IDF version regardless of framework choice.
For ESP-IDF builds this is the framework version directly.
For Arduino builds this is the mapped IDF version from ARDUINO_IDF_VERSION_LOOKUP.
"""
return CORE.data[KEY_ESP32][KEY_IDF_VERSION]
def require_fatfs() -> None:
"""Mark that FATFS support is required by a component.
@@ -1224,6 +1260,43 @@ def _set_default_framework(config):
return config
RESERVED_PARTITION_NAMES = {
"nvs",
"app0",
"app1",
"otadata",
"eeprom",
"spiffs",
"phy_init",
}
VALID_APP_SUBTYPES = {"factory", "test"}
VALID_DATA_SUBTYPES = {
"nvs",
"nvs_keys",
"spiffs",
"coredump",
"efuse",
"fat",
"undefined",
"littlefs",
}
def _validate_custom_partition(config: ConfigType) -> ConfigType:
"""Voluptuous validator for custom partition schema."""
try:
_validate_partition(
config[CONF_NAME],
config[CONF_TYPE],
config[CONF_SUBTYPE],
config[CONF_SIZE],
)
except ValueError as e:
raise cv.Invalid(str(e)) from e
return config
FLASH_SIZES = [
"2MB",
"4MB",
@@ -1246,7 +1319,28 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
*FLASH_SIZES, upper=True
),
cv.Optional(CONF_PARTITIONS): cv.file_,
cv.Optional(CONF_PARTITIONS): cv.Any(
cv.file_,
cv.ensure_list(
cv.All(
cv.Schema(
{
cv.Required(CONF_NAME): cv.string_strict,
cv.Required(CONF_TYPE): cv.All(
cv.Any(cv.string_strict, cv.int_range(0x40, 0xFE)),
cv.int_to_hex_string,
),
cv.Required(CONF_SUBTYPE): cv.All(
cv.Any(cv.string_strict, cv.int_range(0, 0xFE)),
cv.int_to_hex_string,
),
cv.Required(CONF_SIZE): cv.int_range(min=0x1000),
}
),
_validate_custom_partition,
),
),
),
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
}
@@ -1715,9 +1809,18 @@ async def to_code(config):
if use_platformio:
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
)
if isinstance(config[CONF_PARTITIONS], list):
for partition in config[CONF_PARTITIONS]:
add_partition(
partition[CONF_NAME],
partition[CONF_TYPE],
partition[CONF_SUBTYPE],
partition[CONF_SIZE],
)
else:
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
)
if assertion_level := advanced.get(CONF_ASSERTION_LEVEL):
for key, flag in ASSERTION_LEVELS.items():
@@ -1802,6 +1905,33 @@ async def to_code(config):
elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False)
# Disable SHA-384 and SHA-512 in mbedTLS
# ESPHome doesn't use either algorithm. SHA-384 shares the same
# compression function as SHA-512 (mbedtls_internal_sha512_process),
# so both must be disabled to eliminate the ~3KB software fallback
# that IDF 6.0's PSA parallel engine always links in.
# On IDF < 6.0 these are a single config and hardware-only (no
# software fallback), so there was no code size cost to leaving
# them enabled.
# Components that need SHA-384/SHA-512 can call require_mbedtls_sha512()
if idf_version() >= cv.Version(6, 0, 0) and not CORE.data[KEY_ESP32].get(
KEY_MBEDTLS_SHA512_REQUIRED, False
):
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False)
# Disable PicolibC Newlib compatibility shim on IDF 6.0+
# IDF 6.0 switched from Newlib to PicolibC. The shim provides thread-local
# stdin/stdout/stderr and getreent() for code compiled against Newlib.
# ESPHome doesn't link against Newlib-built libraries that use stdio.
# If a component needs it (e.g. precompiled Newlib binaries), re-enable via:
# esp32:
# framework:
# sdkconfig_options:
# CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY: "y"
if idf_version() >= cv.Version(6, 0, 0):
add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", False)
# Disable regi2c control functions in IRAM
# Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
@@ -1836,45 +1966,175 @@ async def to_code(config):
CORE.add_job(_write_arduino_libraries_sdkconfig)
APP_PARTITION_SIZES = {
"2MB": 0x0C0000, # 768 KB
"4MB": 0x1C0000, # 1792 KB
"8MB": 0x3C0000, # 3840 KB
"16MB": 0x7C0000, # 7936 KB
"32MB": 0xFC0000, # 16128 KB
KEY_CUSTOM_PARTITIONS = "custom_partitions"
@dataclass
class PartitionEntry:
name: str
type: str
subtype: str
size: int
# Partition sizes (offsets auto-placed by gen_esp32part.py).
# These constants are the single source of truth — used in both
# the CSV generation and the overhead calculation.
BOOTLOADER_SIZE = 0x8000
PARTITION_TABLE_SIZE = 0x1000
FIRST_PARTITION_OFFSET = BOOTLOADER_SIZE + PARTITION_TABLE_SIZE
OTADATA_SIZE = 0x2000
PHY_INIT_SIZE = 0x1000
EEPROM_SIZE = 0x1000 # Arduino only
SPIFFS_SIZE = 0xF000 # Arduino only
ARDUINO_NVS_SIZE = 0x60000
IDF_NVS_SIZE = 0x70000
def _get_partition_overhead() -> int:
"""Total non-app partition budget (system partitions + nvs + padding).
Custom partitions are appended at the end and steal from app.
"""
# otadata + phy_init are followed by app0 which requires 64KB alignment,
# so pad up to the next 64KB boundary.
overhead = (
FIRST_PARTITION_OFFSET + OTADATA_SIZE + PHY_INIT_SIZE + 0xFFFF
) & ~0xFFFF
if CORE.using_arduino:
overhead += EEPROM_SIZE + SPIFFS_SIZE + ARDUINO_NVS_SIZE
else:
overhead += IDF_NVS_SIZE
return overhead
VALID_SUBTYPES: dict[str, set[str]] = {
"app": VALID_APP_SUBTYPES,
"data": VALID_DATA_SUBTYPES,
}
def get_arduino_partition_csv(flash_size: str):
app_partition_size = APP_PARTITION_SIZES[flash_size]
eeprom_partition_size = 0x1000 # 4 KB
spiffs_partition_size = 0xF000 # 60 KB
app0_partition_start = 0x010000 # 64 KB
app1_partition_start = app0_partition_start + app_partition_size
eeprom_partition_start = app1_partition_start + app_partition_size
spiffs_partition_start = eeprom_partition_start + eeprom_partition_size
return f"""\
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xE000, 0x2000,
app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X},
app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X},
eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X},
spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X}
"""
def _validate_partition(
name: str, p_type: str | int, subtype: str | int, size: int
) -> None:
"""Validate partition parameters. Raises ValueError on invalid input."""
if name in RESERVED_PARTITION_NAMES:
raise ValueError(f"Partition name '{name}' is reserved.")
if size % 0x1000 != 0:
raise ValueError("Partition size must be 4KB (0x1000) aligned.")
# Numeric or already-normalized hex types/subtypes skip string validation
if not isinstance(p_type, str) or p_type.startswith("0x"):
return
if p_type not in VALID_SUBTYPES:
raise ValueError(
f"Type '{p_type}' is invalid. Only 'app' and 'data' are allowed."
" Use numbers for custom types."
)
if not isinstance(subtype, str) or subtype.startswith("0x"):
return
valid = VALID_SUBTYPES[p_type]
if subtype not in valid:
raise ValueError(
f"Subtype '{subtype}' is invalid for {p_type} type."
f" Only {', '.join(sorted(valid))} are allowed."
" Use numbers for custom subtypes."
)
def get_idf_partition_csv(flash_size: str):
app_partition_size = APP_PARTITION_SIZES[flash_size]
def add_partition(name: str, p_type: str | int, subtype: str | int, size: int) -> None:
"""Register a custom partition to be appended to the partition table.
return f"""\
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
app0, app, ota_0, , 0x{app_partition_size:X},
app1, app, ota_1, , 0x{app_partition_size:X},
nvs, data, nvs, , 0x6D000,
"""
Called from component to_code() to request additional flash partitions.
Size must be 4KB aligned. Integer types/subtypes are converted to hex strings.
"""
if name in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}):
raise ValueError(f"Partition name '{name}' is already defined.")
_validate_partition(name, p_type, subtype, size)
p_type_str = f"0x{p_type:X}" if isinstance(p_type, int) else p_type
subtype_str = f"0x{subtype:X}" if isinstance(subtype, int) else subtype
custom_partitions = CORE.data[KEY_ESP32].setdefault(KEY_CUSTOM_PARTITIONS, {})
custom_partitions[name] = PartitionEntry(
name=name, type=p_type_str, subtype=subtype_str, size=size
)
def _flash_size_to_bytes(flash_size_mb: str) -> int:
"""Convert flash size string (e.g. '4MB') to bytes."""
return int(flash_size_mb.removesuffix("MB")) * 1024 * 1024
def _get_custom_partitions_total_size() -> int:
"""Total size of custom partitions including alignment padding."""
size = 0
for partition in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values():
if partition.type == "app":
size = (size + 0xFFFF) & ~0xFFFF # align to 64KB
size += partition.size
return size
def _get_app_partition_size(flash_size_mb: str) -> int:
flash_bytes = _flash_size_to_bytes(flash_size_mb)
custom_total = _get_custom_partitions_total_size()
# Align down to 64KB — app partitions require 64KB-aligned offsets,
# so the size must also be aligned to avoid unbudgeted padding.
raw_size = (flash_bytes - _get_partition_overhead() - custom_total) // 2
app_size = raw_size & ~0xFFFF
wasted = (raw_size - app_size) * 2
if wasted:
_LOGGER.info(
"Custom partitions cause %dKB of wasted flash due to 64KB app partition alignment.",
wasted // 1024,
)
if app_size <= 0x10000: # 64 KB
raise ValueError(
"Custom partitions are too large to fit in the available flash size. "
"Reduce custom partition sizes."
)
if app_size <= 0x80000: # 512 KB
_LOGGER.warning(
"App partition size is only %dKB. This may be too small for firmware with "
"many components. Consider reducing custom partition sizes or using a "
"larger flash chip.",
app_size // 1024,
)
return app_size
def get_partition_csv(flash_size_mb: str) -> str:
app_size = _get_app_partition_size(flash_size_mb)
partitions: list[PartitionEntry] = [
PartitionEntry(name="otadata", type="data", subtype="ota", size=OTADATA_SIZE),
PartitionEntry(name="phy_init", type="data", subtype="phy", size=PHY_INIT_SIZE),
PartitionEntry(name="app0", type="app", subtype="ota_0", size=app_size),
PartitionEntry(name="app1", type="app", subtype="ota_1", size=app_size),
]
if CORE.using_arduino:
partitions.append(
PartitionEntry(name="eeprom", type="data", subtype="0x99", size=EEPROM_SIZE)
)
partitions.append(
PartitionEntry(
name="spiffs", type="data", subtype="spiffs", size=SPIFFS_SIZE
)
)
partitions.append(
PartitionEntry(
name="nvs", type="data", subtype="nvs", size=ARDUINO_NVS_SIZE
)
)
else:
partitions.append(
PartitionEntry(name="nvs", type="data", subtype="nvs", size=IDF_NVS_SIZE)
)
partitions.extend(CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values())
csv = "".join(
f"{p.name}, {p.type}, {p.subtype}, , 0x{p.size:X},\n" for p in partitions
)
_LOGGER.debug("Partition table:\n%s", csv)
return csv
def _format_sdkconfig_val(value: SdkconfigValueType) -> str:
@@ -1981,16 +2241,10 @@ def copy_files():
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
if CORE.using_arduino:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(flash_size),
)
else:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_idf_partition_csv(flash_size),
)
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_partition_csv(flash_size),
)
# IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo,
# and no version.txt file exists, the CMake script fails for some setups.
+1
View File
@@ -15,6 +15,7 @@ KEY_PATH = "path"
KEY_SUBMODULES = "submodules"
KEY_EXTRA_BUILD_FILES = "extra_build_files"
KEY_FULL_CERT_BUNDLE = "full_cert_bundle"
KEY_IDF_VERSION = "idf_version"
VARIANT_ESP32 = "ESP32"
VARIANT_ESP32C2 = "ESP32C2"
-3
View File
@@ -53,9 +53,6 @@ void arch_init() {
}
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0;
-7
View File
@@ -14,18 +14,11 @@
namespace esphome {
uint32_t random_uint32() { return esp_random(); }
bool random_bytes(uint8_t *data, size_t len) {
esp_fill_random(data, len);
return true;
}
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
Mutex::~Mutex() {}
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
void Mutex::unlock() { xSemaphoreGive(this->handle_); }
// only affects the executing core
// so should not be used as a mutex lock, only to get accurate timing
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
@@ -0,0 +1,27 @@
#pragma once
#ifdef USE_ESP32
#include <cstddef>
#include <cstdint>
namespace esphome::esp32 {
class ESP32PreferenceBackend final {
public:
bool save(const uint8_t *data, size_t len);
bool load(uint8_t *data, size_t len);
uint32_t key;
uint32_t nvs_handle;
};
class ESP32Preferences;
ESP32Preferences *get_preferences();
} // namespace esphome::esp32
namespace esphome {
using PreferenceBackend = esp32::ESP32PreferenceBackend;
} // namespace esphome
#endif // USE_ESP32
+148 -160
View File
@@ -1,18 +1,16 @@
#ifdef USE_ESP32
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#include <nvs_flash.h>
#include <cinttypes>
#include <cstring>
#include <memory>
#include <vector>
namespace esphome {
namespace esp32 {
namespace esphome::esp32 {
static const char *const TAG = "esp32.preferences";
static const char *const TAG = "preferences";
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
static constexpr size_t KEY_BUFFER_SIZE = 12;
@@ -24,185 +22,175 @@ struct NVSData {
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
class ESP32PreferenceBackend : public ESPPreferenceBackend {
public:
uint32_t key;
uint32_t nvs_handle;
bool save(const uint8_t *data, size_t len) override {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
return true;
}
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
return true;
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
bool load(uint8_t *data, size_t len) override {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
return true;
}
}
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
size_t actual_len;
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
return false;
}
if (actual_len != len) {
ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
return false;
}
err = nvs_get_blob(this->nvs_handle, key_str, data, &len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return false;
} else {
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len);
}
return true;
}
void ESP32Preferences::open() {
nvs_flash_init();
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
if (err == 0)
return;
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
nvs_flash_deinit();
nvs_flash_erase();
nvs_flash_init();
err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
if (err != 0) {
this->nvs_handle = 0;
}
}
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = this->nvs_handle;
pref->key = type;
return ESPPreferenceObject(pref);
}
bool ESP32Preferences::sync() {
if (s_pending_save.empty())
return true;
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
int cached = 0, written = 0, failed = 0;
esp_err_t last_err = ESP_OK;
uint32_t last_key = 0;
for (const auto &save : s_pending_save) {
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
size_t actual_len;
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
return false;
}
if (actual_len != len) {
ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
return false;
}
err = nvs_get_blob(this->nvs_handle, key_str, data, &len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return false;
} else {
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len);
}
return true;
}
};
class ESP32Preferences : public ESPPreferences {
public:
uint32_t nvs_handle;
void open() {
nvs_flash_init();
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &nvs_handle);
if (err == 0)
return;
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
nvs_flash_deinit();
nvs_flash_erase();
nvs_flash_init();
err = nvs_open("esphome", NVS_READWRITE, &nvs_handle);
if (err != 0) {
nvs_handle = 0;
}
}
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
return this->make_preference(length, type);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = this->nvs_handle;
pref->key = type;
return ESPPreferenceObject(pref);
}
bool sync() override {
if (s_pending_save.empty())
return true;
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
int cached = 0, written = 0, failed = 0;
esp_err_t last_err = ESP_OK;
uint32_t last_key = 0;
for (const auto &save : s_pending_save) {
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
failed++;
last_err = err;
last_key = save.key;
continue;
}
written++;
} else {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
cached++;
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
failed++;
last_err = err;
last_key = save.key;
continue;
}
written++;
} else {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
cached++;
}
s_pending_save.clear();
}
s_pending_save.clear();
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed);
if (failed > 0) {
ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err),
last_key);
}
// note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
esp_err_t err = nvs_commit(this->nvs_handle);
if (err != 0) {
ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err));
return false;
}
return failed == 0;
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed);
if (failed > 0) {
ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err),
last_key);
}
protected:
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
size_t actual_len;
esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
return true;
}
// Check size first before allocating memory
if (actual_len != to_save.data.size()) {
return true;
}
// Most preferences are small, use stack buffer with heap fallback for large ones
SmallBufferWithHeapFallback<256> stored_data(actual_len);
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return true;
}
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
// note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
esp_err_t err = nvs_commit(this->nvs_handle);
if (err != 0) {
ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err));
return false;
}
bool reset() override {
ESP_LOGD(TAG, "Erasing storage");
s_pending_save.clear();
return failed == 0;
}
nvs_flash_deinit();
nvs_flash_erase();
// Make the handle invalid to prevent any saves until restart
nvs_handle = 0;
bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
size_t actual_len;
esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
return true;
}
};
// Check size first before allocating memory
if (actual_len != to_save.data.size()) {
return true;
}
// Most preferences are small, use stack buffer with heap fallback for large ones
SmallBufferWithHeapFallback<256> stored_data(actual_len);
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return true;
}
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
}
bool ESP32Preferences::reset() {
ESP_LOGD(TAG, "Erasing storage");
s_pending_save.clear();
nvs_flash_deinit();
nvs_flash_erase();
// Make the handle invalid to prevent any saves until restart
this->nvs_handle = 0;
return true;
}
static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
ESP32Preferences *get_preferences() { return &s_preferences; }
void setup_preferences() {
s_preferences.open();
global_preferences = &s_preferences;
}
} // namespace esp32
} // namespace esphome::esp32
namespace esphome {
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif // USE_ESP32
+25 -4
View File
@@ -1,12 +1,33 @@
#pragma once
#ifdef USE_ESP32
namespace esphome {
namespace esp32 {
#include "esphome/core/preference_backend.h"
namespace esphome::esp32 {
struct NVSData;
class ESP32Preferences final : public PreferencesMixin<ESP32Preferences> {
public:
using PreferencesMixin<ESP32Preferences>::make_preference;
void open();
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) {
return this->make_preference(length, type);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type);
bool sync();
bool reset();
uint32_t nvs_handle;
protected:
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str);
};
void setup_preferences();
} // namespace esp32
} // namespace esphome
} // namespace esphome::esp32
DECLARE_PREFERENCE_ALIASES(esphome::esp32::ESP32Preferences)
#endif // USE_ESP32
-2
View File
@@ -81,8 +81,6 @@ void ESP32BLE::disable() {
this->state_ = BLE_COMPONENT_STATE_DISABLE;
}
bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
#ifdef USE_ESP32_BLE_ADVERTISING
void ESP32BLE::advertising_start() {
this->advertising_init_();
+1 -1
View File
@@ -135,7 +135,7 @@ class ESP32BLE : public Component {
void enable();
void disable();
bool is_active();
ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
void setup() override;
void loop() override;
void dump_config() override;
@@ -7,7 +7,6 @@
#ifdef USE_ESP32
#include <algorithm>
#include <nvs_flash.h>
#include <freertos/FreeRTOSConfig.h>
#include <esp_bt_main.h>
@@ -39,16 +38,17 @@ void BLEServer::loop() {
case RUNNING: {
// Start all services that are pending to start
if (!this->services_to_start_.empty()) {
for (auto &service : this->services_to_start_) {
size_t write_idx = 0;
for (auto *service : this->services_to_start_) {
if (service->is_created()) {
service->start(); // Needs to be called once per characteristic in the service
}
// Remove services that have started or are starting
if (!service->is_starting() && !service->is_running()) {
this->services_to_start_[write_idx++] = service;
}
}
// Remove services that have been started
this->services_to_start_.erase(
std::remove_if(this->services_to_start_.begin(), this->services_to_start_.end(),
[](BLEService *service) { return service->is_starting() || service->is_running(); }),
this->services_to_start_.end());
this->services_to_start_.erase(this->services_to_start_.begin() + write_idx, this->services_to_start_.end());
}
break;
}
@@ -91,8 +91,6 @@ void BLEServer::loop() {
}
}
bool BLEServer::is_running() { return this->parent_->is_active() && this->state_ == RUNNING; }
bool BLEServer::can_proceed() { return this->is_running() || !this->parent_->is_active(); }
void BLEServer::restart_advertising_() {
@@ -32,7 +32,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
float get_setup_priority() const override;
bool can_proceed() override;
bool is_running();
ESPHOME_ALWAYS_INLINE bool is_running() { return this->parent_->is_active() && this->state_ == RUNNING; }
void set_manufacturer_data(const std::vector<uint8_t> &data) {
this->manufacturer_data_ = data;
@@ -27,8 +27,14 @@
#include <esp_coexist.h>
#endif
#ifdef USE_ESP32_BLE_DEVICE
#ifdef USE_BLE_TRACKER_PSA_AES
#include <psa/crypto.h>
#else
#define MBEDTLS_AES_ALT
#include <aes_alt.h>
#endif
#endif // USE_ESP32_BLE_DEVICE
// bt_trace.h
#undef TAG
@@ -738,23 +744,48 @@ void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) {
}
bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
uint8_t ecb_key[16];
uint8_t ecb_plaintext[16];
uint8_t ecb_ciphertext[16];
static constexpr size_t AES_BLOCK_SIZE = 16;
static constexpr size_t AES_KEY_BITS = 128;
uint8_t ecb_key[AES_BLOCK_SIZE];
uint8_t ecb_plaintext[AES_BLOCK_SIZE];
uint8_t ecb_ciphertext[AES_BLOCK_SIZE];
uint64_t addr64 = esp32_ble::ble_addr_to_uint64(this->address_);
memcpy(&ecb_key, irk, 16);
memset(&ecb_plaintext, 0, 16);
memcpy(&ecb_key, irk, AES_BLOCK_SIZE);
memset(&ecb_plaintext, 0, AES_BLOCK_SIZE);
ecb_plaintext[13] = (addr64 >> 40) & 0xff;
ecb_plaintext[14] = (addr64 >> 32) & 0xff;
ecb_plaintext[15] = (addr64 >> 24) & 0xff;
#ifdef USE_BLE_TRACKER_PSA_AES
// Use PSA Crypto API (mbedtls 4.0 / IDF 6.0+)
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attributes, AES_KEY_BITS);
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT);
psa_set_key_algorithm(&attributes, PSA_ALG_ECB_NO_PADDING);
mbedtls_svc_key_id_t key_id;
if (psa_import_key(&attributes, ecb_key, AES_BLOCK_SIZE, &key_id) != PSA_SUCCESS) {
return false;
}
size_t output_length;
psa_status_t status = psa_cipher_encrypt(key_id, PSA_ALG_ECB_NO_PADDING, ecb_plaintext, AES_BLOCK_SIZE,
ecb_ciphertext, AES_BLOCK_SIZE, &output_length);
psa_destroy_key(key_id);
if (status != PSA_SUCCESS || output_length != AES_BLOCK_SIZE) {
return false;
}
#else
// Use legacy mbedtls AES API (IDF < 6.0)
mbedtls_aes_context ctx = {0, 0, {0}};
mbedtls_aes_init(&ctx);
if (mbedtls_aes_setkey_enc(&ctx, ecb_key, 128) != 0) {
if (mbedtls_aes_setkey_enc(&ctx, ecb_key, AES_KEY_BITS) != 0) {
mbedtls_aes_free(&ctx);
return false;
}
@@ -765,6 +796,7 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
}
mbedtls_aes_free(&ctx);
#endif
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
@@ -12,6 +12,13 @@
#ifdef USE_ESP32
#include <esp_idf_version.h>
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
// mbedtls 4.0 (IDF 6.0) removed the legacy mbedtls AES API.
// Use the PSA Crypto API instead.
#define USE_BLE_TRACKER_PSA_AES
#endif
#include <esp_bt_defs.h>
#include <esp_gap_ble_api.h>
#include <esp_gattc_api.h>
+1 -1
View File
@@ -400,7 +400,7 @@ async def to_code(config):
if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG":
cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION")
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
add_idf_component(name="espressif/esp32-camera", ref="2.1.5")
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True)
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False)
+4 -11
View File
@@ -4,14 +4,7 @@ from pathlib import Path
from esphome import pins
from esphome.components import esp32
import esphome.config_validation as cv
from esphome.const import (
CONF_CLK_PIN,
CONF_RESET_PIN,
CONF_VARIANT,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE
from esphome.const import CONF_CLK_PIN, CONF_RESET_PIN, CONF_VARIANT
from esphome.cpp_generator import add_define
CODEOWNERS = ["@swoboda1337"]
@@ -100,9 +93,9 @@ async def to_code(config):
int(config[CONF_SDIO_FREQUENCY] // 1000),
)
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
if framework_ver >= cv.Version(5, 5, 0):
idf_ver = esp32.idf_version()
os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}"
if idf_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.1")
+35 -20
View File
@@ -12,58 +12,73 @@ namespace esp32_improv {
class ESP32ImprovProvisionedTrigger : public Trigger<> {
public:
explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if (state == improv::STATE_PROVISIONED && !parent->is_failed()) {
trigger();
explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) {
this->trigger();
}
});
}
protected:
ESP32ImprovComponent *parent_;
};
class ESP32ImprovProvisioningTrigger : public Trigger<> {
public:
explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if (state == improv::STATE_PROVISIONING && !parent->is_failed()) {
trigger();
explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) {
this->trigger();
}
});
}
protected:
ESP32ImprovComponent *parent_;
};
class ESP32ImprovStartTrigger : public Trigger<> {
public:
explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) &&
!parent->is_failed()) {
trigger();
!this->parent_->is_failed()) {
this->trigger();
}
});
}
protected:
ESP32ImprovComponent *parent_;
};
class ESP32ImprovStateTrigger : public Trigger<improv::State, improv::Error> {
public:
explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if (!parent->is_failed()) {
trigger(state, error);
explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if (!this->parent_->is_failed()) {
this->trigger(state, error);
}
});
}
protected:
ESP32ImprovComponent *parent_;
};
class ESP32ImprovStoppedTrigger : public Trigger<> {
public:
explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) {
parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) {
if (state == improv::STATE_STOPPED && !parent->is_failed()) {
trigger();
explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) {
this->trigger();
}
});
}
protected:
ESP32ImprovComponent *parent_;
};
} // namespace esp32_improv
@@ -350,8 +350,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());
auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this);
this->set_timeout("wifi-connect-timeout", 30000, f);
this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); });
this->incoming_data_.clear();
break;
}
@@ -48,8 +48,8 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
bool should_start() const { return this->should_start_; }
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
void add_on_state_callback(std::function<void(improv::State, improv::Error)> &&callback) {
this->state_callback_.add(std::move(callback));
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
#endif
#ifdef USE_BINARY_SENSOR
@@ -99,8 +99,6 @@ void ESP32RMTLEDStripLightOutput::setup() {
channel.gpio_num = gpio_num_t(this->pin_);
channel.mem_block_symbols = this->rmt_symbols_;
channel.trans_queue_depth = 1;
channel.flags.io_loop_back = 0;
channel.flags.io_od_mode = 0;
channel.flags.invert_out = this->invert_out_;
channel.flags.with_dma = this->use_dma_;
channel.intr_priority = 0;
@@ -360,11 +360,16 @@ void ESP32TouchComponent::loop() {
}
// Publish initial OFF state for sensors that haven't received events yet
bool all_initial_published = true;
for (auto *child : this->children_) {
this->publish_initial_state_if_needed_(child, now);
if (!child->initial_state_published_) {
all_initial_published = false;
}
}
if (!this->setup_mode_) {
// Only disable loop once all initial states are published
if (!this->setup_mode_ && all_initial_published) {
this->disable_loop();
}
}
+2 -6
View File
@@ -12,12 +12,8 @@ namespace esphome {
uint32_t random_uint32() { return os_random(); }
bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; }
// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
Mutex::Mutex() {}
Mutex::~Mutex() {}
void Mutex::lock() {}
bool Mutex::try_lock() { return true; }
void Mutex::unlock() {}
// ESP8266 Mutex is defined inline as a no-op in helpers.h when USE_ESP8266 (or USE_RP2040) is set,
// independent of the ESPHOME_THREAD_SINGLE thread model define.
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
@@ -0,0 +1,29 @@
#pragma once
#ifdef USE_ESP8266
#include <cstddef>
#include <cstdint>
namespace esphome::esp8266 {
class ESP8266PreferenceBackend final {
public:
bool save(const uint8_t *data, size_t len);
bool load(uint8_t *data, size_t len);
uint32_t type = 0;
uint16_t offset = 0;
uint8_t length_words = 0; // Max 255 words (1020 bytes of data)
bool in_flash = false;
};
class ESP8266Preferences;
ESP8266Preferences *get_preferences();
} // namespace esphome::esp8266
namespace esphome {
using PreferenceBackend = esp8266::ESP8266PreferenceBackend;
} // namespace esphome
#endif // USE_ESP8266
+114 -136
View File
@@ -5,17 +5,15 @@ extern "C" {
#include "spi_flash.h"
}
#include "esphome/core/defines.h"
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#include "preferences.h"
#include <cstring>
namespace esphome::esp8266 {
static const char *const TAG = "esp8266.preferences";
static const char *const TAG = "preferences";
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
@@ -137,155 +135,135 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
static constexpr size_t PREF_MAX_BUFFER_WORDS =
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
class ESP8266PreferenceBackend : public ESPPreferenceBackend {
public:
uint32_t type = 0;
uint16_t offset = 0;
uint8_t length_words = 0; // Max 255 words (1020 bytes of data)
bool in_flash = false;
bool ESP8266PreferenceBackend::save(const uint8_t *data, size_t len) {
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len);
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
: save_to_rtc(this->offset, buffer, buffer_size);
}
bool save(const uint8_t *data, size_t len) override {
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len);
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
: save_to_rtc(this->offset, buffer, buffer_size);
bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) {
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
: load_from_rtc(this->offset, buffer, buffer_size);
if (!ret)
return false;
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
return false;
memcpy(data, buffer, len);
return true;
}
void ESP8266Preferences::setup() {
ESP_LOGVV(TAG, "Loading preferences from flash");
{
InterruptLock lock;
spi_flash_read(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4);
}
}
ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t type, bool in_flash) {
const uint32_t length_words = bytes_to_words(length);
if (length_words > MAX_PREFERENCE_WORDS) {
ESP_LOGE(TAG, "Preference too large: %u words", static_cast<unsigned int>(length_words));
return {};
}
bool load(uint8_t *data, size_t len) override {
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
: load_from_rtc(this->offset, buffer, buffer_size);
if (!ret)
return false;
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
return false;
memcpy(data, buffer, len);
return true;
}
};
const uint32_t total_words = length_words + 1; // +1 for CRC
uint16_t offset;
class ESP8266Preferences : public ESPPreferences {
public:
uint32_t current_offset = 0;
uint32_t current_flash_offset = 0; // in words
void setup() {
ESP_LOGVV(TAG, "Loading preferences from flash");
{
InterruptLock lock;
spi_flash_read(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4);
}
}
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
const uint32_t length_words = bytes_to_words(length);
if (length_words > MAX_PREFERENCE_WORDS) {
ESP_LOGE(TAG, "Preference too large: %u words", static_cast<unsigned int>(length_words));
if (in_flash) {
if (this->current_flash_offset + total_words > ESP8266_FLASH_STORAGE_SIZE)
return {};
offset = static_cast<uint16_t>(this->current_flash_offset);
this->current_flash_offset += total_words;
} else {
uint32_t start = this->current_offset;
bool in_normal = start < RTC_NORMAL_REGION_WORDS;
// Normal: offset 0-95 maps to RTC offset 32-127
// Eboot: offset 96-127 maps to RTC offset 0-31
if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) {
// start is in normal but end is not -> switch to Eboot
this->current_offset = start = RTC_NORMAL_REGION_WORDS;
in_normal = false;
}
const uint32_t total_words = length_words + 1; // +1 for CRC
uint16_t offset;
if (in_flash) {
if (this->current_flash_offset + total_words > ESP8266_FLASH_STORAGE_SIZE)
return {};
offset = static_cast<uint16_t>(this->current_flash_offset);
this->current_flash_offset += total_words;
} else {
uint32_t start = this->current_offset;
bool in_normal = start < RTC_NORMAL_REGION_WORDS;
// Normal: offset 0-95 maps to RTC offset 32-127
// Eboot: offset 96-127 maps to RTC offset 0-31
if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) {
// start is in normal but end is not -> switch to Eboot
this->current_offset = start = RTC_NORMAL_REGION_WORDS;
in_normal = false;
}
if (start + total_words > PREF_TOTAL_WORDS)
return {}; // Doesn't fit in RTC memory
// Convert preference offset to RTC memory offset
offset = static_cast<uint16_t>(in_normal ? start + RTC_EBOOT_REGION_WORDS : start - RTC_NORMAL_REGION_WORDS);
this->current_offset = start + total_words;
}
auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->offset = offset;
pref->type = type;
pref->length_words = static_cast<uint8_t>(length_words);
pref->in_flash = in_flash;
return pref;
if (start + total_words > PREF_TOTAL_WORDS)
return {}; // Doesn't fit in RTC memory
// Convert preference offset to RTC memory offset
offset = static_cast<uint16_t>(in_normal ? start + RTC_EBOOT_REGION_WORDS : start - RTC_NORMAL_REGION_WORDS);
this->current_offset = start + total_words;
}
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
#ifdef USE_ESP8266_PREFERENCES_FLASH
return make_preference(length, type, true);
#else
return make_preference(length, type, false);
#endif
}
auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->offset = offset;
pref->type = type;
pref->length_words = static_cast<uint8_t>(length_words);
pref->in_flash = in_flash;
return ESPPreferenceObject(pref);
}
bool sync() override {
if (!s_flash_dirty)
return true;
if (s_prevent_write)
return false;
ESP_LOGD(TAG, "Saving");
SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK;
{
InterruptLock lock;
erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
if (erase_res == SPI_FLASH_RESULT_OK) {
write_res = spi_flash_write(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4);
}
}
if (erase_res != SPI_FLASH_RESULT_OK) {
ESP_LOGE(TAG, "Erasing failed");
return false;
}
if (write_res != SPI_FLASH_RESULT_OK) {
ESP_LOGE(TAG, "Writing failed");
return false;
}
s_flash_dirty = false;
bool ESP8266Preferences::sync() {
if (!s_flash_dirty)
return true;
if (s_prevent_write)
return false;
ESP_LOGD(TAG, "Saving");
SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK;
{
InterruptLock lock;
erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
if (erase_res == SPI_FLASH_RESULT_OK) {
write_res = spi_flash_write(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4);
}
}
if (erase_res != SPI_FLASH_RESULT_OK) {
ESP_LOGE(TAG, "Erasing failed");
return false;
}
if (write_res != SPI_FLASH_RESULT_OK) {
ESP_LOGE(TAG, "Writing failed");
return false;
}
bool reset() override {
ESP_LOGD(TAG, "Erasing storage");
SpiFlashOpResult erase_res;
{
InterruptLock lock;
erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
}
if (erase_res != SPI_FLASH_RESULT_OK) {
ESP_LOGE(TAG, "Erasing failed");
return false;
}
s_flash_dirty = false;
return true;
}
// Protect flash from writing till restart
s_prevent_write = true;
return true;
bool ESP8266Preferences::reset() {
ESP_LOGD(TAG, "Erasing storage");
SpiFlashOpResult erase_res;
{
InterruptLock lock;
erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
}
};
if (erase_res != SPI_FLASH_RESULT_OK) {
ESP_LOGE(TAG, "Erasing failed");
return false;
}
// Protect flash from writing till restart
s_prevent_write = true;
return true;
}
static ESP8266Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
ESP8266Preferences *get_preferences() { return &s_preferences; }
void setup_preferences() {
s_preferences.setup();
global_preferences = &s_preferences;
+23 -1
View File
@@ -1,12 +1,34 @@
#pragma once
#ifdef USE_ESP8266
#include "esphome/core/preference_backend.h"
namespace esphome::esp8266 {
class ESP8266Preferences final : public PreferencesMixin<ESP8266Preferences> {
public:
using PreferencesMixin<ESP8266Preferences>::make_preference;
void setup();
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash);
ESPPreferenceObject make_preference(size_t length, uint32_t type) {
#ifdef USE_ESP8266_PREFERENCES_FLASH
return this->make_preference(length, type, true);
#else
return this->make_preference(length, type, false);
#endif
}
bool sync();
bool reset();
uint32_t current_offset = 0;
uint32_t current_flash_offset = 0; // in words
};
void setup_preferences();
void preferences_prevent_write(bool prevent);
} // namespace esphome::esp8266
DECLARE_PREFERENCE_ALIASES(esphome::esp8266::ESP8266Preferences)
#endif // USE_ESP8266
@@ -332,12 +332,13 @@ void ESPHomeOTAComponent::handle_data_() {
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
ssize_t read = this->client_->read(buf, requested);
if (read == -1) {
if (this->would_block_(errno)) {
const int err = errno;
if (this->would_block_(err)) {
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
App.feed_wdt();
continue;
}
ESP_LOGW(TAG, "Read err %d", errno);
ESP_LOGW(TAG, "Read err %d", err);
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} else if (read == 0) {
ESP_LOGW(TAG, "Remote closed");
@@ -426,8 +427,9 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
ssize_t read = this->client_->read(buf + at, len - at);
if (read == -1) {
if (!this->would_block_(errno)) {
ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, errno);
const int err = errno;
if (!this->would_block_(err)) {
ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, err);
return false;
}
} else if (read == 0) {
@@ -455,8 +457,9 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
ssize_t written = this->client_->write(buf + at, len - at);
if (written == -1) {
if (!this->would_block_(errno)) {
ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, errno);
const int err = errno;
if (!this->would_block_(err)) {
ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, err);
return false;
}
// EWOULDBLOCK: on raw TCP writes never block, delay(1) prevents spinning
+273 -129
View File
@@ -1,23 +1,10 @@
from dataclasses import dataclass
import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
add_idf_component,
add_idf_sdkconfig_option,
get_esp32_variant,
include_builtin_idf_component,
)
from esphome.components.network import ip_address_literal
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
@@ -49,6 +36,9 @@ from esphome.const import (
CONF_VALUE,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_NATIVE_IDF,
Platform,
PlatformFramework,
)
from esphome.core import (
CORE,
@@ -60,12 +50,14 @@ import esphome.final_validate as fv
from esphome.types import ConfigType
CONFLICTS_WITH = ["wifi"]
DEPENDENCIES = ["esp32"]
AUTO_LOAD = ["network"]
LOGGER = logging.getLogger(__name__)
# Key for tracking IP state listener count in CORE.data
ETHERNET_IP_STATE_LISTENERS_KEY = "ethernet_ip_state_listeners"
# Key for tracking configured ethernet type
ETHERNET_TYPE_KEY = "ethernet_type"
KEY_ETHERNET = "ethernet"
def request_ethernet_ip_state_listener() -> None:
@@ -139,10 +131,35 @@ _PHY_TYPE_TO_DEFINE = {
"JL1101": "USE_ETHERNET_JL1101",
"KSZ8081": "USE_ETHERNET_KSZ8081",
"KSZ8081RNA": "USE_ETHERNET_KSZ8081",
"W5500": "USE_ETHERNET_W5500",
"DM9051": "USE_ETHERNET_DM9051",
"LAN8670": "USE_ETHERNET_LAN8670",
}
@dataclass(frozen=True)
class IDFRegistryComponent:
"""An ESP-IDF component from the Espressif Component Registry."""
name: str
version: str
# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry.
_IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = {
"LAN8720": IDFRegistryComponent("espressif/lan87xx", "1.0.0"),
"RTL8201": IDFRegistryComponent("espressif/rtl8201", "1.0.1"),
"DP83848": IDFRegistryComponent("espressif/dp83848", "1.0.0"),
"IP101": IDFRegistryComponent("espressif/ip101", "1.0.0"),
"KSZ8081": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"),
"KSZ8081RNA": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"),
"W5500": IDFRegistryComponent("espressif/w5500", "1.0.1"),
"DM9051": IDFRegistryComponent("espressif/dm9051", "1.0.0"),
}
SPI_ETHERNET_TYPES = ["W5500", "DM9051"]
# RP2040-supported SPI ethernet types
RP2040_SPI_ETHERNET_TYPES = ["W5500"]
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10)
emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
@@ -173,16 +190,22 @@ EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component)
ManualIP = ethernet_ns.struct("ManualIP")
def _is_framework_spi_polling_mode_supported():
# SPI Ethernet without IRQ feature is added in
# esp-idf >= (5.3+ ,5.2.1+, 5.1.4)
# Note: Arduino now uses ESP-IDF as a component, so we only check IDF version
framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
if framework_version >= cv.Version(5, 3, 0):
def _is_framework_spi_polling_mode_supported() -> bool:
"""Check if ESP-IDF framework supports SPI polling mode (ESP32 only).
SPI Ethernet without IRQ feature is added in
esp-idf >= (5.3+, 5.2.1+, 5.1.4)
"""
if not CORE.is_esp32:
return False
from esphome.components.esp32 import idf_version
ver = idf_version()
if ver >= cv.Version(5, 3, 0):
return True
if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1):
if cv.Version(5, 3, 0) > ver >= cv.Version(5, 2, 1):
return True
if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): # noqa: SIM103
if cv.Version(5, 2, 0) > ver >= cv.Version(5, 1, 4): # noqa: SIM103
return True
return False
@@ -195,52 +218,68 @@ def _validate(config):
use_address = CORE.name + config[CONF_DOMAIN]
config[CONF_USE_ADDRESS] = use_address
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
if _is_framework_spi_polling_mode_supported():
if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config:
raise cv.Invalid(
f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}"
)
if CONF_POLLING_INTERVAL not in config and CONF_INTERRUPT_PIN not in config:
config[CONF_POLLING_INTERVAL] = SPI_ETHERNET_DEFAULT_POLLING_INTERVAL
else:
if CONF_POLLING_INTERVAL in config:
raise cv.Invalid(
"In this version of the framework "
f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
f"'{CONF_POLLING_INTERVAL}' is not supported."
)
if CONF_INTERRUPT_PIN not in config:
raise cv.Invalid(
"In this version of the framework "
f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]."
)
elif config[CONF_TYPE] != "OPENETH":
if CONF_CLK_MODE in config:
mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]]
LOGGER.warning(
"[ethernet] The 'clk_mode' option is deprecated. "
"Please replace 'clk_mode: %s' with:\n"
" clk:\n"
" mode: %s\n"
" pin: %s\n"
"Removal scheduled for 2026.7.0.",
config[CONF_CLK_MODE],
mode,
pin,
)
config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin})
del config[CONF_CLK_MODE]
elif CONF_CLK not in config:
raise cv.Invalid("'clk' is a required option for [ethernet].")
variant = get_esp32_variant()
if variant not in (VARIANT_ESP32, VARIANT_ESP32P4):
raise cv.Invalid(
f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported "
f"on ESP32 classic and ESP32-P4, not {variant}"
if CORE.is_esp32:
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
if _is_framework_spi_polling_mode_supported():
if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config:
raise cv.Invalid(
f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}"
)
if (
CONF_POLLING_INTERVAL not in config
and CONF_INTERRUPT_PIN not in config
):
config[CONF_POLLING_INTERVAL] = (
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL
)
else:
if CONF_POLLING_INTERVAL in config:
raise cv.Invalid(
"In this version of the framework "
f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
f"'{CONF_POLLING_INTERVAL}' is not supported."
)
if CONF_INTERRUPT_PIN not in config:
raise cv.Invalid(
"In this version of the framework "
f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]."
)
elif config[CONF_TYPE] != "OPENETH":
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32P4,
get_esp32_variant,
)
if CONF_CLK_MODE in config:
mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]]
LOGGER.warning(
"[ethernet] The 'clk_mode' option is deprecated. "
"Please replace 'clk_mode: %s' with:\n"
" clk:\n"
" mode: %s\n"
" pin: %s\n"
"Removal scheduled for 2026.7.0.",
config[CONF_CLK_MODE],
mode,
pin,
)
config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin})
del config[CONF_CLK_MODE]
elif CONF_CLK not in config:
raise cv.Invalid("'clk' is a required option for [ethernet].")
variant = get_esp32_variant()
if variant not in (VARIANT_ESP32, VARIANT_ESP32P4):
raise cv.Invalid(
f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported "
f"on ESP32 classic and ESP32-P4, not {variant}"
)
elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_SPI_ETHERNET_TYPES:
raise cv.Invalid(
f"Only {', '.join(RP2040_SPI_ETHERNET_TYPES)} are supported on RP2040, "
f"not {config[CONF_TYPE]}"
)
return config
@@ -269,41 +308,50 @@ CLK_SCHEMA = cv.Schema(
cv.Required(CONF_PIN): pins.internal_gpio_pin_number,
}
)
RMII_SCHEMA = BASE_SCHEMA.extend(
cv.Schema(
{
cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_CLK_MODE): cv.enum(
CLK_MODES_DEPRECATED, upper=True, space="_"
),
cv.Optional(CONF_CLK): CLK_SCHEMA,
cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31),
cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA),
}
)
RMII_SCHEMA = cv.All(
BASE_SCHEMA.extend(
cv.Schema(
{
cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_CLK_MODE): cv.enum(
CLK_MODES_DEPRECATED, upper=True, space="_"
),
cv.Optional(CONF_CLK): CLK_SCHEMA,
cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31),
cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA),
}
)
),
cv.only_on([Platform.ESP32]),
)
SPI_SCHEMA = BASE_SCHEMA.extend(
cv.Schema(
{
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number,
cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All(
cv.frequency, cv.int_range(int(8e6), int(80e6))
),
# Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate()
cv.Optional(CONF_POLLING_INTERVAL): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(min=TimePeriodMilliseconds(milliseconds=1)),
),
}
SPI_SCHEMA = cv.All(
BASE_SCHEMA.extend(
cv.Schema(
{
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number,
cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
cv.SplitDefault(CONF_CLOCK_SPEED, esp32="26.67MHz"): cv.All(
cv.only_on_esp32,
cv.frequency,
cv.int_range(int(8e6), int(80e6)),
),
# Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate()
cv.Optional(CONF_POLLING_INTERVAL): cv.All(
cv.only_on_esp32,
cv.positive_time_period_milliseconds,
cv.Range(min=TimePeriodMilliseconds(milliseconds=1)),
),
}
),
),
cv.only_on([Platform.ESP32, Platform.RP2040]),
)
CONFIG_SCHEMA = cv.All(
@@ -317,7 +365,7 @@ CONFIG_SCHEMA = cv.All(
"KSZ8081": RMII_SCHEMA,
"KSZ8081RNA": RMII_SCHEMA,
"W5500": SPI_SCHEMA,
"OPENETH": BASE_SCHEMA,
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
"DM9051": SPI_SCHEMA,
"LAN8670": RMII_SCHEMA,
},
@@ -328,8 +376,21 @@ CONFIG_SCHEMA = cv.All(
def _final_validate_spi(config):
if not CORE.is_esp32:
return # SPI interface validation is ESP32-only
if config[CONF_TYPE] not in SPI_ETHERNET_TYPES:
return
from esphome.components.esp32 import (
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
get_esp32_variant,
)
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
if spi_configs := fv.full_config.get().get(CONF_SPI):
variant = get_esp32_variant()
if variant in (
@@ -378,6 +439,51 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if CORE.is_esp32:
await _to_code_esp32(var, config)
elif CORE.is_rp2040:
await _to_code_rp2040(var, config)
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
CORE.data.setdefault(KEY_ETHERNET, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE]
if CONF_MANUAL_IP in config:
cg.add_define("USE_ETHERNET_MANUAL_IP")
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
# Add compile-time define for PHY types with specific code
if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]):
cg.add_define(phy_define)
if mac_address := config.get(CONF_MAC_ADDRESS):
cg.add(var.set_fixed_mac(mac_address.parts))
cg.add_define("USE_ETHERNET")
if on_connect_config := config.get(CONF_ON_CONNECT):
cg.add_define("USE_ETHERNET_CONNECT_TRIGGER")
await automation.build_automation(
var.get_connect_trigger(), [], on_connect_config
)
if on_disconnect_config := config.get(CONF_ON_DISCONNECT):
cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER")
await automation.build_automation(
var.get_disconnect_trigger(), [], on_disconnect_config
)
CORE.add_job(final_step)
async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
from esphome.components.esp32 import (
add_idf_component,
add_idf_sdkconfig_option,
idf_version,
include_builtin_idf_component,
)
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
cg.add(var.set_clk_pin(config[CONF_CLK_PIN]))
cg.add(var.set_miso_pin(config[CONF_MISO_PIN]))
@@ -395,7 +501,11 @@ async def to_code(config):
cg.add_define("USE_ETHERNET_SPI")
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
add_idf_sdkconfig_option(f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True)
# CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0
if idf_version() < cv.Version(6, 0, 0):
add_idf_sdkconfig_option(
f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True
)
elif config[CONF_TYPE] == "OPENETH":
cg.add_define("USE_ETHERNET_OPENETH")
add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True)
@@ -415,22 +525,6 @@ async def to_code(config):
)
cg.add(var.add_phy_register(reg))
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
if CONF_MANUAL_IP in config:
cg.add_define("USE_ETHERNET_MANUAL_IP")
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
# Add compile-time define for PHY types with specific code
if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]):
cg.add_define(phy_define)
if mac_address := config.get(CONF_MAC_ADDRESS):
cg.add(var.set_fixed_mac(mac_address.parts))
cg.add_define("USE_ETHERNET")
# Disable WiFi when using Ethernet to save memory
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False)
# Also disable WiFi/BT coexistence since WiFi is disabled
@@ -443,27 +537,41 @@ async def to_code(config):
# Add LAN867x 10BASE-T1S PHY support component
add_idf_component(name="espressif/lan867x", ref="2.0.0")
if on_connect_config := config.get(CONF_ON_CONNECT):
cg.add_define("USE_ETHERNET_CONNECT_TRIGGER")
await automation.build_automation(
var.get_connect_trigger(), [], on_connect_config
)
# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry
if idf_version() >= cv.Version(6, 0, 0) and (
component := _IDF6_ETHERNET_COMPONENTS.get(config[CONF_TYPE])
):
add_idf_component(name=component.name, ref=component.version)
if on_disconnect_config := config.get(CONF_ON_DISCONNECT):
cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER")
await automation.build_automation(
var.get_disconnect_trigger(), [], on_disconnect_config
)
CORE.add_job(final_step)
async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None:
cg.add(var.set_clk_pin(config[CONF_CLK_PIN]))
cg.add(var.set_miso_pin(config[CONF_MISO_PIN]))
cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN]))
cg.add(var.set_cs_pin(config[CONF_CS_PIN]))
if CONF_INTERRUPT_PIN in config:
cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN]))
if CONF_RESET_PIN in config:
cg.add(var.set_reset_pin(config[CONF_RESET_PIN]))
cg.add_define("USE_ETHERNET_SPI")
cg.add_library("lwIP_w5500", None)
def _final_validate_rmii_pins(config: ConfigType) -> None:
"""Validate that RMII pins are not used by other components."""
if not CORE.is_esp32:
return # RMII validation is ESP32-only
# Only validate for RMII-based PHYs on ESP32/ESP32P4
if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH":
return # SPI and OPENETH don't use RMII
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32P4,
get_esp32_variant,
)
variant = get_esp32_variant()
if variant == VARIANT_ESP32:
rmii_pins = ESP32_RMII_FIXED_PINS
@@ -521,3 +629,39 @@ async def final_step():
if ip_state_count := CORE.data.get(ETHERNET_IP_STATE_LISTENERS_KEY, 0):
cg.add_define("USE_ETHERNET_IP_STATE_LISTENERS")
cg.add_define("ESPHOME_ETHERNET_IP_STATE_LISTENERS", ip_state_count)
_platform_filter = filter_source_files_from_platform(
{
"ethernet_component_esp32.cpp": {
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
},
"ethernet_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"esp_eth_phy_jl1101.c": {
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
},
}
)
def _filter_source_files() -> list[str]:
excluded = _platform_filter()
eth_data = CORE.data.get(KEY_ETHERNET, {})
eth_type = eth_data.get(ETHERNET_TYPE_KEY)
# Only compile the custom JL1101 driver when JL1101 is configured
# and pioarduino doesn't have it builtin (IDF 5.4.2 to 5.x)
if eth_type != "JL1101":
excluded.append("esp_eth_phy_jl1101.c")
elif CORE.is_esp32 and not CORE.data.get(KEY_NATIVE_IDF, False):
from esphome.components.esp32 import idf_version
# pioarduino has JL1101 builtin on IDF 5.4.2-5.x; exclude custom driver
# to avoid shadowing. Native IDF builds always need the custom driver.
if cv.Version(5, 4, 2) <= idf_version() < cv.Version(6, 0, 0):
excluded.append("esp_eth_phy_jl1101.c")
return excluded
FILTER_SOURCE_FILES = _filter_source_files
@@ -29,7 +29,8 @@
#include "esp_rom_sys.h"
#include "esp_idf_version.h"
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) || \
ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
static const char *TAG = "jl1101";
#define PHY_CHECK(a, str, goto_tag, ...) \
@@ -1,547 +1,28 @@
#include "ethernet_component.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#ifdef USE_ETHERNET
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#ifdef USE_ESP32
#include <lwip/dns.h>
#include <cinttypes>
#include "esp_event.h"
#ifdef USE_ETHERNET_LAN8670
#include "esp_eth_phy_lan867x.h"
#endif
#ifdef USE_ETHERNET_SPI
#include <driver/gpio.h>
#include <driver/spi_master.h>
#endif
namespace esphome::ethernet {
static const char *const TAG = "ethernet";
// PHY register size for hex logging
static constexpr size_t PHY_REG_SIZE = 2;
EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err));
this->mark_failed();
}
#define ESPHL_ERROR_CHECK(err, message) \
if ((err) != ESP_OK) { \
this->log_error_and_mark_failed_(err, message); \
return; \
}
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \
if ((err) != ESP_OK) { \
this->log_error_and_mark_failed_(err, message); \
return ret; \
}
EthernetComponent::EthernetComponent() { global_eth_component = this; }
void EthernetComponent::setup() {
if (esp_reset_reason() != ESP_RST_DEEPSLEEP) {
// Delay here to allow power to stabilise before Ethernet is initialized.
delay(300); // NOLINT
}
esp_err_t err;
#ifdef USE_ETHERNET_SPI
// Install GPIO ISR handler to be able to service SPI Eth modules interrupts
gpio_install_isr_service(0);
spi_bus_config_t buscfg = {
.mosi_io_num = this->mosi_pin_,
.miso_io_num = this->miso_pin_,
.sclk_io_num = this->clk_pin_,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.data4_io_num = -1,
.data5_io_num = -1,
.data6_io_num = -1,
.data7_io_num = -1,
.max_transfer_sz = 0,
.flags = 0,
.intr_flags = 0,
};
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \
defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
auto host = SPI2_HOST;
#else
auto host = SPI3_HOST;
#endif
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
#endif
err = esp_netif_init();
ESPHL_ERROR_CHECK(err, "ETH netif init error");
err = esp_event_loop_create_default();
ESPHL_ERROR_CHECK(err, "ETH event loop error");
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
this->eth_netif_ = esp_netif_new(&cfg);
// Init MAC and PHY configs to default
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module
spi_device_interface_config_t devcfg = {
.command_bits = 0,
.address_bits = 0,
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.clock_speed_hz = this->clock_speed_,
.input_delay_ns = 0,
.spics_io_num = this->cs_pin_,
.flags = 0,
.queue_size = 20,
.pre_cb = nullptr,
.post_cb = nullptr,
};
#if CONFIG_ETH_SPI_ETHERNET_W5500
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg);
#endif
#if CONFIG_ETH_SPI_ETHERNET_DM9051
eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg);
#endif
#if CONFIG_ETH_SPI_ETHERNET_W5500
w5500_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
w5500_config.poll_period_ms = this->polling_interval_;
#endif
#endif
#if CONFIG_ETH_SPI_ETHERNET_DM9051
dm9051_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
dm9051_config.poll_period_ms = this->polling_interval_;
#endif
#endif
phy_config.phy_addr = this->phy_addr_spi_;
phy_config.reset_gpio_num = this->reset_pin_;
esp_eth_mac_t *mac = nullptr;
#elif defined(USE_ETHERNET_OPENETH)
esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config);
#else
phy_config.phy_addr = this->phy_addr_;
phy_config.reset_gpio_num = this->power_pin_;
eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_;
esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_;
#else
esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_;
esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_;
#endif
esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_;
esp32_emac_config.clock_config.rmii.clock_gpio = (emac_rmii_clock_gpio_t) this->clk_pin_;
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
#endif
switch (this->type_) {
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH: {
phy_config.autonego_timeout_ms = 1000;
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
break;
}
#endif
#if CONFIG_ETH_USE_ESP32_EMAC
#ifdef USE_ETHERNET_LAN8720
case ETHERNET_TYPE_LAN8720: {
this->phy_ = esp_eth_phy_new_lan87xx(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_RTL8201
case ETHERNET_TYPE_RTL8201: {
this->phy_ = esp_eth_phy_new_rtl8201(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_DP83848
case ETHERNET_TYPE_DP83848: {
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_IP101
case ETHERNET_TYPE_IP101: {
this->phy_ = esp_eth_phy_new_ip101(&phy_config);
break;
}
#endif
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
case ETHERNET_TYPE_JL1101: {
this->phy_ = esp_eth_phy_new_jl1101(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_KSZ8081
case ETHERNET_TYPE_KSZ8081:
case ETHERNET_TYPE_KSZ8081RNA: {
this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_LAN8670
case ETHERNET_TYPE_LAN8670: {
this->phy_ = esp_eth_phy_new_lan867x(&phy_config);
break;
}
#endif
#endif
#ifdef USE_ETHERNET_SPI
#if CONFIG_ETH_SPI_ETHERNET_W5500
case ETHERNET_TYPE_W5500: {
mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
this->phy_ = esp_eth_phy_new_w5500(&phy_config);
break;
}
#endif
#if CONFIG_ETH_SPI_ETHERNET_DM9051
case ETHERNET_TYPE_DM9051: {
mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config);
this->phy_ = esp_eth_phy_new_dm9051(&phy_config);
break;
}
#endif
#endif
default: {
this->mark_failed();
return;
}
}
esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, this->phy_);
this->eth_handle_ = nullptr;
err = esp_eth_driver_install(&eth_config, &this->eth_handle_);
ESPHL_ERROR_CHECK(err, "ETH driver install error");
#ifndef USE_ETHERNET_SPI
#ifdef USE_ETHERNET_KSZ8081
if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) {
// KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide.
this->ksz8081_set_clock_reference_(mac);
}
#endif // USE_ETHERNET_KSZ8081
for (const auto &phy_register : this->phy_registers_) {
this->write_phy_register_(mac, phy_register);
}
#endif
// use ESP internal eth mac
uint8_t mac_addr[6];
if (this->fixed_mac_.has_value()) {
memcpy(mac_addr, this->fixed_mac_->data(), 6);
} else {
esp_read_mac(mac_addr, ESP_MAC_ETH);
}
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr);
ESPHL_ERROR_CHECK(err, "set mac address error");
/* attach Ethernet driver to TCP/IP stack */
err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_));
ESPHL_ERROR_CHECK(err, "ETH netif attach error");
// Register user defined event handers
err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr);
ESPHL_ERROR_CHECK(err, "ETH event handler register error");
err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr);
ESPHL_ERROR_CHECK(err, "GOT IP event handler register error");
#if USE_NETWORK_IPV6
err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr);
ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error");
#endif /* USE_NETWORK_IPV6 */
/* start Ethernet driver state machine */
err = esp_eth_start(this->eth_handle_);
ESPHL_ERROR_CHECK(err, "ETH start error");
}
void EthernetComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
switch (this->state_) {
case EthernetComponentState::STOPPED:
if (this->started_) {
ESP_LOGI(TAG, "Starting connection");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTING:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
} else if (this->connected_) {
// connection established
ESP_LOGI(TAG, "Connected");
this->state_ = EthernetComponentState::CONNECTED;
this->dump_connect_params_();
this->status_clear_warning();
#ifdef USE_ETHERNET_CONNECT_TRIGGER
this->connect_trigger_.trigger();
#endif
} else if (now - this->connect_begin_ > 15000) {
ESP_LOGW(TAG, "Connecting failed; reconnecting");
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTED:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else if (!this->connected_) {
ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else {
this->finish_connect_();
// When connected and stable, disable the loop to save CPU cycles
this->disable_loop();
}
break;
}
}
void EthernetComponent::dump_config() {
const char *eth_type;
switch (this->type_) {
#ifdef USE_ETHERNET_LAN8720
case ETHERNET_TYPE_LAN8720:
eth_type = "LAN8720";
break;
#endif
#ifdef USE_ETHERNET_RTL8201
case ETHERNET_TYPE_RTL8201:
eth_type = "RTL8201";
break;
#endif
#ifdef USE_ETHERNET_DP83848
case ETHERNET_TYPE_DP83848:
eth_type = "DP83848";
break;
#endif
#ifdef USE_ETHERNET_IP101
case ETHERNET_TYPE_IP101:
eth_type = "IP101";
break;
#endif
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
case ETHERNET_TYPE_JL1101:
eth_type = "JL1101";
break;
#endif
#ifdef USE_ETHERNET_KSZ8081
case ETHERNET_TYPE_KSZ8081:
eth_type = "KSZ8081";
break;
case ETHERNET_TYPE_KSZ8081RNA:
eth_type = "KSZ8081RNA";
break;
#endif
#if CONFIG_ETH_SPI_ETHERNET_W5500
case ETHERNET_TYPE_W5500:
eth_type = "W5500";
break;
#endif
#if CONFIG_ETH_SPI_ETHERNET_DM9051
case ETHERNET_TYPE_DM9051:
eth_type = "DM9051";
break;
#endif
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH:
eth_type = "OPENETH";
break;
#endif
#ifdef USE_ETHERNET_LAN8670
case ETHERNET_TYPE_LAN8670:
eth_type = "LAN8670";
break;
#endif
default:
eth_type = "Unknown";
break;
}
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Connected: %s",
YESNO(this->is_connected()));
this->dump_connect_params_();
#ifdef USE_ETHERNET_SPI
ESP_LOGCONFIG(TAG,
" CLK Pin: %u\n"
" MISO Pin: %u\n"
" MOSI Pin: %u\n"
" CS Pin: %u",
this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_);
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
if (this->polling_interval_ != 0) {
ESP_LOGCONFIG(TAG, " Polling Interval: %lu ms", this->polling_interval_);
} else
#endif
{
ESP_LOGCONFIG(TAG, " IRQ Pin: %d", this->interrupt_pin_);
}
ESP_LOGCONFIG(TAG,
" Reset Pin: %d\n"
" Clock Speed: %d MHz",
this->reset_pin_, this->clock_speed_ / 1000000);
#else
if (this->power_pin_ != -1) {
ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_);
}
ESP_LOGCONFIG(TAG,
" CLK Pin: %u\n"
" MDC Pin: %u\n"
" MDIO Pin: %u\n"
" PHY addr: %u",
this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_);
#endif
ESP_LOGCONFIG(TAG, " Type: %s", eth_type);
}
float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; }
network::IPAddresses EthernetComponent::get_ip_addresses() {
network::IPAddresses addresses;
esp_netif_ip_info_t ip;
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
// TODO: do something smarter
// return false;
} else {
addresses[0] = network::IPAddress(&ip.ip);
}
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
assert(count < addresses.size());
for (int i = 0; i < count; i++) {
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
}
#endif /* USE_NETWORK_IPV6 */
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
return addresses;
}
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
LwIPLock lock;
const ip_addr_t *dns_ip = dns_getserver(num);
return dns_ip;
}
void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) {
const char *event_name;
switch (event) {
case ETHERNET_EVENT_START:
event_name = "ETH started";
global_eth_component->started_ = true;
global_eth_component->enable_loop_soon_any_context();
break;
case ETHERNET_EVENT_STOP:
event_name = "ETH stopped";
global_eth_component->started_ = false;
global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break;
case ETHERNET_EVENT_CONNECTED:
event_name = "ETH connected";
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
if (global_eth_component->manual_ip_.has_value()) {
global_eth_component->notify_ip_state_listeners_();
}
#ifdef USE_ETHERNET_MANUAL_IP
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
#endif
break;
case ETHERNET_EVENT_DISCONNECTED:
event_name = "ETH disconnected";
global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break;
default:
return;
}
ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event);
}
// set_use_address() is guaranteed to be called during component setup by Python code generation,
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
const char *EthernetComponent::get_use_address() const { return this->use_address_; }
void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
const esp_netif_ip_info_t *ip_info = &event->ip_info;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip));
global_eth_component->got_ipv4_address_ = true;
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#else
global_eth_component->connected_ = true;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif /* USE_NETWORK_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#if USE_NETWORK_IPV6
void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data) {
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip));
global_eth_component->ipv6_count_ += 1;
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#else
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#endif /* USE_NETWORK_IPV6 */
void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void EthernetComponent::notify_ip_state_listeners_() {
@@ -554,315 +35,6 @@ void EthernetComponent::notify_ip_state_listeners_() {
}
#endif // USE_ETHERNET_IP_STATE_LISTENERS
void EthernetComponent::finish_connect_() {
#if USE_NETWORK_IPV6
// Retry IPv6 link-local setup if it failed during initial connect
// This handles the case where min_ipv6_addr_count is NOT set (or is 0),
// allowing us to reach CONNECTED state with just IPv4.
// If IPv6 setup failed in start_connect_() because the interface wasn't ready:
// - Bootup timing issues (#10281)
// - Cable unplugged/network interruption (#10705)
// We can now retry since we're in CONNECTED state and the interface is definitely up.
if (!this->ipv6_setup_done_) {
esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err == ESP_OK) {
ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)");
}
// Always set the flag to prevent continuous retries
// If IPv6 setup fails here with the interface up and stable, it's
// likely a persistent issue (IPv6 disabled at router, hardware
// limitation, etc.) that won't be resolved by further retries.
// The device continues to work with IPv4.
this->ipv6_setup_done_ = true;
}
#endif /* USE_NETWORK_IPV6 */
}
void EthernetComponent::start_connect_() {
global_eth_component->got_ipv4_address_ = false;
#if USE_NETWORK_IPV6
global_eth_component->ipv6_count_ = 0;
this->ipv6_setup_done_ = false;
#endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis();
this->status_set_warning(LOG_STR("waiting for IP configuration"));
esp_err_t err;
err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str());
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
}
esp_netif_ip_info_t info;
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
info.ip = this->manual_ip_->static_ip;
info.gw = this->manual_ip_->gateway;
info.netmask = this->manual_ip_->subnet;
} else
#endif
{
info.ip.addr = 0;
info.gw.addr = 0;
info.netmask.addr = 0;
}
esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT;
err = esp_netif_dhcpc_get_status(this->eth_netif_, &status);
ESPHL_ERROR_CHECK(err, "DHCPC Get Status Failed!");
ESP_LOGV(TAG, "DHCP Client Status: %d", status);
err = esp_netif_dhcpc_stop(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
ESPHL_ERROR_CHECK(err, "DHCPC stop error");
}
err = esp_netif_set_ip_info(this->eth_netif_, &info);
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns1;
dns_setserver(0, &d);
}
if (this->manual_ip_->dns2.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
} else
#endif
{
err = esp_netif_dhcpc_start(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
ESPHL_ERROR_CHECK(err, "DHCPC start error");
}
}
#if USE_NETWORK_IPV6
// Attempt to create IPv6 link-local address
// We MUST attempt this here, not just in finish_connect_(), because with
// min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6.
// However, this may fail with ESP_FAIL if the interface is not up yet:
// - At bootup when link isn't ready (#10281)
// - After disconnection/cable unplugged (#10705)
// We'll retry in finish_connect_() if it fails here.
err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err != ESP_OK) {
if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) {
// This is a programming error, not a transient failure
ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters");
} else {
// ESP_FAIL means the interface isn't up yet
// This is expected and non-fatal, happens in multiple scenarios:
// - During reconnection after network interruptions (#10705)
// - At bootup when the link isn't ready yet (#10281)
// We'll retry once we reach CONNECTED state and the interface is up
ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err));
// Don't mark component as failed - this is a transient error
}
}
#endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis();
this->status_set_warning();
}
void EthernetComponent::dump_connect_params_() {
esp_netif_ip_info_t ip;
esp_netif_get_ip_info(this->eth_netif_, &ip);
const ip_addr_t *dns_ip1;
const ip_addr_t *dns_ip2;
{
LwIPLock lock;
dns_ip1 = dns_getserver(0);
dns_ip2 = dns_getserver(1);
}
// Use stack buffers for IP address formatting to avoid heap allocations
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
" IP Address: %s\n"
" Hostname: '%s'\n"
" Subnet: %s\n"
" Gateway: %s\n"
" DNS1: %s\n"
" DNS2: %s\n"
" MAC Address: %s\n"
" Is Full Duplex: %s\n"
" Link Speed: %u",
network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(),
network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf),
network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf),
this->get_eth_mac_address_pretty_into_buffer(mac_buf),
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10);
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
for (int i = 0; i < count; i++) {
ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i]));
}
#endif /* USE_NETWORK_IPV6 */
}
#ifdef USE_ETHERNET_SPI
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
#endif
#else
void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; }
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
#endif
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
#ifdef USE_ETHERNET_MANUAL_IP
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
#endif
// set_use_address() is guaranteed to be called during component setup by Python code generation,
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
const char *EthernetComponent::get_use_address() const { return this->use_address_; }
void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
esp_err_t err;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac);
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
}
std::string EthernetComponent::get_eth_mac_address_pretty() {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
}
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
uint8_t mac[6];
get_eth_mac_address_raw(mac);
format_mac_addr_upper(mac, buf.data());
return buf.data();
}
eth_duplex_t EthernetComponent::get_duplex_mode() {
esp_err_t err;
eth_duplex_t duplex_mode;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode);
ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_DUPLEX_MODE error", ETH_DUPLEX_HALF);
return duplex_mode;
}
eth_speed_t EthernetComponent::get_link_speed() {
esp_err_t err;
eth_speed_t speed;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed);
ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_SPEED error", ETH_SPEED_10M);
return speed;
}
bool EthernetComponent::powerdown() {
ESP_LOGI(TAG, "Powering down ethernet PHY");
if (this->phy_ == nullptr) {
ESP_LOGE(TAG, "Ethernet PHY not assigned");
return false;
}
this->connected_ = false;
this->started_ = false;
// No need to enable_loop() here as this is only called during shutdown/reboot
if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) {
ESP_LOGE(TAG, "Error powering down ethernet PHY");
return false;
}
return true;
}
#ifndef USE_ETHERNET_SPI
#ifdef USE_ETHERNET_KSZ8081
constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F;
void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
esp_err_t err;
uint32_t phy_control_2;
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
#endif
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
/*
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
* KSZ8081RNA:
* 0 - clock input to XI (Pin 8) is 25 MHz for RMII - 25 MHz clock mode.
* 1 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode.
* KSZ8081RND:
* 0 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode.
* 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII - 25 MHz clock mode.
*/
if ((phy_control_2 & (1 << 7)) != (1 << 7)) {
phy_control_2 |= 1 << 7;
err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2);
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
}
}
#endif // USE_ETHERNET_KSZ8081
void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) {
esp_err_t err;
#ifdef USE_ETHERNET_RTL8201
constexpr uint8_t eth_phy_psr_reg_addr = 0x1F;
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page);
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page);
ESPHL_ERROR_CHECK(err, "Select PHY Register page failed");
}
#endif
ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value);
err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value);
ESPHL_ERROR_CHECK(err, "Writing PHY Register failed");
#ifdef USE_ETHERNET_RTL8201
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
ESP_LOGD(TAG, "Select PHY Register Page 0x00");
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0);
ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed");
}
#endif
}
#endif
} // namespace esphome::ethernet
#endif // USE_ESP32
#endif // USE_ETHERNET
@@ -7,8 +7,9 @@
#include "esphome/core/automation.h"
#include "esphome/components/network/ip_address.h"
#ifdef USE_ESP32
#ifdef USE_ETHERNET
#ifdef USE_ESP32
#include "esp_eth.h"
#include "esp_eth_mac.h"
#include "esp_eth_mac_esp.h"
@@ -19,6 +20,11 @@
#if CONFIG_ETH_USE_ESP32_EMAC
extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void);
#endif
#endif // USE_ESP32
#ifdef USE_RP2040
#include <W5500lwIP.h>
#endif
namespace esphome::ethernet {
@@ -73,7 +79,13 @@ enum class EthernetComponentState : uint8_t {
CONNECTED,
};
class EthernetComponent : public Component {
// Platform-neutral duplex/speed types
#ifndef USE_ESP32
enum eth_duplex_t { ETH_DUPLEX_HALF, ETH_DUPLEX_FULL };
enum eth_speed_t { ETH_SPEED_10M, ETH_SPEED_100M };
#endif
class EthernetComponent final : public Component {
public:
EthernetComponent();
void setup() override;
@@ -83,6 +95,28 @@ class EthernetComponent : public Component {
void on_powerdown() override { powerdown(); }
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
void set_type(EthernetType type);
#ifdef USE_ETHERNET_MANUAL_IP
void set_manual_ip(const ManualIP &manual_ip);
#endif
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
network::IPAddresses get_ip_addresses();
network::IPAddress get_dns_address(uint8_t num);
const char *get_use_address() const;
void set_use_address(const char *use_address);
void get_eth_mac_address_raw(uint8_t *mac);
// Remove before 2026.9.0
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0")
std::string get_eth_mac_address_pretty();
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
eth_duplex_t get_duplex_mode();
eth_speed_t get_link_speed();
bool powerdown();
#ifdef USE_ESP32
esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; }
#ifdef USE_ETHERNET_SPI
void set_clk_pin(uint8_t clk_pin);
void set_miso_pin(uint8_t miso_pin);
@@ -102,26 +136,17 @@ class EthernetComponent : public Component {
void set_clk_pin(uint8_t clk_pin);
void set_clk_mode(emac_rmii_clock_mode_t clk_mode);
void add_phy_register(PHYRegister register_value);
#endif
void set_type(EthernetType type);
#ifdef USE_ETHERNET_MANUAL_IP
void set_manual_ip(const ManualIP &manual_ip);
#endif
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
#endif // USE_ETHERNET_SPI
#endif // USE_ESP32
network::IPAddresses get_ip_addresses();
network::IPAddress get_dns_address(uint8_t num);
const char *get_use_address() const;
void set_use_address(const char *use_address);
void get_eth_mac_address_raw(uint8_t *mac);
// Remove before 2026.9.0
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0")
std::string get_eth_mac_address_pretty();
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
eth_duplex_t get_duplex_mode();
eth_speed_t get_link_speed();
esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; }
bool powerdown();
#ifdef USE_RP2040
void set_clk_pin(uint8_t clk_pin);
void set_miso_pin(uint8_t miso_pin);
void set_mosi_pin(uint8_t mosi_pin);
void set_cs_pin(uint8_t cs_pin);
void set_interrupt_pin(int8_t interrupt_pin);
void set_reset_pin(int8_t reset_pin);
#endif // USE_RP2040
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); }
@@ -133,19 +158,22 @@ class EthernetComponent : public Component {
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; }
#endif
protected:
void start_connect_();
void finish_connect_();
void dump_connect_params_();
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void notify_ip_state_listeners_();
#endif
#ifdef USE_ESP32
static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
#if LWIP_IPV6
static void got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
#endif /* LWIP_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void notify_ip_state_listeners_();
#endif
void start_connect_();
void finish_connect_();
void dump_connect_params_();
void log_error_and_mark_failed_(esp_err_t err, const char *message);
#ifdef USE_ETHERNET_KSZ8081
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
@@ -177,7 +205,27 @@ class EthernetComponent : public Component {
uint8_t phy_addr_{0};
uint8_t mdc_pin_{23};
uint8_t mdio_pin_{18};
#endif
#endif // USE_ETHERNET_SPI
// ESP32 pointers
esp_netif_t *eth_netif_{nullptr};
esp_eth_handle_t eth_handle_;
esp_eth_phy_t *phy_{nullptr};
#endif // USE_ESP32
#ifdef USE_RP2040
static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls
Wiznet5500lwIP *eth_{nullptr};
uint32_t last_link_check_{0};
uint8_t clk_pin_;
uint8_t miso_pin_;
uint8_t mosi_pin_;
uint8_t cs_pin_;
int8_t interrupt_pin_{-1};
int8_t reset_pin_{-1};
#endif // USE_RP2040
// Common members
#ifdef USE_ETHERNET_MANUAL_IP
optional<ManualIP> manual_ip_{};
#endif
@@ -194,10 +242,6 @@ class EthernetComponent : public Component {
bool ipv6_setup_done_{false};
#endif /* LWIP_IPV6 */
// Pointers at the end (naturally aligned)
esp_netif_t *eth_netif_{nullptr};
esp_eth_handle_t eth_handle_;
esp_eth_phy_t *phy_{nullptr};
optional<std::array<uint8_t, 6>> fixed_mac_;
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
@@ -219,10 +263,13 @@ class EthernetComponent : public Component {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern EthernetComponent *global_eth_component;
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
#ifdef USE_ESP32
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) || \
ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config);
#endif
#endif // USE_ESP32
} // namespace esphome::ethernet
#endif // USE_ESP32
#endif // USE_ETHERNET
@@ -0,0 +1,878 @@
#include "ethernet_component.h"
#if defined(USE_ETHERNET) && defined(USE_ESP32)
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <lwip/dns.h>
#include <cinttypes>
#include "esp_event.h"
// IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry;
// they are no longer included via esp_eth.h and need explicit includes.
// On IDF 5.x these headers don't exist as standalone files.
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
#ifdef USE_ETHERNET_LAN8720
#include "esp_eth_phy_lan87xx.h"
#endif
#ifdef USE_ETHERNET_RTL8201
#include "esp_eth_phy_rtl8201.h"
#endif
#ifdef USE_ETHERNET_DP83848
#include "esp_eth_phy_dp83848.h"
#endif
#ifdef USE_ETHERNET_IP101
#include "esp_eth_phy_ip101.h"
#endif
#ifdef USE_ETHERNET_KSZ8081
#include "esp_eth_phy_ksz80xx.h"
#endif
#ifdef USE_ETHERNET_W5500
#include "esp_eth_mac_w5500.h"
#include "esp_eth_phy_w5500.h"
#endif
#ifdef USE_ETHERNET_DM9051
#include "esp_eth_mac_dm9051.h"
#include "esp_eth_phy_dm9051.h"
#endif
#endif // ESP_IDF_VERSION >= 6.0.0
// LAN867x header exists on all IDF versions (external component since IDF 5.3)
#ifdef USE_ETHERNET_LAN8670
#include "esp_eth_phy_lan867x.h"
#endif
#ifdef USE_ETHERNET_SPI
#include <driver/gpio.h>
#include <driver/spi_master.h>
#endif
namespace esphome::ethernet {
static const char *const TAG = "ethernet";
// PHY register size for hex logging
static constexpr size_t PHY_REG_SIZE = 2;
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err));
this->mark_failed();
}
#define ESPHL_ERROR_CHECK(err, message) \
if ((err) != ESP_OK) { \
this->log_error_and_mark_failed_(err, message); \
return; \
}
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \
if ((err) != ESP_OK) { \
this->log_error_and_mark_failed_(err, message); \
return ret; \
}
void EthernetComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
switch (this->state_) {
case EthernetComponentState::STOPPED:
if (this->started_) {
ESP_LOGI(TAG, "Starting connection");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTING:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
} else if (this->connected_) {
// connection established
ESP_LOGI(TAG, "Connected");
this->state_ = EthernetComponentState::CONNECTED;
this->dump_connect_params_();
this->status_clear_warning();
#ifdef USE_ETHERNET_CONNECT_TRIGGER
this->connect_trigger_.trigger();
#endif
} else if (now - this->connect_begin_ > 15000) {
ESP_LOGW(TAG, "Connecting failed; reconnecting");
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTED:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else if (!this->connected_) {
ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else {
this->finish_connect_();
// When connected and stable, disable the loop to save CPU cycles
this->disable_loop();
}
break;
}
}
void EthernetComponent::setup() {
if (esp_reset_reason() != ESP_RST_DEEPSLEEP) {
// Delay here to allow power to stabilise before Ethernet is initialized.
delay(300); // NOLINT
}
esp_err_t err;
#ifdef USE_ETHERNET_SPI
// Install GPIO ISR handler to be able to service SPI Eth modules interrupts
gpio_install_isr_service(0);
spi_bus_config_t buscfg = {
.mosi_io_num = this->mosi_pin_,
.miso_io_num = this->miso_pin_,
.sclk_io_num = this->clk_pin_,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.data4_io_num = -1,
.data5_io_num = -1,
.data6_io_num = -1,
.data7_io_num = -1,
.max_transfer_sz = 0,
.flags = 0,
.intr_flags = 0,
};
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \
defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
auto host = SPI2_HOST;
#else
auto host = SPI3_HOST;
#endif
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
#endif
err = esp_netif_init();
ESPHL_ERROR_CHECK(err, "ETH netif init error");
err = esp_event_loop_create_default();
ESPHL_ERROR_CHECK(err, "ETH event loop error");
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
this->eth_netif_ = esp_netif_new(&cfg);
// Init MAC and PHY configs to default
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module
spi_device_interface_config_t devcfg = {
.command_bits = 0,
.address_bits = 0,
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.clock_speed_hz = this->clock_speed_,
.input_delay_ns = 0,
.spics_io_num = this->cs_pin_,
.flags = 0,
.queue_size = 20,
.pre_cb = nullptr,
.post_cb = nullptr,
};
#ifdef USE_ETHERNET_W5500
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg);
#endif
#ifdef USE_ETHERNET_DM9051
eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg);
#endif
#ifdef USE_ETHERNET_W5500
w5500_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
w5500_config.poll_period_ms = this->polling_interval_;
#endif
#endif
#ifdef USE_ETHERNET_DM9051
dm9051_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
dm9051_config.poll_period_ms = this->polling_interval_;
#endif
#endif
phy_config.phy_addr = this->phy_addr_spi_;
phy_config.reset_gpio_num = this->reset_pin_;
esp_eth_mac_t *mac = nullptr;
#elif defined(USE_ETHERNET_OPENETH)
esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config);
#else
phy_config.phy_addr = this->phy_addr_;
phy_config.reset_gpio_num = this->power_pin_;
eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_;
esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_;
#else
esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_;
esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_;
#endif
esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_;
esp32_emac_config.clock_config.rmii.clock_gpio =
static_cast<decltype(esp32_emac_config.clock_config.rmii.clock_gpio)>(this->clk_pin_);
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
#endif
switch (this->type_) {
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH: {
phy_config.autonego_timeout_ms = 1000;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
this->phy_ = esp_eth_phy_new_generic(&phy_config);
#else
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
#endif
break;
}
#endif
#if CONFIG_ETH_USE_ESP32_EMAC
#ifdef USE_ETHERNET_LAN8720
case ETHERNET_TYPE_LAN8720: {
this->phy_ = esp_eth_phy_new_lan87xx(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_RTL8201
case ETHERNET_TYPE_RTL8201: {
this->phy_ = esp_eth_phy_new_rtl8201(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_DP83848
case ETHERNET_TYPE_DP83848: {
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_IP101
case ETHERNET_TYPE_IP101: {
this->phy_ = esp_eth_phy_new_ip101(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_JL1101
case ETHERNET_TYPE_JL1101: {
// PlatformIO (pioarduino): builtin esp_eth_phy_new_jl1101() on all IDF versions
// Non-PlatformIO: custom ESPHome driver (esp_eth_phy_jl1101.c)
this->phy_ = esp_eth_phy_new_jl1101(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_KSZ8081
case ETHERNET_TYPE_KSZ8081:
case ETHERNET_TYPE_KSZ8081RNA: {
this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_LAN8670
case ETHERNET_TYPE_LAN8670: {
this->phy_ = esp_eth_phy_new_lan867x(&phy_config);
break;
}
#endif
#endif
#ifdef USE_ETHERNET_SPI
#ifdef USE_ETHERNET_W5500
case ETHERNET_TYPE_W5500: {
mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
this->phy_ = esp_eth_phy_new_w5500(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_DM9051
case ETHERNET_TYPE_DM9051: {
mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config);
this->phy_ = esp_eth_phy_new_dm9051(&phy_config);
break;
}
#endif
#endif
default: {
this->mark_failed();
return;
}
}
esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, this->phy_);
this->eth_handle_ = nullptr;
err = esp_eth_driver_install(&eth_config, &this->eth_handle_);
ESPHL_ERROR_CHECK(err, "ETH driver install error");
#ifndef USE_ETHERNET_SPI
#ifdef USE_ETHERNET_KSZ8081
if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) {
// KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide.
this->ksz8081_set_clock_reference_(mac);
}
#endif // USE_ETHERNET_KSZ8081
for (const auto &phy_register : this->phy_registers_) {
this->write_phy_register_(mac, phy_register);
}
#endif
// use ESP internal eth mac
uint8_t mac_addr[6];
if (this->fixed_mac_.has_value()) {
memcpy(mac_addr, this->fixed_mac_->data(), 6);
} else {
esp_read_mac(mac_addr, ESP_MAC_ETH);
}
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr);
ESPHL_ERROR_CHECK(err, "set mac address error");
/* attach Ethernet driver to TCP/IP stack */
err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_));
ESPHL_ERROR_CHECK(err, "ETH netif attach error");
// Register user defined event handers
err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr);
ESPHL_ERROR_CHECK(err, "ETH event handler register error");
err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr);
ESPHL_ERROR_CHECK(err, "GOT IP event handler register error");
#if USE_NETWORK_IPV6
err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr);
ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error");
#endif /* USE_NETWORK_IPV6 */
/* start Ethernet driver state machine */
err = esp_eth_start(this->eth_handle_);
ESPHL_ERROR_CHECK(err, "ETH start error");
}
void EthernetComponent::dump_config() {
const char *eth_type;
switch (this->type_) {
#ifdef USE_ETHERNET_LAN8720
case ETHERNET_TYPE_LAN8720:
eth_type = "LAN8720";
break;
#endif
#ifdef USE_ETHERNET_RTL8201
case ETHERNET_TYPE_RTL8201:
eth_type = "RTL8201";
break;
#endif
#ifdef USE_ETHERNET_DP83848
case ETHERNET_TYPE_DP83848:
eth_type = "DP83848";
break;
#endif
#ifdef USE_ETHERNET_IP101
case ETHERNET_TYPE_IP101:
eth_type = "IP101";
break;
#endif
#ifdef USE_ETHERNET_JL1101
case ETHERNET_TYPE_JL1101:
eth_type = "JL1101";
break;
#endif
#ifdef USE_ETHERNET_KSZ8081
case ETHERNET_TYPE_KSZ8081:
eth_type = "KSZ8081";
break;
case ETHERNET_TYPE_KSZ8081RNA:
eth_type = "KSZ8081RNA";
break;
#endif
#ifdef USE_ETHERNET_W5500
case ETHERNET_TYPE_W5500:
eth_type = "W5500";
break;
#endif
#ifdef USE_ETHERNET_DM9051
case ETHERNET_TYPE_DM9051:
eth_type = "DM9051";
break;
#endif
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH:
eth_type = "OPENETH";
break;
#endif
#ifdef USE_ETHERNET_LAN8670
case ETHERNET_TYPE_LAN8670:
eth_type = "LAN8670";
break;
#endif
default:
eth_type = "Unknown";
break;
}
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Connected: %s",
YESNO(this->is_connected()));
this->dump_connect_params_();
#ifdef USE_ETHERNET_SPI
ESP_LOGCONFIG(TAG,
" CLK Pin: %u\n"
" MISO Pin: %u\n"
" MOSI Pin: %u\n"
" CS Pin: %u",
this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_);
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
if (this->polling_interval_ != 0) {
ESP_LOGCONFIG(TAG, " Polling Interval: %" PRIu32 " ms", this->polling_interval_);
} else
#endif
{
ESP_LOGCONFIG(TAG, " IRQ Pin: %d", this->interrupt_pin_);
}
ESP_LOGCONFIG(TAG,
" Reset Pin: %d\n"
" Clock Speed: %d MHz",
this->reset_pin_, this->clock_speed_ / 1000000);
#else
if (this->power_pin_ != -1) {
ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_);
}
ESP_LOGCONFIG(TAG,
" CLK Pin: %u\n"
" MDC Pin: %u\n"
" MDIO Pin: %u\n"
" PHY addr: %u",
this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_);
#endif
ESP_LOGCONFIG(TAG, " Type: %s", eth_type);
}
network::IPAddresses EthernetComponent::get_ip_addresses() {
network::IPAddresses addresses;
esp_netif_ip_info_t ip;
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
// TODO: do something smarter
// return false;
} else {
addresses[0] = network::IPAddress(&ip.ip);
}
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
assert(count < addresses.size());
for (int i = 0; i < count; i++) {
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
}
#endif /* USE_NETWORK_IPV6 */
return addresses;
}
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
LwIPLock lock;
const ip_addr_t *dns_ip = dns_getserver(num);
return dns_ip;
}
void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) {
const char *event_name;
switch (event) {
case ETHERNET_EVENT_START:
event_name = "ETH started";
global_eth_component->started_ = true;
global_eth_component->enable_loop_soon_any_context();
break;
case ETHERNET_EVENT_STOP:
event_name = "ETH stopped";
global_eth_component->started_ = false;
global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break;
case ETHERNET_EVENT_CONNECTED:
event_name = "ETH connected";
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
if (global_eth_component->manual_ip_.has_value()) {
global_eth_component->notify_ip_state_listeners_();
}
#endif
break;
case ETHERNET_EVENT_DISCONNECTED:
event_name = "ETH disconnected";
global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break;
default:
return;
}
ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event);
}
void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
const esp_netif_ip_info_t *ip_info = &event->ip_info;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip));
global_eth_component->got_ipv4_address_ = true;
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#else
global_eth_component->connected_ = true;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif /* USE_NETWORK_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#if USE_NETWORK_IPV6
void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data) {
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip));
global_eth_component->ipv6_count_ += 1;
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#else
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#endif /* USE_NETWORK_IPV6 */
void EthernetComponent::finish_connect_() {
#if USE_NETWORK_IPV6
// Retry IPv6 link-local setup if it failed during initial connect
// This handles the case where min_ipv6_addr_count is NOT set (or is 0),
// allowing us to reach CONNECTED state with just IPv4.
// If IPv6 setup failed in start_connect_() because the interface wasn't ready:
// - Bootup timing issues (#10281)
// - Cable unplugged/network interruption (#10705)
// We can now retry since we're in CONNECTED state and the interface is definitely up.
if (!this->ipv6_setup_done_) {
esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err == ESP_OK) {
ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)");
}
// Always set the flag to prevent continuous retries
// If IPv6 setup fails here with the interface up and stable, it's
// likely a persistent issue (IPv6 disabled at router, hardware
// limitation, etc.) that won't be resolved by further retries.
// The device continues to work with IPv4.
this->ipv6_setup_done_ = true;
}
#endif /* USE_NETWORK_IPV6 */
}
void EthernetComponent::start_connect_() {
global_eth_component->got_ipv4_address_ = false;
#if USE_NETWORK_IPV6
global_eth_component->ipv6_count_ = 0;
this->ipv6_setup_done_ = false;
#endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis();
this->status_set_warning(LOG_STR("waiting for IP configuration"));
esp_err_t err;
err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str());
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
}
esp_netif_ip_info_t info;
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
info.ip = this->manual_ip_->static_ip;
info.gw = this->manual_ip_->gateway;
info.netmask = this->manual_ip_->subnet;
} else
#endif
{
info.ip.addr = 0;
info.gw.addr = 0;
info.netmask.addr = 0;
}
esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT;
err = esp_netif_dhcpc_get_status(this->eth_netif_, &status);
ESPHL_ERROR_CHECK(err, "DHCPC Get Status Failed!");
ESP_LOGV(TAG, "DHCP Client Status: %d", status);
err = esp_netif_dhcpc_stop(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
ESPHL_ERROR_CHECK(err, "DHCPC stop error");
}
err = esp_netif_set_ip_info(this->eth_netif_, &info);
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns1;
dns_setserver(0, &d);
}
if (this->manual_ip_->dns2.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
} else
#endif
{
err = esp_netif_dhcpc_start(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
ESPHL_ERROR_CHECK(err, "DHCPC start error");
}
}
#if USE_NETWORK_IPV6
// Attempt to create IPv6 link-local address
// We MUST attempt this here, not just in finish_connect_(), because with
// min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6.
// However, this may fail with ESP_FAIL if the interface is not up yet:
// - At bootup when link isn't ready (#10281)
// - After disconnection/cable unplugged (#10705)
// We'll retry in finish_connect_() if it fails here.
err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err != ESP_OK) {
if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) {
// This is a programming error, not a transient failure
ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters");
} else {
// ESP_FAIL means the interface isn't up yet
// This is expected and non-fatal, happens in multiple scenarios:
// - During reconnection after network interruptions (#10705)
// - At bootup when the link isn't ready yet (#10281)
// We'll retry once we reach CONNECTED state and the interface is up
ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err));
// Don't mark component as failed - this is a transient error
}
}
#endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis();
this->status_set_warning();
}
void EthernetComponent::dump_connect_params_() {
esp_netif_ip_info_t ip;
esp_netif_get_ip_info(this->eth_netif_, &ip);
const ip_addr_t *dns_ip1;
const ip_addr_t *dns_ip2;
{
LwIPLock lock;
dns_ip1 = dns_getserver(0);
dns_ip2 = dns_getserver(1);
}
// Use stack buffers for IP address formatting to avoid heap allocations
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
" IP Address: %s\n"
" Hostname: '%s'\n"
" Subnet: %s\n"
" Gateway: %s\n"
" DNS1: %s\n"
" DNS2: %s\n"
" MAC Address: %s\n"
" Is Full Duplex: %s\n"
" Link Speed: %u",
network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(),
network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf),
network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf),
this->get_eth_mac_address_pretty_into_buffer(mac_buf),
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10);
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
for (int i = 0; i < count; i++) {
ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i]));
}
#endif /* USE_NETWORK_IPV6 */
}
#ifdef USE_ETHERNET_SPI
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
#endif
#else
void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; }
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
#endif
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
esp_err_t err;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac);
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
}
std::string EthernetComponent::get_eth_mac_address_pretty() {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
}
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
uint8_t mac[6];
get_eth_mac_address_raw(mac);
format_mac_addr_upper(mac, buf.data());
return buf.data();
}
eth_duplex_t EthernetComponent::get_duplex_mode() {
esp_err_t err;
eth_duplex_t duplex_mode;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode);
ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_DUPLEX_MODE error", ETH_DUPLEX_HALF);
return duplex_mode;
}
eth_speed_t EthernetComponent::get_link_speed() {
esp_err_t err;
eth_speed_t speed;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed);
ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_SPEED error", ETH_SPEED_10M);
return speed;
}
bool EthernetComponent::powerdown() {
ESP_LOGI(TAG, "Powering down ethernet PHY");
if (this->phy_ == nullptr) {
ESP_LOGE(TAG, "Ethernet PHY not assigned");
return false;
}
this->connected_ = false;
this->started_ = false;
// No need to enable_loop() here as this is only called during shutdown/reboot
if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) {
ESP_LOGE(TAG, "Error powering down ethernet PHY");
return false;
}
return true;
}
#ifndef USE_ETHERNET_SPI
#ifdef USE_ETHERNET_KSZ8081
constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F;
void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
esp_err_t err;
uint32_t phy_control_2;
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
#endif
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
/*
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
* KSZ8081RNA:
* 0 - clock input to XI (Pin 8) is 25 MHz for RMII - 25 MHz clock mode.
* 1 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode.
* KSZ8081RND:
* 0 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode.
* 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII - 25 MHz clock mode.
*/
if ((phy_control_2 & (1 << 7)) != (1 << 7)) {
phy_control_2 |= 1 << 7;
err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2);
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
}
}
#endif // USE_ETHERNET_KSZ8081
void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) {
esp_err_t err;
#ifdef USE_ETHERNET_RTL8201
constexpr uint8_t eth_phy_psr_reg_addr = 0x1F;
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page);
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page);
ESPHL_ERROR_CHECK(err, "Select PHY Register page failed");
}
#endif
ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value);
err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value);
ESPHL_ERROR_CHECK(err, "Writing PHY Register failed");
#ifdef USE_ETHERNET_RTL8201
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
ESP_LOGD(TAG, "Select PHY Register Page 0x00");
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0);
ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed");
}
#endif
}
#endif
} // namespace esphome::ethernet
#endif // USE_ETHERNET && USE_ESP32
@@ -0,0 +1,315 @@
#include "ethernet_component.h"
#if defined(USE_ETHERNET) && defined(USE_RP2040)
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/components/rp2040/gpio.h"
#include <SPI.h>
#include <lwip/dns.h>
#include <lwip/netif.h>
namespace esphome::ethernet {
static const char *const TAG = "ethernet";
void EthernetComponent::setup() {
// Configure SPI pins
SPI.setRX(this->miso_pin_);
SPI.setTX(this->mosi_pin_);
SPI.setSCK(this->clk_pin_);
// Toggle reset pin if configured
if (this->reset_pin_ >= 0) {
rp2040::RP2040GPIOPin reset_pin;
reset_pin.set_pin(this->reset_pin_);
reset_pin.set_flags(gpio::FLAG_OUTPUT);
reset_pin.setup();
reset_pin.digital_write(false);
delay(1); // NOLINT
reset_pin.digital_write(true);
delay(10); // NOLINT - wait for W5500 to initialize after reset
}
// Create the W5500 device instance
this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
// Set hostname before begin() so the LWIP netif gets it
this->eth_->hostname(App.get_name().c_str());
// Configure static IP if set (must be done before begin())
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
IPAddress ip(this->manual_ip_->static_ip);
IPAddress gateway(this->manual_ip_->gateway);
IPAddress subnet(this->manual_ip_->subnet);
IPAddress dns1(this->manual_ip_->dns1);
IPAddress dns2(this->manual_ip_->dns2);
this->eth_->config(ip, gateway, subnet, dns1, dns2);
}
#endif
// Begin with fixed MAC or auto-generated
bool success;
if (this->fixed_mac_.has_value()) {
success = this->eth_->begin(this->fixed_mac_->data());
} else {
success = this->eth_->begin();
}
if (!success) {
ESP_LOGE(TAG, "Failed to initialize W5500 Ethernet");
delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory)
this->eth_ = nullptr;
this->mark_failed();
return;
}
// Make this the default interface for routing
this->eth_->setDefault(true);
// The arduino-pico LwipIntfDev automatically handles packet processing
// via __addEthernetPacketHandler when no interrupt pin is used,
// or via GPIO interrupt when one is provided.
// Don't set started_ here — let the link polling in loop() set it
// when the W5500 link is actually up. Setting it prematurely causes
// a "Starting → Stopped → Starting" log sequence because the W5500
// needs time after begin() before the PHY link is ready.
}
void EthernetComponent::loop() {
// On RP2040, we need to poll connection state since there are no events.
const uint32_t now = App.get_loop_component_start_time();
// Throttle link/IP polling to avoid excessive SPI transactions from linkStatus()
// which reads the W5500 PHY register via SPI on every call.
// connected() reads netif->ip_addr without LwIPLock, but this is a single
// 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale
// value, which is benign for polling.
if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) {
this->last_link_check_ = now;
bool link_up = this->eth_->linkStatus() == LinkON;
bool has_ip = this->eth_->connected();
if (!link_up) {
if (this->started_) {
this->started_ = false;
this->connected_ = false;
}
} else {
if (!this->started_) {
this->started_ = true;
}
bool was_connected = this->connected_;
this->connected_ = has_ip;
if (this->connected_ && !was_connected) {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
this->notify_ip_state_listeners_();
#endif
}
}
}
// State machine
switch (this->state_) {
case EthernetComponentState::STOPPED:
if (this->started_) {
ESP_LOGI(TAG, "Starting connection");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTING:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
} else if (this->connected_) {
// connection established
ESP_LOGI(TAG, "Connected");
this->state_ = EthernetComponentState::CONNECTED;
this->dump_connect_params_();
this->status_clear_warning();
#ifdef USE_ETHERNET_CONNECT_TRIGGER
this->connect_trigger_.trigger();
#endif
} else if (now - this->connect_begin_ > 15000) {
ESP_LOGW(TAG, "Connecting failed; reconnecting");
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTED:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else if (!this->connected_) {
ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else {
this->finish_connect_();
}
break;
}
}
void EthernetComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Type: W5500\n"
" Connected: %s\n"
" CLK Pin: %u\n"
" MISO Pin: %u\n"
" MOSI Pin: %u\n"
" CS Pin: %u\n"
" IRQ Pin: %d\n"
" Reset Pin: %d",
YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
this->interrupt_pin_, this->reset_pin_);
this->dump_connect_params_();
}
network::IPAddresses EthernetComponent::get_ip_addresses() {
network::IPAddresses addresses;
if (this->eth_ != nullptr) {
LwIPLock lock;
addresses[0] = network::IPAddress(this->eth_->localIP());
}
return addresses;
}
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
LwIPLock lock;
const ip_addr_t *dns_ip = dns_getserver(num);
return dns_ip;
}
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
if (this->eth_ != nullptr) {
this->eth_->macAddress(mac);
} else {
memset(mac, 0, 6);
}
}
std::string EthernetComponent::get_eth_mac_address_pretty() {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
}
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
uint8_t mac[6];
get_eth_mac_address_raw(mac);
format_mac_addr_upper(mac, buf.data());
return buf.data();
}
eth_duplex_t EthernetComponent::get_duplex_mode() {
// W5500 is always full duplex
return ETH_DUPLEX_FULL;
}
eth_speed_t EthernetComponent::get_link_speed() {
// W5500 is always 100Mbps
return ETH_SPEED_100M;
}
bool EthernetComponent::powerdown() {
ESP_LOGI(TAG, "Powering down ethernet");
if (this->eth_ != nullptr) {
this->eth_->end();
}
this->connected_ = false;
this->started_ = false;
return true;
}
void EthernetComponent::start_connect_() {
this->got_ipv4_address_ = false;
this->connect_begin_ = millis();
this->status_set_warning(LOG_STR("waiting for IP configuration"));
// Hostname is already set in setup() via LwipIntf::setHostname()
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
// Static IP was already configured before begin() in setup()
// Set DNS servers
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns1;
dns_setserver(0, &d);
}
if (this->manual_ip_->dns2.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
}
#endif
}
void EthernetComponent::finish_connect_() {
// No additional work needed on RP2040 for now
// IPv6 link-local could be added here in the future
}
void EthernetComponent::dump_connect_params_() {
if (this->eth_ == nullptr) {
return;
}
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
// Copy all lwIP state under the lock to avoid races with IRQ callbacks
ip_addr_t ip_addr, netmask, gw, dns1_addr, dns2_addr;
{
LwIPLock lock;
auto *netif = this->eth_->getNetIf();
ip_addr = netif->ip_addr;
netmask = netif->netmask;
gw = netif->gw;
dns1_addr = *dns_getserver(0);
dns2_addr = *dns_getserver(1);
}
ESP_LOGCONFIG(TAG,
" IP Address: %s\n"
" Hostname: '%s'\n"
" Subnet: %s\n"
" Gateway: %s\n"
" DNS1: %s\n"
" DNS2: %s\n"
" MAC Address: %s",
network::IPAddress(&ip_addr).str_to(ip_buf), App.get_name().c_str(),
network::IPAddress(&netmask).str_to(subnet_buf), network::IPAddress(&gw).str_to(gateway_buf),
network::IPAddress(&dns1_addr).str_to(dns1_buf), network::IPAddress(&dns2_addr).str_to(dns2_buf),
this->get_eth_mac_address_pretty_into_buffer(mac_buf));
}
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
} // namespace esphome::ethernet
#endif // USE_ETHERNET && USE_RP2040

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