Compare commits

...

10 Commits

Author SHA1 Message Date
clydebarrow
4c0e0b8d76 Add conditions 2026-01-23 18:10:01 +11:00
clydebarrow
48b80858a7 undo some templating 2026-01-23 17:50:46 +11:00
clydebarrow
081081f69a Fix constants; reduce logging spam 2026-01-23 16:51:52 +11:00
clydebarrow
8abb783b64 Refactored using templates 2026-01-23 16:42:32 +11:00
copilot-swe-agent[bot]
bccfe9eead Move cover-specific constants from const.py to cover component
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-23 03:15:06 +00:00
copilot-swe-agent[bot]
35fb44da36 Address code review feedback: add comments and fix trigger initialization
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-23 03:06:05 +00:00
copilot-swe-agent[bot]
1dfb8926d3 Test and validate cover triggers implementation
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-23 03:03:39 +00:00
copilot-swe-agent[bot]
0d63c755b7 Add new cover triggers: on_opening, on_closing, on_idle, on_opened
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-23 02:58:20 +00:00
copilot-swe-agent[bot]
6f2ca4c2a7 Initial plan 2026-01-23 02:54:22 +00:00
Keith Burzinski
cfb61bc50a [ir_rf_proxy] Remove unnecessary headers, add tests (#13464) 2026-01-22 20:35:37 -06:00
19 changed files with 264 additions and 73 deletions

View File

@@ -1,3 +1,5 @@
import logging
from esphome import automation
from esphome.automation import Condition, maybe_simple_id
import esphome.codegen as cg
@@ -9,6 +11,7 @@ from esphome.const import (
CONF_ICON,
CONF_ID,
CONF_MQTT_ID,
CONF_ON_IDLE,
CONF_ON_OPEN,
CONF_POSITION,
CONF_POSITION_COMMAND_TOPIC,
@@ -53,6 +56,8 @@ DEVICE_CLASSES = [
DEVICE_CLASS_WINDOW,
]
_LOGGER = logging.getLogger(__name__)
cover_ns = cg.esphome_ns.namespace("cover")
Cover = cover_ns.class_("Cover", cg.EntityBase)
@@ -83,14 +88,30 @@ ControlAction = cover_ns.class_("ControlAction", automation.Action)
CoverPublishAction = cover_ns.class_("CoverPublishAction", automation.Action)
CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition)
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()
)
CoverTrigger = cover_ns.class_("CoverTrigger", automation.Trigger.template())
# Cover-specific constants
CONF_ON_CLOSED = "on_closed"
CONF_ON_OPENED = "on_opened"
CONF_ON_OPENING = "on_opening"
CONF_ON_CLOSING = "on_closing"
OPERATIONS = (
CONF_ON_CLOSING,
CONF_ON_OPENING,
CONF_ON_IDLE,
)
def get_operation_from_conf_(conf: str) -> CoverOperation:
return getattr(CoverOperation, "COVER_OPERATION_" + conf.split("_")[1].upper())
_COVER_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
@@ -111,9 +132,15 @@ _COVER_SCHEMA = (
cv.Optional(CONF_TILT_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.subscribe_topic
),
# Deprecated trigger
cv.Optional(CONF_ON_OPEN): 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_OPENED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenedTrigger),
}
),
cv.Optional(CONF_ON_CLOSED): automation.validate_automation(
@@ -121,6 +148,16 @@ _COVER_SCHEMA = (
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverClosedTrigger),
}
),
**{
cv.Optional(conf): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
CoverTrigger.template(get_operation_from_conf_(conf))
),
}
)
for conf in OPERATIONS
},
}
)
)
@@ -157,12 +194,23 @@ async def setup_cover_core_(var, config):
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
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_CLOSED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
if on_opens := config.get(CONF_ON_OPEN):
_LOGGER.warning(
"The 'on_open' trigger for covers is deprecated and will be removed in a future release. Please use 'on_opened' instead."
)
for conf in on_opens:
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for on_op in OPERATIONS:
if triggers := config.get(on_op):
for conf in triggers:
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for on_state in [CONF_ON_OPENED, CONF_ON_CLOSED]:
if triggers := config.get(on_state):
for conf in triggers:
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)
@@ -258,6 +306,26 @@ async def cover_control_to_code(config, action_id, template_arg, args):
return var
@automation.register_condition(
"cover.is_open",
CoverIsOpenCondition,
cv.maybe_simple_value({cv.Required(CONF_ID): cv.use_id(Cover)}, key=CONF_ID),
)
async def cover_is_open_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
@automation.register_condition(
"cover.is_closed",
CoverIsClosedCondition,
cv.maybe_simple_value({cv.Required(CONF_ID): cv.use_id(Cover)}, key=CONF_ID),
)
async def cover_is_closed_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config):
cg.add_global(cover_ns.using)

