esphome 2025.4.2__py3-none-any.whl → 2025.5.0b3__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 (440) hide show
  1. esphome/__main__.py +16 -14
  2. esphome/components/ac_dimmer/ac_dimmer.cpp +3 -2
  3. esphome/components/adc/__init__.py +51 -34
  4. esphome/components/airthings_wave_base/__init__.py +1 -1
  5. esphome/components/alarm_control_panel/__init__.py +37 -2
  6. esphome/components/am43/cover/__init__.py +4 -5
  7. esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp +6 -4
  8. esphome/components/analog_threshold/analog_threshold_binary_sensor.h +4 -5
  9. esphome/components/analog_threshold/binary_sensor.py +10 -8
  10. esphome/components/anova/climate.py +4 -5
  11. esphome/components/api/__init__.py +25 -8
  12. esphome/components/api/api_connection.cpp +81 -13
  13. esphome/components/api/api_connection.h +13 -1
  14. esphome/components/api/api_frame_helper.cpp +232 -177
  15. esphome/components/api/api_frame_helper.h +61 -8
  16. esphome/components/api/api_noise_context.h +13 -4
  17. esphome/components/api/api_pb2.cpp +1422 -1
  18. esphome/components/api/api_pb2.h +255 -1
  19. esphome/components/api/api_pb2_service.cpp +162 -49
  20. esphome/components/api/api_pb2_service.h +90 -51
  21. esphome/components/api/api_pb2_size.h +361 -0
  22. esphome/components/api/api_server.cpp +110 -34
  23. esphome/components/api/api_server.h +8 -0
  24. esphome/components/api/proto.h +86 -17
  25. esphome/components/as7341/as7341.h +1 -1
  26. esphome/components/atm90e32/__init__.py +1 -0
  27. esphome/components/atm90e32/atm90e32.cpp +576 -199
  28. esphome/components/atm90e32/atm90e32.h +128 -31
  29. esphome/components/atm90e32/atm90e32_reg.h +4 -2
  30. esphome/components/atm90e32/button/__init__.py +62 -10
  31. esphome/components/atm90e32/button/atm90e32_button.cpp +63 -4
  32. esphome/components/atm90e32/button/atm90e32_button.h +36 -4
  33. esphome/components/atm90e32/number/__init__.py +130 -0
  34. esphome/components/atm90e32/number/atm90e32_number.h +16 -0
  35. esphome/components/atm90e32/sensor.py +21 -4
  36. esphome/components/atm90e32/text_sensor/__init__.py +48 -0
  37. esphome/components/audio/__init__.py +96 -49
  38. esphome/components/audio/audio.h +48 -0
  39. esphome/components/audio/audio_decoder.cpp +1 -1
  40. esphome/components/audio/audio_resampler.cpp +2 -0
  41. esphome/components/audio/audio_resampler.h +1 -0
  42. esphome/components/ballu/climate.py +2 -9
  43. esphome/components/bang_bang/climate.py +5 -6
  44. esphome/components/bedjet/bedjet_hub.cpp +1 -0
  45. esphome/components/bedjet/climate/__init__.py +3 -8
  46. esphome/components/bedjet/fan/__init__.py +2 -11
  47. esphome/components/binary/fan/__init__.py +13 -16
  48. esphome/components/binary_sensor/__init__.py +13 -10
  49. esphome/components/bl0906/constants.h +16 -16
  50. esphome/components/ble_client/text_sensor/__init__.py +3 -5
  51. esphome/components/bluetooth_proxy/bluetooth_connection.cpp +4 -6
  52. esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +136 -21
  53. esphome/components/bluetooth_proxy/bluetooth_proxy.h +7 -0
  54. esphome/components/button/__init__.py +11 -8
  55. esphome/components/canbus/canbus.cpp +3 -0
  56. esphome/components/canbus/canbus.h +16 -0
  57. esphome/components/ccs811/sensor.py +9 -6
  58. esphome/components/climate/__init__.py +35 -2
  59. esphome/components/climate/climate_mode.h +1 -1
  60. esphome/components/climate/climate_traits.h +63 -57
  61. esphome/components/climate_ir/__init__.py +57 -17
  62. esphome/components/climate_ir_lg/climate.py +2 -5
  63. esphome/components/climate_ir_lg/climate_ir_lg.cpp +7 -7
  64. esphome/components/climate_ir_lg/climate_ir_lg.h +1 -1
  65. esphome/components/color/__init__.py +2 -0
  66. esphome/components/const/__init__.py +5 -0
  67. esphome/components/coolix/climate.py +2 -9
  68. esphome/components/copy/cover/__init__.py +10 -9
  69. esphome/components/copy/fan/__init__.py +11 -9
  70. esphome/components/copy/lock/__init__.py +11 -9
  71. esphome/components/copy/text/__init__.py +9 -6
  72. esphome/components/cover/__init__.py +37 -2
  73. esphome/components/cse7766/cse7766.cpp +2 -1
  74. esphome/components/cst226/binary_sensor/__init__.py +28 -0
  75. esphome/components/cst226/binary_sensor/cs226_button.h +22 -0
  76. esphome/components/cst226/binary_sensor/cstt6_button.cpp +19 -0
  77. esphome/components/cst226/touchscreen/cst226_touchscreen.cpp +27 -5
  78. esphome/components/cst226/touchscreen/cst226_touchscreen.h +10 -10
  79. esphome/components/current_based/cover.py +37 -36
  80. esphome/components/current_based/current_based_cover.cpp +2 -1
  81. esphome/components/daikin/climate.py +2 -9
  82. esphome/components/daikin/daikin.cpp +15 -9
  83. esphome/components/daikin/daikin.h +5 -5
  84. esphome/components/daikin_arc/climate.py +2 -7
  85. esphome/components/daikin_brc/climate.py +3 -5
  86. esphome/components/dallas_temp/dallas_temp.cpp +17 -24
  87. esphome/components/dallas_temp/dallas_temp.h +0 -1
  88. esphome/components/daly_bms/daly_bms.cpp +2 -1
  89. esphome/components/debug/debug_component.cpp +6 -1
  90. esphome/components/debug/debug_component.h +6 -0
  91. esphome/components/debug/debug_esp32.cpp +109 -254
  92. esphome/components/debug/sensor.py +14 -0
  93. esphome/components/deep_sleep/deep_sleep_esp32.cpp +13 -1
  94. esphome/components/delonghi/climate.py +2 -9
  95. esphome/components/demo/__init__.py +18 -20
  96. esphome/components/dfrobot_sen0395/switch/__init__.py +21 -22
  97. esphome/components/dps310/sensor.py +6 -6
  98. esphome/components/ee895/sensor.py +9 -9
  99. esphome/components/emmeti/climate.py +2 -9
  100. esphome/components/endstop/cover.py +17 -16
  101. esphome/components/endstop/endstop_cover.cpp +2 -1
  102. esphome/components/ens160_base/__init__.py +12 -9
  103. esphome/components/esp32/__init__.py +60 -3
  104. esphome/components/esp32/core.cpp +11 -5
  105. esphome/components/esp32/gpio.cpp +86 -24
  106. esphome/components/esp32/gpio.py +15 -16
  107. esphome/components/esp32/gpio_esp32.py +1 -2
  108. esphome/components/esp32/gpio_esp32_c2.py +1 -1
  109. esphome/components/esp32/gpio_esp32_c3.py +1 -1
  110. esphome/components/esp32/gpio_esp32_c6.py +1 -1
  111. esphome/components/esp32/gpio_esp32_h2.py +1 -1
  112. esphome/components/esp32_ble/ble.cpp +1 -0
  113. esphome/components/esp32_ble/ble.h +5 -3
  114. esphome/components/esp32_ble/ble_advertising.cpp +2 -1
  115. esphome/components/esp32_ble/ble_advertising.h +1 -0
  116. esphome/components/esp32_ble_server/__init__.py +3 -0
  117. esphome/components/esp32_ble_tracker/__init__.py +7 -1
  118. esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +192 -118
  119. esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +29 -3
  120. esphome/components/esp32_camera/esp32_camera.cpp +2 -1
  121. esphome/components/esp32_camera/esp32_camera.h +1 -1
  122. esphome/components/esp32_can/esp32_can.cpp +1 -1
  123. esphome/components/esp32_improv/esp32_improv_component.cpp +1 -1
  124. esphome/components/esp32_rmt_led_strip/led_strip.cpp +1 -1
  125. esphome/components/esp32_rmt_led_strip/led_strip.h +7 -5
  126. esphome/components/esp32_rmt_led_strip/light.py +9 -1
  127. esphome/components/esp32_touch/esp32_touch.cpp +1 -1
  128. esphome/components/esp8266/gpio.cpp +69 -8
  129. esphome/components/ethernet/ethernet_component.cpp +1 -1
  130. esphome/components/event/__init__.py +13 -10
  131. esphome/components/factory_reset/switch/__init__.py +7 -21
  132. esphome/components/fan/__init__.py +52 -5
  133. esphome/components/fastled_base/__init__.py +1 -4
  134. esphome/components/fastled_base/fastled_light.cpp +1 -1
  135. esphome/components/feedback/cover.py +38 -33
  136. esphome/components/feedback/feedback_cover.cpp +2 -1
  137. esphome/components/fujitsu_general/climate.py +2 -9
  138. esphome/components/gcja5/gcja5.cpp +2 -1
  139. esphome/components/gpio/one_wire/gpio_one_wire.cpp +45 -43
  140. esphome/components/gpio/one_wire/gpio_one_wire.h +2 -1
  141. esphome/components/gpio_expander/cached_gpio.h +22 -7
  142. esphome/components/gps/__init__.py +47 -17
  143. esphome/components/gps/gps.cpp +42 -23
  144. esphome/components/gps/gps.h +17 -13
  145. esphome/components/graph/__init__.py +1 -2
  146. esphome/components/gree/climate.py +4 -6
  147. esphome/components/gree/gree.cpp +16 -2
  148. esphome/components/gree/gree.h +2 -2
  149. esphome/components/growatt_solar/growatt_solar.cpp +2 -1
  150. esphome/components/haier/climate.py +37 -34
  151. esphome/components/hbridge/fan/__init__.py +19 -17
  152. esphome/components/he60r/cover.py +4 -5
  153. esphome/components/heatpumpir/climate.py +3 -6
  154. esphome/components/hitachi_ac344/climate.py +2 -9
  155. esphome/components/hitachi_ac424/climate.py +2 -9
  156. esphome/components/hm3301/hm3301.h +1 -1
  157. esphome/components/hte501/sensor.py +6 -6
  158. esphome/components/http_request/__init__.py +39 -6
  159. esphome/components/http_request/http_request.cpp +20 -0
  160. esphome/components/http_request/http_request.h +57 -15
  161. esphome/components/http_request/http_request_arduino.cpp +22 -6
  162. esphome/components/http_request/http_request_arduino.h +4 -3
  163. esphome/components/http_request/http_request_host.cpp +141 -0
  164. esphome/components/http_request/http_request_host.h +37 -0
  165. esphome/components/http_request/http_request_idf.cpp +35 -3
  166. esphome/components/http_request/http_request_idf.h +10 -3
  167. esphome/components/http_request/httplib.h +9691 -0
  168. esphome/components/http_request/update/__init__.py +11 -8
  169. esphome/components/hyt271/sensor.py +6 -6
  170. esphome/components/i2c/i2c.h +4 -0
  171. esphome/components/i2c/i2c_bus_esp_idf.cpp +1 -1
  172. esphome/components/i2s_audio/__init__.py +131 -22
  173. esphome/components/i2s_audio/i2s_audio.h +44 -4
  174. esphome/components/i2s_audio/media_player/__init__.py +19 -9
  175. esphome/components/i2s_audio/microphone/__init__.py +63 -5
  176. esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +351 -61
  177. esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +40 -6
  178. esphome/components/i2s_audio/speaker/__init__.py +31 -5
  179. esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +155 -19
  180. esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +17 -4
  181. esphome/components/ili9xxx/ili9xxx_init.h +1 -1
  182. esphome/components/image/__init__.py +37 -17
  183. esphome/components/image/image.cpp +25 -8
  184. esphome/components/internal_temperature/internal_temperature.cpp +6 -4
  185. esphome/components/key_collector/__init__.py +35 -0
  186. esphome/components/key_collector/key_collector.cpp +8 -0
  187. esphome/components/key_collector/key_collector.h +10 -0
  188. esphome/components/kuntze/kuntze.cpp +2 -1
  189. esphome/components/ld2410/ld2410.h +1 -1
  190. esphome/components/ld2450/ld2450.h +1 -1
  191. esphome/components/light/__init__.py +57 -0
  192. esphome/components/lock/__init__.py +51 -4
  193. esphome/components/lock/automation.h +2 -13
  194. esphome/components/logger/__init__.py +22 -0
  195. esphome/components/logger/logger.cpp +154 -103
  196. esphome/components/logger/logger.h +211 -36
  197. esphome/components/logger/task_log_buffer.cpp +138 -0
  198. esphome/components/logger/task_log_buffer.h +69 -0
  199. esphome/components/lvgl/__init__.py +13 -5
  200. esphome/components/lvgl/automation.py +50 -1
  201. esphome/components/lvgl/defines.py +0 -1
  202. esphome/components/lvgl/lvgl_esphome.cpp +5 -1
  203. esphome/components/lvgl/text/__init__.py +1 -2
  204. esphome/components/mapping/__init__.py +134 -0
  205. esphome/components/matrix_keypad/matrix_keypad.cpp +2 -1
  206. esphome/components/max7219digit/max7219digit.cpp +28 -27
  207. esphome/components/mdns/__init__.py +11 -5
  208. esphome/components/mdns/mdns_component.cpp +11 -5
  209. esphome/components/mdns/mdns_component.h +3 -2
  210. esphome/components/mdns/mdns_esp32.cpp +4 -3
  211. esphome/components/mdns/mdns_esp8266.cpp +4 -2
  212. esphome/components/mdns/mdns_libretiny.cpp +4 -2
  213. esphome/components/mdns/mdns_rp2040.cpp +4 -2
  214. esphome/components/media_player/__init__.py +33 -1
  215. esphome/components/mhz19/sensor.py +11 -7
  216. esphome/components/micro_wake_word/__init__.py +99 -31
  217. esphome/components/micro_wake_word/automation.h +54 -0
  218. esphome/components/micro_wake_word/micro_wake_word.cpp +331 -319
  219. esphome/components/micro_wake_word/micro_wake_word.h +58 -105
  220. esphome/components/micro_wake_word/preprocessor_settings.h +19 -2
  221. esphome/components/micro_wake_word/streaming_model.cpp +158 -41
  222. esphome/components/micro_wake_word/streaming_model.h +85 -13
  223. esphome/components/microphone/__init__.py +139 -9
  224. esphome/components/microphone/automation.h +14 -2
  225. esphome/components/microphone/microphone.cpp +21 -0
  226. esphome/components/microphone/microphone.h +14 -5
  227. esphome/components/microphone/microphone_source.cpp +95 -0
  228. esphome/components/microphone/microphone_source.h +80 -0
  229. esphome/components/mics_4514/sensor.py +25 -14
  230. esphome/components/midea/climate.py +3 -4
  231. esphome/components/midea_ir/climate.py +3 -5
  232. esphome/components/mipi_spi/__init__.py +15 -0
  233. esphome/components/mipi_spi/display.py +474 -0
  234. esphome/components/mipi_spi/mipi_spi.cpp +481 -0
  235. esphome/components/mipi_spi/mipi_spi.h +171 -0
  236. esphome/components/mipi_spi/models/__init__.py +65 -0
  237. esphome/components/mipi_spi/models/amoled.py +72 -0
  238. esphome/components/mipi_spi/models/commands.py +82 -0
  239. esphome/components/mipi_spi/models/cyd.py +10 -0
  240. esphome/components/mipi_spi/models/ili.py +749 -0
  241. esphome/components/mipi_spi/models/jc.py +260 -0
  242. esphome/components/mipi_spi/models/lanbon.py +15 -0
  243. esphome/components/mipi_spi/models/lilygo.py +60 -0
  244. esphome/components/mipi_spi/models/waveshare.py +139 -0
  245. esphome/components/mitsubishi/climate.py +2 -5
  246. esphome/components/mitsubishi/mitsubishi.cpp +9 -9
  247. esphome/components/mixer/speaker/mixer_speaker.cpp +12 -22
  248. esphome/components/mixer/speaker/mixer_speaker.h +1 -3
  249. esphome/components/mlx90393/sensor.py +5 -0
  250. esphome/components/mlx90393/sensor_mlx90393.cpp +195 -13
  251. esphome/components/mlx90393/sensor_mlx90393.h +21 -4
  252. esphome/components/modbus/modbus.cpp +2 -1
  253. esphome/components/mqtt/__init__.py +1 -1
  254. esphome/components/mqtt/mqtt_client.cpp +6 -2
  255. esphome/components/mqtt/mqtt_const.h +4 -0
  256. esphome/components/mqtt/mqtt_fan.cpp +39 -0
  257. esphome/components/mqtt/mqtt_fan.h +2 -0
  258. esphome/components/ms5611/sensor.py +6 -6
  259. esphome/components/ms8607/sensor.py +3 -3
  260. esphome/components/network/__init__.py +1 -1
  261. esphome/components/nextion/base_component.py +17 -16
  262. esphome/components/nextion/display.py +11 -2
  263. esphome/components/nextion/nextion.cpp +39 -1
  264. esphome/components/nextion/nextion.h +50 -0
  265. esphome/components/noblex/climate.py +2 -9
  266. esphome/components/number/__init__.py +12 -9
  267. esphome/components/one_wire/one_wire_bus.cpp +14 -10
  268. esphome/components/one_wire/one_wire_bus.h +14 -8
  269. esphome/components/online_image/bmp_image.cpp +48 -11
  270. esphome/components/online_image/bmp_image.h +2 -0
  271. esphome/components/opentherm/binary_sensor/__init__.py +2 -4
  272. esphome/components/opentherm/number/__init__.py +11 -20
  273. esphome/components/opentherm/sensor/__init__.py +3 -3
  274. esphome/components/opentherm/switch/__init__.py +3 -5
  275. esphome/components/output/lock/__init__.py +11 -9
  276. esphome/components/packages/__init__.py +33 -31
  277. esphome/components/packet_transport/__init__.py +201 -0
  278. esphome/components/packet_transport/binary_sensor.py +19 -0
  279. esphome/components/packet_transport/packet_transport.cpp +534 -0
  280. esphome/components/packet_transport/packet_transport.h +154 -0
  281. esphome/components/packet_transport/sensor.py +19 -0
  282. esphome/components/pca9685/pca9685_output.cpp +2 -1
  283. esphome/components/pid/climate.py +2 -4
  284. esphome/components/pm2005/__init__.py +1 -0
  285. esphome/components/pm2005/pm2005.cpp +123 -0
  286. esphome/components/pm2005/pm2005.h +46 -0
  287. esphome/components/pm2005/sensor.py +86 -0
  288. esphome/components/pmsa003i/pmsa003i.cpp +43 -16
  289. esphome/components/pmsa003i/pmsa003i.h +25 -25
  290. esphome/components/pmsx003/pmsx003.cpp +195 -230
  291. esphome/components/pmsx003/pmsx003.h +51 -33
  292. esphome/components/pmsx003/sensor.py +21 -11
  293. esphome/components/pn7150/pn7150.h +2 -2
  294. esphome/components/pn7160/pn7160.h +2 -2
  295. esphome/components/prometheus/prometheus_handler.cpp +174 -0
  296. esphome/components/prometheus/prometheus_handler.h +17 -0
  297. esphome/components/psram/__init__.py +7 -5
  298. esphome/components/pulse_meter/pulse_meter_sensor.cpp +32 -12
  299. esphome/components/pulse_meter/pulse_meter_sensor.h +5 -5
  300. esphome/components/pzem004t/pzem004t.cpp +2 -1
  301. esphome/components/qspi_dbi/__init__.py +0 -1
  302. esphome/components/qspi_dbi/display.py +2 -1
  303. esphome/components/qspi_dbi/models.py +1 -2
  304. esphome/components/remote_base/__init__.py +91 -0
  305. esphome/components/remote_base/beo4_protocol.cpp +153 -0
  306. esphome/components/remote_base/beo4_protocol.h +43 -0
  307. esphome/components/remote_base/gobox_protocol.cpp +131 -0
  308. esphome/components/remote_base/gobox_protocol.h +54 -0
  309. esphome/components/remote_receiver/remote_receiver_esp32.cpp +16 -9
  310. esphome/components/resampler/speaker/resampler_speaker.cpp +12 -10
  311. esphome/components/resampler/speaker/resampler_speaker.h +1 -1
  312. esphome/components/rf_bridge/rf_bridge.cpp +2 -1
  313. esphome/components/scd30/sensor.py +2 -3
  314. esphome/components/scd4x/sensor.py +4 -5
  315. esphome/components/sdp3x/sensor.py +2 -1
  316. esphome/components/sds011/sds011.cpp +2 -1
  317. esphome/components/select/__init__.py +19 -20
  318. esphome/components/sen5x/sen5x.cpp +55 -36
  319. esphome/components/sen5x/sensor.py +1 -1
  320. esphome/components/senseair/sensor.py +3 -3
  321. esphome/components/sensor/__init__.py +158 -14
  322. esphome/components/sensor/filter.cpp +23 -0
  323. esphome/components/sensor/filter.h +22 -0
  324. esphome/components/sgp30/sensor.py +14 -16
  325. esphome/components/sgp4x/sensor.py +1 -1
  326. esphome/components/sht4x/sht4x.cpp +43 -22
  327. esphome/components/sht4x/sht4x.h +1 -1
  328. esphome/components/shtcx/sensor.py +6 -6
  329. esphome/components/slow_pwm/slow_pwm_output.cpp +2 -1
  330. esphome/components/sml/text_sensor/__init__.py +4 -6
  331. esphome/components/sound_level/__init__.py +0 -0
  332. esphome/components/sound_level/sensor.py +97 -0
  333. esphome/components/sound_level/sound_level.cpp +194 -0
  334. esphome/components/sound_level/sound_level.h +73 -0
  335. esphome/components/speaker/media_player/__init__.py +4 -8
  336. esphome/components/speaker/media_player/speaker_media_player.cpp +0 -18
  337. esphome/components/speaker/media_player/speaker_media_player.h +0 -11
  338. esphome/components/speaker/speaker.h +4 -7
  339. esphome/components/speed/fan/__init__.py +17 -16
  340. esphome/components/spi/spi.h +11 -1
  341. esphome/components/sprinkler/__init__.py +18 -19
  342. esphome/components/sprinkler/sprinkler.cpp +6 -5
  343. esphome/components/switch/__init__.py +32 -42
  344. esphome/components/syslog/__init__.py +41 -0
  345. esphome/components/syslog/esphome_syslog.cpp +49 -0
  346. esphome/components/syslog/esphome_syslog.h +27 -0
  347. esphome/components/t6615/sensor.py +3 -3
  348. esphome/components/t6615/t6615.cpp +2 -1
  349. esphome/components/tca9555/tca9555.cpp +11 -6
  350. esphome/components/tcl112/climate.py +2 -9
  351. esphome/components/template/alarm_control_panel/__init__.py +7 -6
  352. esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +21 -17
  353. esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +2 -1
  354. esphome/components/template/cover/__init__.py +27 -21
  355. esphome/components/template/fan/__init__.py +14 -12
  356. esphome/components/template/lock/__init__.py +20 -25
  357. esphome/components/template/lock/automation.h +18 -0
  358. esphome/components/template/text/__init__.py +4 -3
  359. esphome/components/template/valve/__init__.py +32 -21
  360. esphome/components/template/valve/automation.h +24 -0
  361. esphome/components/text/__init__.py +32 -1
  362. esphome/components/text_sensor/__init__.py +24 -29
  363. esphome/components/thermostat/climate.py +5 -5
  364. esphome/components/time_based/cover.py +17 -16
  365. esphome/components/time_based/time_based_cover.cpp +2 -1
  366. esphome/components/tm1638/switch/__init__.py +10 -7
  367. esphome/components/tormatic/cover.py +4 -5
  368. esphome/components/toshiba/climate.py +3 -5
  369. esphome/components/touchscreen/touchscreen.cpp +3 -1
  370. esphome/components/tuya/climate/__init__.py +5 -6
  371. esphome/components/tuya/cover/__init__.py +6 -11
  372. esphome/components/tuya/select/__init__.py +15 -5
  373. esphome/components/tuya/select/tuya_select.cpp +6 -1
  374. esphome/components/tuya/select/tuya_select.h +5 -1
  375. esphome/components/uart/packet_transport/__init__.py +20 -0
  376. esphome/components/uart/packet_transport/uart_transport.cpp +88 -0
  377. esphome/components/uart/packet_transport/uart_transport.h +41 -0
  378. esphome/components/uart/switch/uart_switch.cpp +2 -1
  379. esphome/components/udp/__init__.py +126 -128
  380. esphome/components/udp/automation.h +40 -0
  381. esphome/components/udp/binary_sensor.py +3 -25
  382. esphome/components/udp/packet_transport/__init__.py +29 -0
  383. esphome/components/udp/packet_transport/udp_transport.cpp +36 -0
  384. esphome/components/udp/packet_transport/udp_transport.h +28 -0
  385. esphome/components/udp/sensor.py +3 -25
  386. esphome/components/udp/udp_component.cpp +26 -470
  387. esphome/components/udp/udp_component.h +21 -128
  388. esphome/components/update/__init__.py +31 -1
  389. esphome/components/uponor_smatrix/climate/__init__.py +4 -9
  390. esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +2 -1
  391. esphome/components/uponor_smatrix/uponor_smatrix.cpp +2 -1
  392. esphome/components/uptime/text_sensor/__init__.py +47 -7
  393. esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +12 -7
  394. esphome/components/uptime/text_sensor/uptime_text_sensor.h +19 -0
  395. esphome/components/valve/__init__.py +34 -3
  396. esphome/components/valve/automation.h +1 -19
  397. esphome/components/vl53l0x/sensor.py +11 -0
  398. esphome/components/vl53l0x/vl53l0x_sensor.cpp +5 -1
  399. esphome/components/vl53l0x/vl53l0x_sensor.h +2 -1
  400. esphome/components/voice_assistant/__init__.py +36 -10
  401. esphome/components/voice_assistant/voice_assistant.cpp +170 -144
  402. esphome/components/voice_assistant/voice_assistant.h +26 -25
  403. esphome/components/waveshare_epaper/display.py +6 -0
  404. esphome/components/waveshare_epaper/waveshare_epaper.cpp +439 -37
  405. esphome/components/waveshare_epaper/waveshare_epaper.h +60 -11
  406. esphome/components/whirlpool/climate.py +3 -5
  407. esphome/components/whynter/climate.py +3 -5
  408. esphome/components/xpt2046/touchscreen/xpt2046.cpp +1 -1
  409. esphome/components/yashima/climate.py +6 -6
  410. esphome/components/zhlt01/climate.py +2 -7
  411. esphome/config.py +13 -13
  412. esphome/config_validation.py +38 -58
  413. esphome/const.py +15 -1
  414. esphome/core/__init__.py +2 -0
  415. esphome/core/application.cpp +23 -10
  416. esphome/core/application.h +9 -1
  417. esphome/core/automation.h +4 -3
  418. esphome/core/component.cpp +28 -7
  419. esphome/core/component.h +10 -1
  420. esphome/core/defines.h +23 -17
  421. esphome/core/macros.h +4 -0
  422. esphome/core/scheduler.cpp +7 -1
  423. esphome/cpp_generator.py +6 -2
  424. esphome/dashboard/web_server.py +3 -3
  425. esphome/helpers.py +39 -0
  426. esphome/loader.py +4 -0
  427. esphome/log.py +15 -19
  428. esphome/mqtt.py +23 -10
  429. esphome/platformio_api.py +1 -1
  430. esphome/schema_extractors.py +0 -1
  431. esphome/voluptuous_schema.py +3 -1
  432. esphome/vscode.py +15 -0
  433. esphome/wizard.py +47 -37
  434. esphome/zeroconf.py +7 -3
  435. {esphome-2025.4.2.dist-info → esphome-2025.5.0b3.dist-info}/METADATA +10 -11
  436. {esphome-2025.4.2.dist-info → esphome-2025.5.0b3.dist-info}/RECORD +440 -380
  437. {esphome-2025.4.2.dist-info → esphome-2025.5.0b3.dist-info}/WHEEL +1 -1
  438. {esphome-2025.4.2.dist-info → esphome-2025.5.0b3.dist-info}/entry_points.txt +0 -0
  439. {esphome-2025.4.2.dist-info → esphome-2025.5.0b3.dist-info}/licenses/LICENSE +0 -0
  440. {esphome-2025.4.2.dist-info → esphome-2025.5.0b3.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,7 @@
5
5
  #include "esphome/core/helpers.h"
6
6
  #include "esphome/core/application.h"
7
7
  #include "proto.h"
8
+ #include "api_pb2_size.h"
8
9
  #include <cstring>
9
10
 
10
11
  namespace esphome {
@@ -72,6 +73,91 @@ const char *api_error_to_str(APIError err) {
72
73
  return "UNKNOWN";
73
74
  }
74
75
 
76
+ // Common implementation for writing raw data to socket
77
+ template<typename StateEnum>
78
+ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket,
79
+ std::vector<uint8_t> &tx_buf, const std::string &info, StateEnum &state,
80
+ StateEnum failed_state) {
81
+ // This method writes data to socket or buffers it
82
+ // Returns APIError::OK if successful (or would block, but data has been buffered)
83
+ // Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to failed_state
84
+
85
+ if (iovcnt == 0)
86
+ return APIError::OK; // Nothing to do, success
87
+
88
+ size_t total_write_len = 0;
89
+ for (int i = 0; i < iovcnt; i++) {
90
+ #ifdef HELPER_LOG_PACKETS
91
+ ESP_LOGVV(TAG, "Sending raw: %s",
92
+ format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
93
+ #endif
94
+ total_write_len += iov[i].iov_len;
95
+ }
96
+
97
+ if (!tx_buf.empty()) {
98
+ // try to empty tx_buf first
99
+ while (!tx_buf.empty()) {
100
+ ssize_t sent = socket->write(tx_buf.data(), tx_buf.size());
101
+ if (is_would_block(sent)) {
102
+ break;
103
+ } else if (sent == -1) {
104
+ ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
105
+ state = failed_state;
106
+ return APIError::SOCKET_WRITE_FAILED; // Socket write failed
107
+ }
108
+ // TODO: inefficient if multiple packets in txbuf
109
+ // replace with deque of buffers
110
+ tx_buf.erase(tx_buf.begin(), tx_buf.begin() + sent);
111
+ }
112
+ }
113
+
114
+ if (!tx_buf.empty()) {
115
+ // tx buf not empty, can't write now because then stream would be inconsistent
116
+ // Reserve space upfront to avoid multiple reallocations
117
+ tx_buf.reserve(tx_buf.size() + total_write_len);
118
+ for (int i = 0; i < iovcnt; i++) {
119
+ tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
120
+ reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
121
+ }
122
+ return APIError::OK; // Success, data buffered
123
+ }
124
+
125
+ ssize_t sent = socket->writev(iov, iovcnt);
126
+ if (is_would_block(sent)) {
127
+ // operation would block, add buffer to tx_buf
128
+ // Reserve space upfront to avoid multiple reallocations
129
+ tx_buf.reserve(tx_buf.size() + total_write_len);
130
+ for (int i = 0; i < iovcnt; i++) {
131
+ tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
132
+ reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
133
+ }
134
+ return APIError::OK; // Success, data buffered
135
+ } else if (sent == -1) {
136
+ // an error occurred
137
+ ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
138
+ state = failed_state;
139
+ return APIError::SOCKET_WRITE_FAILED; // Socket write failed
140
+ } else if ((size_t) sent != total_write_len) {
141
+ // partially sent, add end to tx_buf
142
+ size_t remaining = total_write_len - sent;
143
+ // Reserve space upfront to avoid multiple reallocations
144
+ tx_buf.reserve(tx_buf.size() + remaining);
145
+
146
+ size_t to_consume = sent;
147
+ for (int i = 0; i < iovcnt; i++) {
148
+ if (to_consume >= iov[i].iov_len) {
149
+ to_consume -= iov[i].iov_len;
150
+ } else {
151
+ tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
152
+ reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
153
+ to_consume = 0;
154
+ }
155
+ }
156
+ return APIError::OK; // Success, data buffered
157
+ }
158
+ return APIError::OK; // Success, all data sent
159
+ }
160
+
75
161
  #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
76
162
  // uncomment to log raw packets
77
163
  //#define HELPER_LOG_PACKETS
@@ -407,9 +493,12 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea
407
493
  std::vector<uint8_t> data;
408
494
  data.resize(reason.length() + 1);
409
495
  data[0] = 0x01; // failure
410
- for (size_t i = 0; i < reason.length(); i++) {
411
- data[i + 1] = (uint8_t) reason[i];
496
+
497
+ // Copy error message in bulk
498
+ if (!reason.empty()) {
499
+ std::memcpy(data.data() + 1, reason.c_str(), reason.length());
412
500
  }
501
+
413
502
  // temporarily remove failed state
414
503
  auto orig_state = state_;
415
504
  state_ = State::EXPLICIT_REJECT;
@@ -471,7 +560,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
471
560
  return APIError::OK;
472
561
  }
473
562
  bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
474
- APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
563
+ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
475
564
  int err;
476
565
  APIError aerr;
477
566
  aerr = state_action_();
@@ -483,31 +572,36 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
483
572
  return APIError::WOULD_BLOCK;
484
573
  }
485
574
 
575
+ std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
576
+ // Message data starts after padding
577
+ size_t payload_len = raw_buffer->size() - frame_header_padding_;
486
578
  size_t padding = 0;
487
579
  size_t msg_len = 4 + payload_len + padding;
488
- size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_);
489
- auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]};
490
- if (tmpbuf == nullptr) {
491
- HELPER_LOG("Could not allocate for writing packet");
492
- return APIError::OUT_OF_MEMORY;
493
- }
494
580
 
