#pragma once #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/optional.h" #include "esphome/core/preferences.h" #include "esphome/core/string_ref.h" #include "light_call.h" #include "light_color_values.h" #include "light_effect.h" #include "light_traits.h" #include "light_transformer.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/progmem.h" #include #include 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, LIGHT_RESTORE_DEFAULT_ON, LIGHT_ALWAYS_OFF, LIGHT_ALWAYS_ON, LIGHT_RESTORE_INVERTED_DEFAULT_OFF, LIGHT_RESTORE_INVERTED_DEFAULT_ON, LIGHT_RESTORE_AND_OFF, LIGHT_RESTORE_AND_ON, }; struct LightStateRTCState { LightStateRTCState(ColorMode color_mode, bool state, float brightness, float color_brightness, float red, float green, float blue, float white, float color_temp, float cold_white, float warm_white) : brightness(brightness), color_brightness(color_brightness), red(red), green(green), blue(blue), white(white), color_temp(color_temp), cold_white(cold_white), warm_white(warm_white), effect(0), color_mode(color_mode), state(state) {} LightStateRTCState() = default; // Group 4-byte aligned members first float brightness{1.0f}; float color_brightness{1.0f}; float red{1.0f}; float green{1.0f}; float blue{1.0f}; float white{1.0f}; float color_temp{1.0f}; float cold_white{1.0f}; float warm_white{1.0f}; uint32_t effect{0}; // Group smaller members at the end ColorMode color_mode{ColorMode::UNKNOWN}; bool state{false}; }; /** This class represents the communication layer between the front-end MQTT layer and the * hardware output layer. */ class LightState : public EntityBase, public Component { public: LightState(LightOutput *output); LightTraits get_traits(); /// Make a light state call LightCall turn_on(); LightCall turn_off(); LightCall toggle(); LightCall make_call(); // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Load state from preferences void setup() override; void dump_config() override; void loop() override; /// Shortly after HARDWARE. float get_setup_priority() const override; /** The current values of the light as outputted to the light. * * These values represent the "real" state of the light - During transitions this * property will be changed continuously (in contrast to .remote_values, where they * are constant during transitions). * * This value does not have gamma correction applied. * * This property is read-only for users. Any changes to it will be ignored. */ LightColorValues current_values; /** The remote color values reported to the frontend. * * These are different from the "current" values: For example transitions will * continuously change the "current" values. But the remote values will immediately * switch to the target value for a transition, reducing the number of packets sent. * * This value does not have gamma correction applied. * * This property is read-only for users. Any changes to it will be ignored. */ LightColorValues remote_values; /// Publish the currently active state to the frontend. void publish_state(); /// Get the light output associated with this object. LightOutput *get_output() const; /// Return the name of the current effect, or if no effect is active "None". StringRef get_effect_name(); /** 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_remote_values_listener(LightRemoteValuesListener *listener); /** 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_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); uint32_t get_default_transition_length() const; /// Set the flash transition length void set_flash_transition_length(uint32_t flash_transition_length); uint32_t get_flash_transition_length() const; /// Set the gamma correction factor void set_gamma_correct(float gamma_correct); float get_gamma_correct() const { return this->gamma_correct_; } #ifdef USE_LIGHT_GAMMA_LUT /// Set pre-computed gamma forward lookup table (256-entry uint16 PROGMEM array) void set_gamma_table(const uint16_t *forward) { this->gamma_table_ = forward; } /// Get the forward gamma lookup table const uint16_t *get_gamma_table() const { return this->gamma_table_; } /// Apply gamma correction using the pre-computed forward LUT float gamma_correct_lut(float value) const; /// Reverse gamma correction by binary-searching the forward LUT float gamma_uncorrect_lut(float value) const; #else /// No gamma LUT — passthrough float gamma_correct_lut(float value) const { return value; } float gamma_uncorrect_lut(float value) const { return value; } #endif // USE_LIGHT_GAMMA_LUT /// Set the restore mode of this light void set_restore_mode(LightRestoreMode restore_mode); /// Set a callback to populate the initial state defaults during setup. /// The callback is called once, then cleared. Values live in flash as code. void set_initial_state(void (*callback)(LightStateRTCState &)); /// Return whether the light has any effects that meet the trait requirements. bool supports_effects(); /// Get all effects for this light state. const FixedVector &get_effects() const; /// Add effects for this light state. void add_effects(const std::initializer_list &effects); /// Get the total number of effects available for this light. size_t get_effect_count() const { return this->effects_.size(); } /// Get the currently active effect index (0 = no effect, 1+ = effect index). uint32_t get_current_effect_index() const { return this->active_effect_index_; } /// Get effect index by name. Returns 0 if effect not found. uint32_t get_effect_index(const std::string &effect_name) const { if (str_equals_case_insensitive(effect_name, "none")) { return 0; } for (size_t i = 0; i < this->effects_.size(); i++) { if (str_equals_case_insensitive(effect_name, this->effects_[i]->get_name())) { return i + 1; // Effects are 1-indexed in active_effect_index_ } } return 0; // Effect not found } /// Get effect index by name (const char* overload, avoids std::string construction). uint32_t get_effect_index(const char *name, size_t len) const { if (len == 4 && ESPHOME_strncasecmp_P(name, ESPHOME_PSTR("none"), 4) == 0) { return 0; } StringRef ref(name, len); for (size_t i = 0; i < this->effects_.size(); i++) { if (str_equals_case_insensitive(ref, this->effects_[i]->get_name())) { return i + 1; } } return 0; } /// Get effect by index. Returns nullptr if index is invalid. LightEffect *get_effect_by_index(uint32_t index) const { if (index == 0 || index > this->effects_.size()) { return nullptr; } return this->effects_[index - 1]; // Effects are 1-indexed in active_effect_index_ } /// Get effect name by index. Returns "None" for index 0, empty string for invalid index. std::string get_effect_name_by_index(uint32_t index) const { if (index == 0) { return "None"; } if (index > this->effects_.size()) { return ""; // Invalid index } return std::string(this->effects_[index - 1]->get_name()); } /// The result of all the current_values_as_* methods have gamma correction applied. void current_values_as_binary(bool *binary); void current_values_as_brightness(float *brightness); void current_values_as_rgb(float *red, float *green, float *blue); void current_values_as_rgbw(float *red, float *green, float *blue, float *white); 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_rgbct(float *red, float *green, float *blue, float *color_temperature, float *white_brightness); void current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false); void current_values_as_ct(float *color_temperature, float *white_brightness); /** * Indicator if a transformer (e.g. transition) is active. This is useful * for effects e.g. at the start of the apply() method, add a check like: * * if (this->state_->is_transformer_active()) { * // Something is already running. * return; * } */ bool is_transformer_active(); protected: friend LightOutput; friend LightCall; friend class AddressableLight; /// Internal method to start an effect with the given index void start_effect_(uint32_t effect_index); /// Internal method to get the currently active effect LightEffect *get_active_effect_(); /// Internal method to stop the current effect (if one is active). void stop_effect_(); /// Internal method to start a transition to the target color with the given length. void start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values); /// Internal method to start a flash for the specified amount of time. void start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values); /// Internal method to set the color values to target immediately (with no transition). void set_immediately_(const LightColorValues &target, bool set_remote_values); /// Internal method to save the current remote_values to the preferences void save_remote_values_(); /// Disable loop if neither transformer nor effect is active void disable_loop_if_idle_(); /// Schedule a write to the light output and enable the loop to process it void schedule_write_() { this->next_write_ = true; this->enable_loop(); } /// Store the output to allow effects to have more access. LightOutput *output_; /// The currently active transformer for this light (transition/flash). std::unique_ptr transformer_{nullptr}; /// List of effects for this light. FixedVector effects_; /// Object used to store the persisted values of the light. ESPPreferenceObject rtc_; /** 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. */ std::unique_ptr> remote_values_listeners_; /** 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. */ std::unique_ptr> target_state_reached_listeners_; /// Callback to populate initial state defaults — called once during setup, then cleared. /// Values live in flash as function body; no per-instance data storage beyond this pointer. void (*initial_state_callback_)(LightStateRTCState &){nullptr}; /// Value for storing the index of the currently active effect. 0 if no effect is active uint32_t active_effect_index_{}; /// Default transition length for all transitions in ms. uint32_t default_transition_length_{}; /// Transition length to use for flash transitions. uint32_t flash_transition_length_{}; /// Gamma correction factor for the light. float gamma_correct_{}; #ifdef USE_LIGHT_GAMMA_LUT const uint16_t *gamma_table_{nullptr}; #endif // USE_LIGHT_GAMMA_LUT /// Whether the light value should be written in the next cycle. bool next_write_{true}; // for effects, true if a transformer (transition) is active. bool is_transformer_active_{false}; /// Restore mode of the light. LightRestoreMode restore_mode_; }; } // namespace esphome::light