From 0417de007a71a6c262b8309401aee350e100f5b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2026 16:37:54 -1000 Subject: [PATCH] [binary_sensor] Restore virtual set_new_state, remove on_state_changed hook Making set_new_state virtual means callers like send_state_internal and invalidate_state resolve via vtable dispatch to the .cpp, avoiding template bloat without needing out-of-line tricks or hiding declarations. BinarySensor overrides set_new_state directly for logging and ControllerRegistry notification, matching the original pattern. --- .../components/binary_sensor/binary_sensor.cpp | 18 ++++++++++-------- .../components/binary_sensor/binary_sensor.h | 5 +---- esphome/core/entity_base.h | 17 +++++++++-------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index a78fc9229c..4d093b3630 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -32,17 +32,19 @@ void BinarySensor::publish_initial_state(bool new_state) { this->invalidate_state(); this->publish_state(new_state); } -// Defined out-of-line to prevent set_new_state template from being inlined at each call site, -// which would duplicate ~189 bytes of template code per caller (publish_state, Filter::output, etc.) -void BinarySensor::send_state_internal(bool new_state) { this->set_new_state_(new_state); } -void BinarySensor::invalidate_state() { this->set_new_state_({}); } +// Defined out-of-line: set_new_state is virtual so callers in other TUs (filter.cpp, automation.h) +// dispatch here without inlining the ~189 byte template body at each call site. +void BinarySensor::send_state_internal(bool new_state) { this->set_new_state(new_state); } -void BinarySensor::on_state_changed(const optional &old_state, const optional &new_state, bool had_state) { - StatefulEntityBase::on_state_changed(old_state, new_state, had_state); +bool BinarySensor::set_new_state(const optional &new_state) { + if (StatefulEntityBase::set_new_state(new_state)) { #if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY) - ControllerRegistry::notify_binary_sensor_update(this); + ControllerRegistry::notify_binary_sensor_update(this); #endif - ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); + ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); + return true; + } + return false; } #ifdef USE_BINARY_SENSOR_FILTER diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 109fdb0f1a..5ad2bd2edf 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -57,10 +57,7 @@ class BinarySensor : public StatefulEntityBase { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - /// Defined in .cpp to avoid inlining set_new_state_ template code at every call site. void send_state_internal(bool new_state); - /// Hides base class inline version to prevent template bloat from automation.h and filter.cpp callers. - void invalidate_state(); /// Return whether this binary sensor has outputted a state. virtual bool is_status_binary_sensor() const; @@ -77,7 +74,7 @@ class BinarySensor : public StatefulEntityBase { Filter *filter_list_{nullptr}; #endif - void on_state_changed(const optional &old_state, const optional &new_state, bool had_state) override; + bool set_new_state(const optional &new_state) override; }; class BinarySensorInitiallyOff : public BinarySensor { diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 7cf6804c4c..30a06a7f8a 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -302,7 +302,8 @@ void log_entity_unit_of_measurement(const char *tag, const char *prefix, const E * - 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() / set_trigger_on_initial_state(): control initial callback behavior - * - on_state_changed() (optional override): called after state updates, for logging/notifications + * + * Subclasses may override set_new_state() for additional behavior (logging, notifications). * * This class does not store the state value — subclasses own their storage. Whether a state * has been set is tracked by EntityBase::has_state(). @@ -312,6 +313,9 @@ void log_entity_unit_of_measurement(const char *tag, const char *prefix, const E * - 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 * + * invalidate_state() and callers of set_new_state() should be defined out-of-line in the + * subclass .cpp to avoid inlining the template body (~189 bytes) at every call site. + * * @tparam T The type of the state value */ template class StatefulEntityBase : public EntityBase { @@ -321,7 +325,7 @@ template class StatefulEntityBase : public EntityBase { /// 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_({}); } + void invalidate_state() { this->set_new_state({}); } template void add_full_state_callback(F &&callback) { this->full_state_callbacks_.add(std::forward(callback)); @@ -342,8 +346,9 @@ template class StatefulEntityBase : public EntityBase { * * 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. */ - bool set_new_state_(const optional &new_state) { + virtual bool set_new_state(const optional &new_state) { // Access flags_ directly to avoid function call overhead in this hot path bool had_state = this->flags_.has_state; if (new_state.has_value()) { @@ -362,14 +367,10 @@ template class StatefulEntityBase : public EntityBase { if (new_state.has_value()) { this->set_state_value(new_state.value()); } - this->on_state_changed(old_state, new_state, had_state); - return true; - } - /// Called after state storage is updated. Subclasses override for logging/notifications. - virtual void on_state_changed(const optional &old_state, const optional &new_state, bool had_state) { this->full_state_callbacks_.call(old_state, new_state); if (new_state.has_value() && (this->get_trigger_on_initial_state() || had_state)) this->state_callbacks_.call(new_state.value()); + return true; } /// Subclasses implement this to store the actual value into their own storage. virtual void set_state_value(const T &value) = 0;