495
- tmpbuf[0] = 0x01; // indicator
496
- // tmpbuf[1], tmpbuf[2] to be set later
581
+ // We need to resize to include MAC space, but we already reserved it in create_buffer
582
+ raw_buffer->resize(raw_buffer->size() + frame_footer_size_);
583
+
584
+ // Write the noise header in the padded area
585
+ // Buffer layout:
586
+ // [0] - 0x01 indicator byte
587
+ // [1-2] - Size of encrypted payload (filled after encryption)
588
+ // [3-4] - Message type (encrypted)
589
+ // [5-6] - Payload length (encrypted)
590
+ // [7...] - Actual payload data (encrypted)
591
+ uint8_t *buf_start = raw_buffer->data();
592
+ buf_start[0] = 0x01; // indicator
593
+ // buf_start[1], buf_start[2] to be set later after encryption
497
594
  const uint8_t msg_offset = 3;
498
- const uint8_t payload_offset = msg_offset + 4;
499
- tmpbuf[msg_offset + 0] = (uint8_t) (type >> 8); // type
500
- tmpbuf[msg_offset + 1] = (uint8_t) type;
501
- tmpbuf[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len
502
- tmpbuf[msg_offset + 3] = (uint8_t) payload_len;
503
- // copy data
504
- std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]);
505
- // fill padding with zeros
506
- std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0);
595
+ buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte
596
+ buf_start[msg_offset + 1] = (uint8_t) type; // type low byte
597
+ buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte
598
+ buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte
599
+ // payload data is already in the buffer starting at position 7
507
600
 
