[cover] Add operation-based triggers and fix repeated trigger firing (#13471)

This commit is contained in:
Copilot
2026-02-05 06:52:14 +11:00
committed by GitHub
parent c1b412d5f3
commit 4a579700a0
5 changed files with 133 additions and 48 deletions
+62 -21
View File
@@ -1,3 +1,5 @@
import logging
from esphome import automation from esphome import automation
from esphome.automation import Condition, maybe_simple_id from esphome.automation import Condition, maybe_simple_id
import esphome.codegen as cg import esphome.codegen as cg
@@ -9,6 +11,7 @@ from esphome.const import (
CONF_ICON, CONF_ICON,
CONF_ID, CONF_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_ON_IDLE,
CONF_ON_OPEN, CONF_ON_OPEN,
CONF_POSITION, CONF_POSITION,
CONF_POSITION_COMMAND_TOPIC, CONF_POSITION_COMMAND_TOPIC,
@@ -32,9 +35,10 @@ from esphome.const import (
DEVICE_CLASS_SHUTTER, DEVICE_CLASS_SHUTTER,
DEVICE_CLASS_WINDOW, DEVICE_CLASS_WINDOW,
) )
from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType, TemplateArgsType
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@@ -53,6 +57,8 @@ DEVICE_CLASSES = [
DEVICE_CLASS_WINDOW, DEVICE_CLASS_WINDOW,
] ]
_LOGGER = logging.getLogger(__name__)
cover_ns = cg.esphome_ns.namespace("cover") cover_ns = cg.esphome_ns.namespace("cover")
Cover = cover_ns.class_("Cover", cg.EntityBase) Cover = cover_ns.class_("Cover", cg.EntityBase)
@@ -83,14 +89,29 @@ ControlAction = cover_ns.class_("ControlAction", automation.Action)
CoverPublishAction = cover_ns.class_("CoverPublishAction", automation.Action) CoverPublishAction = cover_ns.class_("CoverPublishAction", automation.Action)
CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition) CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition)
CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition) CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition)
CoverOpenedTrigger = cover_ns.class_(
# Triggers "CoverOpenedTrigger", automation.Trigger.template()
CoverOpenTrigger = cover_ns.class_("CoverOpenTrigger", automation.Trigger.template()) )
CoverClosedTrigger = cover_ns.class_( CoverClosedTrigger = cover_ns.class_(
"CoverClosedTrigger", automation.Trigger.template() "CoverClosedTrigger", automation.Trigger.template()
) )
CoverTrigger = cover_ns.class_("CoverTrigger", automation.Trigger.template())
# Cover-specific constants
CONF_ON_CLOSED = "on_closed" CONF_ON_CLOSED = "on_closed"
CONF_ON_OPENED = "on_opened"
CONF_ON_OPENING = "on_opening"
CONF_ON_CLOSING = "on_closing"
TRIGGERS = {
CONF_ON_OPEN: CoverOpenedTrigger, # Deprecated, use on_opened
CONF_ON_OPENED: CoverOpenedTrigger,
CONF_ON_CLOSED: CoverClosedTrigger,
CONF_ON_CLOSING: CoverTrigger.template(CoverOperation.COVER_OPERATION_CLOSING),
CONF_ON_OPENING: CoverTrigger.template(CoverOperation.COVER_OPERATION_OPENING),
CONF_ON_IDLE: CoverTrigger.template(CoverOperation.COVER_OPERATION_IDLE),
}
_COVER_SCHEMA = ( _COVER_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
@@ -111,16 +132,14 @@ _COVER_SCHEMA = (
cv.Optional(CONF_TILT_STATE_TOPIC): cv.All( cv.Optional(CONF_TILT_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.subscribe_topic cv.requires_component("mqtt"), cv.subscribe_topic
), ),
cv.Optional(CONF_ON_OPEN): automation.validate_automation( **{
{ cv.Optional(conf): automation.validate_automation(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenTrigger), {
} cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(trigger_class),
), }
cv.Optional(CONF_ON_CLOSED): automation.validate_automation( )
{ for conf, trigger_class in TRIGGERS.items()
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverClosedTrigger), },
}
),
} }
) )
) )
@@ -157,12 +176,14 @@ async def setup_cover_core_(var, config):
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class)) cg.add(var.set_device_class(device_class))
for conf in config.get(CONF_ON_OPEN, []): if CONF_ON_OPEN in config:
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) _LOGGER.warning(
await automation.build_automation(trigger, [], conf) "'on_open' is deprecated, use 'on_opened'. Will be removed in 2026.8.0"
for conf in config.get(CONF_ON_CLOSED, []): )
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) for trigger_conf in TRIGGERS:
await automation.build_automation(trigger, [], conf) for conf in config.get(trigger_conf, []):
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: if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
@@ -258,6 +279,26 @@ async def cover_control_to_code(config, action_id, template_arg, args):
return var return var
COVER_CONDITION_SCHEMA = cv.maybe_simple_value(
{cv.Required(CONF_ID): cv.use_id(Cover)}, key=CONF_ID
)
async def cover_condition_to_code(
config: ConfigType, condition_id: ID, template_arg: MockObj, args: TemplateArgsType
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
automation.register_condition(
"cover.is_open", CoverIsOpenCondition, COVER_CONDITION_SCHEMA
)(cover_condition_to_code)
automation.register_condition(
"cover.is_closed", CoverIsClosedCondition, COVER_CONDITION_SCHEMA
)(cover_condition_to_code)
@coroutine_with_priority(CoroPriority.CORE) @coroutine_with_priority(CoroPriority.CORE)
async def to_code(config): async def to_code(config):
cg.add_global(cover_ns.using) cg.add_global(cover_ns.using)
+31 -22
View File
@@ -90,44 +90,53 @@ template<typename... Ts> class CoverPublishAction : public Action<Ts...> {
Cover *cover_; Cover *cover_;
}; };
template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> { template<bool OPEN, typename... Ts> class CoverPositionCondition : public Condition<Ts...> {
public: public:
CoverIsOpenCondition(Cover *cover) : cover_(cover) {} CoverPositionCondition(Cover *cover) : cover_(cover) {}
bool check(const Ts &...x) override { return this->cover_->is_fully_open(); }
bool check(const Ts &...x) override { return this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED); }
protected: protected:
Cover *cover_; Cover *cover_;
}; };
template<typename... Ts> class CoverIsClosedCondition : public Condition<Ts...> { template<typename... Ts> using CoverIsOpenCondition = CoverPositionCondition<true, Ts...>;
template<typename... Ts> using CoverIsClosedCondition = CoverPositionCondition<false, Ts...>;
template<bool OPEN> class CoverPositionTrigger : public Trigger<> {
public: public:
CoverIsClosedCondition(Cover *cover) : cover_(cover) {} CoverPositionTrigger(Cover *a_cover) {
bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); } a_cover->add_on_state_callback([this, a_cover]() {
if (a_cover->position != this->last_position_) {
this->last_position_ = a_cover->position;
if (a_cover->position == (OPEN ? COVER_OPEN : COVER_CLOSED))
this->trigger();
}
});
}
protected: protected:
Cover *cover_; float last_position_{NAN};
}; };
class CoverOpenTrigger : public Trigger<> { using CoverOpenedTrigger = CoverPositionTrigger<true>;
using CoverClosedTrigger = CoverPositionTrigger<false>;
template<CoverOperation OP> class CoverTrigger : public Trigger<> {
public: public:
CoverOpenTrigger(Cover *a_cover) { CoverTrigger(Cover *a_cover) {
a_cover->add_on_state_callback([this, a_cover]() { a_cover->add_on_state_callback([this, a_cover]() {
if (a_cover->is_fully_open()) { auto current_op = a_cover->current_operation;
this->trigger(); if (current_op == OP) {
if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) {
this->trigger();
}
} }
this->last_operation_ = current_op;
}); });
} }
};
class CoverClosedTrigger : public Trigger<> { protected:
public: optional<CoverOperation> last_operation_{};
CoverClosedTrigger(Cover *a_cover) {
a_cover->add_on_state_callback([this, a_cover]() {
if (a_cover->is_fully_closed()) {
this->trigger();
}
});
}
}; };
} // namespace esphome::cover } // namespace esphome::cover
-3
View File
@@ -10,9 +10,6 @@ namespace esphome::cover {
static const char *const TAG = "cover"; static const char *const TAG = "cover";
const float COVER_OPEN = 1.0f;
const float COVER_CLOSED = 0.0f;
const LogString *cover_command_to_str(float pos) { const LogString *cover_command_to_str(float pos) {
if (pos == COVER_OPEN) { if (pos == COVER_OPEN) {
return LOG_STR("OPEN"); return LOG_STR("OPEN");
+2 -2
View File
@@ -10,8 +10,8 @@
namespace esphome::cover { namespace esphome::cover {
const extern float COVER_OPEN; static constexpr float COVER_OPEN = 1.0f;
const extern float COVER_CLOSED; static constexpr float COVER_CLOSED = 0.0f;
#define LOG_COVER(prefix, type, obj) \ #define LOG_COVER(prefix, type, obj) \
if ((obj) != nullptr) { \ if ((obj) != nullptr) { \
@@ -245,6 +245,44 @@ cover:
stop_action: stop_action:
- logger.log: stop_action - logger.log: stop_action
optimistic: true 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"
- logger.log: "Cover stopped moving"
- if:
condition:
cover.is_open: template_cover_with_triggers
then:
logger.log: Cover is open
- if:
condition:
cover.is_closed: template_cover_with_triggers
then:
logger.log: Cover is closed
number: number:
- platform: template - platform: template