View File

@@ -5,7 +5,6 @@
#include "cover.h"
namespace esphome::cover {
template<typename... Ts> class OpenAction : public Action<Ts...> {
public:
explicit OpenAction(Cover *cover) : cover_(cover) {}
@@ -72,6 +71,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
template<typename... Ts> class CoverPublishAction : public Action<Ts...> {
public:
CoverPublishAction(Cover *cover) : cover_(cover) {}
TEMPLATABLE_VALUE(float, position)
TEMPLATABLE_VALUE(float, tilt)
TEMPLATABLE_VALUE(CoverOperation, current_operation)
@@ -93,7 +93,8 @@ template<typename... Ts> class CoverPublishAction : public Action<Ts...> {
template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> {
public:
CoverIsOpenCondition(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 == COVER_OPEN; }
protected:
Cover *cover_;
@@ -102,32 +103,60 @@ template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> {
template<typename... Ts> class CoverIsClosedCondition : public Condition<Ts...> {
public:
CoverIsClosedCondition(Cover *cover) : cover_(cover) {}
bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); }
bool check(const Ts &...x) override { return this->cover_->position == COVER_CLOSED; }
protected:
Cover *cover_;
};
class CoverOpenTrigger : public Trigger<> {
class CoverOpenedTrigger : public Trigger<> {
public:
CoverOpenTrigger(Cover *a_cover) {
CoverOpenedTrigger(Cover *a_cover) {
a_cover->add_on_state_callback([this, a_cover]() {
if (a_cover->is_fully_open()) {
this->trigger();
if (a_cover->position != this->last_position_) {
this->last_position_ = a_cover->position;
if (a_cover->position == COVER_OPEN)
this->trigger();
}
});
}
protected:
float last_position_{NAN};
};
class CoverClosedTrigger : public Trigger<> {
public:
CoverClosedTrigger(Cover *a_cover) {
a_cover->add_on_state_callback([this, a_cover]() {
if (a_cover->is_fully_closed()) {
this->trigger();
if (a_cover->position != this->last_position_) {
this->last_position_ = a_cover->position;
if (a_cover->position == COVER_CLOSED)
this->trigger();
}
});
}
protected:
float last_position_{NAN};
};
template<CoverOperation OP> class CoverTrigger : public Trigger<> {
public:
CoverTrigger(Cover *a_cover) {
a_cover->add_on_state_callback([this, a_cover]() {
auto current_op = a_cover->current_operation;
if (current_op == OP) {
if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) {
this->trigger();
}
}
this->last_operation_ = current_op;
});
}
protected:
optional<CoverOperation> last_operation_{};
};
} // namespace esphome::cover

View File

@@ -10,9 +10,6 @@ namespace esphome::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) {
if (pos == COVER_OPEN) {
return LOG_STR("OPEN");
@@ -153,23 +150,23 @@ void Cover::publish_state(bool save) {
this->position = clamp(this->position, 0.0f, 1.0f);
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
ESP_LOGV(TAG, "'%s' >>", this->name_.c_str());
auto traits = this->get_traits();
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);
ESP_LOGV(TAG, " Position: %.0f%%", this->position * 100.0f);
} else {
if (this->position == COVER_OPEN) {
ESP_LOGD(TAG, " State: OPEN");
ESP_LOGV(TAG, " State: OPEN");
} else if (this->position == COVER_CLOSED) {
ESP_LOGD(TAG, " State: CLOSED");
ESP_LOGV(TAG, " State: CLOSED");
} else {
ESP_LOGD(TAG, " State: UNKNOWN");
ESP_LOGV(TAG, " State: UNKNOWN");
}
}
if (traits.get_supports_tilt()) {
ESP_LOGD(TAG, " Tilt: %.0f%%", this->tilt * 100.0f);
ESP_LOGV(TAG, " Tilt: %.0f%%", this->tilt * 100.0f);
}
ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation)));
ESP_LOGV(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation)));
this->state_callback_.call();
#if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY)

View File

@@ -10,8 +10,8 @@
namespace esphome::cover {
const extern float COVER_OPEN;
const extern float COVER_CLOSED;
static constexpr const float COVER_OPEN = 1.0f;
static constexpr const float COVER_CLOSED = 0.0f;
#define LOG_COVER(prefix, type, obj) \
if ((obj) != nullptr) { \

View File

@@ -5,8 +5,6 @@
// Once the API is considered stable, this warning will be removed.
#include "esphome/components/infrared/infrared.h"
#include "esphome/components/remote_transmitter/remote_transmitter.h"
#include "esphome/components/remote_receiver/remote_receiver.h"
namespace esphome::ir_rf_proxy {

View File

@@ -0,0 +1,18 @@
remote_receiver:
id: ir_receiver
pin: ${rx_pin}
# Test various hardware types with transmitter/receiver using infrared platform
infrared:
# Infrared receiver
- platform: ir_rf_proxy
id: ir_rx
name: "IR Receiver"
remote_receiver_id: ir_receiver
# RF 900MHz receiver
- platform: ir_rf_proxy
id: rf_900_rx
name: "RF 900 Receiver"
frequency: 900 MHz
remote_receiver_id: ir_receiver

View File

@@ -0,0 +1,19 @@
remote_transmitter:
id: ir_transmitter
pin: ${tx_pin}
carrier_duty_percent: 50%
# Test various hardware types with transmitter/receiver using infrared platform
infrared:
# Infrared transmitter
- platform: ir_rf_proxy
id: ir_tx
name: "IR Transmitter"
remote_transmitter_id: ir_transmitter
# RF 433MHz transmitter
- platform: ir_rf_proxy
id: rf_433_tx
name: "RF 433 Transmitter"
frequency: 433 MHz
remote_transmitter_id: ir_transmitter

View File

@@ -1,42 +1,7 @@
network:
wifi:
ssid: MySSID
password: password1
api:
remote_transmitter:
id: ir_transmitter
pin: ${tx_pin}
carrier_duty_percent: 50%
remote_receiver:
id: ir_receiver
pin: ${rx_pin}
# Test various hardware types with transmitter/receiver using infrared platform
infrared:
# Infrared transmitter
- platform: ir_rf_proxy
id: ir_tx
name: "IR Transmitter"
remote_transmitter_id: ir_transmitter
# Infrared receiver
- platform: ir_rf_proxy
id: ir_rx
name: "IR Receiver"
remote_receiver_id: ir_receiver
# RF 433MHz transmitter
- platform: ir_rf_proxy
id: rf_433_tx
name: "RF 433 Transmitter"
frequency: 433 MHz
remote_transmitter_id: ir_transmitter
# RF 900MHz receiver
- platform: ir_rf_proxy
id: rf_900_rx
name: "RF 900 Receiver"
frequency: 900 MHz
remote_receiver_id: ir_receiver

View File

@@ -0,0 +1,7 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
rx: !include common-rx.yaml

View File

@@ -0,0 +1,7 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
rx: !include common-rx.yaml

View File

@@ -0,0 +1,7 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
rx: !include common-rx.yaml

View File

@@ -0,0 +1,7 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
tx: !include common-tx.yaml

View File

@@ -0,0 +1,7 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
tx: !include common-tx.yaml

View File

@@ -0,0 +1,7 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
tx: !include common-tx.yaml

View File

@@ -0,0 +1,8 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
rx: !include common-rx.yaml
tx: !include common-tx.yaml

View File

@@ -2,4 +2,7 @@ substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml
packages:
common: !include common.yaml
rx: !include common-rx.yaml
tx: !include common-tx.yaml

View File

@@ -2,4 +2,7 @@ substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml
packages:
common: !include common.yaml
rx: !include common-rx.yaml
tx: !include common-tx.yaml

View File

@@ -2,4 +2,7 @@ substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml
packages:
common: !include common.yaml
rx: !include common-rx.yaml
tx: !include common-tx.yaml

View File

@@ -245,6 +245,44 @@ 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"
- 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:
- platform: template