diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 383daee083..673f0f1ad7 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -9,7 +9,11 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_MQTT_ID, + CONF_ON_CLOSING, + CONF_ON_IDLE, CONF_ON_OPEN, + CONF_ON_OPENED, + CONF_ON_OPENING, CONF_POSITION, CONF_POSITION_COMMAND_TOPIC, CONF_POSITION_STATE_TOPIC, @@ -86,9 +90,13 @@ CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition) # Triggers CoverOpenTrigger = cover_ns.class_("CoverOpenTrigger", automation.Trigger.template()) +CoverOpenedTrigger = cover_ns.class_("CoverOpenedTrigger", automation.Trigger.template()) CoverClosedTrigger = cover_ns.class_( "CoverClosedTrigger", automation.Trigger.template() ) +CoverOpeningTrigger = cover_ns.class_("CoverOpeningTrigger", automation.Trigger.template()) +CoverClosingTrigger = cover_ns.class_("CoverClosingTrigger", automation.Trigger.template()) +CoverIdleTrigger = cover_ns.class_("CoverIdleTrigger", automation.Trigger.template()) CONF_ON_CLOSED = "on_closed" @@ -111,9 +119,17 @@ _COVER_SCHEMA = ( cv.Optional(CONF_TILT_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic ), - cv.Optional(CONF_ON_OPEN): automation.validate_automation( + cv.Optional(CONF_ON_OPEN): cv.All( + cv.deprecated(CONF_ON_OPENED, "2026.2.0"), + automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenTrigger), + } + ), + ), + cv.Optional(CONF_ON_OPENED): automation.validate_automation( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenTrigger), + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenedTrigger), } ), cv.Optional(CONF_ON_CLOSED): automation.validate_automation( @@ -121,6 +137,21 @@ _COVER_SCHEMA = ( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverClosedTrigger), } ), + cv.Optional(CONF_ON_OPENING): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpeningTrigger), + } + ), + cv.Optional(CONF_ON_CLOSING): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverClosingTrigger), + } + ), + cv.Optional(CONF_ON_IDLE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverIdleTrigger), + } + ), } ) ) @@ -160,9 +191,21 @@ async def setup_cover_core_(var, config): for conf in config.get(CONF_ON_OPEN, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_OPENED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_CLOSED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_OPENING, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_CLOSING, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_IDLE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index c0345a7cc6..28f665f883 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -119,6 +119,17 @@ class CoverOpenTrigger : public Trigger<> { } }; +class CoverOpenedTrigger : public Trigger<> { + public: + CoverOpenedTrigger(Cover *a_cover) { + a_cover->add_on_state_callback([this, a_cover]() { + if (a_cover->is_fully_open()) { + this->trigger(); + } + }); + } +}; + class CoverClosedTrigger : public Trigger<> { public: CoverClosedTrigger(Cover *a_cover) { @@ -130,4 +141,52 @@ class CoverClosedTrigger : public Trigger<> { } }; +class CoverOpeningTrigger : public Trigger<> { + public: + CoverOpeningTrigger(Cover *a_cover) { + a_cover->add_on_state_callback([this, a_cover]() { + auto current_op = a_cover->current_operation; + if (current_op == COVER_OPERATION_OPENING && this->last_operation_ != COVER_OPERATION_OPENING) { + this->trigger(); + } + this->last_operation_ = current_op; + }); + } + + protected: + CoverOperation last_operation_{COVER_OPERATION_IDLE}; +}; + +class CoverClosingTrigger : public Trigger<> { + public: + CoverClosingTrigger(Cover *a_cover) { + a_cover->add_on_state_callback([this, a_cover]() { + auto current_op = a_cover->current_operation; + if (current_op == COVER_OPERATION_CLOSING && this->last_operation_ != COVER_OPERATION_CLOSING) { + this->trigger(); + } + this->last_operation_ = current_op; + }); + } + + protected: + CoverOperation last_operation_{COVER_OPERATION_IDLE}; +}; + +class CoverIdleTrigger : public Trigger<> { + public: + CoverIdleTrigger(Cover *a_cover) { + a_cover->add_on_state_callback([this, a_cover]() { + auto current_op = a_cover->current_operation; + if (current_op == COVER_OPERATION_IDLE && this->last_operation_ != COVER_OPERATION_IDLE) { + this->trigger(); + } + this->last_operation_ = current_op; + }); + } + + protected: + CoverOperation last_operation_{COVER_OPERATION_IDLE}; +}; + } // namespace esphome::cover diff --git a/esphome/const.py b/esphome/const.py index 4243b2e25d..aee9b59cea 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -703,6 +703,10 @@ CONF_ON_LOOP = "on_loop" CONF_ON_MESSAGE = "on_message" CONF_ON_MULTI_CLICK = "on_multi_click" CONF_ON_OPEN = "on_open" +CONF_ON_OPENED = "on_opened" +CONF_ON_OPENING = "on_opening" +CONF_ON_CLOSING = "on_closing" +CONF_ON_IDLE = "on_idle" CONF_ON_OSCILLATING_SET = "on_oscillating_set" CONF_ON_PRESET_SET = "on_preset_set" CONF_ON_PRESS = "on_press" diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 9dc65fbab8..93672243fc 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -245,6 +245,33 @@ cover: stop_action: - logger.log: stop_action optimistic: true + - platform: template + name: "Template Cover with Triggers" + id: template_cover_with_triggers + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } + return COVER_CLOSED; + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true + on_open: + - logger.log: "Cover on_open (deprecated)" + on_opened: + - logger.log: "Cover fully opened" + on_closed: + - logger.log: "Cover fully closed" + on_opening: + - logger.log: "Cover started opening" + on_closing: + - logger.log: "Cover started closing" + on_idle: + - logger.log: "Cover stopped moving" number: - platform: template