esphome 2025.9.3__py3-none-any.whl → 2025.10.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. esphome/__main__.py +94 -31
  2. esphome/address_cache.py +142 -0
  3. esphome/automation.py +130 -32
  4. esphome/build_gen/platformio.py +1 -3
  5. esphome/codegen.py +1 -0
  6. esphome/components/animation/animation.cpp +2 -2
  7. esphome/components/api/__init__.py +166 -3
  8. esphome/components/api/api_connection.cpp +84 -41
  9. esphome/components/api/api_connection.h +22 -16
  10. esphome/components/api/api_frame_helper.cpp +33 -19
  11. esphome/components/api/api_frame_helper.h +19 -4
  12. esphome/components/api/api_frame_helper_noise.cpp +41 -53
  13. esphome/components/api/api_frame_helper_noise.h +1 -1
  14. esphome/components/api/api_frame_helper_plaintext.cpp +22 -31
  15. esphome/components/api/api_frame_helper_plaintext.h +1 -1
  16. esphome/components/api/api_pb2.cpp +189 -15
  17. esphome/components/api/api_pb2.h +132 -20
  18. esphome/components/api/api_pb2_dump.cpp +97 -9
  19. esphome/components/api/api_pb2_service.cpp +118 -160
  20. esphome/components/api/api_pb2_service.h +31 -3
  21. esphome/components/api/api_server.cpp +68 -10
  22. esphome/components/api/api_server.h +32 -4
  23. esphome/components/api/custom_api_device.h +8 -8
  24. esphome/components/api/homeassistant_service.h +123 -6
  25. esphome/components/api/proto.h +6 -2
  26. esphome/components/api/user_services.h +2 -2
  27. esphome/components/as7341/sensor.py +1 -1
  28. esphome/components/audio/__init__.py +1 -1
  29. esphome/components/audio/audio.cpp +1 -1
  30. esphome/components/audio/audio_decoder.cpp +9 -9
  31. esphome/components/bl0906/bl0906.cpp +2 -2
  32. esphome/components/bl0942/bl0942.cpp +2 -2
  33. esphome/components/ble_client/__init__.py +1 -1
  34. esphome/components/bluetooth_proxy/__init__.py +4 -30
  35. esphome/components/bluetooth_proxy/bluetooth_connection.cpp +11 -4
  36. esphome/components/bluetooth_proxy/bluetooth_connection.h +2 -2
  37. esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +2 -2
  38. esphome/components/camera_encoder/__init__.py +2 -4
  39. esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp +4 -2
  40. esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h +3 -1
  41. esphome/components/canbus/canbus.cpp +7 -5
  42. esphome/components/canbus/canbus.h +7 -7
  43. esphome/components/captive_portal/__init__.py +18 -1
  44. esphome/components/captive_portal/captive_portal.cpp +40 -46
  45. esphome/components/captive_portal/captive_portal.h +20 -22
  46. esphome/components/captive_portal/dns_server_esp32_idf.cpp +205 -0
  47. esphome/components/captive_portal/dns_server_esp32_idf.h +27 -0
  48. esphome/components/ccs811/ccs811.cpp +1 -1
  49. esphome/components/climate/climate.cpp +10 -7
  50. esphome/components/cm1106/cm1106.cpp +1 -1
  51. esphome/components/copy/lock/copy_lock.cpp +1 -1
  52. esphome/components/cover/cover.cpp +1 -0
  53. esphome/components/daikin_arc/daikin_arc.cpp +19 -12
  54. esphome/components/dashboard_import/dashboard_import.cpp +1 -1
  55. esphome/components/dashboard_import/dashboard_import.h +1 -1
  56. esphome/components/deep_sleep/__init__.py +9 -2
  57. esphome/components/deep_sleep/deep_sleep_component.h +11 -9
  58. esphome/components/deep_sleep/deep_sleep_esp32.cpp +51 -27
  59. esphome/components/ektf2232/touchscreen/__init__.py +8 -5
  60. esphome/components/ektf2232/touchscreen/ektf2232.cpp +4 -4
  61. esphome/components/ektf2232/touchscreen/ektf2232.h +2 -2
  62. esphome/components/epaper_spi/__init__.py +1 -0
  63. esphome/components/epaper_spi/display.py +80 -0
  64. esphome/components/epaper_spi/epaper_spi.cpp +227 -0
  65. esphome/components/epaper_spi/epaper_spi.h +93 -0
  66. esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp +42 -0
  67. esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h +45 -0
  68. esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp +135 -0
  69. esphome/components/epaper_spi/epaper_spi_spectra_e6.h +23 -0
  70. esphome/components/es7210/es7210.cpp +3 -3
  71. esphome/components/esp32/__init__.py +256 -340
  72. esphome/components/esp32/boards.py +81 -0
  73. esphome/components/esp32/preferences.cpp +23 -17
  74. esphome/components/esp32_ble/__init__.py +167 -44
  75. esphome/components/esp32_ble/ble.cpp +47 -3
  76. esphome/components/esp32_ble/ble.h +18 -0
  77. esphome/components/esp32_ble/ble_advertising.cpp +7 -3
  78. esphome/components/esp32_ble/ble_advertising.h +4 -0
  79. esphome/components/esp32_ble/ble_uuid.cpp +16 -42
  80. esphome/components/esp32_ble_beacon/__init__.py +3 -4
  81. esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +0 -4
  82. esphome/components/esp32_ble_client/ble_client_base.cpp +14 -12
  83. esphome/components/esp32_ble_server/__init__.py +28 -14
  84. esphome/components/esp32_ble_server/ble_characteristic.cpp +67 -57
  85. esphome/components/esp32_ble_server/ble_characteristic.h +27 -16
  86. esphome/components/esp32_ble_server/ble_descriptor.cpp +4 -3
  87. esphome/components/esp32_ble_server/ble_descriptor.h +13 -9
  88. esphome/components/esp32_ble_server/ble_server.cpp +59 -24
  89. esphome/components/esp32_ble_server/ble_server.h +38 -20
  90. esphome/components/esp32_ble_server/ble_server_automations.cpp +49 -33
  91. esphome/components/esp32_ble_server/ble_server_automations.h +39 -24
  92. esphome/components/esp32_ble_tracker/__init__.py +25 -80
  93. esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +2 -8
  94. esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +0 -3
  95. esphome/components/esp32_camera/__init__.py +1 -3
  96. esphome/components/esp32_can/esp32_can.cpp +22 -4
  97. esphome/components/esp32_can/esp32_can.h +3 -0
  98. esphome/components/esp32_hosted/__init__.py +2 -1
  99. esphome/components/esp32_improv/esp32_improv_component.cpp +135 -65
  100. esphome/components/esp32_improv/esp32_improv_component.h +7 -1
  101. esphome/components/esp32_rmt_led_strip/led_strip.cpp +1 -1
  102. esphome/components/esp8266/__init__.py +3 -3
  103. esphome/components/esphome/ota/__init__.py +21 -2
  104. esphome/components/esphome/ota/ota_esphome.cpp +456 -146
  105. esphome/components/esphome/ota/ota_esphome.h +49 -2
  106. esphome/components/ethernet/__init__.py +39 -22
  107. esphome/components/ethernet/ethernet_component.cpp +28 -5
  108. esphome/components/ethernet/ethernet_component.h +5 -1
  109. esphome/components/external_components/__init__.py +8 -6
  110. esphome/components/fingerprint_grow/fingerprint_grow.cpp +1 -1
  111. esphome/components/fingerprint_grow/fingerprint_grow.h +2 -1
  112. esphome/components/font/__init__.py +5 -5
  113. esphome/components/graph/graph.cpp +1 -1
  114. esphome/components/graphical_display_menu/graphical_display_menu.cpp +3 -2
  115. esphome/components/haier/hon_climate.cpp +2 -2
  116. esphome/components/haier/hon_climate.h +1 -1
  117. esphome/components/hdc1080/hdc1080.cpp +42 -34
  118. esphome/components/hdc1080/hdc1080.h +1 -3
  119. esphome/components/homeassistant/number/homeassistant_number.cpp +2 -2
  120. esphome/components/homeassistant/switch/homeassistant_switch.cpp +2 -2
  121. esphome/components/http_request/__init__.py +3 -3
  122. esphome/components/htu21d/htu21d.cpp +13 -18
  123. esphome/components/htu21d/htu21d.h +1 -1
  124. esphome/components/i2s_audio/__init__.py +1 -2
  125. esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +1 -1
  126. esphome/components/ili9xxx/ili9xxx_display.cpp +2 -2
  127. esphome/components/improv_serial/improv_serial_component.cpp +12 -15
  128. esphome/components/improv_serial/improv_serial_component.h +6 -8
  129. esphome/components/json/json_util.cpp +42 -44
  130. esphome/components/json/json_util.h +57 -0
  131. esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +2 -2
  132. esphome/components/key_collector/key_collector.h +4 -4
  133. esphome/components/libretiny/__init__.py +6 -6
  134. esphome/components/libretiny/preferences.cpp +23 -16
  135. esphome/components/light/light_call.cpp +98 -120
  136. esphome/components/light/light_call.h +17 -7
  137. esphome/components/lm75b/__init__.py +0 -0
  138. esphome/components/lm75b/lm75b.cpp +39 -0
  139. esphome/components/lm75b/lm75b.h +19 -0
  140. esphome/components/lm75b/sensor.py +34 -0
  141. esphome/components/lock/lock.h +12 -6
  142. esphome/components/logger/__init__.py +15 -27
  143. esphome/components/logger/logger.cpp +10 -20
  144. esphome/components/logger/logger.h +105 -62
  145. esphome/components/logger/logger_esp32.cpp +0 -48
  146. esphome/components/logger/logger_zephyr.cpp +2 -3
  147. esphome/components/logger/select/logger_level_select.cpp +6 -7
  148. esphome/components/logger/select/logger_level_select.h +7 -0
  149. esphome/components/ltr501/ltr501.cpp +7 -6
  150. esphome/components/ltr_als_ps/ltr_als_ps.cpp +7 -6
  151. esphome/components/matrix_keypad/matrix_keypad.h +4 -4
  152. esphome/components/max7219digit/max7219digit.cpp +1 -1
  153. esphome/components/mcp23xxx_base/mcp23xxx_base.h +3 -3
  154. esphome/components/mcp2515/mcp2515.cpp +31 -3
  155. esphome/components/mcp2515/mcp2515_defs.h +3 -1
  156. esphome/components/md5/md5.cpp +0 -26
  157. esphome/components/md5/md5.h +10 -20
  158. esphome/components/mdns/__init__.py +93 -19
  159. esphome/components/mdns/mdns_component.cpp +57 -94
  160. esphome/components/mdns/mdns_component.h +35 -11
  161. esphome/components/mdns/mdns_esp32.cpp +7 -13
  162. esphome/components/mdns/mdns_esp8266.cpp +7 -7
  163. esphome/components/mdns/mdns_libretiny.cpp +3 -4
  164. esphome/components/mdns/mdns_rp2040.cpp +3 -4
  165. esphome/components/mipi/__init__.py +1 -5
  166. esphome/components/mipi_spi/display.py +24 -8
  167. esphome/components/mipi_spi/mipi_spi.h +3 -3
  168. esphome/components/mixer/speaker/mixer_speaker.cpp +3 -3
  169. esphome/components/mmc5603/mmc5603.cpp +3 -3
  170. esphome/components/modbus/modbus.cpp +27 -13
  171. esphome/components/modbus/modbus.h +5 -3
  172. esphome/components/modbus/modbus_definitions.h +86 -0
  173. esphome/components/modbus_controller/__init__.py +29 -1
  174. esphome/components/modbus_controller/const.py +4 -0
  175. esphome/components/modbus_controller/modbus_controller.cpp +38 -13
  176. esphome/components/modbus_controller/modbus_controller.h +18 -29
  177. esphome/components/mpr121/mpr121.cpp +41 -42
  178. esphome/components/mpr121/mpr121.h +0 -1
  179. esphome/components/nau7802/nau7802.cpp +2 -2
  180. esphome/components/network/__init__.py +7 -3
  181. esphome/components/nextion/display.py +4 -4
  182. esphome/components/nextion/nextion.cpp +8 -8
  183. esphome/components/number/__init__.py +2 -0
  184. esphome/components/number/number_call.cpp +23 -12
  185. esphome/components/number/number_call.h +5 -0
  186. esphome/components/online_image/bmp_image.cpp +2 -1
  187. esphome/components/online_image/jpeg_image.cpp +4 -2
  188. esphome/components/opentherm/opentherm.cpp +5 -5
  189. esphome/components/opentherm/opentherm.h +3 -3
  190. esphome/components/openthread/openthread.cpp +11 -10
  191. esphome/components/openthread/openthread.h +0 -1
  192. esphome/components/ota/ota_backend.h +1 -0
  193. esphome/components/packages/__init__.py +10 -8
  194. esphome/components/packet_transport/packet_transport.cpp +2 -0
  195. esphome/components/pid/pid_controller.cpp +1 -1
  196. esphome/components/prometheus/prometheus_handler.cpp +239 -239
  197. esphome/components/psram/__init__.py +30 -28
  198. esphome/components/qmc5883l/qmc5883l.cpp +15 -0
  199. esphome/components/qmc5883l/qmc5883l.h +3 -0
  200. esphome/components/qmc5883l/sensor.py +31 -12
  201. esphome/components/remote_base/gobox_protocol.cpp +3 -3
  202. esphome/components/remote_receiver/__init__.py +14 -2
  203. esphome/components/remote_receiver/{remote_receiver_esp8266.cpp → remote_receiver.cpp} +2 -2
  204. esphome/components/remote_receiver/remote_receiver.h +4 -0
  205. esphome/components/remote_receiver/remote_receiver_esp32.cpp +18 -1
  206. esphome/components/remote_transmitter/__init__.py +2 -2
  207. esphome/components/remote_transmitter/remote_transmitter.cpp +103 -0
  208. esphome/components/rp2040/__init__.py +11 -11
  209. esphome/components/rtttl/rtttl.cpp +2 -2
  210. esphome/components/scd30/sensor.py +1 -1
  211. esphome/components/script/__init__.py +1 -1
  212. esphome/components/script/script.h +7 -7
  213. esphome/components/select/select.cpp +5 -4
  214. esphome/components/select/select_call.cpp +1 -1
  215. esphome/components/sensirion_common/i2c_sensirion.cpp +2 -1
  216. esphome/components/sensor/__init__.py +2 -0
  217. esphome/components/sha256/__init__.py +22 -0
  218. esphome/components/sha256/sha256.cpp +116 -0
  219. esphome/components/sha256/sha256.h +60 -0
  220. esphome/components/socket/lwip_raw_tcp_impl.cpp +34 -6
  221. esphome/components/sonoff_d1/sonoff_d1.cpp +1 -1
  222. esphome/components/spi/__init__.py +0 -3
  223. esphome/components/split_buffer/__init__.py +5 -0
  224. esphome/components/split_buffer/split_buffer.cpp +133 -0
  225. esphome/components/split_buffer/split_buffer.h +40 -0
  226. esphome/components/sps30/sps30.cpp +14 -10
  227. esphome/components/sps30/sps30.h +2 -0
  228. esphome/components/st7567_i2c/st7567_i2c.cpp +3 -1
  229. esphome/components/st7789v/st7789v.cpp +3 -2
  230. esphome/components/statsd/statsd.cpp +1 -1
  231. esphome/components/substitutions/__init__.py +3 -1
  232. esphome/components/substitutions/jinja.py +13 -3
  233. esphome/components/sx126x/__init__.py +16 -0
  234. esphome/components/sx126x/sx126x.cpp +15 -1
  235. esphome/components/sx126x/sx126x.h +9 -1
  236. esphome/components/sx126x/sx126x_reg.h +2 -0
  237. esphome/components/text_sensor/text_sensor.cpp +16 -0
  238. esphome/components/text_sensor/text_sensor.h +3 -10
  239. esphome/components/tormatic/tormatic_cover.cpp +1 -1
  240. esphome/components/tuya/select/tuya_select.cpp +1 -1
  241. esphome/components/tuya/tuya.cpp +29 -4
  242. esphome/components/uart/__init__.py +37 -27
  243. esphome/components/uart/uart.h +6 -0
  244. esphome/components/uart/uart_component.cpp +8 -0
  245. esphome/components/uart/uart_component.h +28 -0
  246. esphome/components/uart/uart_component_esp_idf.cpp +64 -10
  247. esphome/components/uart/uart_component_esp_idf.h +5 -2
  248. esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +1 -1
  249. esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp +1 -1
  250. esphome/components/uponor_smatrix/uponor_smatrix.cpp +3 -3
  251. esphome/components/usb_host/__init__.py +12 -2
  252. esphome/components/usb_host/usb_host.h +89 -14
  253. esphome/components/usb_host/usb_host_client.cpp +157 -22
  254. esphome/components/usb_host/usb_host_component.cpp +1 -1
  255. esphome/components/usb_uart/__init__.py +0 -1
  256. esphome/components/usb_uart/ch34x.cpp +4 -4
  257. esphome/components/usb_uart/cp210x.cpp +3 -3
  258. esphome/components/usb_uart/usb_uart.cpp +88 -32
  259. esphome/components/usb_uart/usb_uart.h +30 -6
  260. esphome/components/valve/valve.cpp +1 -0
  261. esphome/components/veml7700/veml7700.cpp +7 -6
  262. esphome/components/version/version_text_sensor.cpp +2 -1
  263. esphome/components/voice_assistant/voice_assistant.cpp +3 -2
  264. esphome/components/waveshare_epaper/waveshare_epaper.cpp +4 -4
  265. esphome/components/web_server/list_entities.cpp +3 -4
  266. esphome/components/web_server/list_entities.h +8 -10
  267. esphome/components/web_server/ota/__init__.py +1 -1
  268. esphome/components/web_server/ota/ota_web_server.cpp +9 -3
  269. esphome/components/web_server/web_server.cpp +509 -404
  270. esphome/components/web_server/web_server.h +5 -6
  271. esphome/components/web_server/web_server_v1.cpp +21 -19
  272. esphome/components/web_server_base/__init__.py +5 -2
  273. esphome/components/web_server_base/web_server_base.h +27 -7
  274. esphome/components/web_server_idf/__init__.py +1 -1
  275. esphome/components/web_server_idf/multipart.cpp +2 -2
  276. esphome/components/web_server_idf/multipart.h +2 -2
  277. esphome/components/web_server_idf/utils.cpp +2 -2
  278. esphome/components/web_server_idf/utils.h +2 -2
  279. esphome/components/web_server_idf/web_server_idf.cpp +118 -26
  280. esphome/components/web_server_idf/web_server_idf.h +12 -10
  281. esphome/components/wifi/__init__.py +13 -11
  282. esphome/components/wifi/wifi_component.cpp +74 -56
  283. esphome/components/wifi/wifi_component.h +4 -4
  284. esphome/components/wifi/wifi_component_esp8266.cpp +1 -1
  285. esphome/components/wifi/wifi_component_esp_idf.cpp +24 -4
  286. esphome/components/wireguard/__init__.py +1 -1
  287. esphome/components/wts01/__init__.py +0 -0
  288. esphome/components/wts01/sensor.py +41 -0
  289. esphome/components/wts01/wts01.cpp +91 -0
  290. esphome/components/wts01/wts01.h +27 -0
  291. esphome/components/zephyr/__init__.py +5 -5
  292. esphome/components/zwave_proxy/__init__.py +43 -0
  293. esphome/components/zwave_proxy/zwave_proxy.cpp +346 -0
  294. esphome/components/zwave_proxy/zwave_proxy.h +93 -0
  295. esphome/config.py +79 -24
  296. esphome/config_validation.py +13 -15
  297. esphome/const.py +9 -2
  298. esphome/core/__init__.py +33 -22
  299. esphome/core/component.cpp +28 -18
  300. esphome/core/component_iterator.h +2 -1
  301. esphome/core/config.py +15 -15
  302. esphome/core/defines.h +21 -0
  303. esphome/core/entity_helpers.py +9 -6
  304. esphome/core/hash_base.h +56 -0
  305. esphome/core/helpers.cpp +19 -3
  306. esphome/core/helpers.h +26 -0
  307. esphome/core/scheduler.cpp +5 -21
  308. esphome/core/scheduler.h +19 -8
  309. esphome/core/string_ref.h +1 -1
  310. esphome/core/time.cpp +5 -5
  311. esphome/cpp_generator.py +4 -29
  312. esphome/dashboard/const.py +21 -4
  313. esphome/dashboard/core.py +10 -8
  314. esphome/dashboard/dns.py +15 -0
  315. esphome/dashboard/entries.py +15 -21
  316. esphome/dashboard/models.py +76 -0
  317. esphome/dashboard/settings.py +7 -7
  318. esphome/dashboard/status/mdns.py +46 -2
  319. esphome/dashboard/web_server.py +367 -93
  320. esphome/espota2.py +112 -32
  321. esphome/external_files.py +6 -7
  322. esphome/git.py +8 -0
  323. esphome/helpers.py +124 -77
  324. esphome/loader.py +8 -9
  325. esphome/pins.py +2 -2
  326. esphome/platformio_api.py +56 -18
  327. esphome/storage_json.py +26 -21
  328. esphome/types.py +30 -2
  329. esphome/util.py +32 -16
  330. esphome/vscode.py +8 -8
  331. esphome/wizard.py +10 -10
  332. esphome/writer.py +50 -15
  333. esphome/yaml_util.py +37 -31
  334. esphome/zeroconf.py +12 -3
  335. {esphome-2025.9.3.dist-info → esphome-2025.10.0b2.dist-info}/METADATA +12 -12
  336. {esphome-2025.9.3.dist-info → esphome-2025.10.0b2.dist-info}/RECORD +340 -320
  337. esphome/components/event_emitter/__init__.py +0 -5
  338. esphome/components/event_emitter/event_emitter.cpp +0 -14
  339. esphome/components/event_emitter/event_emitter.h +0 -63
  340. esphome/components/remote_receiver/remote_receiver_libretiny.cpp +0 -125
  341. esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +0 -107
  342. esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp +0 -110
  343. esphome/components/uart/uart_component_esp32_arduino.cpp +0 -214
  344. esphome/components/uart/uart_component_esp32_arduino.h +0 -60
  345. esphome/components/wifi/wifi_component_esp32_arduino.cpp +0 -860
  346. esphome/core/string_ref.cpp +0 -12
  347. esphome/dashboard/util/file.py +0 -63
  348. {esphome-2025.9.3.dist-info → esphome-2025.10.0b2.dist-info}/WHEEL +0 -0
  349. {esphome-2025.9.3.dist-info → esphome-2025.10.0b2.dist-info}/entry_points.txt +0 -0
  350. {esphome-2025.9.3.dist-info → esphome-2025.10.0b2.dist-info}/licenses/LICENSE +0 -0
  351. {esphome-2025.9.3.dist-info → esphome-2025.10.0b2.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,13 @@
1
1
  #include "ota_esphome.h"
2
2
  #ifdef USE_OTA
3
+ #ifdef USE_OTA_PASSWORD
4
+ #ifdef USE_OTA_MD5
3
5
  #include "esphome/components/md5/md5.h"
6
+ #endif
7
+ #ifdef USE_OTA_SHA256
8
+ #include "esphome/components/sha256/sha256.h"
9
+ #endif
10
+ #endif
4
11
  #include "esphome/components/network/util.h"
5
12
  #include "esphome/components/ota/ota_backend.h"
6
13
  #include "esphome/components/ota/ota_backend_arduino_esp32.h"
@@ -10,6 +17,7 @@
10
17
  #include "esphome/components/ota/ota_backend_esp_idf.h"
11
18
  #include "esphome/core/application.h"
12
19
  #include "esphome/core/hal.h"
20
+ #include "esphome/core/helpers.h"
13
21
  #include "esphome/core/log.h"
14
22
  #include "esphome/core/util.h"
15
23
 
@@ -20,9 +28,19 @@ namespace esphome {
20
28
 
21
29
  static const char *const TAG = "esphome.ota";
22
30
  static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
23
- static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake
31
+ static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer
32
+ static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
24
33
  static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
25
34
 
35
+ #ifdef USE_OTA_PASSWORD
36
+ #ifdef USE_OTA_MD5
37
+ static constexpr size_t MD5_HEX_SIZE = 32; // MD5 hash as hex string (16 bytes * 2)
38
+ #endif
39
+ #ifdef USE_OTA_SHA256
40
+ static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2)
41
+ #endif
42
+ #endif // USE_OTA_PASSWORD
43
+
26
44
  void ESPHomeOTAComponent::setup() {
27
45
  #ifdef USE_OTA_STATE_CALLBACK
28
46
  ota::register_ota_platform(this);
@@ -63,7 +81,7 @@ void ESPHomeOTAComponent::setup() {
63
81
  return;
64
82
  }
65
83
 
66
- err = this->server_->listen(4);
84
+ err = this->server_->listen(1); // Only one client at a time
67
85
  if (err != 0) {
68
86
  this->log_socket_error_(LOG_STR("listen"));
69
87
  this->mark_failed();
@@ -95,13 +113,22 @@ void ESPHomeOTAComponent::loop() {
95
113
  }
96
114
 
97
115
  static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
116
+ #ifdef USE_OTA_SHA256
117
+ static const uint8_t FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
118
+ #endif
119
+
120
+ // Temporary flag to allow MD5 downgrade for ~3 versions (until 2026.1.0)
121
+ // This allows users to downgrade via OTA if they encounter issues after updating.
122
+ // Without this, users would need to do a serial flash to downgrade.
123
+ // TODO: Remove this flag and all associated code in 2026.1.0
124
+ #define ALLOW_OTA_DOWNGRADE_MD5
98
125
 
99
126
  void ESPHomeOTAComponent::handle_handshake_() {
100
- /// Handle the initial OTA handshake.
127
+ /// Handle the OTA handshake and authentication.
101
128
  ///
102
129
  /// This method is non-blocking and will return immediately if no data is available.
103
- /// It reads all 5 magic bytes (0x6C, 0x26, 0xF7, 0x5C, 0x45) non-blocking
104
- /// before proceeding to handle_data_(). A 10-second timeout is enforced from initial connection.
130
+ /// It manages the state machine through connection, magic bytes validation, feature
131
+ /// negotiation, and authentication before entering the blocking data transfer phase.
105
132
 
106
133
  if (this->client_ == nullptr) {
107
134
  // We already checked server_->ready() in loop(), so we can accept directly
@@ -126,7 +153,8 @@ void ESPHomeOTAComponent::handle_handshake_() {
126
153
  }
127
154
  this->log_start_(LOG_STR("handshake"));
128
155
  this->client_connect_time_ = App.get_loop_component_start_time();
129
- this->magic_buf_pos_ = 0; // Reset magic buffer position
156
+ this->handshake_buf_pos_ = 0; // Reset handshake buffer position
157
+ this->ota_state_ = OTAState::MAGIC_READ;
130
158
  }
131
159
 
132
160
  // Check for handshake timeout
@@ -137,46 +165,99 @@ void ESPHomeOTAComponent::handle_handshake_() {
137
165
  return;
138
166
  }
139
167
 
140
- // Try to read remaining magic bytes
141
- if (this->magic_buf_pos_ < 5) {
142
- // Read as many bytes as available
143
- uint8_t bytes_to_read = 5 - this->magic_buf_pos_;
144
- ssize_t read = this->client_->read(this->magic_buf_ + this->magic_buf_pos_, bytes_to_read);
168
+ switch (this->ota_state_) {
169
+ case OTAState::MAGIC_READ: {
170
+ // Try to read remaining magic bytes (5 total)
171
+ if (!this->try_read_(5, LOG_STR("read magic"))) {
172
+ return;
173
+ }
145
174
 
146
- if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
147
- return; // No data yet, try again next loop
175
+ // Validate magic bytes
176
+ static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
177
+ if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) {
178
+ ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0],
179
+ this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]);
180
+ this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC);
181
+ return;
182
+ }
183
+
184
+ // Magic bytes valid, move to next state
185
+ this->transition_ota_state_(OTAState::MAGIC_ACK);
186
+ this->handshake_buf_[0] = ota::OTA_RESPONSE_OK;
187
+ this->handshake_buf_[1] = USE_OTA_VERSION;
188
+ [[fallthrough]];
148
189
  }
149
190
 
150
- if (read <= 0) {
151
- // Error or connection closed
152
- if (read == -1) {
153
- this->log_socket_error_(LOG_STR("reading magic bytes"));
154
- } else {
155
- ESP_LOGW(TAG, "Remote closed during handshake");
191
+ case OTAState::MAGIC_ACK: {
192
+ // Send OK and version - 2 bytes
193
+ if (!this->try_write_(2, LOG_STR("ack magic"))) {
194
+ return;
156
195
  }
157
- this->cleanup_connection_();
158
- return;
196
+ // All bytes sent, create backend and move to next state
197
+ this->backend_ = ota::make_ota_backend();
198
+ this->transition_ota_state_(OTAState::FEATURE_READ);
199
+ [[fallthrough]];
159
200
  }
160
201
 
161
- this->magic_buf_pos_ += read;
162
- }
202
+ case OTAState::FEATURE_READ: {
203
+ // Read features - 1 byte
204
+ if (!this->try_read_(1, LOG_STR("read feature"))) {
205
+ return;
206
+ }
207
+ this->ota_features_ = this->handshake_buf_[0];
208
+ ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_);
209
+ this->transition_ota_state_(OTAState::FEATURE_ACK);
210
+ this->handshake_buf_[0] =
211
+ ((this->ota_features_ & FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression())
212
+ ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION
213
+ : ota::OTA_RESPONSE_HEADER_OK;
214
+ [[fallthrough]];
215
+ }
163
216
 
164
- // Check if we have all 5 magic bytes
165
- if (this->magic_buf_pos_ == 5) {
166
- // Validate magic bytes
167
- static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
168
- if (memcmp(this->magic_buf_, MAGIC_BYTES, 5) != 0) {
169
- ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->magic_buf_[0],
170
- this->magic_buf_[1], this->magic_buf_[2], this->magic_buf_[3], this->magic_buf_[4]);
171
- // Send error response (non-blocking, best effort)
172
- uint8_t error = static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_MAGIC);
173
- this->client_->write(&error, 1);
174
- this->cleanup_connection_();
175
- return;
217
+ case OTAState::FEATURE_ACK: {
218
+ // Acknowledge header - 1 byte
219
+ if (!this->try_write_(1, LOG_STR("ack feature"))) {
220
+ return;
221
+ }
222
+ #ifdef USE_OTA_PASSWORD
223
+ // If password is set, move to auth phase
224
+ if (!this->password_.empty()) {
225
+ this->transition_ota_state_(OTAState::AUTH_SEND);
226
+ } else
227
+ #endif
228
+ {
229
+ // No password, move directly to data phase
230
+ this->transition_ota_state_(OTAState::DATA);
231
+ }
232
+ [[fallthrough]];
233
+ }
234
+
235
+ #ifdef USE_OTA_PASSWORD
236
+ case OTAState::AUTH_SEND: {
237
+ // Non-blocking authentication send
238
+ if (!this->handle_auth_send_()) {
239
+ return;
240
+ }
241
+ this->transition_ota_state_(OTAState::AUTH_READ);
242
+ [[fallthrough]];
243
+ }
244
+
245
+ case OTAState::AUTH_READ: {
246
+ // Non-blocking authentication read & verify
247
+ if (!this->handle_auth_read_()) {
248
+ return;
249
+ }
250
+ this->transition_ota_state_(OTAState::DATA);
251
+ [[fallthrough]];
176
252
  }
253
+ #endif
254
+
255
+ case OTAState::DATA:
256
+ this->handle_data_();
257
+ return;
177
258
 
178
- // All 5 magic bytes are valid, continue with data handling
179
- this->handle_data_();
259
+ default:
260
+ break;
180
261
  }
181
262
  }
