diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 17c83c54f1..6852afe937 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -227,7 +227,7 @@ bool APIServer::check_password(const std::string &password) const { void APIServer::handle_disconnect(APIConnection *conn) {} #ifdef USE_BINARY_SENSOR -void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { +void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { if (obj->is_internal()) return; for (auto &c : this->clients_) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 71e470d4f8..971c192e4b 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -54,7 +54,7 @@ class APIServer : public Component, public Controller { void handle_disconnect(APIConnection *conn); #ifdef USE_BINARY_SENSOR - void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; + void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override; #endif #ifdef USE_COVER void on_cover_update(cover::Cover *obj) override; diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index ec1c4e8a0c..bc26c09622 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -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) diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index 12b07a05e3..b46436dc41 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -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 { } }; +class StateChangeTrigger : public Trigger, optional > { + public: + explicit StateChangeTrigger(BinarySensor *parent) { + parent->add_full_state_callback( + [this](optional old_state, optional state) { this->trigger(old_state, state); }); + } +}; + template class BinarySensorCondition : public Condition { public: BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {} @@ -154,5 +162,15 @@ template class BinarySensorPublishAction : public Action BinarySensor *sensor_; }; +template class BinarySensorInvalidateAction : public Action { + public: + explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {} + + void play(Ts... x) override { this->sensor_->invalidate_state(); } + + protected: + BinarySensor *sensor_; +}; + } // namespace binary_sensor } // namespace esphome diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 20604a0b7e..02b83af552 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -7,42 +7,25 @@ namespace binary_sensor { static const char *const TAG = "binary_sensor"; -void BinarySensor::add_on_state_callback(std::function &&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 &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 diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 57cae9e2f5..d61be7a49b 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -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, 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 &&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 &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 state_callback_{}; Filter *filter_list_{nullptr}; - bool has_state_{false}; - bool publish_initial_state_{false}; - Deduplicator publish_dedup_; }; class BinarySensorInitiallyOff : public BinarySensor { diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 8f94b108ac..41d0553b35 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -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 DelayedOnOffFilter::new_value(bool value, bool is_initial) { +optional 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 DelayedOnFilter::new_value(bool value, bool is_initial) { +optional 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 DelayedOnFilter::new_value(bool value, bool is_initial) { float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional DelayedOffFilter::new_value(bool value, bool is_initial) { +optional 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 DelayedOffFilter::new_value(bool value, bool is_initial) { float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional InvertFilter::new_value(bool value, bool is_initial) { return !value; } +optional InvertFilter::new_value(bool value) { return !value; } AutorepeatFilter::AutorepeatFilter(std::vector timings) : timings_(std::move(timings)) {} -optional AutorepeatFilter::new_value(bool value, bool is_initial) { +optional 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(bool)> f) : f_(std::move(f)) {} -optional LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } +optional LambdaFilter::new_value(bool value) { return this->f_(value); } -optional SettleFilter::new_value(bool value, bool is_initial) { +optional 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; } diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index f7342db2fb..65838da49d 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -14,11 +14,11 @@ class BinarySensor; class Filter { public: - virtual optional new_value(bool value, bool is_initial) = 0; + virtual optional 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 new_value(bool value, bool is_initial) override; + optional 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 new_value(bool value, bool is_initial) override; + optional 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 new_value(bool value, bool is_initial) override; + optional 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 new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; }; struct AutorepeatFilterTiming { @@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component { public: explicit AutorepeatFilter(std::vector timings); - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; float get_setup_priority() const override; @@ -102,7 +102,7 @@ class LambdaFilter : public Filter { public: explicit LambdaFilter(std::function(bool)> f); - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; protected: std::function(bool)> f_; @@ -110,7 +110,7 @@ class LambdaFilter : public Filter { class SettleFilter : public Filter, public Component { public: - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; float get_setup_priority() const override; diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index a73849e67d..66a5fe5d81 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -3,4 +3,5 @@ CODEOWNERS = ["@esphome/core"] CONF_DRAW_ROUNDING = "draw_rounding" +CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index 5ce8894a8a..d1fb618695 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -6,16 +6,8 @@ namespace template_ { static const char *const TAG = "template.binary_sensor"; -void TemplateBinarySensor::setup() { - if (!this->publish_initial_state_) - return; +void TemplateBinarySensor::setup() { this->loop(); } - if (this->f_ != nullptr) { - this->publish_initial_state(this->f_().value_or(false)); - } else { - this->publish_initial_state(false); - } -} void TemplateBinarySensor::loop() { if (this->f_ == nullptr) return; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 870932d266..7ae30522f4 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -555,7 +555,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) #endif #ifdef USE_BINARY_SENSOR -void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { +void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { if (this->events_.empty()) return; this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index e4f044c50b..f4d6ad8e86 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -269,7 +269,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_BINARY_SENSOR - void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; + void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override; /// Handle a binary sensor request under '/binary_sensor/'. void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index d6d98a4316..f7ff5a9734 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -7,8 +7,10 @@ namespace esphome { void Controller::setup_controller(bool include_internal) { #ifdef USE_BINARY_SENSOR for (auto *obj : App.get_binary_sensors()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](bool state) { this->on_binary_sensor_update(obj, state); }); + if (include_internal || !obj->is_internal()) { + obj->add_full_state_callback( + [this, obj](optional previous, optional state) { this->on_binary_sensor_update(obj); }); + } } #endif #ifdef USE_FAN diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 39e0b2ba26..1a5b9ea6b4 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -71,7 +71,7 @@ class Controller { public: void setup_controller(bool include_internal = false); #ifdef USE_BINARY_SENSOR - virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state){}; + virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj){}; #endif #ifdef USE_FAN virtual void on_fan_update(fan::Fan *obj){}; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 4ca21f9ee5..a2e1d4adbc 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -3,6 +3,8 @@ #include #include #include "string_ref.h" +#include "helpers.h" +#include "log.h" namespace esphome { @@ -29,7 +31,7 @@ class EntityBase { // Get the unique Object ID of this Entity uint32_t get_object_id_hash(); - // Get/set whether this Entity should be hidden from outside of ESPHome + // Get/set whether this Entity should be hidden outside ESPHome bool is_internal() const; void set_internal(bool internal); @@ -56,11 +58,12 @@ class EntityBase { StringRef name_; const char *object_id_c_str_{nullptr}; const char *icon_c_str_{nullptr}; - uint32_t object_id_hash_; + uint32_t object_id_hash_{}; bool has_own_name_{false}; bool internal_{false}; bool disabled_by_default_{false}; EntityCategory entity_category_{ENTITY_CATEGORY_NONE}; + bool has_state_{}; }; class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) @@ -85,4 +88,58 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override }; +/** + * An entity that has a state. + * @tparam T The type of the state + */ +template class StatefulEntityBase : public EntityBase { + public: + virtual bool has_state() const { return this->state_.has_value(); } + virtual const T &get_state() const { return this->state_.value(); } + virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); } + void invalidate_state() { this->set_state_({}); } + + void add_full_state_callback(std::function previous, optional current)> &&callback) { + if (this->full_state_callbacks_ == nullptr) + this->full_state_callbacks_ = new CallbackManager previous, optional current)>(); // NOLINT + this->full_state_callbacks_->add(std::move(callback)); + } + void add_on_state_callback(std::function &&callback) { + if (this->state_callbacks_ == nullptr) + this->state_callbacks_ = new CallbackManager(); // NOLINT + this->state_callbacks_->add(std::move(callback)); + } + + void set_trigger_on_initial_state(bool trigger_on_initial_state) { + this->trigger_on_initial_state_ = trigger_on_initial_state; + } + + protected: + optional state_{}; + /** + * Set a new state for this entity. This will trigger callbacks only if the new state is different from the previous. + * + * @param state The new state. + * @return True if the state was changed, false if it was the same as before. + */ + bool set_state_(const optional &state) { + if (this->state_ != state) { + // call the full state callbacks with the previous and new state + if (this->full_state_callbacks_ != nullptr) + this->full_state_callbacks_->call(this->state_, state); + // trigger legacy callbacks only if the new state is valid and either the trigger on initial state is enabled or + // the previous state was valid + auto had_state = this->has_state(); + this->state_ = state; + if (this->state_callbacks_ != nullptr && state.has_value() && (this->trigger_on_initial_state_ || had_state)) + this->state_callbacks_->call(state.value()); + return true; + } + return false; + } + bool trigger_on_initial_state_{true}; + // callbacks with full state and previous state + CallbackManager previous, optional current)> *full_state_callbacks_{}; + CallbackManager *state_callbacks_{}; +}; } // namespace esphome diff --git a/esphome/core/log.h b/esphome/core/log.h index adf72e4bac..cade6a74c1 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -165,6 +165,8 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #define YESNO(b) ((b) ? "YES" : "NO") #define ONOFF(b) ((b) ? "ON" : "OFF") #define TRUEFALSE(b) ((b) ? "TRUE" : "FALSE") +// for use with optional values +#define ONOFFMAYBE(b) (((b).has_value()) ? ONOFF((b).value()) : "UNKNOWN") // Helper class that identifies strings that may be stored in flash storage (similar to Arduino's __FlashStringHelper) struct LogString; diff --git a/esphome/core/optional.h b/esphome/core/optional.h index 591bc7aa68..7f9db7817d 100644 --- a/esphome/core/optional.h +++ b/esphome/core/optional.h @@ -52,6 +52,11 @@ template class optional { // NOLINT reset(); return *this; } + bool operator==(optional const &rhs) const { + if (has_value() && rhs.has_value()) + return value() == rhs.value(); + return !has_value() && !rhs.has_value(); + } template optional &operator=(optional const &other) { has_value_ = other.has_value(); diff --git a/tests/components/binary_sensor/common.yaml b/tests/components/binary_sensor/common.yaml new file mode 100644 index 0000000000..148b7d2405 --- /dev/null +++ b/tests/components/binary_sensor/common.yaml @@ -0,0 +1,15 @@ +binary_sensor: + - platform: template + trigger_on_initial_state: true + id: some_binary_sensor + name: "Random binary" + lambda: return (random_uint32() & 1) == 0; + on_state_change: + then: + - logger.log: + format: "Old state was %s" + args: ['x_previous.has_value() ? ONOFF(x_previous) : "Unknown"'] + - logger.log: + format: "New state is %s" + args: ['x.has_value() ? ONOFF(x) : "Unknown"'] + - binary_sensor.invalidate_state: some_binary_sensor diff --git a/tests/components/binary_sensor/test.bk72xx-ard.yaml b/tests/components/binary_sensor/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.bk72xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-ard.yaml b/tests/components/binary_sensor/test.esp32-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.esp32-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-c3-ard.yaml b/tests/components/binary_sensor/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.esp32-c3-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-c3-idf.yaml b/tests/components/binary_sensor/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.esp32-c3-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-idf.yaml b/tests/components/binary_sensor/test.esp32-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-s3-idf.yaml b/tests/components/binary_sensor/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.esp32-s3-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp8266-ard.yaml b/tests/components/binary_sensor/test.esp8266-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/binary_sensor/test.rp2040-ard.yaml b/tests/components/binary_sensor/test.rp2040-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.rp2040-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 174df56749..59602414a7 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -63,7 +63,7 @@ binary_sensor: id: lvgl_pressbutton name: Pressbutton widget: spin_up - publish_initial_state: true + trigger_on_initial_state: true - platform: lvgl name: ButtonMatrix button widget: button_a