508
601
  NoiseBuffer mbuf;
509
602
  noise_buffer_init(mbuf);
510
- noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset);
603
+ // The capacity parameter should be msg_len + frame_footer_size_ (MAC length) to allow space for encryption
604
+ noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_);
511
605
  err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
512
606
  if (err != 0) {
513
607
  state_ = State::FAILED;
@@ -516,11 +610,13 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
516
610
  }
517
611
 
518
612
  size_t total_len = 3 + mbuf.size;
519
- tmpbuf[1] = (uint8_t) (mbuf.size >> 8);
520
- tmpbuf[2] = (uint8_t) mbuf.size;
613
+ buf_start[1] = (uint8_t) (mbuf.size >> 8);
614
+ buf_start[2] = (uint8_t) mbuf.size;
521
615
 
522
616
  struct iovec iov;
523
- iov.iov_base = &tmpbuf[0];
617
+ // Point iov_base to the beginning of the buffer (no unused padding in Noise)
618
+ // We send the entire frame: indicator + size + encrypted(type + data_len + payload + MAC)
619
+ iov.iov_base = buf_start;
524
620
  iov.iov_len = total_len;
525
621
 
526
622
  // write raw to not have two packets sent if NAGLE disabled
@@ -546,71 +642,6 @@ APIError APINoiseFrameHelper::try_send_tx_buf_() {
546
642
 
547
643
  return APIError::OK;
548
644
  }
