diff --git a/esphome/automation.py b/esphome/automation.py index 99be12451e..2439b1ddc4 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -15,7 +15,7 @@ from esphome.const import ( CONF_TYPE_ID, CONF_UPDATE_INTERVAL, ) -from esphome.core import ID +from esphome.core import ID, Lambda from esphome.cpp_generator import ( LambdaExpression, MockObj, @@ -310,6 +310,30 @@ async def for_condition_to_code( return var +@register_condition( + "component.is_idle", + LambdaCondition, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(cg.Component), + } + ), +) +async def component_is_idle_condition_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + comp = await cg.get_variable(config[CONF_ID]) + lambda_ = await cg.process_lambda( + Lambda(f"return {comp}->is_idle();"), args, return_type=bool + ) + return new_lambda_pvariable( + condition_id, lambda_, StatelessLambdaCondition, template_arg + ) + + @register_action( "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds) ) diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index 6d293528c6..61685c0566 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -77,6 +77,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga } } else { this->node_state = espbt::ClientState::ESTABLISHED; + // For non-notify characteristics, trigger an immediate read after service discovery + // to avoid peripherals disconnecting due to inactivity + this->update(); } break; } diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index e7da297fa0..b7a6d154db 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -79,6 +79,9 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } } else { this->node_state = espbt::ClientState::ESTABLISHED; + // For non-notify characteristics, trigger an immediate read after service discovery + // to avoid peripherals disconnecting due to inactivity + this->update(); } break; } diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index cfc09f4d53..c402a8c36c 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -121,10 +121,13 @@ void FanRestoreState::apply(Fan &fan) { fan.speed = this->speed; fan.direction = this->direction; - // Use stored preset index to get preset name from traits - const auto &preset_modes = fan.get_traits().supported_preset_modes(); - if (this->preset_mode < preset_modes.size()) { - fan.set_preset_mode_(preset_modes[this->preset_mode]); + auto traits = fan.get_traits(); + if (traits.supports_preset_modes()) { + // Use stored preset index to get preset name from traits + const auto &preset_modes = traits.supported_preset_modes(); + if (this->preset_mode < preset_modes.size()) { + fan.set_preset_mode_(preset_modes[this->preset_mode]); + } } fan.publish_state(); @@ -228,7 +231,7 @@ void Fan::save_state_() { state.direction = this->direction; const char *preset = this->get_preset_mode(); - if (preset != nullptr) { + if (traits.supports_preset_modes() && preset != nullptr) { const auto &preset_modes = traits.supported_preset_modes(); // Find index of current preset mode (pointer comparison is safe since preset is from traits) for (size_t i = 0; i < preset_modes.size(); i++) { diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 593c8c67bb..9b58727f2a 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -137,7 +137,11 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): lvgl = config[CONF_LVGL_ID] timeout = await lv_milliseconds.process(config[CONF_TIMEOUT]) async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: - lv_add(ReturnStatement(lvgl_comp.is_idle(timeout))) + lv_add( + ReturnStatement( + lv_expr.disp_get_inactive_time(lvgl_comp.get_disp()) > timeout + ) + ) var = cg.new_Pvariable( condition_id, TemplateArguments(LvglComponent, *template_arg), diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index ea58fdb85b..50d192fde3 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -175,7 +175,6 @@ class LvglComponent : public PollingComponent { static void monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px); static void render_start_cb(lv_disp_drv_t *disp_drv); void dump_config() override; - bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; } lv_disp_t *get_disp() { return this->disp_; } lv_obj_t *get_scr_act() { return lv_disp_get_scr_act(this->disp_); } // Pause or resume the display. diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 78838c70c8..083bb3ae31 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -103,7 +103,7 @@ template class ForCondition : public Condition, public Co bool check_internal() { bool cond = this->condition_->check(); if (!cond) - this->last_inactive_ = millis(); + this->last_inactive_ = App.get_loop_component_start_time(); return cond; } @@ -433,7 +433,7 @@ template class WaitUntilAction : public Action, public Co if (this->num_running_ == 0) return; - auto now = millis(); + auto now = App.get_loop_component_start_time(); this->var_queue_.remove_if([&](auto &queued) { auto start = std::get(queued); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 11d9501bb8..de3dd99d0c 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -284,6 +284,7 @@ bool Component::is_ready() const { (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; } +bool Component::is_idle() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE; } bool Component::can_proceed() { return true; } bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } diff --git a/esphome/core/component.h b/esphome/core/component.h index e97941374d..462e0e301c 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -141,6 +141,14 @@ class Component { */ bool is_in_loop_state() const; + /** Check if this component is idle. + * Being idle means being in LOOP_DONE state. + * This means the component has completed setup, is not failed, but its loop is currently disabled. + * + * @return True if the component is idle + */ + bool is_idle() const; + /** Mark this component as failed. Any future timeouts/intervals/setup/loop will no longer be called. * * This might be useful if a component wants to indicate that a connection to its peripheral failed. diff --git a/tests/components/esphome/common.yaml b/tests/components/esphome/common.yaml index a4b309b69d..b2d7bccaa5 100644 --- a/tests/components/esphome/common.yaml +++ b/tests/components/esphome/common.yaml @@ -10,7 +10,11 @@ esphome: on_shutdown: logger.log: on_shutdown on_loop: - logger.log: on_loop + if: + condition: + component.is_idle: binary_sensor_id + then: + logger.log: on_loop - sensor idle compile_process_limit: 1 min_version: "2025.1" name_add_mac_suffix: true @@ -34,5 +38,6 @@ esphome: binary_sensor: - platform: template + id: binary_sensor_id name: Other device sensor device_id: other_device diff --git a/tests/components/font/common.yaml b/tests/components/font/common.yaml index 6ba52e3d97..c156b4aea1 100644 --- a/tests/components/font/common.yaml +++ b/tests/components/font/common.yaml @@ -21,12 +21,12 @@ font: id: roboto_greek size: 20 glyphs: ["\u0300", "\u00C5", "\U000000C7"] - - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + - file: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft size: 20 - file: type: web - url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + url: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft2 size: 24 - file: $component_dir/Monocraft.ttf diff --git a/tests/components/font/test.host.yaml b/tests/components/font/test.host.yaml index c5399f2826..387ea47335 100644 --- a/tests/components/font/test.host.yaml +++ b/tests/components/font/test.host.yaml @@ -21,12 +21,12 @@ font: id: roboto_greek size: 20 glyphs: ["\u0300", "\u00C5", "\U000000C7"] - - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + - file: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft size: 20 - file: type: web - url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + url: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft2 size: 24 - file: $component_dir/Monocraft.ttf diff --git a/tests/components/image/common.yaml b/tests/components/image/common.yaml index 864ca41c44..9819068970 100644 --- a/tests/components/image/common.yaml +++ b/tests/components/image/common.yaml @@ -50,16 +50,16 @@ image: transparency: opaque - id: web_svg_image - file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg + file: https://media.esphome.io/logo/logo.svg resize: 256x48 type: BINARY transparency: chroma_key - id: web_tiff_image - file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff + file: https://media.esphome.io/tests/images/SIPI_Jelly_Beans_4.1.07.tiff type: RGB resize: 48x48 - id: web_redirect_image - file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4 + file: https://media.esphome.io/logo/logo.png type: RGB resize: 48x48 - id: mdi_alert