diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 3ccaaf8f03..0fb58082cb 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -32,7 +32,6 @@ void BinarySensor::publish_initial_state(bool new_state) { this->invalidate_state(); this->publish_state(new_state); } - bool BinarySensor::set_new_state(const optional &new_state) { if (StatefulEntityBase::set_new_state(new_state)) { // weirdly, this file could be compiled even without USE_BINARY_SENSOR defined diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 6e6f77e010..28c156763a 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -35,7 +35,7 @@ class BinarySensor : public StatefulEntityBase { explicit BinarySensor() = default; const bool &get_state() const override { return this->state; } - void set_trigger_on_initial_state(bool value) override { this->trigger_on_initial_state_ = value; } + void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; } /** Publish a new state to the front-end. * @@ -57,7 +57,12 @@ class BinarySensor : public StatefulEntityBase { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - void send_state_internal(bool new_state) { this->set_new_state(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; @@ -67,7 +72,7 @@ class BinarySensor : public StatefulEntityBase { 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; } + void set_state_value(const bool &value) override { this->state = value; } bool trigger_on_initial_state_{true}; #ifdef USE_BINARY_SENSOR_FILTER diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index d19ef558ec..d03326ccf8 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -297,15 +297,20 @@ void log_entity_device_class(const char *tag, const char *prefix, const EntityBa void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj); /** Base class for entities that track a typed state value with change-detection and callbacks. - * - * 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() / set_trigger_on_initial_state(): control initial callback behavior * * 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() and + * send_state_internal() dispatch through the vtable to the subclass override in the .cpp, + * avoiding template code bloat at inline call sites. + * * Callback behavior: * - full_state_callbacks_: fired on every change, receives optional previous and current * - state_callbacks_: fired only when the new state has a value, and either this is not the @@ -329,10 +334,6 @@ template class StatefulEntityBase : public EntityBase { this->state_callbacks_.add(std::forward(callback)); } - /// Control whether state_callbacks_ fire on the very first state (before any previous state exists). - /// Subclasses must implement set_trigger_on_initial_state() to store this value. - virtual void set_trigger_on_initial_state(bool value) = 0; - protected: /// Subclasses return whether callbacks should fire on the very first state. virtual bool get_trigger_on_initial_state() const = 0; @@ -341,30 +342,38 @@ 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. */ virtual bool set_new_state(const optional &new_state) { - // Access flags_ directly to avoid virtual/function call overhead in this hot path + // 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 (had_state && this->get_state() == new_state.value()) + if (current != nullptr && *current == new_state.value()) return false; // same value, no change } else if (!had_state) { return false; // already invalidated, no change } - // State changed — capture old state, then update storage before firing callbacks - // so callback code can inspect the entity's current state via get_state()/has_state() - optional old_state = had_state ? optional(this->get_state()) : nullopt; + // Capture old_state before set_state_value — current pointer aliases subclass storage + bool has_full_cbs = !this->full_state_callbacks_.empty(); + optional old_state; + if (has_full_cbs) + old_state = current != nullptr ? optional(*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()); + this->set_state_value(new_state.value()); } - this->full_state_callbacks_.call(old_state, new_state); - if (new_state.has_value() && (this->get_trigger_on_initial_state() || had_state)) + 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; } /// Subclasses implement this to store the actual value into their own storage. - virtual void set_state_value_(const T &value) = 0; + virtual void set_state_value(const T &value) = 0; LazyCallbackManager previous, optional current)> full_state_callbacks_; LazyCallbackManager state_callbacks_; };