182
263
 
@@ -184,104 +265,21 @@ void ESPHomeOTAComponent::handle_data_() {
184
265
  /// Handle the OTA data transfer and update process.
185
266
  ///
186
267
  /// This method is blocking and will not return until the OTA update completes,
187
- /// fails, or times out. It handles authentication, receives the firmware data,
188
- /// writes it to flash, and reboots on success.
268
+ /// fails, or times out. It receives the firmware data, writes it to flash,
269
+ /// and reboots on success.
270
+ ///
271
+ /// Authentication has already been handled in the non-blocking states AUTH_SEND/AUTH_READ.
189
272
  ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
190
273
  bool update_started = false;
191
274
  size_t total = 0;
192
275
  uint32_t last_progress = 0;
193
- uint8_t buf[1024];
276
+ uint8_t buf[OTA_BUFFER_SIZE];
194
277
  char *sbuf = reinterpret_cast<char *>(buf);
195
278
  size_t ota_size;
196
- uint8_t ota_features;
197
- std::unique_ptr<ota::OTABackend> backend;
198
- (void) ota_features;
199
279
  #if USE_OTA_VERSION == 2
200
280
  size_t size_acknowledged = 0;
201
281
  #endif
202
282
 
203
- // Send OK and version - 2 bytes
204
- buf[0] = ota::OTA_RESPONSE_OK;
205
- buf[1] = USE_OTA_VERSION;
206
- this->writeall_(buf, 2);
207
-
208
- backend = ota::make_ota_backend();
209
-
210
- // Read features - 1 byte
211
- if (!this->readall_(buf, 1)) {
212
- this->log_read_error_(LOG_STR("features"));
213
- goto error; // NOLINT(cppcoreguidelines-avoid-goto)
214
- }
215
- ota_features = buf[0]; // NOLINT
216
- ESP_LOGV(TAG, "Features: 0x%02X", ota_features);
217
-
218
- // Acknowledge header - 1 byte
219
- buf[0] = ota::OTA_RESPONSE_HEADER_OK;
220
- if ((ota_features & FEATURE_SUPPORTS_COMPRESSION) != 0 && backend->supports_compression()) {
221
- buf[0] = ota::OTA_RESPONSE_SUPPORTS_COMPRESSION;
222
- }
223
-
224
- this->writeall_(buf, 1);
225
-
226
- #ifdef USE_OTA_PASSWORD
227
- if (!this->password_.empty()) {
228
- buf[0] = ota::OTA_RESPONSE_REQUEST_AUTH;
229
- this->writeall_(buf, 1);
230
- md5::MD5Digest md5{};
231
- md5.init();
232
- sprintf(sbuf, "%08" PRIx32, random_uint32());
233
- md5.add(sbuf, 8);
234
- md5.calculate();
235
- md5.get_hex(sbuf);
236
- ESP_LOGV(TAG, "Auth: Nonce is %s", sbuf);
237
-
238
- // Send nonce, 32 bytes hex MD5
239
- if (!this->writeall_(reinterpret_cast<uint8_t *>(sbuf), 32)) {
240
- ESP_LOGW(TAG, "Auth: Writing nonce failed");
241
- goto error; // NOLINT(cppcoreguidelines-avoid-goto)
242
- }
243
-
244
- // prepare challenge
245
- md5.init();
246
- md5.add(this->password_.c_str(), this->password_.length());
247
- // add nonce
248
- md5.add(sbuf, 32);
249
-
250
- // Receive cnonce, 32 bytes hex MD5
251
- if (!this->readall_(buf, 32)) {
252
- ESP_LOGW(TAG, "Auth: Reading cnonce failed");
253
- goto error; // NOLINT(cppcoreguidelines-avoid-goto)
254
- }
255
- sbuf[32] = '\0';
256
- ESP_LOGV(TAG, "Auth: CNonce is %s", sbuf);
257
- // add cnonce
258
- md5.add(sbuf, 32);
259
-
260
- // calculate result
261
- md5.calculate();
262
- md5.get_hex(sbuf);
263
- ESP_LOGV(TAG, "Auth: Result is %s", sbuf);
264
-
265
- // Receive result, 32 bytes hex MD5
266
- if (!this->readall_(buf + 64, 32)) {
267
- ESP_LOGW(TAG, "Auth: Reading response failed");
268
- goto error; // NOLINT(cppcoreguidelines-avoid-goto)
269
- }
270
- sbuf[64 + 32] = '\0';
271
- ESP_LOGV(TAG, "Auth: Response is %s", sbuf + 64);
272
-
273
- bool matches = true;
274
- for (uint8_t i = 0; i < 32; i++)
275
- matches = matches && buf[i] == buf[64 + i];
276
-
277
- if (!matches) {
278
- ESP_LOGW(TAG, "Auth failed! Passwords do not match");
279
- error_code = ota::OTA_RESPONSE_ERROR_AUTH_INVALID;
280
- goto error; // NOLINT(cppcoreguidelines-avoid-goto)
281
- }
282
- }
283
- #endif // USE_OTA_PASSWORD
284
-
285
283
  // Acknowledge auth OK - 1 byte
286
284
  buf[0] = ota::OTA_RESPONSE_AUTH_OK;
287
285
  this->writeall_(buf, 1);
@@ -309,7 +307,7 @@ void ESPHomeOTAComponent::handle_data_() {
309
307
  #endif
310
308
 
311
309
  // This will block for a few seconds as it locks flash
312
- error_code = backend->begin(ota_size);
310
+ error_code = this->backend_->begin(ota_size);
313
311
  if (error_code != ota::OTA_RESPONSE_OK)
314
312
  goto error; // NOLINT(cppcoreguidelines-avoid-goto)
315
313
  update_started = true;
@@ -325,7 +323,7 @@ void ESPHomeOTAComponent::handle_data_() {
325
323
  }
326
324
  sbuf[32] = '\0';
327
325
  ESP_LOGV(TAG, "Update: Binary MD5 is %s", sbuf);
328
- backend->set_update_md5(sbuf);
326
+ this->backend_->set_update_md5(sbuf);
329
327
 
330
328
  // Acknowledge MD5 OK - 1 byte
331
329
  buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK;
@@ -333,26 +331,24 @@ void ESPHomeOTAComponent::handle_data_() {
333
331
 
334
332
  while (total < ota_size) {
335
333
  // TODO: timeout check
336
- size_t requested = std::min(sizeof(buf), ota_size - total);
334
+ size_t remaining = ota_size - total;
335
+ size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
337
336
  ssize_t read = this->client_->read(buf, requested);
338
337
  if (read == -1) {
339
- if (errno == EAGAIN || errno == EWOULDBLOCK) {
338
+ if (this->would_block_(errno)) {
340
339
  this->yield_and_feed_watchdog_();
341
340
  continue;
342
341
  }
343
- ESP_LOGW(TAG, "Read error, errno %d", errno);
342
+ ESP_LOGW(TAG, "Read err %d", errno);
344
343
  goto error; // NOLINT(cppcoreguidelines-avoid-goto)
345
344
  } else if (read == 0) {
346
- // $ man recv
347
- // "When a stream socket peer has performed an orderly shutdown, the return value will
348
- // be 0 (the traditional "end-of-file" return)."
349
- ESP_LOGW(TAG, "Remote closed connection");
345
+ ESP_LOGW(TAG, "Remote closed");
350
346
  goto error; // NOLINT(cppcoreguidelines-avoid-goto)
351
347
  }
352
348
 
353
- error_code = backend->write(buf, read);
349
+ error_code = this->backend_->write(buf, read);
354
350
  if (error_code != ota::OTA_RESPONSE_OK) {
355
- ESP_LOGW(TAG, "Flash write error, code: %d", error_code);
351
+ ESP_LOGW(TAG, "Flash write err %d", error_code);
356
352
  goto error; // NOLINT(cppcoreguidelines-avoid-goto)
357
353
  }
358
354
  total += read;
@@ -381,9 +377,9 @@ void ESPHomeOTAComponent::handle_data_() {
381
377
  buf[0] = ota::OTA_RESPONSE_RECEIVE_OK;
382
378
  this->writeall_(buf, 1);
383
379
 
384
- error_code = backend->end();
380
+ error_code = this->backend_->end();
385
381
  if (error_code != ota::OTA_RESPONSE_OK) {
386
- ESP_LOGW(TAG, "Error ending update! code: %d", error_code);
382
+ ESP_LOGW(TAG, "End update err %d", error_code);
387
383
  goto error; // NOLINT(cppcoreguidelines-avoid-goto)
388
384
  }
389
385
 
@@ -412,8 +408,8 @@ error:
412
408
  this->writeall_(buf, 1);
413
409
  this->cleanup_connection_();
414
410
 
415
- if (backend != nullptr && update_started) {
416
- backend->abort();
411
+ if (this->backend_ != nullptr && update_started) {
412
+ this->backend_->abort();
417
413
  }
418
414
 
419
415
  this->status_momentary_error("onerror", 5000);
@@ -434,12 +430,12 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
434
430
 
435
431
  ssize_t read = this->client_->read(buf + at, len - at);
436
432
  if (read == -1) {
437
- if (errno != EAGAIN && errno != EWOULDBLOCK) {
438
- ESP_LOGW(TAG, "Error reading %d bytes, errno %d", len, errno);
433
+ if (!this->would_block_(errno)) {
434
+ ESP_LOGW(TAG, "Read err %d bytes, errno %d", len, errno);
439
435
  return false;
440
436
  }
441
437
  } else if (read == 0) {
442
- ESP_LOGW(TAG, "Remote closed connection");
438
+ ESP_LOGW(TAG, "Remote closed");
443
439
  return false;
444
440
  } else {
445
441
  at += read;
@@ -461,8 +457,8 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
461
457
 
462
458
  ssize_t written = this->client_->write(buf + at, len - at);
463
459
  if (written == -1) {
464
- if (errno != EAGAIN && errno != EWOULDBLOCK) {
465
- ESP_LOGW(TAG, "Error writing %d bytes, errno %d", len, errno);
460
+ if (!this->would_block_(errno)) {
461
+ ESP_LOGW(TAG, "Write err %d bytes, errno %d", len, errno);
466
462
  return false;
467
463
  }
468
464
  } else {
@@ -487,11 +483,74 @@ void ESPHomeOTAComponent::log_start_(const LogString *phase) {
487
483
  ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), this->client_->getpeername().c_str());
488
484
  }
489
485
 
486
+ void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) {
487
+ ESP_LOGW(TAG, "Remote closed at %s", LOG_STR_ARG(during));
488
+ }
489
+
490
+ bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *desc) {
491
+ if (read == -1 && this->would_block_(errno)) {
492
+ return false; // No data yet, try again next loop
493
+ }
494
+
495
+ if (read <= 0) {
496
+ read == 0 ? this->log_remote_closed_(desc) : this->log_socket_error_(desc);
497
+ this->cleanup_connection_();
498
+ return false;
499
+ }
500
+ return true;
501
+ }
502
+
503
+ bool ESPHomeOTAComponent::handle_write_error_(ssize_t written, const LogString *desc) {
504
+ if (written == -1) {
505
+ if (this->would_block_(errno)) {
506
+ return false; // Try again next loop
507
+ }
508
+ this->log_socket_error_(desc);
509
+ this->cleanup_connection_();
510
+ return false;
511
+ }
512
+ return true;
513
+ }
514
+
515
+ bool ESPHomeOTAComponent::try_read_(size_t to_read, const LogString *desc) {
516
+ // Read bytes into handshake buffer, starting at handshake_buf_pos_
517
+ size_t bytes_to_read = to_read - this->handshake_buf_pos_;
518
+ ssize_t read = this->client_->read(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_read);
519
+
520
+ if (!this->handle_read_error_(read, desc)) {
521
+ return false;
522
+ }
523
+
524
+ this->handshake_buf_pos_ += read;
525
+ // Return true only if we have all the requested bytes
526
+ return this->handshake_buf_pos_ >= to_read;
527
+ }
528
+
529
+ bool ESPHomeOTAComponent::try_write_(size_t to_write, const LogString *desc) {
530
+ // Write bytes from handshake buffer, starting at handshake_buf_pos_
531
+ size_t bytes_to_write = to_write - this->handshake_buf_pos_;
532
+ ssize_t written = this->client_->write(this->handshake_buf_ + this->handshake_buf_pos_, bytes_to_write);
533
+
534
+ if (!this->handle_write_error_(written, desc)) {
535
+ return false;
536
+ }
537
+
538
+ this->handshake_buf_pos_ += written;
539
+ // Return true only if we have written all the requested bytes
540
+ return this->handshake_buf_pos_ >= to_write;
541
+ }
542
+
490
543
  void ESPHomeOTAComponent::cleanup_connection_() {
491
544
  this->client_->close();
492
545
  this->client_ = nullptr;
493
546
  this->client_connect_time_ = 0;
494
- this->magic_buf_pos_ = 0;
547
+ this->handshake_buf_pos_ = 0;
548
+ this->ota_state_ = OTAState::IDLE;
549
+ this->ota_features_ = 0;
550
+ this->backend_ = nullptr;
551
+ #ifdef USE_OTA_PASSWORD
552
+ this->cleanup_auth_();
553
+ #endif
495
554
  }
496
555
 
497
556
  void ESPHomeOTAComponent::yield_and_feed_watchdog_() {
@@ -499,5 +558,256 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() {
499
558
  delay(1);
500
559
  }
501
560
 
561
+ #ifdef USE_OTA_PASSWORD
562
+ void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); }
563
+
564
+ bool ESPHomeOTAComponent::select_auth_type_() {
565
+ #ifdef USE_OTA_SHA256
566
+ bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0;
567
+
568
+ #ifdef ALLOW_OTA_DOWNGRADE_MD5
569
+ // Allow fallback to MD5 if client doesn't support SHA256
570
+ if (client_supports_sha256) {
571
+ this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH;
572
+ return true;
573
+ }
574
+ #ifdef USE_OTA_MD5
575
+ this->log_auth_warning_(LOG_STR("Using deprecated MD5"));
576
+ this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH;
577
+ return true;
578
+ #else
579
+ this->log_auth_warning_(LOG_STR("SHA256 required"));
580
+ this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
581
+ return false;
582
+ #endif // USE_OTA_MD5
583
+
584
+ #else // !ALLOW_OTA_DOWNGRADE_MD5
585
+ // Require SHA256
586
+ if (!client_supports_sha256) {
587
+ this->log_auth_warning_(LOG_STR("SHA256 required"));
588
+ this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
589
+ return false;
590
+ }
591
+ this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH;
592
+ return true;
593
+ #endif // ALLOW_OTA_DOWNGRADE_MD5
594
+
595
+ #else // !USE_OTA_SHA256
596
+ #ifdef USE_OTA_MD5
597
+ // Only MD5 available
598
+ this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH;
599
+ return true;
600
+ #else
601
+ // No auth methods available
602
+ this->log_auth_warning_(LOG_STR("No auth methods available"));
603
+ this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
604
+ return false;
605
+ #endif // USE_OTA_MD5
606
+ #endif // USE_OTA_SHA256
607
+ }
608
+
609
+ bool ESPHomeOTAComponent::handle_auth_send_() {
610
+ // Initialize auth buffer if not already done
611
+ if (!this->auth_buf_) {
612
+ // Select auth type based on client capabilities and configuration
613
+ if (!this->select_auth_type_()) {
614
+ return false;
615
+ }
616
+
617
+ // Generate nonce - hasher must be created and used in same stack frame
618
+ // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS:
619
+ // 1. Hash objects must NEVER be passed to another function (different stack frame)
620
+ // 2. NO Variable Length Arrays (VLAs) - they corrupt the stack with hardware DMA
621
+ // 3. All hash operations (init/add/calculate) must happen in the SAME function where object is created
622
+ // Violating these causes truncated hash output (20 bytes instead of 32) or memory corruption.
623
+ //
624
+ // Buffer layout after AUTH_READ completes:
625
+ // [0]: auth_type (1 byte)
626
+ // [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND
627
+ // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
628
+ // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
629
+
630
+ // Declare both hash objects in same stack frame, use pointer to select.
631
+ // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3
632
+ // hardware SHA acceleration - the object must exist in this stack frame for all operations.
633
+ // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope.
634
+ #ifdef USE_OTA_SHA256
635
+ sha256::SHA256 sha_hasher;
636
+ #endif
637
+ #ifdef USE_OTA_MD5
638
+ md5::MD5Digest md5_hasher;
639
+ #endif
640
+ HashBase *hasher = nullptr;
641
+
642
+ #ifdef USE_OTA_SHA256
643
+ if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
644
+ hasher = &sha_hasher;
645
+ }
646
+ #endif
647
+ #ifdef USE_OTA_MD5
648
+ if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
649
+ hasher = &md5_hasher;
650
+ }
651
+ #endif
652
+
653
+ const size_t hex_size = hasher->get_size() * 2;
654
+ const size_t nonce_len = hasher->get_size() / 4;
655
+ const size_t auth_buf_size = 1 + 3 * hex_size;
656
+ this->auth_buf_ = std::make_unique<uint8_t[]>(auth_buf_size);
657
+ this->auth_buf_pos_ = 0;
658
+
659
+ char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
660
+ if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
661
+ this->log_auth_warning_(LOG_STR("Random failed"));
662
+ this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN);
663
+ return false;
664
+ }
665
+
666
+ hasher->init();
667
+ hasher->add(buf, nonce_len);
668
+ hasher->calculate();
669
+ this->auth_buf_[0] = this->auth_type_;
670
+ hasher->get_hex(buf);
671
+
672
+ #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
673
+ char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
674
+ memcpy(log_buf, buf, hex_size);
675
+ log_buf[hex_size] = '\0';
676
+ ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf);
677
+ #endif
678
+ }
679
+
680
+ // Try to write auth_type + nonce
681
+ size_t hex_size = this->get_auth_hex_size_();
682
+ const size_t to_write = 1 + hex_size;
683
+ size_t remaining = to_write - this->auth_buf_pos_;
684
+
685
+ ssize_t written = this->client_->write(this->auth_buf_.get() + this->auth_buf_pos_, remaining);
686
+ if (!this->handle_write_error_(written, LOG_STR("ack auth"))) {
687
+ return false;
688
+ }
689
+
690
+ this->auth_buf_pos_ += written;
691
+
692
+ // Check if we still have more to write
693
+ if (this->auth_buf_pos_ < to_write) {
694
+ return false; // More to write, try again next loop
695
+ }
696
+
697
+ // All written, prepare for reading phase
698
+ this->auth_buf_pos_ = 0;
699
+ return true;
700
+ }
701
+
702
+ bool ESPHomeOTAComponent::handle_auth_read_() {
703
+ size_t hex_size = this->get_auth_hex_size_();
704
+ const size_t to_read = hex_size * 2; // CNonce + Response
705
+
706
+ // Try to read remaining bytes (CNonce + Response)
707
+ // We read cnonce+response starting at offset 1+hex_size (after auth_type and our nonce)
708
+ size_t cnonce_offset = 1 + hex_size; // Offset where cnonce should be stored in buffer
709
+ size_t remaining = to_read - this->auth_buf_pos_;
710
+ ssize_t read = this->client_->read(this->auth_buf_.get() + cnonce_offset + this->auth_buf_pos_, remaining);
711
+
712
+ if (!this->handle_read_error_(read, LOG_STR("read auth"))) {
713
+ return false;
714
+ }
715
+
716
+ this->auth_buf_pos_ += read;
717
+
718
+ // Check if we still need more data
719
+ if (this->auth_buf_pos_ < to_read) {
720
+ return false; // More to read, try again next loop
721
+ }
722
+
723
+ // We have all the data, verify it
724
+ const char *nonce = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
725
+ const char *cnonce = nonce + hex_size;
726
+ const char *response = cnonce + hex_size;
727
+
728
+ // CRITICAL ESP32-S3: Hash objects must stay in same stack frame (no passing to other functions).
729
+ // Declare both hash objects in same stack frame, use pointer to select.
730
+ // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3
731
+ // hardware SHA acceleration - the object must exist in this stack frame for all operations.
732
+ // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope.
733
+ #ifdef USE_OTA_SHA256
734
+ sha256::SHA256 sha_hasher;
735
+ #endif
736
+ #ifdef USE_OTA_MD5
737
+ md5::MD5Digest md5_hasher;
738
+ #endif
739
+ HashBase *hasher = nullptr;
740
+
741
+ #ifdef USE_OTA_SHA256
742
+ if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
743
+ hasher = &sha_hasher;
744
+ }
745
+ #endif
746
+ #ifdef USE_OTA_MD5
747
+ if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
748
+ hasher = &md5_hasher;
749
+ }
750
+ #endif
751
+
752
+ hasher->init();
753
+ hasher->add(this->password_.c_str(), this->password_.length());
754
+ hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
755
+ hasher->calculate();
756
+
757
+ #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
758
+ char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
759
+ // Log CNonce
760
+ memcpy(log_buf, cnonce, hex_size);
761
+ log_buf[hex_size] = '\0';
762
+ ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf);
763
+
764
+ // Log computed hash
765
+ hasher->get_hex(log_buf);
766
+ log_buf[hex_size] = '\0';
767
+ ESP_LOGV(TAG, "Auth: Result is %s", log_buf);
768
+
769
+ // Log received response
770
+ memcpy(log_buf, response, hex_size);
771
+ log_buf[hex_size] = '\0';
772
+ ESP_LOGV(TAG, "Auth: Response is %s", log_buf);
773
+ #endif
774
+
775
+ // Compare response
776
+ bool matches = hasher->equals_hex(response);
777
+
778
+ if (!matches) {
779
+ this->log_auth_warning_(LOG_STR("Password mismatch"));
780
+ this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
781
+ return false;
782
+ }
783
+
784
+ // Authentication successful - clean up auth state
785
+ this->cleanup_auth_();
786
+
787
+ return true;
788
+ }
789
+
790
+ size_t ESPHomeOTAComponent::get_auth_hex_size_() const {
791
+ #ifdef USE_OTA_SHA256
792
+ if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
793
+ return SHA256_HEX_SIZE;
794
+ }
795
+ #endif
796
+ #ifdef USE_OTA_MD5
797
+ return MD5_HEX_SIZE;
798
+ #else
799
+ #ifndef USE_OTA_SHA256
800
+ #error "Either USE_OTA_MD5 or USE_OTA_SHA256 must be defined when USE_OTA_PASSWORD is enabled"
801
+ #endif
802
+ #endif
803
+ }
804
+
805
+ void ESPHomeOTAComponent::cleanup_auth_() {
806
+ this->auth_buf_ = nullptr;
807
+ this->auth_buf_pos_ = 0;
808
+ this->auth_type_ = 0;
809
+ }
810
+ #endif // USE_OTA_PASSWORD
811
+
502
812
  } // namespace esphome
503
813
  #endif