[binary_sensor] Remove redundant optional<bool> state_ from StatefulEntityBase

Replace optional<T> state_ in StatefulEntityBase with pure virtual
get_state()/set_state_value_() that subclasses implement. BinarySensor
stores its value in the existing public bool state member and uses
EntityBase::flags_.has_state instead of the optional's discriminant.

Move trigger_on_initial_state_ into EntityBase::flags_ (1 bit from
reserved), eliminating the standalone bool and its padding.

Saves 8 bytes per BinarySensor instance:
- optional<bool> (2B) + padding (2B) = 4B removed
- bool trigger_on_initial_state_ (1B) + padding (3B) = 4B removed
This commit is contained in:
J. Nick Koston
2026-03-22 15:58:31 -10:00
parent fbe3e7d99c
commit 91944988ef
3 changed files with 24 additions and 20 deletions
@@ -33,9 +33,7 @@ void BinarySensor::publish_initial_state(bool new_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
// set_new_state handles de-duplication, updating this->state via set_state_value_(), and triggering callbacks
this->set_new_state(new_state);
}
@@ -32,7 +32,9 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
*/
class BinarySensor : public StatefulEntityBase<bool> {
public:
explicit BinarySensor(){};
explicit BinarySensor() { this->flags_.trigger_on_initial_state = true; }
const bool &get_state() const override { return this->state; }
/** Publish a new state to the front-end.
*
@@ -59,11 +61,11 @@ class BinarySensor : public StatefulEntityBase<bool> {
/// Return whether this binary sensor has outputted a state.
virtual bool is_status_binary_sensor() const;
// For backward compatibility, provide an accessible property
bool state{};
protected:
void set_state_value_(const bool &value) override { this->state = value; }
#ifdef USE_BINARY_SENSOR_FILTER
Filter *filter_list_{nullptr};
#endif
@@ -73,7 +75,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
+17 -13
View File
@@ -266,8 +266,9 @@ class EntityBase {
uint8_t internal : 1;
uint8_t disabled_by_default : 1;
uint8_t has_state : 1;
uint8_t entity_category : 2; // Supports up to 4 categories
uint8_t reserved : 2; // Reserved for future use
uint8_t entity_category : 2; // Supports up to 4 categories
uint8_t trigger_on_initial_state : 1; // Whether callbacks fire on first state (StatefulEntityBase)
uint8_t reserved : 1; // Reserved for future use
} flags_{};
// String table indices — packed into the 3 padding bytes after flags_
#ifdef USE_ENTITY_DEVICE_CLASS
@@ -302,9 +303,8 @@ void log_entity_unit_of_measurement(const char *tag, const char *prefix, const E
*/
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); }
virtual const T &get_state() const = 0;
T get_state_default(T default_value) const { return this->has_state() ? this->get_state() : default_value; }
void invalidate_state() { this->set_new_state({}); }
template<typename F> void add_full_state_callback(F &&callback) {
@@ -315,11 +315,10 @@ template<typename T> class StatefulEntityBase : public EntityBase {
}
void set_trigger_on_initial_state(bool trigger_on_initial_state) {
this->trigger_on_initial_state_ = trigger_on_initial_state;
this->flags_.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.
*
@@ -327,20 +326,25 @@ template<typename T> class StatefulEntityBase : public EntityBase {
* @return True if the state was changed, false if it was the same as before.
*/
virtual bool set_new_state(const optional<T> &new_state) {
if (this->state_ != new_state) {
optional<T> old_state = this->has_state() ? optional<T>(this->get_state()) : nullopt;
if (old_state != new_state) {
// call the full state callbacks with the previous and new state
this->full_state_callbacks_.call(this->state_, new_state);
this->full_state_callbacks_.call(old_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());
this->set_has_state(new_state.has_value());
if (new_state.has_value()) {
this->set_state_value_(new_state.value());
if (this->flags_.trigger_on_initial_state || had_state)
this->state_callbacks_.call(new_state.value());
}
return true;
}
return false;
}
bool trigger_on_initial_state_{true};
/// Subclasses implement this to store the actual value.
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_;
};