1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-12 00:02:21 +01:00

[binary_sensor] Add action to invalidate state and pass to HA (#8961)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Clyde Stubbs
2025-06-12 09:16:20 +10:00
committed by GitHub
parent 0228379a2e
commit 261b561bb2
27 changed files with 250 additions and 115 deletions

View File

@@ -1,7 +1,10 @@
from logging import getLogger
from esphome import automation, core
from esphome.automation import Condition, maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
from esphome.components.const import CONF_ON_STATE_CHANGE
import esphome.config_validation as cv
from esphome.const import (
CONF_DELAY,
@@ -98,6 +101,7 @@ IS_PLATFORM_COMPONENT = True
CONF_TIME_OFF = "time_off"
CONF_TIME_ON = "time_on"
CONF_TRIGGER_ON_INITIAL_STATE = "trigger_on_initial_state"
DEFAULT_DELAY = "1s"
DEFAULT_TIME_OFF = "100ms"
@@ -127,9 +131,17 @@ MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent")
StateTrigger = binary_sensor_ns.class_(
"StateTrigger", automation.Trigger.template(bool)
)
StateChangeTrigger = binary_sensor_ns.class_(
"StateChangeTrigger",
automation.Trigger.template(cg.optional.template(bool), cg.optional.template(bool)),
)
BinarySensorPublishAction = binary_sensor_ns.class_(
"BinarySensorPublishAction", automation.Action
)
BinarySensorInvalidateAction = binary_sensor_ns.class_(
"BinarySensorInvalidateAction", automation.Action
)
# Condition
BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Condition)
@@ -144,6 +156,8 @@ AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Compon
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
_LOGGER = getLogger(__name__)
FILTER_REGISTRY = Registry()
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
@@ -386,6 +400,14 @@ def validate_click_timing(value):
return value
def validate_publish_initial_state(value):
value = cv.boolean(value)
_LOGGER.warning(
"The 'publish_initial_state' option has been replaced by 'trigger_on_initial_state' and will be removed in a future release"
)
return value
_BINARY_SENSOR_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
@@ -395,7 +417,12 @@ _BINARY_SENSOR_SCHEMA = (
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTBinarySensorComponent
),
cv.Optional(CONF_PUBLISH_INITIAL_STATE): cv.boolean,
cv.Exclusive(
CONF_PUBLISH_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
): validate_publish_initial_state,
cv.Exclusive(
CONF_TRIGGER_ON_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
): cv.boolean,
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(CONF_FILTERS): validate_filters,
cv.Optional(CONF_ON_PRESS): automation.validate_automation(
@@ -454,6 +481,11 @@ _BINARY_SENSOR_SCHEMA = (
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
}
),
cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateChangeTrigger),
}
),
}
)
)
@@ -493,8 +525,10 @@ async def setup_binary_sensor_core_(var, config):
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
if publish_initial_state := config.get(CONF_PUBLISH_INITIAL_STATE):
cg.add(var.set_publish_initial_state(publish_initial_state))
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
CONF_PUBLISH_INITIAL_STATE, False
)
cg.add(var.set_trigger_on_initial_state(trigger))
if inverted := config.get(CONF_INVERTED):
cg.add(var.set_inverted(inverted))
if filters_config := config.get(CONF_FILTERS):
@@ -542,6 +576,17 @@ async def setup_binary_sensor_core_(var, config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(bool, "x")], conf)
for conf in config.get(CONF_ON_STATE_CHANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger,
[
(cg.optional.template(bool), "x_previous"),
(cg.optional.template(bool), "x"),
],
conf,
)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
@@ -591,3 +636,18 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args)
async def to_code(config):
cg.add_define("USE_BINARY_SENSOR")
cg.add_global(binary_sensor_ns.using)
@automation.register_action(
"binary_sensor.invalidate_state",
BinarySensorInvalidateAction,
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(BinarySensor),
},
key=CONF_ID,
),
)
async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)

View File

