esphome 2025.8.4__py3-none-any.whl → 2025.9.0__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 (369) hide show
  1. esphome/__main__.py +177 -105
  2. esphome/components/absolute_humidity/absolute_humidity.cpp +3 -5
  3. esphome/components/adc/__init__.py +1 -26
  4. esphome/components/adc/adc_sensor_esp32.cpp +29 -6
  5. esphome/components/adc/sensor.py +20 -0
  6. esphome/components/ade7880/ade7880.cpp +1 -1
  7. esphome/components/ags10/ags10.cpp +3 -18
  8. esphome/components/ags10/ags10.h +2 -12
  9. esphome/components/aht10/aht10.cpp +3 -3
  10. esphome/components/airthings_ble/__init__.py +2 -2
  11. esphome/components/alarm_control_panel/__init__.py +2 -2
  12. esphome/components/am2315c/am2315c.cpp +1 -17
  13. esphome/components/am2315c/am2315c.h +2 -3
  14. esphome/components/api/__init__.py +2 -2
  15. esphome/components/api/api_connection.cpp +38 -34
  16. esphome/components/api/api_connection.h +20 -40
  17. esphome/components/api/api_frame_helper.cpp +25 -25
  18. esphome/components/api/api_frame_helper.h +3 -3
  19. esphome/components/api/api_frame_helper_noise.cpp +75 -40
  20. esphome/components/api/api_frame_helper_noise.h +3 -7
  21. esphome/components/api/api_frame_helper_plaintext.cpp +17 -4
  22. esphome/components/api/api_frame_helper_plaintext.h +1 -4
  23. esphome/components/api/api_pb2.cpp +12 -2
  24. esphome/components/api/api_pb2.h +144 -143
  25. esphome/components/api/api_pb2_dump.cpp +6 -1
  26. esphome/components/api/api_pb2_service.cpp +0 -14
  27. esphome/components/api/api_pb2_service.h +1 -3
  28. esphome/components/api/client.py +5 -3
  29. esphome/components/api/proto.cpp +33 -37
  30. esphome/components/async_tcp/__init__.py +2 -2
  31. esphome/components/atm90e26/sensor.py +2 -0
  32. esphome/components/atm90e32/sensor.py +4 -2
  33. esphome/components/audio_adc/__init__.py +2 -2
  34. esphome/components/audio_dac/__init__.py +2 -2
  35. esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +1 -1
  36. esphome/components/bedjet/bedjet_hub.cpp +1 -1
  37. esphome/components/binary_sensor/__init__.py +2 -2
  38. esphome/components/binary_sensor/binary_sensor.cpp +13 -0
  39. esphome/components/binary_sensor/binary_sensor.h +4 -7
  40. esphome/components/bl0940/__init__.py +6 -1
  41. esphome/components/bl0940/bl0940.cpp +178 -41
  42. esphome/components/bl0940/bl0940.h +121 -76
  43. esphome/components/bl0940/button/__init__.py +27 -0
  44. esphome/components/bl0940/button/calibration_reset_button.cpp +20 -0
  45. esphome/components/bl0940/button/calibration_reset_button.h +19 -0
  46. esphome/components/bl0940/number/__init__.py +94 -0
  47. esphome/components/bl0940/number/calibration_number.cpp +29 -0
  48. esphome/components/bl0940/number/calibration_number.h +26 -0
  49. esphome/components/bl0940/sensor.py +151 -2
  50. esphome/components/bl0942/bl0942.cpp +1 -1
  51. esphome/components/ble_client/output/__init__.py +4 -4
  52. esphome/components/bluetooth_proxy/__init__.py +1 -1
  53. esphome/components/bluetooth_proxy/bluetooth_connection.h +1 -1
  54. esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +15 -7
  55. esphome/components/bluetooth_proxy/bluetooth_proxy.h +6 -3
  56. esphome/components/button/__init__.py +2 -2
  57. esphome/components/button/button.cpp +13 -0
  58. esphome/components/button/button.h +4 -7
  59. esphome/components/camera/buffer.h +18 -0
  60. esphome/components/camera/buffer_impl.cpp +20 -0
  61. esphome/components/camera/buffer_impl.h +26 -0
  62. esphome/components/camera/camera.h +43 -0
  63. esphome/components/camera/encoder.h +69 -0
  64. esphome/components/camera_encoder/__init__.py +62 -0
  65. esphome/components/camera_encoder/encoder_buffer_impl.cpp +23 -0
  66. esphome/components/camera_encoder/encoder_buffer_impl.h +25 -0
  67. esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp +82 -0
  68. esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h +39 -0
  69. esphome/components/captive_portal/__init__.py +2 -2
  70. esphome/components/captive_portal/captive_index.h +77 -97
  71. esphome/components/captive_portal/captive_portal.cpp +35 -12
  72. esphome/components/captive_portal/captive_portal.h +3 -3
  73. esphome/components/ccs811/ccs811.cpp +3 -3
  74. esphome/components/climate/__init__.py +2 -2
  75. esphome/components/climate/climate.cpp +1 -1
  76. esphome/components/cover/__init__.py +5 -5
  77. esphome/components/cover/cover.cpp +1 -1
  78. esphome/components/cover/cover.h +2 -2
  79. esphome/components/dallas_temp/dallas_temp.cpp +2 -2
  80. esphome/components/datetime/__init__.py +2 -2
  81. esphome/components/datetime/date_entity.h +2 -2
  82. esphome/components/datetime/datetime_entity.h +2 -2
  83. esphome/components/datetime/time_entity.h +2 -2
  84. esphome/components/debug/debug_esp32.cpp +1 -1
  85. esphome/components/display/__init__.py +4 -4
  86. esphome/components/duty_time/duty_time_sensor.cpp +1 -1
  87. esphome/components/esp32/__init__.py +0 -5
  88. esphome/components/esp32/gpio.cpp +27 -23
  89. esphome/components/esp32/gpio.h +26 -11
  90. esphome/components/esp32/preferences.cpp +8 -4
  91. esphome/components/esp32_ble/__init__.py +7 -2
  92. esphome/components/esp32_ble/ble_uuid.cpp +30 -9
  93. esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +4 -3
  94. esphome/components/esp32_ble_client/ble_client_base.cpp +7 -3
  95. esphome/components/esp32_ble_client/ble_client_base.h +8 -5
  96. esphome/components/esp32_ble_tracker/__init__.py +2 -2
  97. esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +11 -47
  98. esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +2 -14
  99. esphome/components/esp8266/__init__.py +2 -2
  100. esphome/components/esp8266/core.cpp +2 -2
  101. esphome/components/esp8266/gpio.py +4 -4
  102. esphome/components/esp8266/preferences.cpp +30 -28
  103. esphome/components/esphome/ota/__init__.py +2 -2
  104. esphome/components/esphome/ota/ota_esphome.cpp +21 -19
  105. esphome/components/esphome/ota/ota_esphome.h +6 -5
  106. esphome/components/ethernet/__init__.py +18 -2
  107. esphome/components/ethernet/ethernet_component.cpp +53 -3
  108. esphome/components/ethernet/ethernet_component.h +4 -0
  109. esphome/components/event/__init__.py +2 -2
  110. esphome/components/event/event.h +4 -4
  111. esphome/components/factory_reset/button/factory_reset_button.cpp +18 -1
  112. esphome/components/factory_reset/button/factory_reset_button.h +6 -1
  113. esphome/components/factory_reset/switch/factory_reset_switch.cpp +18 -1
  114. esphome/components/factory_reset/switch/factory_reset_switch.h +5 -1
  115. esphome/components/fan/__init__.py +2 -2
  116. esphome/components/fan/fan.cpp +2 -1
  117. esphome/components/gdk101/gdk101.cpp +4 -4
  118. esphome/components/globals/__init__.py +2 -2
  119. esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +19 -18
  120. esphome/components/gpio_expander/cached_gpio.h +36 -16
  121. esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp +5 -5
  122. esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +1 -1
  123. esphome/components/haier/haier_base.cpp +1 -1
  124. esphome/components/haier/hon_climate.cpp +1 -1
  125. esphome/components/hlw8012/hlw8012.cpp +5 -5
  126. esphome/components/honeywellabp2_i2c/honeywellabp2.cpp +4 -4
  127. esphome/components/host/preferences.h +3 -2
  128. esphome/components/hte501/hte501.cpp +3 -21
  129. esphome/components/hte501/hte501.h +2 -3
  130. esphome/components/http_request/ota/__init__.py +2 -2
  131. esphome/components/i2c/__init__.py +2 -2
  132. esphome/components/i2c/i2c.cpp +13 -9
  133. esphome/components/i2c/i2c_bus.h +36 -6
  134. esphome/components/i2s_audio/__init__.py +8 -2
  135. esphome/components/i2s_audio/media_player/__init__.py +1 -1
  136. esphome/components/i2s_audio/microphone/__init__.py +1 -1
  137. esphome/components/i2s_audio/speaker/__init__.py +1 -1
  138. esphome/components/ina2xx_base/__init__.py +4 -2
  139. esphome/components/inkplate/__init__.py +1 -0
  140. esphome/components/inkplate/const.py +105 -0
  141. esphome/components/inkplate/display.py +238 -0
  142. esphome/components/{inkplate6 → inkplate}/inkplate.cpp +156 -74
  143. esphome/components/{inkplate6 → inkplate}/inkplate.h +28 -68
  144. esphome/components/inkplate6/__init__.py +0 -1
  145. esphome/components/inkplate6/display.py +2 -211
  146. esphome/components/integration/integration_sensor.cpp +1 -1
  147. esphome/components/json/__init__.py +2 -2
  148. esphome/components/lc709203f/lc709203f.cpp +4 -17
  149. esphome/components/lc709203f/lc709203f.h +2 -3
  150. esphome/components/ld2420/text_sensor/{text_sensor.cpp → ld2420_text_sensor.cpp} +1 -1
  151. esphome/components/ld2450/ld2450.cpp +1 -1
  152. esphome/components/libretiny/preferences.cpp +13 -5
  153. esphome/components/light/__init__.py +2 -2
  154. esphome/components/light/addressable_light_effect.h +7 -0
  155. esphome/components/light/base_light_effects.h +8 -0
  156. esphome/components/light/light_call.cpp +22 -20
  157. esphome/components/light/light_effect.cpp +36 -0
  158. esphome/components/light/light_effect.h +14 -0
  159. esphome/components/light/light_json_schema.cpp +9 -1
  160. esphome/components/light/light_state.cpp +2 -2
  161. esphome/components/light/light_state.h +38 -0
  162. esphome/components/lock/__init__.py +2 -2
  163. esphome/components/lock/lock.h +2 -2
  164. esphome/components/logger/__init__.py +2 -2
  165. esphome/components/logger/logger.cpp +25 -4
  166. esphome/components/logger/logger.h +1 -1
  167. esphome/components/logger/logger_esp32.cpp +16 -8
  168. esphome/components/logger/logger_esp8266.cpp +11 -3
  169. esphome/components/logger/logger_libretiny.cpp +13 -3
  170. esphome/components/logger/logger_rp2040.cpp +14 -3
  171. esphome/components/logger/logger_zephyr.cpp +15 -4
  172. esphome/components/lvgl/defines.py +1 -0
  173. esphome/components/lvgl/hello_world.py +96 -33
  174. esphome/components/lvgl/number/lvgl_number.h +1 -1
  175. esphome/components/lvgl/select/lvgl_select.h +1 -1
  176. esphome/components/lvgl/widgets/__init__.py +0 -1
  177. esphome/components/lvgl/widgets/spinbox.py +20 -11
  178. esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp +1 -1
  179. esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp +1 -1
  180. esphome/components/mapping/__init__.py +13 -5
  181. esphome/components/mapping/mapping.h +69 -0
  182. esphome/components/max17043/max17043.cpp +2 -2
  183. esphome/components/mcp23016/__init__.py +1 -0
  184. esphome/components/mcp23016/mcp23016.cpp +20 -5
  185. esphome/components/mcp23016/mcp23016.h +10 -4
  186. esphome/components/mcp23x08_base/mcp23x08_base.cpp +1 -1
  187. esphome/components/mcp23x17_base/mcp23x17_base.cpp +2 -2
  188. esphome/components/md5/md5.cpp +3 -2
  189. esphome/components/mdns/__init__.py +2 -2
  190. esphome/components/mdns/mdns_component.cpp +145 -54
  191. esphome/components/media_player/__init__.py +2 -2
  192. esphome/components/micro_wake_word/__init__.py +2 -2
  193. esphome/components/microphone/__init__.py +2 -2
  194. esphome/components/mipi/__init__.py +77 -33
  195. esphome/components/mipi_rgb/__init__.py +2 -0
  196. esphome/components/mipi_rgb/display.py +321 -0
  197. esphome/components/mipi_rgb/mipi_rgb.cpp +388 -0
  198. esphome/components/mipi_rgb/mipi_rgb.h +127 -0
  199. esphome/components/mipi_rgb/models/guition.py +24 -0
  200. esphome/components/mipi_rgb/models/lilygo.py +228 -0
  201. esphome/components/mipi_rgb/models/rpi.py +9 -0
  202. esphome/components/mipi_rgb/models/st7701s.py +214 -0
  203. esphome/components/mipi_rgb/models/waveshare.py +64 -0
  204. esphome/components/mipi_spi/models/jc.py +229 -0
  205. esphome/components/mlx90614/mlx90614.cpp +1 -16
  206. esphome/components/mlx90614/mlx90614.h +0 -1
  207. esphome/components/mqtt/__init__.py +2 -2
  208. esphome/components/mqtt/mqtt_client.cpp +1 -1
  209. esphome/components/mqtt/mqtt_sensor.cpp +7 -2
  210. esphome/components/ms5611/ms5611.cpp +7 -6
  211. esphome/components/network/__init__.py +2 -2
  212. esphome/components/nextion/nextion_upload.cpp +4 -1
  213. esphome/components/nrf52/__init__.py +49 -6
  214. esphome/components/nrf52/const.py +1 -0
  215. esphome/components/nrf52/dfu.cpp +51 -0
  216. esphome/components/nrf52/dfu.h +24 -0
  217. esphome/components/ntc/ntc.cpp +1 -1
  218. esphome/components/number/__init__.py +2 -2
  219. esphome/components/number/automation.cpp +1 -1
  220. esphome/components/number/number.cpp +21 -0
  221. esphome/components/number/number.h +4 -13
  222. esphome/components/opentherm/hub.h +6 -6
  223. esphome/components/opentherm/number/{number.cpp → opentherm_number.cpp} +2 -2
  224. esphome/components/opentherm/output/{output.cpp → opentherm_output.cpp} +1 -1
  225. esphome/components/opentherm/switch/{switch.cpp → opentherm_switch.cpp} +1 -1
  226. esphome/components/openthread/openthread.cpp +41 -7
  227. esphome/components/openthread/openthread.h +11 -0
  228. esphome/components/ota/__init__.py +2 -2
  229. esphome/components/pca6416a/__init__.py +1 -0
  230. esphome/components/pca6416a/pca6416a.cpp +20 -5
  231. esphome/components/pca6416a/pca6416a.h +12 -5
  232. esphome/components/pca9554/__init__.py +2 -1
  233. esphome/components/pca9554/pca9554.cpp +12 -18
  234. esphome/components/pca9554/pca9554.h +10 -9
  235. esphome/components/pcf8574/__init__.py +1 -0
  236. esphome/components/pcf8574/pcf8574.cpp +14 -5
  237. esphome/components/pcf8574/pcf8574.h +13 -6
  238. esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +7 -7
  239. esphome/components/pipsolar/__init__.py +3 -3
  240. esphome/components/pipsolar/output/__init__.py +4 -4
  241. esphome/components/pulse_width/pulse_width.cpp +2 -2
  242. esphome/components/qmp6988/qmp6988.cpp +81 -126
  243. esphome/components/qmp6988/qmp6988.h +31 -37
  244. esphome/components/radon_eye_ble/__init__.py +2 -2
  245. esphome/components/remote_base/__init__.py +6 -8
  246. esphome/components/rotary_encoder/rotary_encoder.cpp +1 -1
  247. esphome/components/rp2040/__init__.py +2 -2
  248. esphome/components/runtime_stats/runtime_stats.cpp +10 -23
  249. esphome/components/runtime_stats/runtime_stats.h +4 -10
  250. esphome/components/safe_mode/__init__.py +2 -2
  251. esphome/components/safe_mode/safe_mode.cpp +33 -31
  252. esphome/components/script/script.cpp +6 -0
  253. esphome/components/script/script.h +19 -5
  254. esphome/components/sdm_meter/sensor.py +3 -1
  255. esphome/components/select/__init__.py +2 -2
  256. esphome/components/select/select.cpp +3 -3
  257. esphome/components/select/select.h +2 -2
  258. esphome/components/select/select_call.cpp +1 -1
  259. esphome/components/sen5x/sen5x.cpp +57 -55
  260. esphome/components/sen5x/sen5x.h +21 -15
  261. esphome/components/sen5x/sensor.py +67 -44
  262. esphome/components/sensirion_common/i2c_sensirion.cpp +18 -47
  263. esphome/components/sensirion_common/i2c_sensirion.h +39 -55
  264. esphome/components/sensor/__init__.py +2 -2
  265. esphome/components/sensor/automation.h +1 -1
  266. esphome/components/sensor/sensor.cpp +34 -6
  267. esphome/components/sensor/sensor.h +4 -21
  268. esphome/components/sgp30/sgp30.cpp +34 -35
  269. esphome/components/sgp30/sgp30.h +11 -10
  270. esphome/components/sgp4x/sgp4x.cpp +2 -2
  271. esphome/components/shelly_dimmer/light.py +7 -7
  272. esphome/components/sht4x/sht4x.cpp +1 -1
  273. esphome/components/sntp/sntp_component.cpp +36 -9
  274. esphome/components/sntp/sntp_component.h +7 -0
  275. esphome/components/sound_level/sound_level.cpp +1 -1
  276. esphome/components/speaker/__init__.py +2 -2
  277. esphome/components/speaker/media_player/__init__.py +2 -2
  278. esphome/components/speaker/media_player/speaker_media_player.cpp +1 -1
  279. esphome/components/spi/__init__.py +2 -2
  280. esphome/components/sprinkler/sprinkler.cpp +1 -1
  281. esphome/components/sps30/sps30.cpp +18 -23
  282. esphome/components/sps30/sps30.h +3 -3
  283. esphome/components/status_led/__init__.py +2 -2
  284. esphome/components/stepper/__init__.py +2 -2
  285. esphome/components/switch/__init__.py +2 -2
  286. esphome/components/switch/switch.cpp +5 -5
  287. esphome/components/sx1509/__init__.py +1 -1
  288. esphome/components/sx1509/sx1509.cpp +12 -7
  289. esphome/components/sx1509/sx1509.h +11 -4
  290. esphome/components/tca9555/tca9555.cpp +5 -5
  291. esphome/components/tee501/tee501.cpp +2 -21
  292. esphome/components/tee501/tee501.h +2 -4
  293. esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +1 -1
  294. esphome/components/template/datetime/template_date.cpp +1 -1
  295. esphome/components/template/datetime/template_datetime.cpp +2 -2
  296. esphome/components/template/datetime/template_time.cpp +1 -1
  297. esphome/components/template/number/template_number.cpp +1 -1
  298. esphome/components/template/select/template_select.cpp +1 -1
  299. esphome/components/template/text/template_text.cpp +1 -1
  300. esphome/components/text/__init__.py +2 -2
  301. esphome/components/text/text.h +2 -2
  302. esphome/components/text_sensor/__init__.py +2 -2
  303. esphome/components/text_sensor/text_sensor.h +4 -4
  304. esphome/components/thermostat/climate.py +11 -7
  305. esphome/components/thermostat/thermostat_climate.cpp +237 -206
  306. esphome/components/thermostat/thermostat_climate.h +52 -41
  307. esphome/components/time/__init__.py +2 -2
  308. esphome/components/tmp1075/tmp1075.cpp +1 -1
  309. esphome/components/total_daily_energy/total_daily_energy.cpp +1 -1
  310. esphome/components/touchscreen/__init__.py +2 -2
  311. esphome/components/tuya/number/tuya_number.cpp +1 -1
  312. esphome/components/udp/udp_component.cpp +3 -3
  313. esphome/components/ufire_ec/ufire_ec.cpp +4 -4
  314. esphome/components/ufire_ise/ufire_ise.cpp +4 -4
  315. esphome/components/update/__init__.py +2 -2
  316. esphome/components/usb_uart/usb_uart.cpp +1 -1
  317. esphome/components/valve/__init__.py +5 -5
  318. esphome/components/valve/valve.cpp +1 -1
  319. esphome/components/valve/valve.h +2 -2
  320. esphome/components/wake_on_lan/wake_on_lan.cpp +2 -2
  321. esphome/components/waveshare_epaper/waveshare_213v3.cpp +1 -1
  322. esphome/components/web_server/__init__.py +2 -2
  323. esphome/components/web_server/ota/__init__.py +2 -2
  324. esphome/components/web_server/ota/ota_web_server.cpp +11 -0
  325. esphome/components/web_server/server_index_v2.h +149 -149
  326. esphome/components/web_server/web_server.cpp +58 -12
  327. esphome/components/web_server_base/__init__.py +2 -2
  328. esphome/components/wifi/__init__.py +5 -5
  329. esphome/components/wifi/wifi_component.cpp +4 -4
  330. esphome/components/wifi/wifi_component_esp_idf.cpp +2 -0
  331. esphome/components/wifi_info/wifi_info_text_sensor.h +3 -2
  332. esphome/config_validation.py +2 -2
  333. esphome/const.py +3 -1
  334. esphome/core/__init__.py +1 -0
  335. esphome/core/application.cpp +89 -51
  336. esphome/core/application.h +1 -0
  337. esphome/core/component.cpp +41 -19
  338. esphome/core/component.h +17 -13
  339. esphome/core/config.py +7 -7
  340. esphome/core/defines.h +5 -0
  341. esphome/core/entity_base.cpp +22 -8
  342. esphome/core/entity_base.h +43 -0
  343. esphome/core/helpers.cpp +34 -20
  344. esphome/core/helpers.h +33 -3
  345. esphome/core/ring_buffer.cpp +6 -2
  346. esphome/core/ring_buffer.h +2 -1
  347. esphome/core/scheduler.cpp +178 -97
  348. esphome/core/scheduler.h +67 -36
  349. esphome/core/time.cpp +6 -20
  350. esphome/coroutine.py +80 -3
  351. esphome/cpp_generator.py +13 -0
  352. esphome/cpp_helpers.py +2 -2
  353. esphome/dashboard/web_server.py +69 -15
  354. esphome/espota2.py +13 -6
  355. esphome/helpers.py +68 -83
  356. esphome/resolver.py +67 -0
  357. esphome/util.py +9 -6
  358. esphome/wizard.py +81 -34
  359. esphome/writer.py +13 -0
  360. {esphome-2025.8.4.dist-info → esphome-2025.9.0.dist-info}/METADATA +9 -9
  361. {esphome-2025.8.4.dist-info → esphome-2025.9.0.dist-info}/RECORD +369 -338
  362. /esphome/components/ld2420/text_sensor/{text_sensor.h → ld2420_text_sensor.h} +0 -0
  363. /esphome/components/opentherm/number/{number.h → opentherm_number.h} +0 -0
  364. /esphome/components/opentherm/output/{output.h → opentherm_output.h} +0 -0
  365. /esphome/components/opentherm/switch/{switch.h → opentherm_switch.h} +0 -0
  366. {esphome-2025.8.4.dist-info → esphome-2025.9.0.dist-info}/WHEEL +0 -0
  367. {esphome-2025.8.4.dist-info → esphome-2025.9.0.dist-info}/entry_points.txt +0 -0
  368. {esphome-2025.8.4.dist-info → esphome-2025.9.0.dist-info}/licenses/LICENSE +0 -0
  369. {esphome-2025.8.4.dist-info → esphome-2025.9.0.dist-info}/top_level.txt +0 -0
