[binary_sensor] Remove redundant optional<bool> state_, save 8 bytes per instance (#15095)

This commit is contained in:
J. Nick Koston
2026-03-29 12:15:18 -10:00
committed by GitHub
parent a9aaf29d83
commit d6475eaeed
3 changed files with 77 additions and 39 deletions
@@ -32,13 +32,6 @@ 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_new_state_ de-dups and will only trigger callbacks if the state has actually changed
this->set_new_state(new_state);
}
bool BinarySensor::set_new_state(const optional<bool> &new_state) {
if (StatefulEntityBase::set_new_state(new_state)) {
// weirdly, this file could be compiled even without USE_BINARY_SENSOR defined
@@ -32,7 +32,10 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
*/
class BinarySensor : public StatefulEntityBase<bool> {
public:
explicit BinarySensor(){};
explicit BinarySensor() = default;
const bool &get_state() const override { return this->state; }
void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; }
/** Publish a new state to the front-end.
*
@@ -54,16 +57,24 @@ class BinarySensor : public StatefulEntityBase<bool> {
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void send_state_internal(bool new_state);
void send_state_internal(bool new_state) {
// Fast path: skip virtual dispatch when state hasn't changed
if (this->flags_.has_state && this->state == new_state)
return;
this->set_new_state(new_state);
}
/// Return whether this binary sensor has outputted a state.
virtual bool is_status_binary_sensor() const;
// For backward compatibility, provide an accessible property
/// The current state of this binary sensor. Also used as the backing storage for StatefulEntityBase.
bool state{};
protected:
bool get_trigger_on_initial_state() const override { return this->trigger_on_initial_state_; }
void set_state_value(const bool &value) override { this->state = value; }
bool trigger_on_initial_state_{true};
#ifdef USE_BINARY_SENSOR_FILTER
Filter *filter_list_{nullptr};
#endif
@@ -73,7 +84,7 @@ class BinarySensor : public StatefulEntityBase<bool> {
class BinarySensorInitiallyOff : public BinarySensor {
public:
bool has_state() const override { return true; }
BinarySensorInitiallyOff() { this->set_has_state(true); }
};
} // namespace esphome::binary_sensor
+61 -27
View File
@@ -296,15 +296,36 @@ void log_entity_device_class(const char *tag, const char *prefix, const EntityBa
#define LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj) log_entity_unit_of_measurement(tag, prefix, obj)
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj);
/**
* An entity that has a state.
* @tparam T The type of the state
/** Base class for entities that track a typed state value with change-detection and callbacks.
*
* This class does not store the state value — subclasses own their storage. Whether a state
* has been set is tracked by EntityBase::has_state().
*
* Subclasses must implement:
* - get_state(): return a const reference to the current value
* - set_state_value(): store a new value (called only when the state actually changes)
* - get_trigger_on_initial_state(): return whether callbacks should fire on the first state
*
* Subclasses may override set_new_state() to add behavior (logging, notifications) after calling
* the base implementation. Since set_new_state() is virtual, callers like invalidate_state()
* dispatch through the vtable to the subclass override in the .cpp, avoiding template code
* bloat at inline call sites. Subclasses may also add a fast-path dedup check before calling
* set_new_state() to skip virtual dispatch entirely when the state hasn't changed.
*
* Callback behavior:
* - full_state_callbacks_: fired on every change, receives optional<T> previous and current
* - state_callbacks_: fired only when the new state has a value, and either this is not the
* first state (had_state) or trigger_on_initial_state is set
*
* @tparam T The type of the state value
*/
template<typename T> 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(); } // NOLINT(bugprone-unchecked-optional-access)
virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); }
/// Return the current state value. Only valid when has_state() is true.
virtual const T &get_state() const = 0;
/// Return the current state if available, otherwise return the provided default.
T get_state_default(T default_value) const { return this->has_state() ? this->get_state() : default_value; }
/// Clear the state — sets has_state() to false and fires callbacks with nullopt.
void invalidate_state() { this->set_new_state({}); }
template<typename F> void add_full_state_callback(F &&callback) {
@@ -314,33 +335,46 @@ template<typename T> class StatefulEntityBase : public EntityBase {
this->state_callbacks_.add(std::forward<F>(callback));
}
void set_trigger_on_initial_state(bool trigger_on_initial_state) {
this->trigger_on_initial_state_ = trigger_on_initial_state;
}
protected:
optional<T> state_{};
/**
* Set a new state for this entity. This will trigger callbacks only if the new state is different from the previous.
/// Subclasses return whether callbacks should fire on the very first state.
virtual bool get_trigger_on_initial_state() const = 0;
/** Apply a new state, de-duplicating and firing callbacks as needed.
*
* @param new_state The new state.
* @return True if the state was changed, false if it was the same as before.
* Pass nullopt to invalidate (clear) the state. Pass a value to set it.
* Returns true if the state actually changed, false if it was the same.
* Subclasses may override to add logging/notifications after calling the base.
*/
virtual bool set_new_state(const optional<T> &new_state) {
if (this->state_ != new_state) {
// call the full state callbacks with the previous and new state
this->full_state_callbacks_.call(this->state_, new_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_ = new_state;
if (new_state.has_value() && (this->trigger_on_initial_state_ || had_state))
this->state_callbacks_.call(new_state.value());
return true;
// Access flags_ directly to avoid function call overhead in this hot path
bool had_state = this->flags_.has_state;
// Use pointer to avoid requiring T to be default-constructible
const T *current = had_state ? &this->get_state() : nullptr;
if (new_state.has_value()) {
if (current != nullptr && *current == new_state.value())
return false; // same value, no change
} else if (!had_state) {
return false; // already invalidated, no change
}
return false;
// Capture old_state before set_state_value — current pointer aliases subclass storage
bool has_full_cbs = !this->full_state_callbacks_.empty();
optional<T> old_state;
if (has_full_cbs)
old_state = current != nullptr ? optional<T>(*current) : nullopt;
// Update storage before firing callbacks so callback code can inspect current state
this->flags_.has_state = new_state.has_value();
if (new_state.has_value()) {
this->set_state_value(new_state.value());
}
if (has_full_cbs)
this->full_state_callbacks_.call(old_state, new_state);
// had_state first: on every change except the first, skips the virtual call
if (new_state.has_value() && (had_state || this->get_trigger_on_initial_state()))
this->state_callbacks_.call(new_state.value());
return true;
}
bool trigger_on_initial_state_{true};
/// Subclasses implement this to store the actual value into their own storage.
virtual void set_state_value(const T &value) = 0;
LazyCallbackManager<void(optional<T> previous, optional<T> current)> full_state_callbacks_;
LazyCallbackManager<void(T)> state_callbacks_;
};