Merge branch 'light_lazy_callbacks' into integration

This commit is contained in:
J. Nick Koston
2025-11-28 12:38:28 -06:00
6 changed files with 109 additions and 55 deletions

View File

@@ -120,46 +120,54 @@ template<typename... Ts> class LightIsOffCondition : public Condition<Ts...> {
LightState *state_;
};
class LightTurnOnTrigger : public Trigger<> {
class LightTurnOnTrigger : public Trigger<>, public LightRemoteValuesListener {
public:
LightTurnOnTrigger(LightState *a_light) {
a_light->add_new_remote_values_callback([this, a_light]() {
// using the remote value because of transitions we need to trigger as early as possible
auto is_on = a_light->remote_values.is_on();
// only trigger when going from off to on
auto should_trigger = is_on && !this->last_on_;
// Set new state immediately so that trigger() doesn't devolve
// into infinite loop
this->last_on_ = is_on;
if (should_trigger) {
this->trigger();
}
});
explicit LightTurnOnTrigger(LightState *a_light) : light_(a_light) {
a_light->add_remote_values_listener(this);
this->last_on_ = a_light->current_values.is_on();
}
void on_light_remote_values_update() override {
// using the remote value because of transitions we need to trigger as early as possible
auto is_on = this->light_->remote_values.is_on();
// only trigger when going from off to on
auto should_trigger = is_on && !this->last_on_;
// Set new state immediately so that trigger() doesn't devolve
// into infinite loop
this->last_on_ = is_on;
if (should_trigger) {
this->trigger();
}
}
protected:
LightState *light_;
bool last_on_;
};
class LightTurnOffTrigger : public Trigger<> {
class LightTurnOffTrigger : public Trigger<>, public LightTargetStateReachedListener {
public:
LightTurnOffTrigger(LightState *a_light) {
a_light->add_new_target_state_reached_callback([this, a_light]() {
auto is_on = a_light->current_values.is_on();
// only trigger when going from on to off
if (!is_on) {
this->trigger();
}
});
explicit LightTurnOffTrigger(LightState *a_light) : light_(a_light) {
a_light->add_target_state_reached_listener(this);
}
void on_light_target_state_reached() override {
auto is_on = this->light_->current_values.is_on();
// only trigger when going from on to off
if (!is_on) {
this->trigger();
}
}
protected:
LightState *light_;
};
class LightStateTrigger : public Trigger<> {
class LightStateTrigger : public Trigger<>, public LightRemoteValuesListener {
public:
LightStateTrigger(LightState *a_light) {
a_light->add_new_remote_values_callback([this]() { this->trigger(); });
}
explicit LightStateTrigger(LightState *a_light) { a_light->add_remote_values_listener(this); }
void on_light_remote_values_update() override { this->trigger(); }
};
// This is slightly ugly, but we can't log in headers, and can't make this a static method on AddressableSet

View File

@@ -174,8 +174,10 @@ void LightCall::perform() {
this->parent_->set_immediately_(v, publish);
}
if (!this->has_transition_()) {
this->parent_->target_state_reached_callback_.call();
if (!this->has_transition_() && this->parent_->target_state_reached_listeners_) {
for (auto *listener : *this->parent_->target_state_reached_listeners_) {
listener->on_light_target_state_reached();
}
}
if (publish) {
this->parent_->publish_state();

View File

@@ -127,7 +127,11 @@ void LightState::loop() {
this->transformer_->stop();
this->is_transformer_active_ = false;
this->transformer_ = nullptr;
this->target_state_reached_callback_.call();
if (this->target_state_reached_listeners_) {
for (auto *listener : *this->target_state_reached_listeners_) {
listener->on_light_target_state_reached();
}
}
// Disable loop if idle (no transformer and no effect)
this->disable_loop_if_idle_();
@@ -146,7 +150,11 @@ void LightState::loop() {
float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
void LightState::publish_state() {
this->remote_values_callback_.call();
if (this->remote_values_listeners_) {
for (auto *listener : *this->remote_values_listeners_) {
listener->on_light_remote_values_update();
}
}
#if defined(USE_LIGHT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_light_update(this);
#endif
@@ -171,11 +179,17 @@ StringRef LightState::get_effect_name_ref() {
return EFFECT_NONE_REF;
}
void LightState::add_new_remote_values_callback(std::function<void()> &&send_callback) {
this->remote_values_callback_.add(std::move(send_callback));
void LightState::add_remote_values_listener(LightRemoteValuesListener *listener) {
if (!this->remote_values_listeners_) {
this->remote_values_listeners_ = make_unique<std::vector<LightRemoteValuesListener *>>();
}
this->remote_values_listeners_->push_back(listener);
}
void LightState::add_new_target_state_reached_callback(std::function<void()> &&send_callback) {
this->target_state_reached_callback_.add(std::move(send_callback));
void LightState::add_target_state_reached_listener(LightTargetStateReachedListener *listener) {
if (!this->target_state_reached_listeners_) {
this->target_state_reached_listeners_ = make_unique<std::vector<LightTargetStateReachedListener *>>();
}
this->target_state_reached_listeners_->push_back(listener);
}
void LightState::set_default_transition_length(uint32_t default_transition_length) {

View File

@@ -18,6 +18,29 @@
namespace esphome::light {
class LightOutput;
class LightState;
/** Listener interface for light remote value changes.
*
* Components can implement this interface to receive notifications
* when the light's remote values change (state, brightness, color, etc.)
* without the overhead of std::function callbacks.
*/
class LightRemoteValuesListener {
public:
virtual void on_light_remote_values_update() = 0;
};
/** Listener interface for light target state reached.
*
* Components can implement this interface to receive notifications
* when the light finishes a transition and reaches its target state
* without the overhead of std::function callbacks.
*/
class LightTargetStateReachedListener {
public:
virtual void on_light_target_state_reached() = 0;
};
enum LightRestoreMode : uint8_t {
LIGHT_RESTORE_DEFAULT_OFF,
@@ -121,21 +144,17 @@ class LightState : public EntityBase, public Component {
/// Return the name of the current effect as StringRef (for API usage)
StringRef get_effect_name_ref();
/**
* This lets front-end components subscribe to light change events. This callback is called once
* when the remote color values are changed.
*
* @param send_callback The callback.
/** Add a listener for remote values changes.
* Listener is notified when the light's remote values change (state, brightness, color, etc.)
* Lazily allocates the listener vector on first registration.
*/
void add_new_remote_values_callback(std::function<void()> &&send_callback);
void add_remote_values_listener(LightRemoteValuesListener *listener);
/**
* The callback is called once the state of current_values and remote_values are equal (when the
* transition is finished).
*
* @param send_callback
/** Add a listener for target state reached.
* Listener is notified when the light finishes a transition and reaches its target state.
* Lazily allocates the listener vector on first registration.
*/
void add_new_target_state_reached_callback(std::function<void()> &&send_callback);
void add_target_state_reached_listener(LightTargetStateReachedListener *listener);
/// Set the default transition length, i.e. the transition length when no transition is provided.
void set_default_transition_length(uint32_t default_transition_length);
@@ -279,19 +298,24 @@ class LightState : public EntityBase, public Component {
// for effects, true if a transformer (transition) is active.
bool is_transformer_active_ = false;
/** Callback to call when new values for the frontend are available.
/** Listeners for remote values changes.
*
* "Remote values" are light color values that are reported to the frontend and have a lower
* publish frequency than the "real" color values. For example, during transitions the current
* color value may change continuously, but the remote values will be reported as the target values
* starting with the beginning of the transition.
*
* Lazily allocated - only created when a listener is actually registered.
*/
CallbackManager<void()> remote_values_callback_{};
std::unique_ptr<std::vector<LightRemoteValuesListener *>> remote_values_listeners_;
/** Callback to call when the state of current_values and remote_values are equal
* This should be called once the state of current_values changed and equals the state of remote_values
/** Listeners for target state reached.
* Notified when the state of current_values and remote_values are equal
* (when the transition is finished).
*
* Lazily allocated - only created when a listener is actually registered.
*/
CallbackManager<void()> target_state_reached_callback_{};
std::unique_ptr<std::vector<LightTargetStateReachedListener *>> target_state_reached_listeners_;
/// Initial state of the light.
optional<LightStateRTCState> initial_state_{};

View File

@@ -25,8 +25,11 @@ void MQTTJSONLightComponent::setup() {
call.perform();
});
auto f = std::bind(&MQTTJSONLightComponent::publish_state_, this);
this->state_->add_new_remote_values_callback([this, f]() { this->defer("send", f); });
this->state_->add_remote_values_listener(this);
}
void MQTTJSONLightComponent::on_light_remote_values_update() {
this->defer("send", [this]() { this->publish_state_(); });
}
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}

View File

@@ -11,7 +11,7 @@
namespace esphome {
namespace mqtt {
class MQTTJSONLightComponent : public mqtt::MQTTComponent {
class MQTTJSONLightComponent : public mqtt::MQTTComponent, public light::LightRemoteValuesListener {
public:
explicit MQTTJSONLightComponent(light::LightState *state);
@@ -25,6 +25,9 @@ class MQTTJSONLightComponent : public mqtt::MQTTComponent {
bool send_initial_state() override;
// LightRemoteValuesListener interface
void on_light_remote_values_update() override;
protected:
std::string component_type() const override;
const EntityBase *get_entity() const override;