@@ -96,7 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
: parent_(parent), timing_(std::move(timing)) {}
void setup() override {
this->last_state_ = this->parent_->state;
this->last_state_ = this->parent_->get_state_default(false);
auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1);
this->parent_->add_on_state_callback(f);
}
@@ -130,6 +130,14 @@ class StateTrigger : public Trigger<bool> {
}
};
class StateChangeTrigger : public Trigger<optional<bool>, optional<bool> > {
public:
explicit StateChangeTrigger(BinarySensor *parent) {
parent->add_full_state_callback(
[this](optional<bool> old_state, optional<bool> state) { this->trigger(old_state, state); });
}
};
template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> {
public:
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
@@ -154,5 +162,15 @@ template<typename... Ts> class BinarySensorPublishAction : public Action<Ts...>
BinarySensor *sensor_;
};
template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts...> {
public:
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
void play(Ts... x) override { this->sensor_->invalidate_state(); }
protected:
BinarySensor *sensor_;
};
} // namespace binary_sensor
} // namespace esphome

View File

@@ -7,42 +7,25 @@ namespace binary_sensor {
static const char *const TAG = "binary_sensor";
void BinarySensor::add_on_state_callback(std::function<void(bool)> &&callback) {
this->state_callback_.add(std::move(callback));
}
void BinarySensor::publish_state(bool state) {
if (!this->publish_dedup_.next(state))
return;
void BinarySensor::publish_state(bool new_state) {
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, false);
this->send_state_internal(new_state);
} else {
this->filter_list_->input(state, false);
this->filter_list_->input(new_state);
}
}
void BinarySensor::publish_initial_state(bool state) {
if (!this->publish_dedup_.next(state))
return;
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, true);
} else {
this->filter_list_->input(state, true);
void BinarySensor::publish_initial_state(bool new_state) {
this->invalidate_state();
this->publish_state(new_state);
}
void BinarySensor::send_state_internal(bool new_state) {
// copy the new state to the visible property for backwards compatibility, before any callbacks
this->state = new_state;
// Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed
if (this->set_state_(new_state)) {
ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state));
}
}
void BinarySensor::send_state_internal(bool state, bool is_initial) {
if (is_initial) {
ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state));
} else {
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), ONOFF(state));
}
this->has_state_ = true;
this->state = state;
if (!is_initial || this->publish_initial_state_) {
this->state_callback_.call(state);
}
}
BinarySensor::BinarySensor() : state(false) {}
void BinarySensor::add_filter(Filter *filter) {
filter->parent_ = this;
@@ -60,7 +43,6 @@ void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
this->add_filter(filter);
}
}
bool BinarySensor::has_state() const { return this->has_state_; }
bool BinarySensor::is_status_binary_sensor() const { return false; }
} // namespace binary_sensor

View File

