mirror of
https://github.com/esphome/esphome.git
synced 2026-09-30 00:10:22 +00:00
[cover] Add operation-based triggers and fix repeated trigger firing (#13471)
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user