549
- /** Write the data to the socket, or buffer it a write would block
550
- *
551
- * @param data The data to write
552
- * @param len The length of data
553
- */
554
- APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
555
- if (iovcnt == 0)
556
- return APIError::OK;
557
- APIError aerr;
558
-
559
- size_t total_write_len = 0;
560
- for (int i = 0; i < iovcnt; i++) {
561
- #ifdef HELPER_LOG_PACKETS
562
- ESP_LOGVV(TAG, "Sending raw: %s",
563
- format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
564
- #endif
565
- total_write_len += iov[i].iov_len;
566
- }
567
-
568
- if (!tx_buf_.empty()) {
569
- // try to empty tx_buf_ first
570
- aerr = try_send_tx_buf_();
571
- if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
572
- return aerr;
573
- }
574
-
575
- if (!tx_buf_.empty()) {
576
- // tx buf not empty, can't write now because then stream would be inconsistent
577
- for (int i = 0; i < iovcnt; i++) {
578
- tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
579
- reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
580
- }
581
- return APIError::OK;
582
- }
583
-
584
- ssize_t sent = socket_->writev(iov, iovcnt);
585
- if (is_would_block(sent)) {
586
- // operation would block, add buffer to tx_buf
587
- for (int i = 0; i < iovcnt; i++) {
588
- tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
589
- reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
590
- }
591
- return APIError::OK;
592
- } else if (sent == -1) {
593
- // an error occurred
594
- state_ = State::FAILED;
595
- HELPER_LOG("Socket write failed with errno %d", errno);
596
- return APIError::SOCKET_WRITE_FAILED;
597
- } else if ((size_t) sent != total_write_len) {
598
- // partially sent, add end to tx_buf
599
- size_t to_consume = sent;
600
- for (int i = 0; i < iovcnt; i++) {
601
- if (to_consume >= iov[i].iov_len) {
602
- to_consume -= iov[i].iov_len;
603
- } else {
604
- tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
605
- reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
606
- to_consume = 0;
607
- }
608
- }
609
- return APIError::OK;
610
- }
611
- // fully sent
612
- return APIError::OK;
613
- }
614
645
  APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
