esphome 2025.8.4__py3-none-any.whl → 2025.9.0b1__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 (344) hide show
  1. esphome/__main__.py +36 -42
  2. esphome/components/absolute_humidity/absolute_humidity.cpp +3 -5
  3. esphome/components/adc/adc_sensor_esp32.cpp +29 -6
  4. esphome/components/ags10/ags10.cpp +3 -18
  5. esphome/components/ags10/ags10.h +2 -12
  6. esphome/components/aht10/aht10.cpp +3 -3
  7. esphome/components/airthings_ble/__init__.py +2 -2
  8. esphome/components/alarm_control_panel/__init__.py +2 -2
  9. esphome/components/am2315c/am2315c.cpp +1 -17
  10. esphome/components/am2315c/am2315c.h +2 -3
  11. esphome/components/api/__init__.py +2 -2
  12. esphome/components/api/api_connection.cpp +34 -23
  13. esphome/components/api/api_connection.h +20 -39
  14. esphome/components/api/api_frame_helper.cpp +25 -25
  15. esphome/components/api/api_frame_helper.h +3 -3
  16. esphome/components/api/api_frame_helper_noise.cpp +75 -40
  17. esphome/components/api/api_frame_helper_noise.h +3 -7
  18. esphome/components/api/api_frame_helper_plaintext.cpp +17 -4
  19. esphome/components/api/api_frame_helper_plaintext.h +1 -4
  20. esphome/components/api/api_pb2.cpp +20 -2
  21. esphome/components/api/api_pb2.h +146 -141
  22. esphome/components/api/api_pb2_dump.cpp +12 -1
  23. esphome/components/api/proto.cpp +33 -37
  24. esphome/components/async_tcp/__init__.py +2 -2
  25. esphome/components/atm90e26/sensor.py +2 -0
  26. esphome/components/atm90e32/sensor.py +4 -2
  27. esphome/components/audio_adc/__init__.py +2 -2
  28. esphome/components/audio_dac/__init__.py +2 -2
  29. esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +1 -1
  30. esphome/components/bedjet/bedjet_hub.cpp +1 -1
  31. esphome/components/binary_sensor/__init__.py +2 -2
  32. esphome/components/binary_sensor/binary_sensor.cpp +13 -0
  33. esphome/components/binary_sensor/binary_sensor.h +4 -7
  34. esphome/components/bl0940/__init__.py +6 -1
  35. esphome/components/bl0940/bl0940.cpp +178 -41
  36. esphome/components/bl0940/bl0940.h +121 -76
  37. esphome/components/bl0940/button/__init__.py +27 -0
  38. esphome/components/bl0940/button/calibration_reset_button.cpp +20 -0
  39. esphome/components/bl0940/button/calibration_reset_button.h +19 -0
  40. esphome/components/bl0940/number/__init__.py +94 -0
  41. esphome/components/bl0940/number/calibration_number.cpp +29 -0
  42. esphome/components/bl0940/number/calibration_number.h +26 -0
  43. esphome/components/bl0940/sensor.py +151 -2
  44. esphome/components/bl0942/bl0942.cpp +1 -1
  45. esphome/components/ble_client/output/__init__.py +4 -4
  46. esphome/components/bluetooth_proxy/__init__.py +1 -1
  47. esphome/components/bluetooth_proxy/bluetooth_connection.h +1 -1
  48. esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +15 -7
  49. esphome/components/bluetooth_proxy/bluetooth_proxy.h +3 -2
  50. esphome/components/button/__init__.py +2 -2
  51. esphome/components/button/button.cpp +13 -0
  52. esphome/components/button/button.h +4 -7
  53. esphome/components/camera/buffer.h +18 -0
  54. esphome/components/camera/buffer_impl.cpp +20 -0
  55. esphome/components/camera/buffer_impl.h +26 -0
  56. esphome/components/camera/camera.h +43 -0
  57. esphome/components/camera/encoder.h +69 -0
  58. esphome/components/camera_encoder/__init__.py +62 -0
  59. esphome/components/camera_encoder/encoder_buffer_impl.cpp +23 -0
  60. esphome/components/camera_encoder/encoder_buffer_impl.h +25 -0
  61. esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp +82 -0
  62. esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h +39 -0
  63. esphome/components/captive_portal/__init__.py +2 -2
  64. esphome/components/captive_portal/captive_portal.cpp +35 -12
  65. esphome/components/captive_portal/captive_portal.h +3 -3
  66. esphome/components/ccs811/ccs811.cpp +3 -3
  67. esphome/components/climate/__init__.py +2 -2
  68. esphome/components/climate/climate.cpp +1 -1
  69. esphome/components/cover/__init__.py +5 -5
  70. esphome/components/cover/cover.cpp +1 -1
  71. esphome/components/cover/cover.h +2 -2
  72. esphome/components/dallas_temp/dallas_temp.cpp +2 -2
  73. esphome/components/datetime/__init__.py +2 -2
  74. esphome/components/datetime/date_entity.h +2 -2
  75. esphome/components/datetime/datetime_entity.h +2 -2
  76. esphome/components/datetime/time_entity.h +2 -2
  77. esphome/components/debug/debug_esp32.cpp +1 -1
  78. esphome/components/display/__init__.py +4 -4
  79. esphome/components/duty_time/duty_time_sensor.cpp +1 -1
  80. esphome/components/esp32/__init__.py +0 -5
  81. esphome/components/esp32/gpio.cpp +27 -23
  82. esphome/components/esp32/gpio.h +26 -11
  83. esphome/components/esp32/preferences.cpp +8 -4
  84. esphome/components/esp32_ble/__init__.py +7 -2
  85. esphome/components/esp32_ble_client/ble_client_base.cpp +7 -3
  86. esphome/components/esp32_ble_tracker/__init__.py +2 -2
  87. esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +9 -44
  88. esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +2 -14
  89. esphome/components/esp8266/__init__.py +2 -2
  90. esphome/components/esp8266/core.cpp +2 -2
  91. esphome/components/esp8266/gpio.py +4 -4
  92. esphome/components/esp8266/preferences.cpp +30 -28
  93. esphome/components/esphome/ota/__init__.py +2 -2
  94. esphome/components/esphome/ota/ota_esphome.cpp +21 -19
  95. esphome/components/esphome/ota/ota_esphome.h +6 -5
  96. esphome/components/ethernet/__init__.py +7 -2
  97. esphome/components/ethernet/ethernet_component.cpp +1 -1
  98. esphome/components/event/__init__.py +2 -2
  99. esphome/components/event/event.h +4 -4
  100. esphome/components/fan/__init__.py +2 -2
  101. esphome/components/fan/fan.cpp +2 -1
  102. esphome/components/gdk101/gdk101.cpp +4 -4
  103. esphome/components/globals/__init__.py +2 -2
  104. esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +19 -18
  105. esphome/components/gpio_expander/cached_gpio.h +36 -16
  106. esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp +5 -5
  107. esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +1 -1
  108. esphome/components/haier/haier_base.cpp +1 -1
  109. esphome/components/haier/hon_climate.cpp +1 -1
  110. esphome/components/hlw8012/hlw8012.cpp +5 -5
  111. esphome/components/honeywellabp2_i2c/honeywellabp2.cpp +4 -4
  112. esphome/components/host/preferences.h +3 -2
  113. esphome/components/hte501/hte501.cpp +3 -21
  114. esphome/components/hte501/hte501.h +2 -3
  115. esphome/components/http_request/ota/__init__.py +2 -2
  116. esphome/components/i2c/__init__.py +2 -2
  117. esphome/components/i2c/i2c.cpp +13 -9
  118. esphome/components/i2c/i2c_bus.h +36 -6
  119. esphome/components/i2s_audio/__init__.py +8 -2
  120. esphome/components/i2s_audio/media_player/__init__.py +1 -1
  121. esphome/components/i2s_audio/microphone/__init__.py +1 -1
  122. esphome/components/i2s_audio/speaker/__init__.py +1 -1
  123. esphome/components/inkplate/__init__.py +1 -0
  124. esphome/components/inkplate/const.py +105 -0
  125. esphome/components/inkplate/display.py +238 -0
  126. esphome/components/{inkplate6 → inkplate}/inkplate.cpp +156 -74
  127. esphome/components/{inkplate6 → inkplate}/inkplate.h +28 -68
  128. esphome/components/inkplate6/__init__.py +0 -1
  129. esphome/components/inkplate6/display.py +2 -211
  130. esphome/components/integration/integration_sensor.cpp +1 -1
  131. esphome/components/json/__init__.py +2 -2
  132. esphome/components/lc709203f/lc709203f.cpp +4 -17
  133. esphome/components/lc709203f/lc709203f.h +2 -3
  134. esphome/components/ld2420/text_sensor/{text_sensor.cpp → ld2420_text_sensor.cpp} +1 -1
  135. esphome/components/ld2450/ld2450.cpp +1 -1
  136. esphome/components/libretiny/preferences.cpp +13 -5
  137. esphome/components/light/__init__.py +2 -2
  138. esphome/components/light/addressable_light_effect.h +7 -0
  139. esphome/components/light/base_light_effects.h +8 -0
  140. esphome/components/light/light_call.cpp +22 -20
  141. esphome/components/light/light_effect.cpp +36 -0
  142. esphome/components/light/light_effect.h +14 -0
  143. esphome/components/light/light_json_schema.cpp +9 -1
  144. esphome/components/light/light_state.cpp +2 -2
  145. esphome/components/light/light_state.h +38 -0
  146. esphome/components/lock/__init__.py +2 -2
  147. esphome/components/lock/lock.h +2 -2
  148. esphome/components/logger/__init__.py +2 -2
  149. esphome/components/logger/logger.cpp +25 -4
  150. esphome/components/logger/logger.h +1 -1
  151. esphome/components/logger/logger_esp32.cpp +16 -8
  152. esphome/components/logger/logger_esp8266.cpp +11 -3
  153. esphome/components/logger/logger_libretiny.cpp +13 -3
  154. esphome/components/logger/logger_rp2040.cpp +14 -3
  155. esphome/components/logger/logger_zephyr.cpp +15 -4
  156. esphome/components/lvgl/defines.py +1 -0
  157. esphome/components/lvgl/hello_world.py +96 -33
  158. esphome/components/lvgl/number/lvgl_number.h +1 -1
  159. esphome/components/lvgl/select/lvgl_select.h +1 -1
  160. esphome/components/lvgl/widgets/__init__.py +0 -1
  161. esphome/components/lvgl/widgets/spinbox.py +20 -11
  162. esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp +1 -1
  163. esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp +1 -1
  164. esphome/components/mapping/__init__.py +13 -5
  165. esphome/components/mapping/mapping.h +69 -0
  166. esphome/components/max17043/max17043.cpp +2 -2
  167. esphome/components/mcp23016/__init__.py +1 -0
  168. esphome/components/mcp23016/mcp23016.cpp +20 -5
  169. esphome/components/mcp23016/mcp23016.h +10 -4
  170. esphome/components/mcp23x08_base/mcp23x08_base.cpp +1 -1
  171. esphome/components/mcp23x17_base/mcp23x17_base.cpp +2 -2
  172. esphome/components/mdns/__init__.py +2 -2
  173. esphome/components/mdns/mdns_component.cpp +145 -54
  174. esphome/components/media_player/__init__.py +2 -2
  175. esphome/components/micro_wake_word/__init__.py +2 -2
  176. esphome/components/microphone/__init__.py +2 -2
  177. esphome/components/mipi/__init__.py +77 -33
  178. esphome/components/mipi_rgb/__init__.py +2 -0
  179. esphome/components/mipi_rgb/display.py +321 -0
  180. esphome/components/mipi_rgb/mipi_rgb.cpp +388 -0
  181. esphome/components/mipi_rgb/mipi_rgb.h +127 -0
  182. esphome/components/mipi_rgb/models/guition.py +24 -0
  183. esphome/components/mipi_rgb/models/lilygo.py +228 -0
  184. esphome/components/mipi_rgb/models/rpi.py +9 -0
  185. esphome/components/mipi_rgb/models/st7701s.py +214 -0
  186. esphome/components/mipi_rgb/models/waveshare.py +64 -0
  187. esphome/components/mipi_spi/models/jc.py +229 -0
  188. esphome/components/mlx90614/mlx90614.cpp +1 -16
  189. esphome/components/mlx90614/mlx90614.h +0 -1
  190. esphome/components/mqtt/__init__.py +2 -2
  191. esphome/components/mqtt/mqtt_sensor.cpp +7 -2
  192. esphome/components/ms5611/ms5611.cpp +7 -6
  193. esphome/components/network/__init__.py +2 -2
  194. esphome/components/nextion/nextion_upload.cpp +4 -1
  195. esphome/components/nrf52/__init__.py +49 -6
  196. esphome/components/nrf52/const.py +1 -0
  197. esphome/components/nrf52/dfu.cpp +51 -0
  198. esphome/components/nrf52/dfu.h +24 -0
  199. esphome/components/ntc/ntc.cpp +1 -1
  200. esphome/components/number/__init__.py +2 -2
  201. esphome/components/number/automation.cpp +1 -1
  202. esphome/components/number/number.cpp +21 -0
  203. esphome/components/number/number.h +4 -13
  204. esphome/components/opentherm/hub.h +6 -6
  205. esphome/components/opentherm/number/{number.cpp → opentherm_number.cpp} +2 -2
  206. esphome/components/opentherm/output/{output.cpp → opentherm_output.cpp} +1 -1
  207. esphome/components/opentherm/switch/{switch.cpp → opentherm_switch.cpp} +1 -1
  208. esphome/components/ota/__init__.py +2 -2
  209. esphome/components/pca6416a/__init__.py +1 -0
  210. esphome/components/pca6416a/pca6416a.cpp +20 -5
  211. esphome/components/pca6416a/pca6416a.h +12 -5
  212. esphome/components/pca9554/__init__.py +2 -1
  213. esphome/components/pca9554/pca9554.cpp +12 -18
  214. esphome/components/pca9554/pca9554.h +10 -9
  215. esphome/components/pcf8574/__init__.py +1 -0
  216. esphome/components/pcf8574/pcf8574.cpp +14 -5
  217. esphome/components/pcf8574/pcf8574.h +13 -6
  218. esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +7 -7
  219. esphome/components/pipsolar/__init__.py +3 -3
  220. esphome/components/pipsolar/output/__init__.py +4 -4
  221. esphome/components/pulse_width/pulse_width.cpp +2 -2
  222. esphome/components/qmp6988/qmp6988.cpp +81 -126
  223. esphome/components/qmp6988/qmp6988.h +31 -37
  224. esphome/components/radon_eye_ble/__init__.py +2 -2
  225. esphome/components/remote_base/__init__.py +6 -8
  226. esphome/components/rotary_encoder/rotary_encoder.cpp +1 -1
  227. esphome/components/rp2040/__init__.py +2 -2
  228. esphome/components/runtime_stats/runtime_stats.cpp +10 -23
  229. esphome/components/runtime_stats/runtime_stats.h +4 -10
  230. esphome/components/safe_mode/__init__.py +2 -2
  231. esphome/components/safe_mode/safe_mode.cpp +33 -31
  232. esphome/components/script/script.cpp +6 -0
  233. esphome/components/script/script.h +19 -5
  234. esphome/components/sdm_meter/sensor.py +3 -1
  235. esphome/components/select/__init__.py +2 -2
  236. esphome/components/select/select.h +2 -2
  237. esphome/components/sen5x/sen5x.cpp +57 -55
  238. esphome/components/sen5x/sen5x.h +21 -15
  239. esphome/components/sen5x/sensor.py +67 -44
  240. esphome/components/sensirion_common/i2c_sensirion.cpp +18 -47
  241. esphome/components/sensirion_common/i2c_sensirion.h +39 -55
  242. esphome/components/sensor/__init__.py +2 -2
  243. esphome/components/sensor/automation.h +1 -1
  244. esphome/components/sensor/sensor.cpp +34 -6
  245. esphome/components/sensor/sensor.h +4 -21
  246. esphome/components/sgp30/sgp30.cpp +34 -35
  247. esphome/components/sgp30/sgp30.h +11 -10
  248. esphome/components/sgp4x/sgp4x.cpp +2 -2
  249. esphome/components/shelly_dimmer/light.py +7 -7
  250. esphome/components/sht4x/sht4x.cpp +1 -1
  251. esphome/components/sntp/sntp_component.cpp +36 -9
  252. esphome/components/sntp/sntp_component.h +7 -0
  253. esphome/components/sound_level/sound_level.cpp +1 -1
  254. esphome/components/speaker/__init__.py +2 -2
  255. esphome/components/speaker/media_player/__init__.py +2 -2
  256. esphome/components/speaker/media_player/speaker_media_player.cpp +1 -1
  257. esphome/components/spi/__init__.py +2 -2
  258. esphome/components/sprinkler/sprinkler.cpp +1 -1
  259. esphome/components/sps30/sps30.cpp +18 -23
  260. esphome/components/sps30/sps30.h +3 -3
  261. esphome/components/status_led/__init__.py +2 -2
  262. esphome/components/stepper/__init__.py +2 -2
  263. esphome/components/switch/__init__.py +2 -2
  264. esphome/components/switch/switch.cpp +5 -5
  265. esphome/components/sx1509/__init__.py +1 -1
  266. esphome/components/sx1509/sx1509.cpp +12 -7
  267. esphome/components/sx1509/sx1509.h +11 -4
  268. esphome/components/tca9555/tca9555.cpp +5 -5
  269. esphome/components/tee501/tee501.cpp +2 -21
  270. esphome/components/tee501/tee501.h +2 -4
  271. esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +1 -1
  272. esphome/components/template/datetime/template_date.cpp +1 -1
  273. esphome/components/template/datetime/template_datetime.cpp +2 -2
  274. esphome/components/template/datetime/template_time.cpp +1 -1
  275. esphome/components/template/number/template_number.cpp +1 -1
  276. esphome/components/template/select/template_select.cpp +1 -1
  277. esphome/components/template/text/template_text.cpp +1 -1
  278. esphome/components/text/__init__.py +2 -2
  279. esphome/components/text/text.h +2 -2
  280. esphome/components/text_sensor/__init__.py +2 -2
  281. esphome/components/text_sensor/text_sensor.h +4 -4
  282. esphome/components/thermostat/climate.py +11 -7
  283. esphome/components/thermostat/thermostat_climate.cpp +237 -206
  284. esphome/components/thermostat/thermostat_climate.h +52 -41
  285. esphome/components/time/__init__.py +2 -2
  286. esphome/components/tmp1075/tmp1075.cpp +1 -1
  287. esphome/components/total_daily_energy/total_daily_energy.cpp +1 -1
  288. esphome/components/touchscreen/__init__.py +2 -2
  289. esphome/components/tuya/number/tuya_number.cpp +1 -1
  290. esphome/components/udp/udp_component.cpp +3 -3
  291. esphome/components/ufire_ec/ufire_ec.cpp +4 -4
  292. esphome/components/ufire_ise/ufire_ise.cpp +4 -4
  293. esphome/components/update/__init__.py +2 -2
  294. esphome/components/usb_uart/usb_uart.cpp +1 -1
  295. esphome/components/valve/__init__.py +5 -5
  296. esphome/components/valve/valve.cpp +1 -1
  297. esphome/components/valve/valve.h +2 -2
  298. esphome/components/wake_on_lan/wake_on_lan.cpp +2 -2
  299. esphome/components/waveshare_epaper/waveshare_213v3.cpp +1 -1
  300. esphome/components/web_server/__init__.py +2 -2
  301. esphome/components/web_server/ota/__init__.py +2 -2
  302. esphome/components/web_server/ota/ota_web_server.cpp +11 -0
  303. esphome/components/web_server/web_server.cpp +58 -12
  304. esphome/components/web_server_base/__init__.py +2 -2
  305. esphome/components/wifi/__init__.py +5 -5
  306. esphome/components/wifi/wifi_component.cpp +3 -3
  307. esphome/components/wifi/wifi_component_esp_idf.cpp +2 -0
  308. esphome/config_validation.py +2 -2
  309. esphome/const.py +2 -1
  310. esphome/core/__init__.py +1 -0
  311. esphome/core/application.cpp +89 -51
  312. esphome/core/application.h +1 -0
  313. esphome/core/component.cpp +41 -19
  314. esphome/core/component.h +17 -13
  315. esphome/core/config.py +7 -7
  316. esphome/core/defines.h +4 -0
  317. esphome/core/entity_base.cpp +22 -8
  318. esphome/core/entity_base.h +43 -0
  319. esphome/core/helpers.cpp +26 -13
  320. esphome/core/helpers.h +4 -3
  321. esphome/core/ring_buffer.cpp +6 -2
  322. esphome/core/ring_buffer.h +2 -1
  323. esphome/core/scheduler.cpp +175 -94
  324. esphome/core/scheduler.h +66 -35
  325. esphome/core/time.cpp +6 -20
  326. esphome/coroutine.py +80 -3
  327. esphome/cpp_generator.py +13 -0
  328. esphome/cpp_helpers.py +2 -2
  329. esphome/dashboard/web_server.py +67 -10
  330. esphome/espota2.py +13 -6
  331. esphome/helpers.py +68 -83
  332. esphome/resolver.py +67 -0
  333. esphome/util.py +9 -6
  334. esphome/wizard.py +39 -26
  335. {esphome-2025.8.4.dist-info → esphome-2025.9.0b1.dist-info}/METADATA +9 -9
  336. {esphome-2025.8.4.dist-info → esphome-2025.9.0b1.dist-info}/RECORD +344 -313
  337. /esphome/components/ld2420/text_sensor/{text_sensor.h → ld2420_text_sensor.h} +0 -0
  338. /esphome/components/opentherm/number/{number.h → opentherm_number.h} +0 -0
  339. /esphome/components/opentherm/output/{output.h → opentherm_output.h} +0 -0
  340. /esphome/components/opentherm/switch/{switch.h → opentherm_switch.h} +0 -0
  341. {esphome-2025.8.4.dist-info → esphome-2025.9.0b1.dist-info}/WHEEL +0 -0
  342. {esphome-2025.8.4.dist-info → esphome-2025.9.0b1.dist-info}/entry_points.txt +0 -0
  343. {esphome-2025.8.4.dist-info → esphome-2025.9.0b1.dist-info}/licenses/LICENSE +0 -0
  344. {esphome-2025.8.4.dist-info → esphome-2025.9.0b1.dist-info}/top_level.txt +0 -0
