ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
light_state.cpp
Go to the documentation of this file.
1#include "light_state.h"
5#include "esphome/core/log.h"
6#include "light_output.h"
7#include "transformers.h"
8
9namespace esphome::light {
10
11static const char *const TAG = "light";
12
13LightState::LightState(LightOutput *output) : output_(output) {}
14
20
22 this->output_->setup_state(this);
23 for (auto *effect : this->effects_) {
24 effect->init_internal(this);
25 }
26
27 // Start with loop disabled if idle - respects any effects/transitions set up during initialization
29
30 // When supported color temperature range is known, initialize color temperature setting within bounds.
31 auto traits = this->get_traits();
32 float min_mireds = traits.get_min_mireds();
33 if (min_mireds > 0) {
34 this->remote_values.set_color_temperature(min_mireds);
35 this->current_values.set_color_temperature(min_mireds);
36 }
37
38 auto call = this->make_call();
39 LightStateRTCState recovered{};
40 if (this->initial_state_callback_) {
41 this->initial_state_callback_(recovered);
42 this->initial_state_callback_ = nullptr; // One-shot — no longer needed
43 }
44 switch (this->restore_mode_) {
50 // Attempt to load from preferences, else fall back to default values
51 if (!this->rtc_.load(&recovered)) {
52 recovered.state = (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON ||
56 // Inverted restore state
57 recovered.state = !recovered.state;
58 }
59 break;
63 this->rtc_.load(&recovered);
64 recovered.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON);
65 break;
67 recovered.state = false;
68 break;
69 case LIGHT_ALWAYS_ON:
70 recovered.state = true;
71 break;
72 }
73
74 // A light coming up on boot must never end up on-but-invisible: if the resolved restore
75 // state is on but its brightness is zero (e.g. a stale/persisted value from before a
76 // forced-on restore mode, or an inverted restore flipping a dim-to-0 off state to on),
77 // reset it to full brightness.
78 if (recovered.state && recovered.brightness == 0.0f) {
79 recovered.brightness = 1.0f;
80 }
81
82 call.set_color_mode_if_supported(recovered.color_mode);
83 call.set_state(recovered.state);
84 call.set_brightness_if_supported(recovered.brightness);
85 call.set_color_brightness_if_supported(recovered.color_brightness);
86 call.set_red_if_supported(recovered.red);
87 call.set_green_if_supported(recovered.green);
88 call.set_blue_if_supported(recovered.blue);
89 call.set_white_if_supported(recovered.white);
90 call.set_color_temperature_if_supported(recovered.color_temp);
91 call.set_cold_white_if_supported(recovered.cold_white);
92 call.set_warm_white_if_supported(recovered.warm_white);
93 if (recovered.effect != 0) {
94 call.set_effect(recovered.effect);
95 } else {
96 call.set_transition_length_if_supported(0);
97 }
98 call.perform();
99}
101 ESP_LOGCONFIG(TAG, "Light '%s'", this->get_name().c_str());
102 auto traits = this->get_traits();
103 if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) {
104 ESP_LOGCONFIG(TAG,
105 " Default Transition Length: %.1fs\n"
106 " Gamma Correct: %.2f",
107 this->default_transition_length_ / 1e3f, this->gamma_correct_);
108 }
109 if (traits.supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) {
110 ESP_LOGCONFIG(TAG,
111 " Min Mireds: %.1f\n"
112 " Max Mireds: %.1f",
113 traits.get_min_mireds(), traits.get_max_mireds());
114 }
115}
117 // Apply effect (if any)
118 auto *effect = this->get_active_effect_();
119 if (effect != nullptr) {
120 effect->apply();
121 }
122
123 // Apply transformer (if any)
124 if (this->transformer_ != nullptr) {
125 auto values = this->transformer_->apply();
126 this->is_transformer_active_ = true;
127 if (values.has_value()) {
128 this->current_values = *values;
129 this->output_->update_state(this);
130 this->next_write_ = true;
131 }
132
133 if (this->transformer_->is_finished()) {
134 // if the transition has written directly to the output, current_values is outdated, so update it
135 this->current_values = this->transformer_->get_target_values();
136
137 this->transformer_->stop();
138 this->is_transformer_active_ = false;
139 this->transformer_ = nullptr;
141 for (auto *listener : *this->target_state_reached_listeners_) {
142 listener->on_light_target_state_reached();
143 }
144 }
145
146 // Disable loop if idle (no transformer and no effect)
147 this->disable_loop_if_idle_();
148 }
149 }
150
151 // Write state to the light
152 if (this->next_write_) {
153 this->next_write_ = false;
154 this->output_->write_state(this);
155 // Disable loop if idle (no transformer and no effect)
156 this->disable_loop_if_idle_();
157 }
158}
159
161
163 if (this->remote_values_listeners_) {
164 for (auto *listener : *this->remote_values_listeners_) {
165 listener->on_light_remote_values_update();
166 }
167 }
168#if defined(USE_LIGHT) && defined(USE_CONTROLLER_REGISTRY)
169 ControllerRegistry::notify_light_update(this);
170#endif
171}
172
174
175static constexpr auto EFFECT_NONE_REF = StringRef::from_lit("None");
176
178 if (this->active_effect_index_ > 0) {
179 return this->effects_[this->active_effect_index_ - 1]->get_name();
180 }
181 return EFFECT_NONE_REF;
182}
183
185 if (!this->remote_values_listeners_) {
186 this->remote_values_listeners_ = make_unique<std::vector<LightRemoteValuesListener *>>();
187 }
188 this->remote_values_listeners_->push_back(listener);
189}
192 this->target_state_reached_listeners_ = make_unique<std::vector<LightTargetStateReachedListener *>>();
193 }
194 this->target_state_reached_listeners_->push_back(listener);
195}
196
198 this->default_transition_length_ = default_transition_length;
199}
202 this->flash_transition_length_ = flash_transition_length;
203}
206void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
207void LightState::set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; }
208bool LightState::supports_effects() { return !this->effects_.empty(); }
210void LightState::add_effects(const std::initializer_list<LightEffect *> &effects) {
211 // Called once from Python codegen during setup with all effects from YAML config
212 this->effects_ = effects;
213}
214
217 this->current_values.as_brightness(brightness);
218 *brightness = this->gamma_correct_lut(*brightness);
219}
220void LightState::current_values_as_rgb(float *red, float *green, float *blue) {
221 this->current_values.as_rgb(red, green, blue);
222 *red = this->gamma_correct_lut(*red);
223 *green = this->gamma_correct_lut(*green);
224 *blue = this->gamma_correct_lut(*blue);
225}
226void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white) {
227 this->current_values.as_rgbw(red, green, blue, white);
228 *red = this->gamma_correct_lut(*red);
229 *green = this->gamma_correct_lut(*green);
230 *blue = this->gamma_correct_lut(*blue);
231 *white = this->gamma_correct_lut(*white);
232}
233void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
234 bool constant_brightness) {
235 this->current_values.as_rgb(red, green, blue);
236 *red = this->gamma_correct_lut(*red);
237 *green = this->gamma_correct_lut(*green);
238 *blue = this->gamma_correct_lut(*blue);
239 this->current_values_as_cwww(cold_white, warm_white, constant_brightness);
240}
241void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature,
242 float *white_brightness) {
243 auto traits = this->get_traits();
244 this->current_values.as_rgbct(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, color_temperature,
245 white_brightness);
246 *red = this->gamma_correct_lut(*red);
247 *green = this->gamma_correct_lut(*green);
248 *blue = this->gamma_correct_lut(*blue);
249 *white_brightness = this->gamma_correct_lut(*white_brightness);
250}
251void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) {
252 if (!constant_brightness) {
253 // Without constant_brightness, gamma commutes with simple multiplication:
254 // gamma(white_level * cw) = gamma(white_level) * gamma(cw)
255 // (since gamma(a*b) = (a*b)^g = a^g * b^g = gamma(a) * gamma(b))
256 // so applying gamma after is mathematically equivalent and simpler.
257 this->current_values.as_cwww(cold_white, warm_white, false);
258 *cold_white = this->gamma_correct_lut(*cold_white);
259 *warm_white = this->gamma_correct_lut(*warm_white);
260 return;
261 }
262
263 // For constant_brightness mode, gamma MUST be applied to the individual
264 // channel values BEFORE the balancing formula (max/sum ratio), not after.
265 //
266 // Why: The cold_white_ and warm_white_ values stored in LightColorValues
267 // are gamma-uncorrected (see transform_parameters_() which applies
268 // gamma_uncorrect to the linear CW/WW fractions derived from color
269 // temperature). Applying gamma_correct here recovers the original linear
270 // fractions, which the constant_brightness formula then uses to distribute
271 // power evenly. The max/sum formula ensures cold+warm PWM output sums to
272 // a constant, keeping total power (and perceived brightness) the same
273 // across all color temperatures.
274 //
275 // Applying gamma AFTER the formula would be incorrect because gamma is
276 // nonlinear: gamma(a/b) != gamma(a)/gamma(b), so the carefully balanced
277 // ratio would be distorted, causing a severe brightness dip at mid-range
278 // color temperatures.
279 const auto &v = this->current_values;
280 if (!(v.get_color_mode() & ColorCapability::COLD_WARM_WHITE)) {
281 *cold_white = *warm_white = 0;
282 return;
283 }
284
285 const float cw_level = this->gamma_correct_lut(v.get_cold_white());
286 const float ww_level = this->gamma_correct_lut(v.get_warm_white());
287 const float white_level = this->gamma_correct_lut(v.get_state() * v.get_brightness());
288 const float sum = cw_level > 0 || ww_level > 0 ? cw_level + ww_level : 1; // Don't divide by zero.
289 *cold_white = white_level * std::max(cw_level, ww_level) * cw_level / sum;
290 *warm_white = white_level * std::max(cw_level, ww_level) * ww_level / sum;
291}
292void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) {
293 auto traits = this->get_traits();
294 this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness);
295 *white_brightness = this->gamma_correct_lut(*white_brightness);
296}
297
298#ifdef USE_LIGHT_GAMMA_LUT
299float LightState::gamma_correct_lut(float value) const {
300 if (value <= 0.0f)
301 return 0.0f;
302 if (value >= 1.0f)
303 return 1.0f;
304 if (this->gamma_table_ == nullptr)
305 return value;
306 float scaled = value * 255.0f;
307 auto idx = static_cast<uint8_t>(scaled);
308 if (idx >= 255)
309 return progmem_read_uint16(&this->gamma_table_[255]) / 65535.0f;
310 float frac = scaled - idx;
311 float a = progmem_read_uint16(&this->gamma_table_[idx]);
312 float b = progmem_read_uint16(&this->gamma_table_[idx + 1]);
313 return (a + frac * (b - a)) / 65535.0f;
314}
315float LightState::gamma_uncorrect_lut(float value) const {
316 if (value <= 0.0f)
317 return 0.0f;
318 if (value >= 1.0f)
319 return 1.0f;
320 if (this->gamma_table_ == nullptr)
321 return value;
322 uint16_t target = static_cast<uint16_t>(value * 65535.0f);
323 uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target);
324 if (lo >= 255)
325 return 1.0f;
326 // Interpolate between lo and lo+1
327 uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]);
328 uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]);
329 if (b == a)
330 return lo / 255.0f;
331 float frac = static_cast<float>(target - a) / static_cast<float>(b - a);
332 return (lo + frac) / 255.0f;
333}
334#endif // USE_LIGHT_GAMMA_LUT
335
337
339 this->stop_effect_();
340 if (effect_index == 0)
341 return;
342
343 this->active_effect_index_ = effect_index;
344 auto *effect = this->get_active_effect_();
345 effect->start_internal();
346 // Enable loop while effect is active
347 this->enable_loop();
348}
350 if (this->active_effect_index_ == 0) {
351 return nullptr;
352 } else {
353 return this->effects_[this->active_effect_index_ - 1];
354 }
355}
357 auto *effect = this->get_active_effect_();
358 if (effect != nullptr) {
359 effect->stop();
360 }
361 this->active_effect_index_ = 0;
362 // Disable loop if idle (no effect and no transformer)
363 this->disable_loop_if_idle_();
364}
365
366void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) {
368 this->transformer_->setup(this->current_values, target, length);
369
370 if (set_remote_values) {
371 this->remote_values = target;
372 }
373 // Enable loop while transition is active
374 this->enable_loop();
375}
376
377void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) {
378 LightColorValues end_colors = this->remote_values;
379 // If starting a flash if one is already happening, set end values to end values of current flash
380 // Hacky but works
381 if (this->transformer_ != nullptr)
382 end_colors = this->transformer_->get_start_values();
383
384 this->transformer_ = make_unique<LightFlashTransformer>(*this);
385 this->transformer_->setup(end_colors, target, length);
386
387 if (set_remote_values) {
388 this->remote_values = target;
389 };
390 // Enable loop while flash is active
391 this->enable_loop();
392}
393
394void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) {
395 this->is_transformer_active_ = false;
396 this->transformer_ = nullptr;
397 this->current_values = target;
398 if (set_remote_values) {
399 this->remote_values = target;
400 }
401 this->output_->update_state(this);
402 this->schedule_write_();
403}
404
406 // Only disable loop if both transformer and effect are inactive, and no pending writes
407 if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr && !this->next_write_) {
408 this->disable_loop();
409 }
410}
411
413 LightStateRTCState saved;
415 switch (this->restore_mode_) {
418 saved.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON);
419 break;
420 default:
421 saved.state = this->remote_values.is_on();
422 break;
423 }
426 saved.red = this->remote_values.get_red();
427 saved.green = this->remote_values.get_green();
428 saved.blue = this->remote_values.get_blue();
429 saved.white = this->remote_values.get_white();
433 saved.effect = this->active_effect_index_;
434 this->rtc_.save(&saved);
435}
436
437} // namespace esphome::light
void enable_loop()
Enable this component's loop.
Definition component.h:246
void disable_loop()
Disable this component's loop.
const StringRef & get_name() const
Definition entity_base.h:71
ESPPreferenceObject make_entity_preference(uint32_t version=0)
Create a preference object for storing this entity's state/settings.
Fixed-capacity vector - allocates once at runtime, never reallocates This avoids std::vector template...
Definition helpers.h:534
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
static constexpr StringRef from_lit(const CharT(&s)[N])
Definition string_ref.h:50
This class represents a requested change in a light state.
Definition light_call.h:22
LightCall & set_state(optional< bool > state)
Set the binary ON/OFF state of the light.
This class represents the color state for a light object.
float get_brightness() const
Get the brightness property of these light color values. In range 0.0 to 1.0.
float get_blue() const
Get the blue property of these light color values. In range 0.0 to 1.0.
float get_white() const
Get the white property of these light color values. In range 0.0 to 1.0.
float get_color_temperature() const
Get the color temperature property of these light color values in mired.
void as_cwww(float *cold_white, float *warm_white, bool constant_brightness=false) const
Convert these light color values to an CWWW representation with the given parameters.
float get_cold_white() const
Get the cold white property of these light color values. In range 0.0 to 1.0.
void as_rgbw(float *red, float *green, float *blue, float *white) const
Convert these light color values to an RGBW representation and write them to red, green,...
void as_rgb(float *red, float *green, float *blue) const
Convert these light color values to an RGB representation and write them to red, green,...
bool is_on() const
Get the binary true/false state of these light color values.
float get_green() const
Get the green property of these light color values. In range 0.0 to 1.0.
void set_color_temperature(float color_temperature)
Set the color temperature property of these light color values in mired.
float get_warm_white() const
Get the warm white property of these light color values. In range 0.0 to 1.0.
void as_binary(bool *binary) const
Convert these light color values to a binary representation and write them to binary.
void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature, float *white_brightness) const
Convert these light color values to a CT+BR representation with the given parameters.
ColorMode get_color_mode() const
Get the color mode of these light color values.
void as_rgbct(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, float *color_temperature, float *white_brightness) const
Convert these light color values to an RGB+CT+BR representation with the given parameters.
float get_red() const
Get the red property of these light color values. In range 0.0 to 1.0.
void as_brightness(float *brightness) const
Convert these light color values to a brightness-only representation and write them to brightness.
float get_color_brightness() const
Get the color brightness property of these light color values. In range 0.0 to 1.0.
Interface to write LightStates to hardware.
virtual void write_state(LightState *state)=0
Called from loop() every time the light state has changed, and should should write the new state to h...
virtual std::unique_ptr< LightTransformer > create_default_transition()
Return the default transformer used for transitions.
virtual LightTraits get_traits()=0
Return the LightTraits of this LightOutput.
virtual void update_state(LightState *state)
Called on every update of the current values of the associated LightState, can optionally be used to ...
virtual void setup_state(LightState *state)
Listener interface for light remote value changes.
Definition light_state.h:31
void add_remote_values_listener(LightRemoteValuesListener *listener)
Add a listener for remote values changes.
float gamma_correct_
Gamma correction factor for the light.
FixedVector< LightEffect * > effects_
List of effects for this light.
StringRef get_effect_name()
Return the name of the current effect, or if no effect is active "None".
void start_effect_(uint32_t effect_index)
Internal method to start an effect with the given index.
void current_values_as_rgb(float *red, float *green, float *blue)
float gamma_correct_lut(float value) const
Apply gamma correction using the pre-computed forward LUT.
void stop_effect_()
Internal method to stop the current effect (if one is active).
void current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, float *white_brightness)
void disable_loop_if_idle_()
Disable loop if neither transformer nor effect is active.
std::unique_ptr< LightTransformer > transformer_
The currently active transformer for this light (transition/flash).
LightColorValues remote_values
The remote color values reported to the frontend.
LightOutput * get_output() const
Get the light output associated with this object.
void set_flash_transition_length(uint32_t flash_transition_length)
Set the flash transition length.
LightState(LightOutput *output)
void current_values_as_binary(bool *binary)
The result of all the current_values_as_* methods have gamma correction applied.
void save_remote_values_()
Internal method to save the current remote_values to the preferences.
void add_target_state_reached_listener(LightTargetStateReachedListener *listener)
Add a listener for target state reached.
LightCall turn_on()
Make a light state call.
uint32_t get_default_transition_length() const
void set_immediately_(const LightColorValues &target, bool set_remote_values)
Internal method to set the color values to target immediately (with no transition).
uint32_t get_flash_transition_length() const
void current_values_as_ct(float *color_temperature, float *white_brightness)
void set_initial_state(void(*callback)(LightStateRTCState &))
Set a callback to populate the initial state defaults during setup.
float gamma_uncorrect_lut(float value) const
Reverse gamma correction by binary-searching the forward LUT.
void current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness=false)
void(* initial_state_callback_)(LightStateRTCState &)
Callback to populate initial state defaults — called once during setup, then cleared.
uint32_t flash_transition_length_
Transition length to use for flash transitions.
void publish_state()
Publish the currently active state to the frontend.
void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness=false)
void current_values_as_brightness(float *brightness)
void setup() override
Load state from preferences.
uint32_t active_effect_index_
Value for storing the index of the currently active effect. 0 if no effect is active.
void start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values)
Internal method to start a flash for the specified amount of time.
LightRestoreMode restore_mode_
Restore mode of the light.
const FixedVector< LightEffect * > & get_effects() const
Get all effects for this light state.
void add_effects(const std::initializer_list< LightEffect * > &effects)
Add effects for this light state.
void schedule_write_()
Schedule a write to the light output and enable the loop to process it.
const uint16_t * gamma_table_
bool supports_effects()
Return whether the light has any effects that meet the trait requirements.
bool next_write_
Whether the light value should be written in the next cycle.
void start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values)
Internal method to start a transition to the target color with the given length.
float get_setup_priority() const override
Shortly after HARDWARE.
void set_restore_mode(LightRestoreMode restore_mode)
Set the restore mode of this light.
LightOutput * output_
Store the output to allow effects to have more access.
ESPPreferenceObject rtc_
Object used to store the persisted values of the light.
void current_values_as_rgbw(float *red, float *green, float *blue, float *white)
uint32_t default_transition_length_
Default transition length for all transitions in ms.
LightColorValues current_values
The current values of the light as outputted to the light.
LightEffect * get_active_effect_()
Internal method to get the currently active effect.
bool is_transformer_active()
Indicator if a transformer (e.g.
std::unique_ptr< std::vector< LightRemoteValuesListener * > > remote_values_listeners_
Listeners for remote values changes.
std::unique_ptr< std::vector< LightTargetStateReachedListener * > > target_state_reached_listeners_
Listeners for target state reached.
void set_gamma_correct(float gamma_correct)
Set the gamma correction factor.
void set_default_transition_length(uint32_t default_transition_length)
Set the default transition length, i.e. the transition length when no transition is provided.
Listener interface for light target state reached.
Definition light_state.h:42
This class is used to represent the capabilities of a light.
Definition light_traits.h:9
@ LIGHT_RESTORE_INVERTED_DEFAULT_ON
Definition light_state.h:53
@ LIGHT_RESTORE_DEFAULT_OFF
Definition light_state.h:48
@ LIGHT_RESTORE_INVERTED_DEFAULT_OFF
Definition light_state.h:52
uint8_t gamma_table_reverse_search(const uint16_t *table, uint16_t target)
Binary search a monotonically increasing uint16[256] PROGMEM table.
static float float b
@ BRIGHTNESS
Master brightness of the light can be controlled.
@ COLOR_TEMPERATURE
Color temperature can be controlled.
@ COLD_WARM_WHITE
Brightness of cold and warm white output can be controlled.
constexpr float HARDWARE
For components that deal with hardware and are very important like GPIO switch.
Definition component.h:43
float gamma_correct(float value, float gamma)
Definition helpers.cpp:647
uint16_t progmem_read_uint16(const uint16_t *addr)
Definition hal.h:45
uint16_t length
Definition tt21100.cpp:0