615
646
  uint8_t header[3];
616
647
  header[0] = 0x01; // indicator
@@ -697,6 +728,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
697
728
  return APIError::HANDSHAKESTATE_SPLIT_FAILED;
698
729
  }
699
730
 
731
+ frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
732
+
700
733
  HELPER_LOG("Handshake complete!");
701
734
  noise_handshakestate_free(handshake_);
702
735
  handshake_ = nullptr;
@@ -744,6 +777,11 @@ void noise_rand_bytes(void *output, size_t len) {
744
777
  }
745
778
  }
746
779
  }
780
+
781
+ // Explicit template instantiation for Noise
782
+ template APIError APIFrameHelper::write_raw_<APINoiseFrameHelper::State>(
783
+ const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
784
+ APINoiseFrameHelper::State &state, APINoiseFrameHelper::State failed_state);
747
785
  #endif // USE_API_NOISE
748
786
 
749
787
  #ifdef USE_API_PLAINTEXT
@@ -804,6 +842,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
804
842
  // read header
805
843
  while (!rx_header_parsed_) {
806
844
  uint8_t data;
845
+ // Reading one byte at a time is fastest in practice for ESP32 when
846
+ // there is no data on the wire (which is the common case).
847
+ // This results in faster failure detection compared to
848
+ // attempting to read multiple bytes at once.
807
849
  ssize_t received = socket_->read(&data, 1);
808
850
  if (received == -1) {
809
851
  if (errno == EWOULDBLOCK || errno == EAGAIN) {
@@ -817,27 +859,60 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
817
859
  HELPER_LOG("Connection closed");
818
860
  return APIError::CONNECTION_CLOSED;
819
861
  }
820
- rx_header_buf_.push_back(data);
821
862
 
822
- // try parse header
823
- if (rx_header_buf_[0] != 0x00) {
863
+ // Successfully read a byte
864
+
865
+ // Process byte according to current buffer position
866
+ if (rx_header_buf_pos_ == 0) { // Case 1: First byte (indicator byte)
867
+ if (data != 0x00) {
868
+ state_ = State::FAILED;
869
+ HELPER_LOG("Bad indicator byte %u", data);
870
+ return APIError::BAD_INDICATOR;
871
+ }
872
+ // We don't store the indicator byte, just increment position
873
+ rx_header_buf_pos_ = 1; // Set to 1 directly
874
+ continue; // Need more bytes before we can parse
875
+ }
876
+
877
+ // Check buffer overflow before storing
878
+ if (rx_header_buf_pos_ == 5) { // Case 2: Buffer would overflow (5 bytes is max allowed)
824
879
  state_ = State::FAILED;
825
- HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
826
- return APIError::BAD_INDICATOR;
880
+ HELPER_LOG("Header buffer overflow");
881
+ return APIError::BAD_DATA_PACKET;
827
882
  }
828
883
 
829
- size_t i = 1;
884
+ // Store byte in buffer (adjust index to account for skipped indicator byte)
885
+ rx_header_buf_[rx_header_buf_pos_ - 1] = data;
886
+
887
+ // Increment position after storing
888
+ rx_header_buf_pos_++;
889
+
890
+ // Case 3: If we only have one varint byte, we need more
891
+ if (rx_header_buf_pos_ == 2) { // Have read indicator + 1 byte
892
+ continue; // Need more bytes before we can parse
893
+ }
894
+
895
+ // At this point, we have at least 3 bytes total:
896
+ // - Validated indicator byte (0x00) but not stored
897
+ // - At least 2 bytes in the buffer for the varints
898
+ // Buffer layout:
899
+ // First 1-3 bytes: Message size varint (variable length)
900
+ // - 2 bytes would only allow up to 16383, which is less than noise's 65535
901
+ // - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
902
+ // Remaining 1-2 bytes: Message type varint (variable length)
903
+ // We now attempt to parse both varints. If either is incomplete,
904
+ // we'll continue reading more bytes.
905
+
830
906
  uint32_t consumed = 0;
831
- auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
907
+ auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[0], rx_header_buf_pos_ - 1, &consumed);
832
908
  if (!msg_size_varint.has_value()) {
833
909
  // not enough data there yet
834
910
  continue;
835
911
  }
836
912
 
837
- i += consumed;
838
913
  rx_header_parsed_len_ = msg_size_varint->as_uint32();
839
914
 
840
- auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
915
+ auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed);
841
916
  if (!msg_type_varint.has_value()) {
842
917
  // not enough data there yet
843
918
  continue;
@@ -883,7 +958,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
883
958
  // consume msg
884
959
  rx_buf_ = {};
885
960
  rx_buf_len_ = 0;
886
- rx_header_buf_.clear();
961
+ rx_header_buf_pos_ = 0;
887
962
  rx_header_parsed_ = false;
888
963
  return APIError::OK;
889
964
  }
@@ -927,26 +1002,66 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
927
1002
  return APIError::OK;
928
1003
  }