@@ -12,6 +12,11 @@
12
12
 
13
13
  namespace esphome {
14
14
 
15
+ // Forward declaration for friend access
16
+ namespace api {
17
+ class APIConnection;
18
+ } // namespace api
19
+
15
20
  enum EntityCategory : uint8_t {
16
21
  ENTITY_CATEGORY_NONE = 0,
17
22
  ENTITY_CATEGORY_CONFIG = 1,
@@ -80,12 +85,50 @@ class EntityBase {
80
85
  // Set has_state - for components that need to manually set this
81
86
  void set_has_state(bool state) { this->flags_.has_state = state; }
82
87
 
88
+ /**
89
+ * @brief Get a unique hash for storing preferences/settings for this entity.
90
+ *
91
+ * This method returns a hash that uniquely identifies the entity for the purpose of
92
+ * storing preferences (such as calibration, state, etc.). Unlike get_object_id_hash(),
93
+ * this hash also incorporates the device_id (if devices are enabled), ensuring uniqueness
94
+ * across multiple devices that may have entities with the same object_id.
95
+ *
96
+ * Use this method when storing or retrieving preferences/settings that should be unique
97
+ * per device-entity pair. Use get_object_id_hash() when you need a hash that identifies
98
+ * the entity regardless of the device it belongs to.
99
+ *
100
+ * For backward compatibility, if device_id is 0 (the main device), the hash is unchanged
101
+ * from previous versions, so existing single-device configurations will continue to work.
102
+ *
103
+ * @return uint32_t The unique hash for preferences, including device_id if available.
104
+ */
105
+ uint32_t get_preference_hash() {
106
+ #ifdef USE_DEVICES
107
+ // Combine object_id_hash with device_id to ensure uniqueness across devices
108
+ // Note: device_id is 0 for the main device, so XORing with 0 preserves the original hash
109
+ // This ensures backward compatibility for existing single-device configurations
110
+ return this->get_object_id_hash() ^ this->get_device_id();
111
+ #else
112
+ // Without devices, just use object_id_hash as before
113
+ return this->get_object_id_hash();
114
+ #endif
115
+ }
116
+
83
117
  protected:
118
+ friend class api::APIConnection;
119
+
120
+ // Get object_id as StringRef when it's static (for API usage)
121
+ // Returns empty StringRef if object_id is dynamic (needs allocation)
122
+ StringRef get_object_id_ref_for_api_() const;
123
+
84
124
  /// The hash_base() function has been deprecated. It is kept in this
85
125
  /// class for now, to prevent external components from not compiling.
86
126
  virtual uint32_t hash_base() { return 0L; }
87
127
  void calc_object_id_();
88
128
 
129
+ /// Check if the object_id is dynamic (changes with MAC suffix)
130
+ bool is_object_id_dynamic_() const;
131
+
89
132
  StringRef name_;
90
133
  const char *object_id_c_str_{nullptr};
91
134
  #ifdef USE_ENTITY_ICON
esphome/core/helpers.cpp CHANGED
@@ -41,17 +41,28 @@ static const uint16_t CRC16_1021_BE_LUT_H[] = {0x0000, 0x1231, 0x2462, 0x3653, 0
41
41
 
42
42
  // Mathematics
43
43
 
44
- uint8_t crc8(const uint8_t *data, uint8_t len) {
45
- uint8_t crc = 0;
46
-
44
+ uint8_t crc8(const uint8_t *data, uint8_t len, uint8_t crc, uint8_t poly, bool msb_first) {
47
45
  while ((len--) != 0u) {
48
46
  uint8_t inbyte = *data++;
49
- for (uint8_t i = 8; i != 0u; i--) {
50
- bool mix = (crc ^ inbyte) & 0x01;
51
- crc >>= 1;
52
- if (mix)
53
- crc ^= 0x8C;
54
- inbyte >>= 1;
47
+ if (msb_first) {
48
+ // MSB first processing (for polynomials like 0x31, 0x07)
49
+ crc ^= inbyte;
50
+ for (uint8_t i = 8; i != 0u; i--) {
51
+ if (crc & 0x80) {
52
+ crc = (crc << 1) ^ poly;
53
+ } else {
54
+ crc <<= 1;
55
+ }
56
+ }
57
+ } else {
58
+ // LSB first processing (default for Dallas/Maxim 0x8C)
59
+ for (uint8_t i = 8; i != 0u; i--) {
60
+ bool mix = (crc ^ inbyte) & 0x01;
61
+ crc >>= 1;
62
+ if (mix)
63
+ crc ^= poly;
64
+ inbyte >>= 1;
65
+ }
55
66
  }
56
67
  }
57
68
  return crc;
@@ -131,11 +142,13 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t poly,
131
142
  return refout ? (crc ^ 0xffff) : crc;
132
143
  }
133
144
 
134
- uint32_t fnv1_hash(const std::string &str) {
145
+ uint32_t fnv1_hash(const char *str) {
135
146
  uint32_t hash = 2166136261UL;
136
- for (char c : str) {
137
- hash *= 16777619UL;
138
- hash ^= c;
147
+ if (str) {
148
+ while (*str) {
149
+ hash *= 16777619UL;
150
+ hash ^= *str++;
151
+ }
139
152
  }
140
153
  return hash;
141
154
  }
esphome/core/helpers.h CHANGED
@@ -145,8 +145,8 @@ template<typename T, typename U> T remap(U value, U min, U max, T min_out, T max
145
145
  return (value - min) * (max_out - min_out) / (max - min) + min_out;
146
146
  }
147
147
 
148
- /// Calculate a CRC-8 checksum of \p data with size \p len using the CRC-8-Dallas/Maxim polynomial.
149
- uint8_t crc8(const uint8_t *data, uint8_t len);
148
+ /// Calculate a CRC-8 checksum of \p data with size \p len.
149
+ uint8_t crc8(const uint8_t *data, uint8_t len, uint8_t crc = 0x00, uint8_t poly = 0x8C, bool msb_first = false);
150
150
 
151
151
  /// Calculate a CRC-16 checksum of \p data with size \p len.
152
152
  uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc = 0xffff, uint16_t reverse_poly = 0xa001,
@@ -155,7 +155,8 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc = 0, uint16_t p
155
155
  bool refout = false);
156
156
 
157
157
  /// Calculate a FNV-1 hash of \p str.
158
- uint32_t fnv1_hash(const std::string &str);
158
+ uint32_t fnv1_hash(const char *str);
159
+ inline uint32_t fnv1_hash(const std::string &str) { return fnv1_hash(str.c_str()); }
159
160
 
160
161
  /// Return a random 32-bit unsigned integer.
161
162
  uint32_t random_uint32();
@@ -78,9 +78,13 @@ size_t RingBuffer::write(const void *data, size_t len) {
78
78
  return this->write_without_replacement(data, len, 0);
79
79
  }
80
80
 
81
- size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait) {
81
+ size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait,
82
+ bool write_partial) {
82
83
  if (!xRingbufferSend(this->handle_, data, len, ticks_to_wait)) {
83
- // Couldn't fit all the data, so only write what will fit
84
+ if (!write_partial) {
85
+ return 0; // Not enough space available and not allowed to write partial data
86
+ }
87
+ // Couldn't fit all the data, write what will fit
84
88
  size_t free = std::min(this->free(), len);
85
89
  if (xRingbufferSend(this->handle_, data, free, 0)) {
86
90
  return free;
@@ -50,7 +50,8 @@ class RingBuffer {
50
50
  * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0)
51
51
  * @return Number of bytes written
52
52
  */
53
- size_t write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait = 0);
53
+ size_t write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait = 0,
54
+ bool write_partial = true);
54
55
 
55
56
  /**
56
57
  * @brief Returns the number of available bytes in the ring buffer.
@@ -14,7 +14,19 @@ namespace esphome {
14
14
 
15
15
  static const char *const TAG = "scheduler";
16
16
 
17
- static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10;
17
+ // Memory pool configuration constants
18
+ // Pool size of 5 matches typical usage patterns (2-4 active timers)
19
+ // - Minimal memory overhead (~250 bytes on ESP32)
20
+ // - Sufficient for most configs with a couple sensors/components
21
+ // - Still prevents heap fragmentation and allocation stalls
22
+ // - Complex setups with many timers will just allocate beyond the pool
23
+ // See https://github.com/esphome/backlog/issues/52
24
+ static constexpr size_t MAX_POOL_SIZE = 5;
25
+
26
+ // Maximum number of logically deleted (cancelled) items before forcing cleanup.
27
+ // Set to 5 to match the pool size - when we have as many cancelled items as our
28
+ // pool can hold, it's time to clean up and recycle them.
29
+ static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5;
18
30
  // Half the 32-bit range - used to detect rollovers vs normal time progression
19
31
  static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
20
32
  // max delay to start an interval sequence
@@ -79,8 +91,28 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
79
91
  return;
80
92
  }
81
93
 
94
+ // Get fresh timestamp BEFORE taking lock - millis_64_ may need to acquire lock itself
95
+ const uint64_t now = this->millis_64_(millis());
96
+
97
+ // Take lock early to protect scheduler_item_pool_ access
98
+ LockGuard guard{this->lock_};
99
+
82
100
  // Create and populate the scheduler item
83
- auto item = make_unique<SchedulerItem>();
101
+ std::unique_ptr<SchedulerItem> item;
102
+ if (!this->scheduler_item_pool_.empty()) {
103
+ // Reuse from pool
104
+ item = std::move(this->scheduler_item_pool_.back());
105
+ this->scheduler_item_pool_.pop_back();
106
+ #ifdef ESPHOME_DEBUG_SCHEDULER
107
+ ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size());
108
+ #endif
109
+ } else {
110
+ // Allocate new if pool is empty
111
+ item = make_unique<SchedulerItem>();
112
+ #ifdef ESPHOME_DEBUG_SCHEDULER
113
+ ESP_LOGD(TAG, "Allocated new item (pool empty)");
114
+ #endif
115
+ }
84
116
  item->component = component;
85
117
  item->set_name(name_cstr, !is_static_string);
86
118
  item->type = type;
@@ -99,7 +131,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
99
131
  // Single-core platforms don't need thread-safe defer handling
100
132
  if (delay == 0 && type == SchedulerItem::TIMEOUT) {
101
133
  // Put in defer queue for guaranteed FIFO execution
102
- LockGuard guard{this->lock_};
103
134
  if (!skip_cancel) {
104
135
  this->cancel_item_locked_(component, name_cstr, type);
105
136
  }
@@ -108,21 +139,18 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
108
139
  }
109
140
  #endif /* not ESPHOME_THREAD_SINGLE */
110
141
 
111
- // Get fresh timestamp for new timer/interval - ensures accurate scheduling
112
- const auto now = this->millis_64_(millis()); // Fresh millis() call
113
-
114
142
  // Type-specific setup
115
143
  if (type == SchedulerItem::INTERVAL) {
116
144
  item->interval = delay;
117
145
  // first execution happens immediately after a random smallish offset
118
146
  // Calculate random offset (0 to min(interval/2, 5s))
119
147
  uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float());
120
- item->next_execution_ = now + offset;
148
+ item->set_next_execution(now + offset);
121
149
  ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr ? name_cstr : "", delay,
122
150
  offset);
123
151
  } else {
124
152
  item->interval = 0;
125
- item->next_execution_ = now + delay;
153
+ item->set_next_execution(now + delay);
126
154
  }
127
155
 
128
156
  #ifdef ESPHOME_DEBUG_SCHEDULER
@@ -134,16 +162,15 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
134
162
  // Debug logging
135
163
  const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval";
136
164
  if (type == SchedulerItem::TIMEOUT) {
137
- ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, item->get_source(),
165
+ ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()),
138
166
  name_cstr ? name_cstr : "(null)", type_str, delay);
139
167
  } else {
140
- ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(),
141
- name_cstr ? name_cstr : "(null)", type_str, delay, static_cast<uint32_t>(item->next_execution_ - now));
168
+ ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()),
169
+ name_cstr ? name_cstr : "(null)", type_str, delay,
170
+ static_cast<uint32_t>(item->get_next_execution() - now));
142
171
  }