@@ -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
@@ -317,8 +345,10 @@ void HOT Scheduler::call(uint32_t now) {
317
345
  // Execute callback without holding lock to prevent deadlocks
318
346
  // if the callback tries to call defer() again
319
347
  if (!this->should_skip_item_(item.get())) {
320
- this->execute_item_(item.get(), now);
348
+ now = 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
+ now = 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,15 +560,19 @@ 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
 
524
570
  // Helper to execute a scheduler item
525
- void HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
571
+ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
526
572
  App.set_current_component(item->component);
527
573
  WarnIfComponentBlockingGuard guard{item->component, now};
528
574
  item->callback();
529
- guard.finish();
575
+ return guard.finish();
530
576
  }
531
577
 
532
578
  // Common implementation for cancel operations
@@ -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
esphome/core/scheduler.h CHANGED
@@ -88,19 +88,22 @@ class Scheduler {
88
88
  struct SchedulerItem {
89
89
  // Ordered by size to minimize padding
90
90
  Component *component;
91
- uint32_t interval;
92
- // 64-bit time to handle millis() rollover. The scheduler combines the 32-bit millis()
93
- // with a 16-bit rollover counter to create a 64-bit time that won't roll over for
94
- // billions of years. This ensures correct scheduling even when devices run for months.
95
- uint64_t next_execution_;
96
-
97
91
  // Optimized name storage using tagged union
98
92
  union {
99
93
  const char *static_name; // For string literals (no allocation)
100
94
  char *dynamic_name; // For allocated strings
101
95
  } name_;
102
-
96
+ uint32_t interval;
97
+ // Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
98
+ // with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
99
+ // This is intentionally limited to 48 bits, not stored as a full 64-bit value.
100
+ // With 49.7 days per 32-bit rollover, the 16-bit counter supports
101
+ // 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
102
+ // even when devices run for months. Split into two fields for better memory
103
+ // alignment on 32-bit systems.
104
+ uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)
103
105
  std::function<void()> callback;
106
+ uint16_t next_execution_high_; // Upper 16 bits (millis_major counter)
104
107
 
105
108
  #ifdef ESPHOME_THREAD_MULTI_ATOMICS
106
109
  // Multi-threaded with atomics: use atomic for lock-free access
@@ -126,7 +129,8 @@ class Scheduler {
126
129
  SchedulerItem()
127
130
  : component(nullptr),
128
131
  interval(0),
129
- next_execution_(0),
132
+ next_execution_low_(0),
133
+ next_execution_high_(0),
130
134
  #ifdef ESPHOME_THREAD_MULTI_ATOMICS
131
135
  // remove is initialized in the member declaration as std::atomic<bool>{false}
132
136
  type(TIMEOUT),
@@ -142,11 +146,7 @@ class Scheduler {
142
146
  }
143
147
 
144
148
  // Destructor to clean up dynamic names
145
- ~SchedulerItem() {
146
- if (name_is_dynamic) {
147
- delete[] name_.dynamic_name;
148
- }
149
- }
149
+ ~SchedulerItem() { clear_dynamic_name(); }
150
150
 
151
151
  // Delete copy operations to prevent accidental copies
152
152
  SchedulerItem(const SchedulerItem &) = delete;
@@ -159,13 +159,19 @@ class Scheduler {
159
159
  // Helper to get the name regardless of storage type
160
160
  const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; }
161
161
 
162
- // Helper to set name with proper ownership
163
- void set_name(const char *name, bool make_copy = false) {
164
- // Clean up old dynamic name if any
162
+ // Helper to clear dynamic name if allocated
163
+ void clear_dynamic_name() {
165
164
  if (name_is_dynamic && name_.dynamic_name) {
166
165
  delete[] name_.dynamic_name;
166
+ name_.dynamic_name = nullptr;
167
167
  name_is_dynamic = false;
168
168
  }
169
+ }
170
+
171
+ // Helper to set name with proper ownership
172
+ void set_name(const char *name, bool make_copy = false) {
173
+ // Clean up old dynamic name if any
174
+ clear_dynamic_name();
169
175
 
170
176
  if (!name) {
171
177
  // nullptr case - no name provided
@@ -183,8 +189,22 @@ class Scheduler {
183
189
  }
184
190
 
185
191
  static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
186
- const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; }
187
- const char *get_source() const { return component ? component->get_component_source() : "unknown"; }
192
+
193
+ // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility.
194
+ // The upper 16 bits of the 64-bit value are always zero, which is fine since
195
+ // millis_major_ is also 16 bits and they must match.
196
+ constexpr uint64_t get_next_execution() const {
197
+ return (static_cast<uint64_t>(next_execution_high_) << 32) | next_execution_low_;
198
+ }
199
+
200
+ constexpr void set_next_execution(uint64_t value) {
201
+ next_execution_low_ = static_cast<uint32_t>(value);
202
+ // Cast to uint16_t intentionally truncates to lower 16 bits of the upper 32 bits.
203
+ // This is correct because millis_major_ that creates these values is also 16 bits.
204
+ next_execution_high_ = static_cast<uint16_t>(value >> 32);
205
+ }
206
+ constexpr const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; }
207
+ const LogString *get_source() const { return component ? component->get_component_log_str() : LOG_STR("unknown"); }
188
208
  };
189
209
 
190
210
  // Common implementation for both timeout and interval
@@ -214,6 +234,15 @@ class Scheduler {
214
234
  // Common implementation for cancel operations
215
235
  bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
216
236
 
237
+ // Helper to check if two scheduler item names match
238
+ inline bool HOT names_match_(const char *name1, const char *name2) const {
239
+ // Check pointer equality first (common for static strings), then string contents
240
+ // The core ESPHome codebase uses static strings (const char*) for component names,
241
+ // making pointer comparison effective. The std::string overloads exist only for
242
+ // compatibility with external components but are rarely used in practice.
243
+ return (name1 != nullptr && name2 != nullptr) && ((name1 == name2) || (strcmp(name1, name2) == 0));
244
+ }
245
+
217
246
  // Helper function to check if item matches criteria for cancellation
218
247
  inline bool HOT matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr,
219
248
  SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const {
@@ -221,29 +250,20 @@ class Scheduler {
221
250
  (match_retry && !item->is_retry)) {
222
251
  return false;
223
252
  }
224
- const char *item_name = item->get_name();
225
- if (item_name == nullptr) {
226
- return false;
227
- }
228
- // Fast path: if pointers are equal
229
- // This is effective because the core ESPHome codebase uses static strings (const char*)
230
- // for component names. The std::string overloads exist only for compatibility with
231
- // external components, but are rarely used in practice.
232
- if (item_name == name_cstr) {
233
- return true;
234
- }
235
- // Slow path: compare string contents
236
- return strcmp(name_cstr, item_name) == 0;
253
+ return this->names_match_(item->get_name(), name_cstr);
237
254
  }
238
255
 
239
256
  // Helper to execute a scheduler item
240
- void execute_item_(SchedulerItem *item, uint32_t now);
257
+ uint32_t execute_item_(SchedulerItem *item, uint32_t now);
241
258
 
242
259
  // Helper to check if item should be skipped
243
- bool should_skip_item_(const SchedulerItem *item) const {
244
- return item->remove || (item->component != nullptr && item->component->is_failed());
260
+ bool should_skip_item_(SchedulerItem *item) const {
261
+ return is_item_removed_(item) || (item->component != nullptr && item->component->is_failed());
245
262
  }
246
263
 
264
+ // Helper to recycle a SchedulerItem
265
+ void recycle_item_(std::unique_ptr<SchedulerItem> item);
266
+
247
267
  // Helper to check if item is marked for removal (platform-specific)
248
268
  // Returns true if item should be skipped, handles platform-specific synchronization
249
269
  // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
@@ -280,8 +300,9 @@ class Scheduler {
280
300
  bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr,
281
301
  bool match_retry) const {
282
302
  for (const auto &item : container) {
283
- if (item->remove && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry,
284
- /* skip_removed= */ false)) {
303
+ if (is_item_removed_(item.get()) &&
304
+ this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry,
305
+ /* skip_removed= */ false)) {
285
306
  return true;
286
307
  }
287
308
  }
@@ -297,6 +318,16 @@ class Scheduler {
297
318
  #endif /* ESPHOME_THREAD_SINGLE */
298
319
  uint32_t to_remove_{0};
299
320
 
321
+ // Memory pool for recycling SchedulerItem objects to reduce heap churn.
322
+ // Design decisions:
323
+ // - std::vector is used instead of a fixed array because many systems only need 1-2 scheduler items
324
+ // - The vector grows dynamically up to MAX_POOL_SIZE (5) only when needed, saving memory on simple setups
325
+ // - Pool size of 5 matches typical usage (2-4 timers) while keeping memory overhead low (~250 bytes on ESP32)
326
+ // - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation
327
+ // can stall the entire system, causing timing issues and dropped events for any components that need
328
+ // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52)
329
+ std::vector<std::unique_ptr<SchedulerItem>> scheduler_item_pool_;
330
+
300
331
  #ifdef ESPHOME_THREAD_MULTI_ATOMICS
301
332
  /*
302
333
  * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates
esphome/core/time.cpp CHANGED
@@ -203,27 +203,13 @@ void ESPTime::recalc_timestamp_local() {
203
203
  }
204
204
 
205
205
  int32_t ESPTime::timezone_offset() {
206
- int32_t offset = 0;
207
206
  time_t now = ::time(nullptr);
208
- auto local = ESPTime::from_epoch_local(now);
209
- auto utc = ESPTime::from_epoch_utc(now);
210
- bool negative = utc.hour > local.hour && local.day_of_year <= utc.day_of_year;
211
-
212
- if (utc.minute > local.minute) {
213
- local.minute += 60;
214
- local.hour -= 1;
215
- }
216
- offset += (local.minute - utc.minute) * 60;
217
-
218
- if (negative) {
219
- offset -= (utc.hour - local.hour) * 3600;
220
- } else {
221
- if (utc.hour > local.hour) {
222
- local.hour += 24;
223
- }
224
- offset += (local.hour - utc.hour) * 3600;
225
- }
226
- return offset;
207
+ struct tm local_tm = *::localtime(&now);
208
+ local_tm.tm_isdst = 0; // Cause mktime to ignore daylight saving time because we want to include it in the offset.
209
+ time_t local_time = mktime(&local_tm);
210
+ struct tm utc_tm = *::gmtime(&now);
211
+ time_t utc_time = mktime(&utc_tm);
212
+ return static_cast<int32_t>(local_time - utc_time);
227
213
  }
228
214
 
229
215
  bool ESPTime::operator<(const ESPTime &other) const { return this->timestamp < other.timestamp; }