929
1004
  bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
930
- APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
1005
+ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
931
1006
  if (state_ != State::DATA) {
932
1007
  return APIError::BAD_STATE;
933
1008
  }
934
1009
 
935
- std::vector<uint8_t> header;
936
- header.push_back(0x00);
937
- ProtoVarInt(payload_len).encode(header);
938
- ProtoVarInt(type).encode(header);
1010
+ std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
1011
+ // Message data starts after padding (frame_header_padding_ = 6)
1012
+ size_t payload_len = raw_buffer->size() - frame_header_padding_;
939
1013
 
940
- struct iovec iov[2];
941
- iov[0].iov_base = &header[0];
942
- iov[0].iov_len = header.size();
943
- if (payload_len == 0) {
944
- return write_raw_(iov, 1);
1014
+ // Calculate varint sizes for header components
1015
+ size_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
1016
+ size_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
1017
+ size_t total_header_len = 1 + size_varint_len + type_varint_len;
1018
+
1019
+ if (total_header_len > frame_header_padding_) {
1020
+ // Header is too large to fit in the padding
1021
+ return APIError::BAD_ARG;
945
1022
  }
946
- iov[1].iov_base = const_cast<uint8_t *>(payload);
947
- iov[1].iov_len = payload_len;
948
1023
 
949
- return write_raw_(iov, 2);
1024
+ // Calculate where to start writing the header
1025
+ // The header starts at the latest possible position to minimize unused padding
1026
+ //
1027
+ // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
1028
+ // [0-2] - Unused padding
1029
+ // [3] - 0x00 indicator byte
1030
+ // [4] - Payload size varint (1 byte, for sizes 0-127)
1031
+ // [5] - Message type varint (1 byte, for types 0-127)
1032
+ // [6...] - Actual payload data
1033
+ //
1034
+ // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
1035
+ // [0-1] - Unused padding
1036
+ // [2] - 0x00 indicator byte
1037
+ // [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
1038
+ // [5] - Message type varint (1 byte, for types 0-127)
1039
+ // [6...] - Actual payload data
1040
+ //
1041
+ // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
1042
+ // [0] - 0x00 indicator byte
1043
+ // [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
1044
+ // [4-5] - Message type varint (2 bytes, for types 128-32767)
1045
+ // [6...] - Actual payload data
1046
+ uint8_t *buf_start = raw_buffer->data();
1047
+ size_t header_offset = frame_header_padding_ - total_header_len;
1048
+
1049
+ // Write the plaintext header
1050
+ buf_start[header_offset] = 0x00; // indicator
1051
+
1052
+ // Encode size varint directly into buffer
1053
+ ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
1054
+
1055
+ // Encode type varint directly into buffer
1056
+ ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
1057
+
1058
+ struct iovec iov;
1059
+ // Point iov_base to the beginning of our header (skip unused padding)
1060
+ // This ensures we only send the actual header and payload, not the empty padding bytes
1061
+ iov.iov_base = buf_start + header_offset;
1062
+ iov.iov_len = total_header_len + payload_len;
1063
+
1064
+ return write_raw_(&iov, 1);
950
1065
  }