143
172
  #endif /* ESPHOME_DEBUG_SCHEDULER */
144
173
 
145
- LockGuard guard{this->lock_};
146
-
147
174
  // For retries, check if there's a cancelled timeout first
148
175
  if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT &&
149
176
  (has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) ||
@@ -285,9 +312,10 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
285
312
  auto &item = this->items_[0];
286
313
  // Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit
287
314
  const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from caller
288
- if (item->next_execution_ < now_64)
315
+ const uint64_t next_exec = item->get_next_execution();
316
+ if (next_exec < now_64)
289
317
  return 0;
290
- return item->next_execution_ - now_64;
318
+ return next_exec - now_64;
291
319
  }
292
320
  void HOT Scheduler::call(uint32_t now) {
293
321
  #ifndef ESPHOME_THREAD_SINGLE
@@ -319,6 +347,8 @@ void HOT Scheduler::call(uint32_t now) {
319
347
  if (!this->should_skip_item_(item.get())) {
320
348
  this->execute_item_(item.get(), now);
321
349
  }
350
+ // Recycle the defer item after execution
351
+ this->recycle_item_(std::move(item));
322
352
  }
323
353
  #endif /* not ESPHOME_THREAD_SINGLE */
324
354
 
@@ -326,6 +356,9 @@ void HOT Scheduler::call(uint32_t now) {
326
356
  const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop()
327
357
  this->process_to_add();
328
358
 
359
+ // Track if any items were added to to_add_ during this call (intervals or from callbacks)
360
+ bool has_added_items = false;
361
+
329
362
  #ifdef ESPHOME_DEBUG_SCHEDULER
330
363
  static uint64_t last_print = 0;
331
364
 
@@ -335,11 +368,11 @@ void HOT Scheduler::call(uint32_t now) {
335
368
  #ifdef ESPHOME_THREAD_MULTI_ATOMICS
336
369
  const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed);
337
370
  const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed);
338
- ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64,
339
- major_dbg, last_dbg);
371
+ ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
372
+ this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg);
340
373
  #else /* not ESPHOME_THREAD_MULTI_ATOMICS */
