mirror of
https://github.com/esphome/esphome.git
synced 2026-10-19 12:42:50 +00:00
[light] Add LightState::set_external_state() for outputs that mirror a light elsewhere (#20787)
This commit is contained in:
@@ -181,6 +181,29 @@ void LightState::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
bool LightState::set_external_state(const LightColorValues &values) {
|
||||
// Either would write its own values over the reported ones
|
||||
if (this->next_write_ || this->transformer_ != nullptr) {
|
||||
return false;
|
||||
}
|
||||
const bool had_effect = this->active_effect_index_ != 0;
|
||||
this->stop_effect_(); // schedules no write, unlike a call that sets effect 0
|
||||
if (!had_effect && values == this->remote_values && values == this->current_values) {
|
||||
return true; // nothing new to tell
|
||||
}
|
||||
this->current_values = values;
|
||||
this->remote_values = values;
|
||||
// Reached at once, as after an instant call, and told before the publish like there: an
|
||||
// automation run by the publish may change the light again, and its own call reports that
|
||||
if (this->target_state_reached_listeners_) {
|
||||
for (auto *listener : *this->target_state_reached_listeners_) {
|
||||
listener->on_light_target_state_reached();
|
||||
}
|
||||
}
|
||||
this->publish_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LightState::publish_state() {
|
||||
if (this->remote_values_listeners_) {
|
||||
for (auto *listener : *this->remote_values_listeners_) {
|
||||
|
||||
@@ -92,6 +92,11 @@ class LightState : public EntityBase, public Component {
|
||||
bool is_transitioning() const { return this->transformer_ != nullptr; }
|
||||
LightCall make_call();
|
||||
|
||||
/// Take over the state of a light mirrored from elsewhere: current and target values at once,
|
||||
/// published but not written or saved; a running effect stops. False while a write is pending or
|
||||
/// a transition or flash runs.
|
||||
bool set_external_state(const LightColorValues &values);
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
/// Load state from preferences
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/light/light_effect.h"
|
||||
#include "esphome/components/light/light_output.h"
|
||||
#include "esphome/components/light/light_state.h"
|
||||
|
||||
namespace esphome::light::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
// Counts the writes, like an output that mirrors a light elsewhere and sends each one on
|
||||
class CountingOutput : public LightOutput {
|
||||
public:
|
||||
LightTraits get_traits() override {
|
||||
LightTraits traits;
|
||||
traits.set_supported_color_modes({ColorMode::BRIGHTNESS});
|
||||
return traits;
|
||||
}
|
||||
void write_state(LightState *state) override { this->writes++; }
|
||||
int writes{0};
|
||||
};
|
||||
|
||||
class NoopEffect : public LightEffect {
|
||||
public:
|
||||
using LightEffect::LightEffect;
|
||||
void apply() override {}
|
||||
};
|
||||
|
||||
class ReachedListener : public LightTargetStateReachedListener {
|
||||
public:
|
||||
void on_light_target_state_reached() override { this->reached++; }
|
||||
int reached{0};
|
||||
};
|
||||
|
||||
class PublishListener : public LightRemoteValuesListener {
|
||||
public:
|
||||
void on_light_remote_values_update() override { this->published++; }
|
||||
int published{0};
|
||||
};
|
||||
|
||||
// Counts the target-reached calls that find the light off, as the on_turn_off trigger does
|
||||
class OffReachedListener : public LightTargetStateReachedListener {
|
||||
public:
|
||||
explicit OffReachedListener(LightState *state) : state_(state) {}
|
||||
void on_light_target_state_reached() override { this->reached_off += !this->state_->current_values.is_on(); }
|
||||
int reached_off{0};
|
||||
|
||||
protected:
|
||||
LightState *state_;
|
||||
};
|
||||
|
||||
// An automation that turns the light off as soon as it is reported on
|
||||
class TurnOffOnPublish : public LightRemoteValuesListener {
|
||||
public:
|
||||
explicit TurnOffOnPublish(LightState *state) : state_(state) {}
|
||||
void on_light_remote_values_update() override {
|
||||
if (this->state_->remote_values.is_on()) {
|
||||
this->state_->make_call().set_state(false).perform();
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
LightState *state_;
|
||||
};
|
||||
|
||||
LightColorValues on_at(float brightness) {
|
||||
LightColorValues values;
|
||||
values.set_color_mode(ColorMode::BRIGHTNESS);
|
||||
values.set_state(true);
|
||||
values.set_brightness(brightness);
|
||||
return values;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(LightStateExternal, TakesTheValuesWithoutAWrite) {
|
||||
CountingOutput output;
|
||||
LightState state(&output);
|
||||
ReachedListener listener;
|
||||
state.add_target_state_reached_listener(&listener);
|
||||
state.loop(); // the initial write of a fresh state
|
||||
const int writes = output.writes;
|
||||
|
||||
EXPECT_TRUE(state.set_external_state(on_at(0.5f)));
|
||||
EXPECT_EQ(state.remote_values, on_at(0.5f));
|
||||
EXPECT_EQ(state.current_values, on_at(0.5f));
|
||||
EXPECT_EQ(listener.reached, 1);
|
||||
state.loop();
|
||||
EXPECT_EQ(output.writes, writes);
|
||||
}
|
||||
|
||||
TEST(LightStateExternal, RefusesWhileALocalChangeWaitsToBeWritten) {
|
||||
CountingOutput output;
|
||||
LightState state(&output);
|
||||
state.loop();
|
||||
|
||||
state.make_call().set_state(true).set_brightness(0.8f).perform();
|
||||
EXPECT_FALSE(state.set_external_state(on_at(0.5f)));
|
||||
EXPECT_FLOAT_EQ(state.remote_values.get_brightness(), 0.8f);
|
||||
state.loop(); // the local change is written
|
||||
EXPECT_TRUE(state.set_external_state(on_at(0.5f)));
|
||||
}
|
||||
|
||||
TEST(LightStateExternal, RefusesDuringATransition) {
|
||||
CountingOutput output;
|
||||
LightState state(&output);
|
||||
state.loop();
|
||||
|
||||
state.make_call().set_state(true).set_brightness(1.0f).set_transition_length(1000).perform();
|
||||
state.loop();
|
||||
EXPECT_TRUE(state.is_transitioning());
|
||||
EXPECT_FALSE(state.set_external_state(on_at(0.5f)));
|
||||
}
|
||||
|
||||
TEST(LightStateExternal, StopsARunningEffect) {
|
||||
CountingOutput output;
|
||||
LightState state(&output);
|
||||
NoopEffect effect("Noop");
|
||||
LightEffect *effects[] = {&effect};
|
||||
state.add_effects(effects, 1);
|
||||
state.loop();
|
||||
|
||||
state.make_call().set_state(true).set_effect("Noop").perform();
|
||||
state.loop();
|
||||
ASSERT_EQ(state.get_current_effect_index(), 1u);
|
||||
const auto writes = output.writes;
|
||||
EXPECT_TRUE(state.set_external_state(on_at(0.5f)));
|
||||
EXPECT_EQ(state.get_current_effect_index(), 0u);
|
||||
EXPECT_EQ(state.remote_values, on_at(0.5f));
|
||||
state.loop();
|
||||
EXPECT_EQ(output.writes, writes); // the stopped effect schedules no write either
|
||||
}
|
||||
|
||||
TEST(LightStateExternal, PublishesWhenOnlyAnEffectStops) {
|
||||
CountingOutput output;
|
||||
LightState state(&output);
|
||||
NoopEffect effect("Noop");
|
||||
LightEffect *effects[] = {&effect};
|
||||
state.add_effects(effects, 1);
|
||||
PublishListener listener;
|
||||
state.add_remote_values_listener(&listener);
|
||||
state.loop();
|
||||
|
||||
state.make_call().set_state(true).set_effect("Noop").perform();
|
||||
state.loop();
|
||||
const int published = listener.published;
|
||||
// The values stay, but the effect name clients show has to go
|
||||
EXPECT_TRUE(state.set_external_state(state.remote_values));
|
||||
EXPECT_EQ(state.get_current_effect_index(), 0u);
|
||||
EXPECT_EQ(listener.published, published + 1);
|
||||
}
|
||||
|
||||
TEST(LightStateExternal, AnAutomationRunByThePublishReportsItsOwnCall) {
|
||||
CountingOutput output;
|
||||
LightState state(&output);
|
||||
OffReachedListener reached(&state);
|
||||
state.add_target_state_reached_listener(&reached);
|
||||
TurnOffOnPublish automation(&state);
|
||||
state.add_remote_values_listener(&automation);
|
||||
state.loop();
|
||||
|
||||
EXPECT_TRUE(state.set_external_state(on_at(0.5f)));
|
||||
EXPECT_FALSE(state.remote_values.is_on());
|
||||
EXPECT_EQ(reached.reached_off, 1); // once from the turn-off call, not again from the report
|
||||
}
|
||||
|
||||
TEST(LightStateExternal, SaysNothingWhenNothingChanges) {
|
||||
CountingOutput output;
|
||||
LightState state(&output);
|
||||
ReachedListener listener;
|
||||
state.add_target_state_reached_listener(&listener);
|
||||
state.loop();
|
||||
|
||||
EXPECT_TRUE(state.set_external_state(on_at(0.5f)));
|
||||
EXPECT_TRUE(state.set_external_state(on_at(0.5f)));
|
||||
EXPECT_EQ(listener.reached, 1);
|
||||
}
|
||||
|
||||
} // namespace esphome::light::testing
|
||||
Reference in New Issue
Block a user