951
1066
  APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
952
1067
  // try send from tx_buf
@@ -966,71 +1081,6 @@ APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
966
1081
 
967
1082
  return APIError::OK;
968
1083
  }
969
- /** Write the data to the socket, or buffer it a write would block
970
- *
971
- * @param data The data to write
972
- * @param len The length of data
973
- */
974
- APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
975
- if (iovcnt == 0)
976
- return APIError::OK;
977
- APIError aerr;
978
-
979
- size_t total_write_len = 0;
980
- for (int i = 0; i < iovcnt; i++) {
981
- #ifdef HELPER_LOG_PACKETS
982
- ESP_LOGVV(TAG, "Sending raw: %s",
983
- format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
984
- #endif
985
- total_write_len += iov[i].iov_len;
986
- }
987
-
988
- if (!tx_buf_.empty()) {
989
- // try to empty tx_buf_ first
990
- aerr = try_send_tx_buf_();
991
- if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
992
- return aerr;
993
- }
994
-
995
- if (!tx_buf_.empty()) {
996
- // tx buf not empty, can't write now because then stream would be inconsistent
997
- for (int i = 0; i < iovcnt; i++) {
998
- tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
999
- reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
1000
- }
1001
- return APIError::OK;
1002
- }
1003
-
1004
- ssize_t sent = socket_->writev(iov, iovcnt);
1005
- if (is_would_block(sent)) {
1006
- // operation would block, add buffer to tx_buf
1007
- for (int i = 0; i < iovcnt; i++) {
1008
- tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
1009
- reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
1010
- }
1011
- return APIError::OK;
1012
- } else if (sent == -1) {
1013
- // an error occurred
1014
- state_ = State::FAILED;
1015
- HELPER_LOG("Socket write failed with errno %d", errno);
1016
- return APIError::SOCKET_WRITE_FAILED;
1017
- } else if ((size_t) sent != total_write_len) {
1018
- // partially sent, add end to tx_buf
1019
- size_t to_consume = sent;
1020
- for (int i = 0; i < iovcnt; i++) {
1021
- if (to_consume >= iov[i].iov_len) {
1022
- to_consume -= iov[i].iov_len;
1023
- } else {
1024
- tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
1025
- reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
1026
- to_consume = 0;
1027
- }
1028
- }
1029
- return APIError::OK;
1030
- }
1031
- // fully sent
1032
- return APIError::OK;
1033
- }
1034
1084
 
1035
1085
  APIError APIPlaintextFrameHelper::close() {
1036
1086
  state_ = State::CLOSED;
@@ -1048,6 +1098,11 @@ APIError APIPlaintextFrameHelper::shutdown(int how) {
1048
1098
  }
1049
1099
  return APIError::OK;
1050
1100
  }