@@ -1,6 +1,5 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/filter.h"
@@ -34,52 +33,39 @@ namespace binary_sensor {
* The sub classes should notify the front-end of new states via the publish_state() method which
* handles inverted inputs for you.
*/
class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceClass {
public:
explicit BinarySensor();
/** Add a callback to be notified of state changes.
*
* @param callback The void(bool) callback.
*/
void add_on_state_callback(std::function<void(bool)> &&callback);
explicit BinarySensor(){};
/** Publish a new state to the front-end.
*
* @param state The new state.
* @param new_state The new state.
*/
void publish_state(bool state);
void publish_state(bool new_state);
/** Publish the initial state, this will not make the callback manager send callbacks
* and is meant only for the initial state on boot.
*
* @param state The new state.
* @param new_state The new state.
*/
void publish_initial_state(bool state);
/// The current reported state of the binary sensor.
bool state{false};
void publish_initial_state(bool new_state);
void add_filter(Filter *filter);
void add_filters(const std::vector<Filter *> &filters);
void set_publish_initial_state(bool publish_initial_state) { this->publish_initial_state_ = publish_initial_state; }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void send_state_internal(bool state, bool is_initial);
void send_state_internal(bool new_state);
/// Return whether this binary sensor has outputted a state.
virtual bool has_state() const;
virtual bool is_status_binary_sensor() const;
// For backward compatibility, provide an accessible property
bool state{};
protected:
CallbackManager<void(bool)> state_callback_{};
Filter *filter_list_{nullptr};
bool has_state_{false};
bool publish_initial_state_{false};
Deduplicator<bool> publish_dedup_;
};
class BinarySensorInitiallyOff : public BinarySensor {

View File

@@ -9,37 +9,36 @@ namespace binary_sensor {
static const char *const TAG = "sensor.filter";
void Filter::output(bool value, bool is_initial) {
void Filter::output(bool value) {
if (this->next_ == nullptr) {
this->parent_->send_state_internal(value);
} else {
this->next_->input(value);
}
}
void Filter::input(bool value) {
if (!this->dedup_.next(value))
return;
if (this->next_ == nullptr) {
this->parent_->send_state_internal(value, is_initial);
} else {
this->next_->input(value, is_initial);
}
}
void Filter::input(bool value, bool is_initial) {
auto b = this->new_value(value, is_initial);
auto b = this->new_value(value);
if (b.has_value()) {
this->output(*b, is_initial);
this->output(*b);
}
}
optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
optional<bool> DelayedOnOffFilter::new_value(bool value) {
if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
} else {
this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
}
return {};
}
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
optional<bool> DelayedOnFilter::new_value(bool value) {
if (value) {
this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
return {};
} else {
this->cancel_timeout("ON");
@@ -49,9 +48,9 @@ optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
optional<bool> DelayedOffFilter::new_value(bool value) {
if (!value) {
this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
return {};
} else {
this->cancel_timeout("OFF");
@@ -61,11 +60,11 @@ optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; }
optional<bool> InvertFilter::new_value(bool value) { return !value; }
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
optional<bool> AutorepeatFilter::new_value(bool value, bool is_initial) {
optional<bool> AutorepeatFilter::new_value(bool value) {
if (value) {
// Ignore if already running
if (this->active_timing_ != 0)
@@ -101,7 +100,7 @@ void AutorepeatFilter::next_timing_() {
void AutorepeatFilter::next_value_(bool val) {
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
this->output(val, false); // This is at least the second one so not initial
this->output(val); // This is at least the second one so not initial
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
}
@@ -109,18 +108,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD
LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {}
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
optional<bool> SettleFilter::new_value(bool value, bool is_initial) {
optional<bool> SettleFilter::new_value(bool value) {
if (!this->steady_) {
this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() {
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
this->steady_ = true;
this->output(value, is_initial);
this->output(value);
});
return {};
} else {
this->steady_ = false;
this->output(value, is_initial);
this->output(value);
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value;
}

View File

@@ -14,11 +14,11 @@ class BinarySensor;
class Filter {
public:
virtual optional<bool> new_value(bool value, bool is_initial) = 0;
virtual optional<bool> new_value(bool value) = 0;
void input(bool value, bool is_initial);
void input(bool value);
void output(bool value, bool is_initial);
void output(bool value);
protected:
friend BinarySensor;
@@ -30,7 +30,7 @@ class Filter {
class DelayedOnOffFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;
@@ -44,7 +44,7 @@ class DelayedOnOffFilter : public Filter, public Component {
class DelayedOnFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;
@@ -56,7 +56,7 @@ class DelayedOnFilter : public Filter, public Component {
class DelayedOffFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;
@@ -68,7 +68,7 @@ class DelayedOffFilter : public Filter, public Component {
class InvertFilter : public Filter {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
};
struct AutorepeatFilterTiming {
@@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component {
public:
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;
@@ -102,7 +102,7 @@ class LambdaFilter : public Filter {
public:
explicit LambdaFilter(std::function<optional<bool>(bool)> f);
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
protected:
std::function<optional<bool>(bool)> f_;
@@ -110,7 +110,7 @@ class LambdaFilter : public Filter {
class SettleFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;