341
- ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64,
342
- this->millis_major_, this->last_millis_);
374
+ ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
375
+ this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_);
343
376
  #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
344
377
  // Cleanup before debug output
345
378
  this->cleanup_();
@@ -352,9 +385,10 @@ void HOT Scheduler::call(uint32_t now) {
352
385
  }
353
386
 
354
387
  const char *name = item->get_name();
355
- ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64,
356
- item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval,
357
- item->next_execution_ - now_64, item->next_execution_);
388
+ bool is_cancelled = is_item_removed_(item.get());
389
+ ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s",
390
+ item->get_type_str(), LOG_STR_ARG(item->get_source()), name ? name : "(null)", item->interval,
391
+ item->get_next_execution() - now_64, item->get_next_execution(), is_cancelled ? " [CANCELLED]" : "");
358
392
 
359
393
  old_items.push_back(std::move(item));
360
394
  }
@@ -369,8 +403,13 @@ void HOT Scheduler::call(uint32_t now) {
369
403
  }
370
404
  #endif /* ESPHOME_DEBUG_SCHEDULER */
371
405
 
372
- // If we have too many items to remove
373
- if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
406
+ // Cleanup removed items before processing
407
+ // First try to clean items from the top of the heap (fast path)
408
+ this->cleanup_();
409
+
410
+ // If we still have too many cancelled items, do a full cleanup
411
+ // This only happens if cancelled items are stuck in the middle/bottom of the heap
412
+ if (this->to_remove_ >= MAX_LOGICALLY_DELETED_ITEMS) {
374
413
  // We hold the lock for the entire cleanup operation because:
375
414
  // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout
376
415
  // 2. Other threads must see either the old state or the new state, not intermediate states
@@ -380,10 +419,13 @@ void HOT Scheduler::call(uint32_t now) {
380
419
 
381
420
  std::vector<std::unique_ptr<SchedulerItem>> valid_items;
382
421
 
383
- // Move all non-removed items to valid_items
422
+ // Move all non-removed items to valid_items, recycle removed ones
384
423
  for (auto &item : this->items_) {
385
- if (!item->remove) {
424
+ if (!is_item_removed_(item.get())) {
386
425
  valid_items.push_back(std::move(item));
426
+ } else {
427
+ // Recycle removed items
428
+ this->recycle_item_(std::move(item));
387
429
  }
388
430
  }
389
431
 
@@ -393,92 +435,92 @@ void HOT Scheduler::call(uint32_t now) {
393
435
  std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
394
436
  this->to_remove_ = 0;
395
437
  }
396
-
397
- // Cleanup removed items before processing
398
- this->cleanup_();
399
438
  while (!this->items_.empty()) {
400
- // use scoping to indicate visibility of `item` variable
401
- {
402
- // Don't copy-by value yet
403
- auto &item = this->items_[0];
404
- if (item->next_execution_ > now_64) {
405
- // Not reached timeout yet, done for this call
406
- break;
407
- }
408
- // Don't run on failed components
409
- if (item->component != nullptr && item->component->is_failed()) {
410
- LockGuard guard{this->lock_};
411
- this->pop_raw_();
412
- continue;
413
- }
439
+ // Don't copy-by value yet
440
+ auto &item = this->items_[0];
441
+ if (item->get_next_execution() > now_64) {
442
+ // Not reached timeout yet, done for this call
443
+ break;
444
+ }
445
+ // Don't run on failed components
446
+ if (item->component != nullptr && item->component->is_failed()) {
447
+ LockGuard guard{this->lock_};
448
+ this->pop_raw_();
449
+ continue;
450
+ }
414
451
 
415
- // Check if item is marked for removal
416
- // This handles two cases:
417
- // 1. Item was marked for removal after cleanup_() but before we got here
418
- // 2. Item is marked for removal but wasn't at the front of the heap during cleanup_()
452
+ // Check if item is marked for removal
453
+ // This handles two cases:
454
+ // 1. Item was marked for removal after cleanup_() but before we got here
455
+ // 2. Item is marked for removal but wasn't at the front of the heap during cleanup_()
419
456
  #ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS
420
- // Multi-threaded platforms without atomics: must take lock to safely read remove flag
421
- {
422
- LockGuard guard{this->lock_};
423
- if (is_item_removed_(item.get())) {
424
- this->pop_raw_();
425
- this->to_remove_--;
426
- continue;
427
- }
428
- }
429
- #else
430
- // Single-threaded or multi-threaded with atomics: can check without lock
457
+ // Multi-threaded platforms without atomics: must take lock to safely read remove flag
458
+ {
459
+ LockGuard guard{this->lock_};
431
460
  if (is_item_removed_(item.get())) {
432
- LockGuard guard{this->lock_};
433
461
  this->pop_raw_();
434
462
  this->to_remove_--;
435
463
  continue;
436
464
  }
465
+ }
466
+ #else
467
+ // Single-threaded or multi-threaded with atomics: can check without lock
468
+ if (is_item_removed_(item.get())) {
469
+ LockGuard guard{this->lock_};
470
+ this->pop_raw_();
471
+ this->to_remove_--;
472
+ continue;
473
+ }
437
474
  #endif
438
475
 
439
476
  #ifdef ESPHOME_DEBUG_SCHEDULER
440
- const char *item_name = item->get_name();
441
- ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
442
- item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval,
443
- item->next_execution_, now_64);
477
+ const char *item_name = item->get_name();
478
+ ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
479
+ item->get_type_str(), LOG_STR_ARG(item->get_source()), item_name ? item_name : "(null)", item->interval,
480
+ item->get_next_execution(), now_64);
444
481
  #endif /* ESPHOME_DEBUG_SCHEDULER */
445
482
 
446
- // Warning: During callback(), a lot of stuff can happen, including:
447
- // - timeouts/intervals get added, potentially invalidating vector pointers
448
- // - timeouts/intervals get cancelled
449
- this->execute_item_(item.get(), now);
450
- }
483
+ // Warning: During callback(), a lot of stuff can happen, including:
484
+ // - timeouts/intervals get added, potentially invalidating vector pointers
485
+ // - timeouts/intervals get cancelled
486
+ this->execute_item_(item.get(), now);
451
487
 
452
- {
453
- LockGuard guard{this->lock_};
488
+ LockGuard guard{this->lock_};
454
489
 
455
- // new scope, item from before might have been moved in the vector
456
- auto item = std::move(this->items_[0]);
457
- // Only pop after function call, this ensures we were reachable
458
- // during the function call and know if we were cancelled.
459
- this->pop_raw_();
490
+ auto executed_item = std::move(this->items_[0]);
491
+ // Only pop after function call, this ensures we were reachable
492
+ // during the function call and know if we were cancelled.
493
+ this->pop_raw_();
460
494
 
461
- if (item->remove) {
462
- // We were removed/cancelled in the function call, stop
463
- this->to_remove_--;
464
- continue;
465
- }
495
+ if (executed_item->remove) {
496
+ // We were removed/cancelled in the function call, stop
497
+ this->to_remove_--;
498
+ continue;
499
+ }
466
500
 
467
- if (item->type == SchedulerItem::INTERVAL) {
468
- item->next_execution_ = now_64 + item->interval;
469
- // Add new item directly to to_add_
470
- // since we have the lock held
471
- this->to_add_.push_back(std::move(item));
472
- }
501
+ if (executed_item->type == SchedulerItem::INTERVAL) {
502
+ executed_item->set_next_execution(now_64 + executed_item->interval);
503
+ // Add new item directly to to_add_
504
+ // since we have the lock held
505
+ this->to_add_.push_back(std::move(executed_item));
506
+ } else {
507
+ // Timeout completed - recycle it
508
+ this->recycle_item_(std::move(executed_item));
473
509
  }
510
+
511
+ has_added_items |= !this->to_add_.empty();
474
512
  }
475
513
 
476
- this->process_to_add();
514
+ if (has_added_items) {
515
+ this->process_to_add();
516
+ }
477
517
  }
478
518
  void HOT Scheduler::process_to_add() {
479
519
  LockGuard guard{this->lock_};
480
520
  for (auto &it : this->to_add_) {
481
- if (it->remove) {
521
+ if (is_item_removed_(it.get())) {
522
+ // Recycle cancelled items
523
+ this->recycle_item_(std::move(it));
482
524
  continue;
483
525
  }
484
526
 
@@ -518,6 +560,10 @@ size_t HOT Scheduler::cleanup_() {
518
560
  }
519
561
  void HOT Scheduler::pop_raw_() {
520
562
  std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
563
+
564
+ // Instead of destroying, recycle the item
565
+ this->recycle_item_(std::move(this->items_.back()));
566
+
521
567
  this->items_.pop_back();
522
568
  }
523
569
 
@@ -552,7 +598,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
552
598
 
553
599
  // Check all containers for matching items
554
600
  #ifndef ESPHOME_THREAD_SINGLE
555
- // Only check defer queue for timeouts (intervals never go there)
601
+ // Mark items in defer queue as cancelled (they'll be skipped when processed)
556
602
  if (type == SchedulerItem::TIMEOUT) {
557
603
  for (auto &item : this->defer_queue_) {
558
604
  if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
@@ -564,11 +610,22 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
564
610
  #endif /* not ESPHOME_THREAD_SINGLE */
565
611
 
566
612
  // Cancel items in the main heap
567
- for (auto &item : this->items_) {
568
- if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
569
- this->mark_item_removed_(item.get());
613
+ // Special case: if the last item in the heap matches, we can remove it immediately
614
+ // (removing the last element doesn't break heap structure)
615
+ if (!this->items_.empty()) {
616
+ auto &last_item = this->items_.back();
617
+ if (this->matches_item_(last_item, component, name_cstr, type, match_retry)) {
618
+ this->recycle_item_(std::move(this->items_.back()));
619
+ this->items_.pop_back();
570
620
  total_cancelled++;
571
- this->to_remove_++; // Track removals for heap items
621
+ }
622
+ // For other items in heap, we can only mark for removal (can't remove from middle of heap)
623
+ for (auto &item : this->items_) {
624
+ if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
625
+ this->mark_item_removed_(item.get());
626
+ total_cancelled++;
627
+ this->to_remove_++; // Track removals for heap items
628
+ }
572
629
  }
573
630
  }
574
631
 
@@ -744,7 +801,31 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
744
801
 
745
802
  bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
746
803
  const std::unique_ptr<SchedulerItem> &b) {
747
- return a->next_execution_ > b->next_execution_;
804
+ // High bits are almost always equal (change only on 32-bit rollover ~49 days)
805
+ // Optimize for common case: check low bits first when high bits are equal
806
+ return (a->next_execution_high_ == b->next_execution_high_) ? (a->next_execution_low_ > b->next_execution_low_)
807
+ : (a->next_execution_high_ > b->next_execution_high_);
808
+ }
809
+
810
+ void Scheduler::recycle_item_(std::unique_ptr<SchedulerItem> item) {
811
+ if (!item)
812
+ return;
813
+
814
+ if (this->scheduler_item_pool_.size() < MAX_POOL_SIZE) {
815
+ // Clear callback to release captured resources
816
+ item->callback = nullptr;
817
+ // Clear dynamic name if any
818
+ item->clear_dynamic_name();
819
+ this->scheduler_item_pool_.push_back(std::move(item));
820
+ #ifdef ESPHOME_DEBUG_SCHEDULER
821
+ ESP_LOGD(TAG, "Recycled item to pool (pool size now: %zu)", this->scheduler_item_pool_.size());
822
+ #endif
823
+ } else {
824
+ #ifdef ESPHOME_DEBUG_SCHEDULER
825
+ ESP_LOGD(TAG, "Pool full (size: %zu), deleting item", this->scheduler_item_pool_.size());
826
+ #endif
827
+ }
828
+ // else: unique_ptr will delete the item when it goes out of scope
748
829
  }
749
830
 
750
831
  } // namespace esphome