esphome 2025.8.4__py3-none-any.whl → 2025.9.0b2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- esphome/__main__.py +177 -105
- esphome/components/absolute_humidity/absolute_humidity.cpp +3 -5
- esphome/components/adc/__init__.py +1 -26
- esphome/components/adc/adc_sensor_esp32.cpp +29 -6
- esphome/components/adc/sensor.py +20 -0
- esphome/components/ags10/ags10.cpp +3 -18
- esphome/components/ags10/ags10.h +2 -12
- esphome/components/aht10/aht10.cpp +3 -3
- esphome/components/airthings_ble/__init__.py +2 -2
- esphome/components/alarm_control_panel/__init__.py +2 -2
- esphome/components/am2315c/am2315c.cpp +1 -17
- esphome/components/am2315c/am2315c.h +2 -3
- esphome/components/api/__init__.py +2 -2
- esphome/components/api/api_connection.cpp +38 -34
- esphome/components/api/api_connection.h +20 -40
- esphome/components/api/api_frame_helper.cpp +25 -25
- esphome/components/api/api_frame_helper.h +3 -3
- esphome/components/api/api_frame_helper_noise.cpp +75 -40
- esphome/components/api/api_frame_helper_noise.h +3 -7
- esphome/components/api/api_frame_helper_plaintext.cpp +17 -4
- esphome/components/api/api_frame_helper_plaintext.h +1 -4
- esphome/components/api/api_pb2.cpp +12 -2
- esphome/components/api/api_pb2.h +144 -143
- esphome/components/api/api_pb2_dump.cpp +6 -1
- esphome/components/api/api_pb2_service.cpp +0 -14
- esphome/components/api/api_pb2_service.h +1 -3
- esphome/components/api/client.py +5 -3
- esphome/components/api/proto.cpp +33 -37
- esphome/components/async_tcp/__init__.py +2 -2
- esphome/components/atm90e26/sensor.py +2 -0
- esphome/components/atm90e32/sensor.py +4 -2
- esphome/components/audio_adc/__init__.py +2 -2
- esphome/components/audio_dac/__init__.py +2 -2
- esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +1 -1
- esphome/components/bedjet/bedjet_hub.cpp +1 -1
- esphome/components/binary_sensor/__init__.py +2 -2
- esphome/components/binary_sensor/binary_sensor.cpp +13 -0
- esphome/components/binary_sensor/binary_sensor.h +4 -7
- esphome/components/bl0940/__init__.py +6 -1
- esphome/components/bl0940/bl0940.cpp +178 -41
- esphome/components/bl0940/bl0940.h +121 -76
- esphome/components/bl0940/button/__init__.py +27 -0
- esphome/components/bl0940/button/calibration_reset_button.cpp +20 -0
- esphome/components/bl0940/button/calibration_reset_button.h +19 -0
- esphome/components/bl0940/number/__init__.py +94 -0
- esphome/components/bl0940/number/calibration_number.cpp +29 -0
- esphome/components/bl0940/number/calibration_number.h +26 -0
- esphome/components/bl0940/sensor.py +151 -2
- esphome/components/bl0942/bl0942.cpp +1 -1
- esphome/components/ble_client/output/__init__.py +4 -4
- esphome/components/bluetooth_proxy/__init__.py +1 -1
- esphome/components/bluetooth_proxy/bluetooth_connection.h +1 -1
- esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +15 -7
- esphome/components/bluetooth_proxy/bluetooth_proxy.h +6 -3
- esphome/components/button/__init__.py +2 -2
- esphome/components/button/button.cpp +13 -0
- esphome/components/button/button.h +4 -7
- esphome/components/camera/buffer.h +18 -0
- esphome/components/camera/buffer_impl.cpp +20 -0
- esphome/components/camera/buffer_impl.h +26 -0
- esphome/components/camera/camera.h +43 -0
- esphome/components/camera/encoder.h +69 -0
- esphome/components/camera_encoder/__init__.py +62 -0
- esphome/components/camera_encoder/encoder_buffer_impl.cpp +23 -0
- esphome/components/camera_encoder/encoder_buffer_impl.h +25 -0
- esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp +82 -0
- esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h +39 -0
- esphome/components/captive_portal/__init__.py +2 -2
- esphome/components/captive_portal/captive_index.h +77 -97
- esphome/components/captive_portal/captive_portal.cpp +35 -12
- esphome/components/captive_portal/captive_portal.h +3 -3
- esphome/components/ccs811/ccs811.cpp +3 -3
- esphome/components/climate/__init__.py +2 -2
- esphome/components/climate/climate.cpp +1 -1
- esphome/components/cover/__init__.py +5 -5
- esphome/components/cover/cover.cpp +1 -1
- esphome/components/cover/cover.h +2 -2
- esphome/components/dallas_temp/dallas_temp.cpp +2 -2
- esphome/components/datetime/__init__.py +2 -2
- esphome/components/datetime/date_entity.h +2 -2
- esphome/components/datetime/datetime_entity.h +2 -2
- esphome/components/datetime/time_entity.h +2 -2
- esphome/components/debug/debug_esp32.cpp +1 -1
- esphome/components/display/__init__.py +4 -4
- esphome/components/duty_time/duty_time_sensor.cpp +1 -1
- esphome/components/esp32/__init__.py +0 -5
- esphome/components/esp32/gpio.cpp +27 -23
- esphome/components/esp32/gpio.h +26 -11
- esphome/components/esp32/preferences.cpp +8 -4
- esphome/components/esp32_ble/__init__.py +7 -2
- esphome/components/esp32_ble/ble_uuid.cpp +30 -9
- esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +4 -3
- esphome/components/esp32_ble_client/ble_client_base.cpp +7 -3
- esphome/components/esp32_ble_client/ble_client_base.h +8 -5
- esphome/components/esp32_ble_tracker/__init__.py +2 -2
- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +11 -47
- esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +2 -14
- esphome/components/esp8266/__init__.py +2 -2
- esphome/components/esp8266/core.cpp +2 -2
- esphome/components/esp8266/gpio.py +4 -4
- esphome/components/esp8266/preferences.cpp +30 -28
- esphome/components/esphome/ota/__init__.py +2 -2
- esphome/components/esphome/ota/ota_esphome.cpp +21 -19
- esphome/components/esphome/ota/ota_esphome.h +6 -5
- esphome/components/ethernet/__init__.py +7 -2
- esphome/components/ethernet/ethernet_component.cpp +49 -3
- esphome/components/ethernet/ethernet_component.h +2 -0
- esphome/components/event/__init__.py +2 -2
- esphome/components/event/event.h +4 -4
- esphome/components/factory_reset/button/factory_reset_button.cpp +18 -1
- esphome/components/factory_reset/button/factory_reset_button.h +6 -1
- esphome/components/factory_reset/switch/factory_reset_switch.cpp +18 -1
- esphome/components/factory_reset/switch/factory_reset_switch.h +5 -1
- esphome/components/fan/__init__.py +2 -2
- esphome/components/fan/fan.cpp +2 -1
- esphome/components/gdk101/gdk101.cpp +4 -4
- esphome/components/globals/__init__.py +2 -2
- esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +19 -18
- esphome/components/gpio_expander/cached_gpio.h +36 -16
- esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp +5 -5
- esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +1 -1
- esphome/components/haier/haier_base.cpp +1 -1
- esphome/components/haier/hon_climate.cpp +1 -1
- esphome/components/hlw8012/hlw8012.cpp +5 -5
- esphome/components/honeywellabp2_i2c/honeywellabp2.cpp +4 -4
- esphome/components/host/preferences.h +3 -2
- esphome/components/hte501/hte501.cpp +3 -21
- esphome/components/hte501/hte501.h +2 -3
- esphome/components/http_request/ota/__init__.py +2 -2
- esphome/components/i2c/__init__.py +2 -2
- esphome/components/i2c/i2c.cpp +13 -9
- esphome/components/i2c/i2c_bus.h +36 -6
- esphome/components/i2s_audio/__init__.py +8 -2
- esphome/components/i2s_audio/media_player/__init__.py +1 -1
- esphome/components/i2s_audio/microphone/__init__.py +1 -1
- esphome/components/i2s_audio/speaker/__init__.py +1 -1
- esphome/components/ina2xx_base/__init__.py +4 -2
- esphome/components/inkplate/__init__.py +1 -0
- esphome/components/inkplate/const.py +105 -0
- esphome/components/inkplate/display.py +238 -0
- esphome/components/{inkplate6 → inkplate}/inkplate.cpp +156 -74
- esphome/components/{inkplate6 → inkplate}/inkplate.h +28 -68
- esphome/components/inkplate6/__init__.py +0 -1
- esphome/components/inkplate6/display.py +2 -211
- esphome/components/integration/integration_sensor.cpp +1 -1
- esphome/components/json/__init__.py +2 -2
- esphome/components/lc709203f/lc709203f.cpp +4 -17
- esphome/components/lc709203f/lc709203f.h +2 -3
- esphome/components/ld2420/text_sensor/{text_sensor.cpp → ld2420_text_sensor.cpp} +1 -1
- esphome/components/ld2450/ld2450.cpp +1 -1
- esphome/components/libretiny/preferences.cpp +13 -5
- esphome/components/light/__init__.py +2 -2
- esphome/components/light/addressable_light_effect.h +7 -0
- esphome/components/light/base_light_effects.h +8 -0
- esphome/components/light/light_call.cpp +22 -20
- esphome/components/light/light_effect.cpp +36 -0
- esphome/components/light/light_effect.h +14 -0
- esphome/components/light/light_json_schema.cpp +9 -1
- esphome/components/light/light_state.cpp +2 -2
- esphome/components/light/light_state.h +38 -0
- esphome/components/lock/__init__.py +2 -2
- esphome/components/lock/lock.h +2 -2
- esphome/components/logger/__init__.py +2 -2
- esphome/components/logger/logger.cpp +25 -4
- esphome/components/logger/logger.h +1 -1
- esphome/components/logger/logger_esp32.cpp +16 -8
- esphome/components/logger/logger_esp8266.cpp +11 -3
- esphome/components/logger/logger_libretiny.cpp +13 -3
- esphome/components/logger/logger_rp2040.cpp +14 -3
- esphome/components/logger/logger_zephyr.cpp +15 -4
- esphome/components/lvgl/defines.py +1 -0
- esphome/components/lvgl/hello_world.py +96 -33
- esphome/components/lvgl/number/lvgl_number.h +1 -1
- esphome/components/lvgl/select/lvgl_select.h +1 -1
- esphome/components/lvgl/widgets/__init__.py +0 -1
- esphome/components/lvgl/widgets/spinbox.py +20 -11
- esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp +1 -1
- esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp +1 -1
- esphome/components/mapping/__init__.py +13 -5
- esphome/components/mapping/mapping.h +69 -0
- esphome/components/max17043/max17043.cpp +2 -2
- esphome/components/mcp23016/__init__.py +1 -0
- esphome/components/mcp23016/mcp23016.cpp +20 -5
- esphome/components/mcp23016/mcp23016.h +10 -4
- esphome/components/mcp23x08_base/mcp23x08_base.cpp +1 -1
- esphome/components/mcp23x17_base/mcp23x17_base.cpp +2 -2
- esphome/components/md5/md5.cpp +3 -2
- esphome/components/mdns/__init__.py +2 -2
- esphome/components/mdns/mdns_component.cpp +145 -54
- esphome/components/media_player/__init__.py +2 -2
- esphome/components/micro_wake_word/__init__.py +2 -2
- esphome/components/microphone/__init__.py +2 -2
- esphome/components/mipi/__init__.py +77 -33
- esphome/components/mipi_rgb/__init__.py +2 -0
- esphome/components/mipi_rgb/display.py +321 -0
- esphome/components/mipi_rgb/mipi_rgb.cpp +388 -0
- esphome/components/mipi_rgb/mipi_rgb.h +127 -0
- esphome/components/mipi_rgb/models/guition.py +24 -0
- esphome/components/mipi_rgb/models/lilygo.py +228 -0
- esphome/components/mipi_rgb/models/rpi.py +9 -0
- esphome/components/mipi_rgb/models/st7701s.py +214 -0
- esphome/components/mipi_rgb/models/waveshare.py +64 -0
- esphome/components/mipi_spi/models/jc.py +229 -0
- esphome/components/mlx90614/mlx90614.cpp +1 -16
- esphome/components/mlx90614/mlx90614.h +0 -1
- esphome/components/mqtt/__init__.py +2 -2
- esphome/components/mqtt/mqtt_sensor.cpp +7 -2
- esphome/components/ms5611/ms5611.cpp +7 -6
- esphome/components/network/__init__.py +2 -2
- esphome/components/nextion/nextion_upload.cpp +4 -1
- esphome/components/nrf52/__init__.py +49 -6
- esphome/components/nrf52/const.py +1 -0
- esphome/components/nrf52/dfu.cpp +51 -0
- esphome/components/nrf52/dfu.h +24 -0
- esphome/components/ntc/ntc.cpp +1 -1
- esphome/components/number/__init__.py +2 -2
- esphome/components/number/automation.cpp +1 -1
- esphome/components/number/number.cpp +21 -0
- esphome/components/number/number.h +4 -13
- esphome/components/opentherm/hub.h +6 -6
- esphome/components/opentherm/number/{number.cpp → opentherm_number.cpp} +2 -2
- esphome/components/opentherm/output/{output.cpp → opentherm_output.cpp} +1 -1
- esphome/components/opentherm/switch/{switch.cpp → opentherm_switch.cpp} +1 -1
- esphome/components/openthread/openthread.cpp +41 -7
- esphome/components/openthread/openthread.h +11 -0
- esphome/components/ota/__init__.py +2 -2
- esphome/components/pca6416a/__init__.py +1 -0
- esphome/components/pca6416a/pca6416a.cpp +20 -5
- esphome/components/pca6416a/pca6416a.h +12 -5
- esphome/components/pca9554/__init__.py +2 -1
- esphome/components/pca9554/pca9554.cpp +12 -18
- esphome/components/pca9554/pca9554.h +10 -9
- esphome/components/pcf8574/__init__.py +1 -0
- esphome/components/pcf8574/pcf8574.cpp +14 -5
- esphome/components/pcf8574/pcf8574.h +13 -6
- esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +7 -7
- esphome/components/pipsolar/__init__.py +3 -3
- esphome/components/pipsolar/output/__init__.py +4 -4
- esphome/components/pulse_width/pulse_width.cpp +2 -2
- esphome/components/qmp6988/qmp6988.cpp +81 -126
- esphome/components/qmp6988/qmp6988.h +31 -37
- esphome/components/radon_eye_ble/__init__.py +2 -2
- esphome/components/remote_base/__init__.py +6 -8
- esphome/components/rotary_encoder/rotary_encoder.cpp +1 -1
- esphome/components/rp2040/__init__.py +2 -2
- esphome/components/runtime_stats/runtime_stats.cpp +10 -23
- esphome/components/runtime_stats/runtime_stats.h +4 -10
- esphome/components/safe_mode/__init__.py +2 -2
- esphome/components/safe_mode/safe_mode.cpp +33 -31
- esphome/components/script/script.cpp +6 -0
- esphome/components/script/script.h +19 -5
- esphome/components/sdm_meter/sensor.py +3 -1
- esphome/components/select/__init__.py +2 -2
- esphome/components/select/select.h +2 -2
- esphome/components/sen5x/sen5x.cpp +57 -55
- esphome/components/sen5x/sen5x.h +21 -15
- esphome/components/sen5x/sensor.py +67 -44
- esphome/components/sensirion_common/i2c_sensirion.cpp +18 -47
- esphome/components/sensirion_common/i2c_sensirion.h +39 -55
- esphome/components/sensor/__init__.py +2 -2
- esphome/components/sensor/automation.h +1 -1
- esphome/components/sensor/sensor.cpp +34 -6
- esphome/components/sensor/sensor.h +4 -21
- esphome/components/sgp30/sgp30.cpp +34 -35
- esphome/components/sgp30/sgp30.h +11 -10
- esphome/components/sgp4x/sgp4x.cpp +2 -2
- esphome/components/shelly_dimmer/light.py +7 -7
- esphome/components/sht4x/sht4x.cpp +1 -1
- esphome/components/sntp/sntp_component.cpp +36 -9
- esphome/components/sntp/sntp_component.h +7 -0
- esphome/components/sound_level/sound_level.cpp +1 -1
- esphome/components/speaker/__init__.py +2 -2
- esphome/components/speaker/media_player/__init__.py +2 -2
- esphome/components/speaker/media_player/speaker_media_player.cpp +1 -1
- esphome/components/spi/__init__.py +2 -2
- esphome/components/sprinkler/sprinkler.cpp +1 -1
- esphome/components/sps30/sps30.cpp +18 -23
- esphome/components/sps30/sps30.h +3 -3
- esphome/components/status_led/__init__.py +2 -2
- esphome/components/stepper/__init__.py +2 -2
- esphome/components/switch/__init__.py +2 -2
- esphome/components/switch/switch.cpp +5 -5
- esphome/components/sx1509/__init__.py +1 -1
- esphome/components/sx1509/sx1509.cpp +12 -7
- esphome/components/sx1509/sx1509.h +11 -4
- esphome/components/tca9555/tca9555.cpp +5 -5
- esphome/components/tee501/tee501.cpp +2 -21
- esphome/components/tee501/tee501.h +2 -4
- esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +1 -1
- esphome/components/template/datetime/template_date.cpp +1 -1
- esphome/components/template/datetime/template_datetime.cpp +2 -2
- esphome/components/template/datetime/template_time.cpp +1 -1
- esphome/components/template/number/template_number.cpp +1 -1
- esphome/components/template/select/template_select.cpp +1 -1
- esphome/components/template/text/template_text.cpp +1 -1
- esphome/components/text/__init__.py +2 -2
- esphome/components/text/text.h +2 -2
- esphome/components/text_sensor/__init__.py +2 -2
- esphome/components/text_sensor/text_sensor.h +4 -4
- esphome/components/thermostat/climate.py +11 -7
- esphome/components/thermostat/thermostat_climate.cpp +237 -206
- esphome/components/thermostat/thermostat_climate.h +52 -41
- esphome/components/time/__init__.py +2 -2
- esphome/components/tmp1075/tmp1075.cpp +1 -1
- esphome/components/total_daily_energy/total_daily_energy.cpp +1 -1
- esphome/components/touchscreen/__init__.py +2 -2
- esphome/components/tuya/number/tuya_number.cpp +1 -1
- esphome/components/udp/udp_component.cpp +3 -3
- esphome/components/ufire_ec/ufire_ec.cpp +4 -4
- esphome/components/ufire_ise/ufire_ise.cpp +4 -4
- esphome/components/update/__init__.py +2 -2
- esphome/components/usb_uart/usb_uart.cpp +1 -1
- esphome/components/valve/__init__.py +5 -5
- esphome/components/valve/valve.cpp +1 -1
- esphome/components/valve/valve.h +2 -2
- esphome/components/wake_on_lan/wake_on_lan.cpp +2 -2
- esphome/components/waveshare_epaper/waveshare_213v3.cpp +1 -1
- esphome/components/web_server/__init__.py +2 -2
- esphome/components/web_server/ota/__init__.py +2 -2
- esphome/components/web_server/ota/ota_web_server.cpp +11 -0
- esphome/components/web_server/server_index_v2.h +149 -149
- esphome/components/web_server/web_server.cpp +58 -12
- esphome/components/web_server_base/__init__.py +2 -2
- esphome/components/wifi/__init__.py +5 -5
- esphome/components/wifi/wifi_component.cpp +4 -4
- esphome/components/wifi/wifi_component_esp_idf.cpp +2 -0
- esphome/components/wifi_info/wifi_info_text_sensor.h +3 -2
- esphome/config_validation.py +2 -2
- esphome/const.py +3 -1
- esphome/core/__init__.py +1 -0
- esphome/core/application.cpp +89 -51
- esphome/core/application.h +1 -0
- esphome/core/component.cpp +41 -19
- esphome/core/component.h +17 -13
- esphome/core/config.py +7 -7
- esphome/core/defines.h +4 -0
- esphome/core/entity_base.cpp +22 -8
- esphome/core/entity_base.h +43 -0
- esphome/core/helpers.cpp +34 -20
- esphome/core/helpers.h +33 -3
- esphome/core/ring_buffer.cpp +6 -2
- esphome/core/ring_buffer.h +2 -1
- esphome/core/scheduler.cpp +178 -97
- esphome/core/scheduler.h +67 -36
- esphome/core/time.cpp +6 -20
- esphome/coroutine.py +80 -3
- esphome/cpp_generator.py +13 -0
- esphome/cpp_helpers.py +2 -2
- esphome/dashboard/web_server.py +67 -10
- esphome/espota2.py +13 -6
- esphome/helpers.py +68 -83
- esphome/resolver.py +67 -0
- esphome/util.py +9 -6
- esphome/wizard.py +39 -26
- {esphome-2025.8.4.dist-info → esphome-2025.9.0b2.dist-info}/METADATA +9 -9
- {esphome-2025.8.4.dist-info → esphome-2025.9.0b2.dist-info}/RECORD +364 -333
- /esphome/components/ld2420/text_sensor/{text_sensor.h → ld2420_text_sensor.h} +0 -0
- /esphome/components/opentherm/number/{number.h → opentherm_number.h} +0 -0
- /esphome/components/opentherm/output/{output.h → opentherm_output.h} +0 -0
- /esphome/components/opentherm/switch/{switch.h → opentherm_switch.h} +0 -0
- {esphome-2025.8.4.dist-info → esphome-2025.9.0b2.dist-info}/WHEEL +0 -0
- {esphome-2025.8.4.dist-info → esphome-2025.9.0b2.dist-info}/entry_points.txt +0 -0
- {esphome-2025.8.4.dist-info → esphome-2025.9.0b2.dist-info}/licenses/LICENSE +0 -0
- {esphome-2025.8.4.dist-info → esphome-2025.9.0b2.dist-info}/top_level.txt +0 -0
esphome/core/scheduler.cpp
CHANGED
@@ -14,7 +14,19 @@ namespace esphome {
|
|
14
14
|
|
15
15
|
static const char *const TAG = "scheduler";
|
16
16
|
|
17
|
-
|
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
|
-
|
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->
|
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->
|
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,
|
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
|
-
|
315
|
+
const uint64_t next_exec = item->get_next_execution();
|
316
|
+
if (next_exec < now_64)
|
289
317
|
return 0;
|
290
|
-
return
|
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(),
|
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(),
|
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
|
-
|
356
|
-
|
357
|
-
item->
|
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
|
-
//
|
373
|
-
|
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
|
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
|
-
//
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
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
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
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
|
-
|
421
|
-
|
422
|
-
|
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
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
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
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
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
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
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
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
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
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
//
|
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
|
-
|
568
|
-
|
569
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
163
|
-
void
|
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
|
-
|
187
|
-
|
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
|
-
|
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
|
-
|
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_(
|
244
|
-
return item
|
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
|
284
|
-
|
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
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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; }
|