Compare commits

...
Author SHA1 Message Date
J. Nick Koston 51808cd0ca [light] Reuse the flash transition for the fade back 2026-10-19 12:23:11 -10:00
4 changed files with 69 additions and 18 deletions
@@ -170,6 +170,8 @@ void AddressableLight::update_state(LightState *state) {
}
void AddressableLightTransformer::start() {
this->last_transition_progress_ = 0.0f;
// don't try to transition over running effects.
if (this->light_.is_effect_active())
return;
+3 -1
View File
@@ -11,6 +11,8 @@ class LightTransformer {
public:
virtual ~LightTransformer() = default;
/// Starts a run from start_values to target_values over length ms. A transformer that has finished may be set up
/// again for another run, as a flash does for its fade back, so start() must reset every member apply() reads.
void setup(const LightColorValues &start_values, const LightColorValues &target_values, uint32_t length) {
this->start_time_ = millis();
this->length_ = length;
@@ -22,7 +24,7 @@ class LightTransformer {
/// Indicates whether this transformation is finished.
virtual bool is_finished() { return this->get_progress_() >= 1.0f; }
/// This will be called before the transition is started.
/// This will be called by setup() before each run of the transition.
virtual void start() {}
/// This will be called while the transformer is active to apply the transition to the light. Can either write to the
+15 -17
View File
@@ -27,8 +27,8 @@ class LightTransitionTransformer : public LightTransformer {
}
// When changing color mode, go through off state, as color modes are orthogonal and there can't be two active.
if (this->start_values_.get_color_mode() != this->end_values_.get_color_mode()) {
this->changing_color_mode_ = true;
this->changing_color_mode_ = this->start_values_.get_color_mode() != this->end_values_.get_color_mode();
if (this->changing_color_mode_) {
this->intermediate_values_ = this->start_values_;
this->intermediate_values_.set_state(false);
}
@@ -73,36 +73,33 @@ class LightFlashTransformer : public LightTransformer {
// first transition to original target
this->transformer_ = this->state_.get_output()->create_default_transition();
this->transformer_->setup(this->state_.current_values, this->target_values_, this->transition_length_);
this->transition_running_ = true;
}
optional<LightColorValues> apply() override {
optional<LightColorValues> result = {};
if (this->transformer_ == nullptr && millis() - this->start_time_ > this->length_ - this->transition_length_) {
// second transition back to start value
this->transformer_ = this->state_.get_output()->create_default_transition();
if (!this->transition_running_ && !this->begun_lightstate_restore_ &&
millis() - this->start_time_ > this->length_ - this->transition_length_) {
// second transition back to start value, run by the same transformer as the first
this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_);
this->begun_lightstate_restore_ = true;
this->transition_running_ = true;
}
if (this->transformer_ != nullptr) {
result = this->transformer_->apply();
if (!this->transition_running_)
return {};
if (this->transformer_->is_finished()) {
this->transformer_->stop();
this->transformer_ = nullptr;
}
optional<LightColorValues> result = this->transformer_->apply();
if (this->transformer_->is_finished()) {
this->transformer_->stop();
this->transition_running_ = false;
}
return result;
}
// Restore the original values after the flash.
void stop() override {
if (this->transformer_ != nullptr) {
if (this->transition_running_)
this->transformer_->stop();
this->transformer_ = nullptr;
}
this->state_.current_values = this->get_start_values();
this->state_.remote_values = this->get_start_values();
#ifdef USE_LIGHT_TRANSITION_PUBLISH_INTERVAL
@@ -120,6 +117,7 @@ class LightFlashTransformer : public LightTransformer {
std::unique_ptr<LightTransformer> transformer_{nullptr};
uint32_t transition_length_;
bool begun_lightstate_restore_;
bool transition_running_;
};
} // namespace esphome::light
@@ -19,6 +19,7 @@ byte of each LED, so we can observe the transition directly.
from __future__ import annotations
import asyncio
from collections.abc import Callable
from aioesphomeapi import LightInfo, SensorInfo, SensorState
import pytest
@@ -117,3 +118,51 @@ async def test_addressable_light_transition(
assert max(final_samples) >= 250, (
f"final raw byte was {max(final_samples)}, expected >= 250"
)
@pytest.mark.asyncio
@pytest.mark.shared_yaml("addressable_light_transition")
async def test_addressable_light_flash_restores_leds(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""A flash writes the LEDs back to where they were when it ends."""
async with run_compiled(yaml_config), api_client_connected() as client:
entities, _ = await client.list_entities_services()
light = require_entity(entities, "test_strip", LightInfo)
sensor = require_entity(entities, "led0_red_raw", SensorInfo)
changed = asyncio.Event()
raw: list[float] = []
def on_state(state: object) -> None:
if not isinstance(state, SensorState) or state.key != sensor.key:
return
raw.append(state.state)
changed.set()
async def wait_for_raw(matches: Callable[[float], bool]) -> None:
async def wait() -> None:
while not raw or not matches(raw[-1]):
changed.clear()
await changed.wait()
await asyncio.wait_for(wait(), timeout=2.0)
initial_state_helper = InitialStateHelper(entities)
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
await initial_state_helper.wait_for_initial_states()
client.light_command(
key=light.key, state=True, rgb=(1.0, 1.0, 1.0), brightness=1.0
)
await wait_for_raw(lambda v: v == 255)
# The flash dims the strip, then its fade back must write the LEDs again:
# the light core does not rewrite them itself when the flash ends.
client.light_command(
key=light.key, state=True, brightness=0.5, flash_length=0.5
)
await wait_for_raw(lambda v: v < 255)
await wait_for_raw(lambda v: v == 255)