1101
+
1102
+ // Explicit template instantiation for Plaintext
1103
+ template APIError APIFrameHelper::write_raw_<APIPlaintextFrameHelper::State>(
1104
+ const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
1105
+ APIPlaintextFrameHelper::State &state, APIPlaintextFrameHelper::State failed_state);
1051
1106
  #endif // USE_API_PLAINTEXT
1052
1107
 
1053
1108
  } // namespace api
@@ -16,6 +16,8 @@
16
16
  namespace esphome {
17
17
  namespace api {
18
18
 
19
+ class ProtoWriteBuffer;
20
+
19
21
  struct ReadPacketBuffer {
20
22
  std::vector<uint8_t> container;
21
23
  uint16_t type;
@@ -65,26 +67,46 @@ class APIFrameHelper {
65
67
  virtual APIError loop() = 0;
66
68
  virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
67
69
  virtual bool can_write_without_blocking() = 0;
68
- virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0;
70
+ virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
69
71
  virtual std::string getpeername() = 0;
70
72
  virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
71
73
  virtual APIError close() = 0;
72
74
  virtual APIError shutdown(int how) = 0;
73
75
  // Give this helper a name for logging
74
76
  virtual void set_log_info(std::string info) = 0;
77
+ // Get the frame header padding required by this protocol
78
+ virtual uint8_t frame_header_padding() = 0;
79
+ // Get the frame footer size required by this protocol
80
+ virtual uint8_t frame_footer_size() = 0;
81
+
82
+ protected:
83
+ // Common implementation for writing raw data to socket
84
+ template<typename StateEnum>
85
+ APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
86
+ const std::string &info, StateEnum &state, StateEnum failed_state);
87
+
88
+ uint8_t frame_header_padding_{0};
89
+ uint8_t frame_footer_size_{0};
75
90
  };
76
91
 
77
92
  #ifdef USE_API_NOISE
78
93
  class APINoiseFrameHelper : public APIFrameHelper {
79
94
  public:
80
95
  APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
81
- : socket_(std::move(socket)), ctx_(std::move(std::move(ctx))) {}
96
+ : socket_(std::move(socket)), ctx_(std::move(ctx)) {
97
+ // Noise header structure:
98
+ // Pos 0: indicator (0x01)
99
+ // Pos 1-2: encrypted payload size (16-bit big-endian)
100
+ // Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
101
+ // Pos 7+: actual payload data
102
+ frame_header_padding_ = 7;
103
+ }
82
104
  ~APINoiseFrameHelper() override;
83
105
  APIError init() override;
84
106
  APIError loop() override;
85
107
  APIError read_packet(ReadPacketBuffer *buffer) override;
86
108
  bool can_write_without_blocking() override;
87
- APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
109
+ APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
88
110
  std::string getpeername() override { return this->socket_->getpeername(); }
89
111
  int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
90
112
  return this->socket_->getpeername(addr, addrlen);
@@ -93,6 +115,10 @@ class APINoiseFrameHelper : public APIFrameHelper {
93
115
  APIError shutdown(int how) override;
94
116
  // Give this helper a name for logging
95
117
  void set_log_info(std::string info) override { info_ = std::move(info); }
118
+ // Get the frame header padding required by this protocol
119
+ uint8_t frame_header_padding() override { return frame_header_padding_; }
120
+ // Get the frame footer size required by this protocol
121
+ uint8_t frame_footer_size() override { return frame_footer_size_; }
96
122
 
97
123
  protected:
98
124
  struct ParsedFrame {
@@ -103,7 +129,9 @@ class APINoiseFrameHelper : public APIFrameHelper {
103
129
  APIError try_read_frame_(ParsedFrame *frame);
104
130
  APIError try_send_tx_buf_();
105
131
  APIError write_frame_(const uint8_t *data, size_t len);
106
- APIError write_raw_(const struct iovec *iov, int iovcnt);
132
+ inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
133
+ return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
134
+ }
107
135
  APIError init_handshake_();
108
136
  APIError check_handshake_finished_();
109
137
  void send_explicit_handshake_reject_(const std::string &reason);
@@ -111,6 +139,9 @@ class APINoiseFrameHelper : public APIFrameHelper {
111
139
  std::unique_ptr<socket::Socket> socket_;
112
140
 
113
141
  std::string info_;
142
+ // Fixed-size header buffer for noise protocol:
143
+ // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
144
+ // Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase
114
145
  uint8_t rx_header_buf_[3];
115
146
  size_t rx_header_buf_len_ = 0;
116
147
  std::vector<uint8_t> rx_buf_;
@@ -141,13 +172,20 @@ class APINoiseFrameHelper : public APIFrameHelper {
141
172
  #ifdef USE_API_PLAINTEXT
142
173
  class APIPlaintextFrameHelper : public APIFrameHelper {
143
174
  public:
144
- APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
175
+ APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {
176
+ // Plaintext header structure (worst case):
177
+ // Pos 0: indicator (0x00)
178
+ // Pos 1-3: payload size varint (up to 3 bytes)
179
+ // Pos 4-5: message type varint (up to 2 bytes)
180
+ // Pos 6+: actual payload data
181
+ frame_header_padding_ = 6;
182
+ }
145
183
  ~APIPlaintextFrameHelper() override = default;
146
184
  APIError init() override;
147
185
  APIError loop() override;
148
186
  APIError read_packet(ReadPacketBuffer *buffer) override;
149
187
  bool can_write_without_blocking() override;
150
- APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
188
+ APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
151
189
  std::string getpeername() override { return this->socket_->getpeername(); }
152
190
  int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
153
191
  return this->socket_->getpeername(addr, addrlen);
@@ -156,6 +194,10 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
156
194
  APIError shutdown(int how) override;
157
195
  // Give this helper a name for logging
158
196
  void set_log_info(std::string info) override { info_ = std::move(info); }
197
+ // Get the frame header padding required by this protocol
198
+ uint8_t frame_header_padding() override { return frame_header_padding_; }
199
+ // Get the frame footer size required by this protocol
200
+ uint8_t frame_footer_size() override { return frame_footer_size_; }
159
201
 
160
202
  protected:
161
203
  struct ParsedFrame {
@@ -164,12 +206,23 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
164
206
 
165
207
  APIError try_read_frame_(ParsedFrame *frame);
166
208
  APIError try_send_tx_buf_();
167
- APIError write_raw_(const struct iovec *iov, int iovcnt);
209
+ inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
210
+ return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
211
+ }
168
212
 
169
213
  std::unique_ptr<socket::Socket> socket_;
170
214
 
171
215
  std::string info_;
172
- std::vector<uint8_t> rx_header_buf_;
216
+ // Fixed-size header buffer for plaintext protocol:
217
+ // We only need space for the two varints since we validate the indicator byte separately.
218
+ // To match noise protocol's maximum message size (65535), we need:
219
+ // 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
220
+ //
221
+ // While varints could theoretically be up to 10 bytes each for 64-bit values,
222
+ // attempting to process messages with headers that large would likely crash the
223
+ // ESP32 due to memory constraints.
224
+ uint8_t rx_header_buf_[5]; // 5 bytes for varints (3 for size + 2 for type)
225
+ uint8_t rx_header_buf_pos_ = 0;
173
226
  bool rx_header_parsed_ = false;
174
227
  uint32_t rx_header_parsed_type_ = 0;
175
228
  uint32_t rx_header_parsed_len_ = 0;