Addressable Light Now Backed by a Color Accessor
Components that implement the AddressableLight interface must now provide an ESPColorAccessor to access the LED strip’s color and effect data instead of overriding AddressableLight::get_view_internal(). This change improves performance, prevents crashes, cleans up the API, and offers components more control over how their LED strip data is stored and how color correction is applied.
This is a developer breaking change for external components in ESPHome 2026.11.0 and later.
The user API remains backwards-compatible except that ESPColorSettable::set(ESPHSVColor) and ESPRangeView::set(ESPHSVColor) have been deprecated to remove ambiguity from the API. Use the already existing set_hsv(ESPHSVColor) method instead.
Background
Section titled “Background”PR #21577: Refactor ESPColorView and related types
This change aims to improve the architecture of AddressableLight to better support current and future use-cases.
For example, fixing a possible null-pointer dereference for unallocated LED buffers previously required adding a comparison in ESPColorView on every LED access as well as code in every subclass of AddressableLight that implemented get_view_internal() to check whether the buffer was allocated and skip the indexing calculations which was inefficient. Moving common code out to a function in PR #21447 helped a bit but it didn’t resolve the underlying design issues.
Decoupling buffer access from the AddressableLight class enables further opportunities for code reuse and long term evolution.
As part of this change, we also took the opportunity to tidy up the API and add more documentation.
Who This Affects
Section titled “Who This Affects”External components that subclass AddressableLight
Lambdas in YAML configurations that call the deprecated set(ESPHSVColor) method or removed developer API functions.
What’s Changing
Section titled “What’s Changing”This change introduces ESPColorAccessor, an abstract class, with virtual methods to get and set LED data by index according to some strategy determined by the implementation.
AddressableLight now has a field called accessor_ that points to the ESPColorAccessor that provides access to its LED data.
When an AddressableLight component is constructed, its accessor_ field initially points to the singleton NullColorAccessor that simply returns zero on every access until the component sets up the real accessor. Thus the component is shielded from potential issues if the user attempts to get/set LEDs before the component has been setup or in case setup failed and the buffers were not allocated.
Once the AddressableLight component is ready to provide access to its LED data, it reassigns accessor_ to point to the appropriate accessor instance which can be one of the following types:
BufferColorAccessoris the base class for accessors that operate on a linear buffer whose layout is defined by aChannelColorsobject. It provides functions for allocating those buffers, clearing effect data, and accessing the contents of the buffers.CorrectedBufferColorAccessorapplies color correction and uncorrection during each access to the color data. This is the most commonly used implementation as of the time of this writing.RawBufferColorAccessorgets/sets raw color data without color correction and uncorrection. It is useful for addressable light components that perform color correction elsewhere in the pipeline and for unit tests.- Some other subclass of
ESPColorAccessorthat implements another strategy. For example,PartitionLightOutputhas a color accessor that resolves the provided index to an LED within a segment of some other addressable light component.
AddressableLight implements operator[int32_t index] to return an ESPColorView that holds its accessor pointer along with the requested index. Likewise the component’s range access functions, such as all(), return an ESPRangeView that holds the accessor pointer along with the start and end index of the range. Thus ESPColorView and ESPRangeView remain the main user-facing API for LED access whereas ESPColorAccessor is hidden from their view and is mainly of interest to component developers.
This change breaks external addressable light components by removing the AddressableLight::get_view_internal() method and removing the ESPColorView constructor that those components previously used. It was not practical to provide a deprecation path because the responsibilities of ESPColorView have changed significantly and there’s nowhere to put the state that it used to hold (without resorting to some questionable hacks). This post provides a migration guide.
The user API remains compatible with existing code though some function signatures have changed slightly such as to pass colors by value instead of by reference because they’re small enough to fit in a register.
Immediate benefits
Section titled “Immediate benefits”- Less copying of objects on the stack:
ESPColorViewis 8 bytes (1 pointer + 1 index) instead of 24 bytes (6 pointers).ESPRangeIteratoris 8 bytes (1 pointer + 1 index) instead of 16 bytes (1 pointer + 3 indices).Coloris passed by value in a register instead of being passed by reference and copied to the stack.
- Components can implement their own
ESPColorAccessorsubclass when they need to do something special with less impact on the core architecture (aslight_partitiondoes). - Begins to resolve some technical debt related to how
light_partitionapplies color correction to a segment of an addressable light strip by isolating its dependencies. - Cleans up the code a lot!
Long-term benefits
Section titled “Long-term benefits”With this new architecture, it becomes possible to implement new kinds of addressable light components that use the color buffer in different ways such as to defer color correction to a later point in the pipeline to improve color fidelity and dynamic range. It should also be easier to make incremental changes such as perhaps adding support for 5-channel RGB/WW/CW LED strips someday.
Removed and conditionally excluded APIs (effective immediately)
Section titled “Removed and conditionally excluded APIs (effective immediately)”AddressableLight:
get_view_internal(int32_t): Setaccessor_field instead of overriding this method.
ESPColorView:
ESPColorView(uint8_t *, uint8_t *, uint8_t *, uint8_t *, uint8_t *, const ESPColorCorrection *): Was used byget_view_internal(). No replacement.raw_set_color_correction(const ESPColorCorrection *): Was only used byPartitionLightOutputwhich now declares its own accessor and applies color correction as a wrapper. No replacement.get_color_correction(): Guarded by#ifdefto only be included when thePartitionLightOutputis included in the build. The eventual goal is to remove this method altogether to maintain encapsulation.
interpret_index(int32_t, int32_t): Call sites updated to avoid size lookup in the common case where the index is positive. No replacement.
New APIs
Section titled “New APIs”Color:
- Added const to
operator==(const Color&)and other non-mutating methods. - Added
COLOR_CHANNEL_R/_G/_B/_Wthat can be used to directly index intorawfor per-channel access.
ChannelColors:
- Changed to hold a union of
r/g/b/woffsets as discrete fields and arawarray for per-channel access with the newCOLOR_CHANNEL_*constants. - Added 0, 3, and 4 argument constructors to initialize the new union members properly. This change breaks designated initializers.
- Added
get(),get_channel(),set(), andset_channel()methods to access LED data at a specific index within a buffer. - Added
RESERVED_WHITEto specify a 4 byte encoding without an actual white channel (as used by some SPI LED strips).
ESPColorAccessor:
- Defined the abstract base class and documented its invariants.
- Defined the subclasses
NullColorAccessor,BufferColorAccessor,RawBufferColorAccessor, andCorrectedBufferColorAccessoras described earlier in this post. - Added
allocate()andclear_effect_data()helpers toBufferColorAccessorto simplify common implementation tasks for lights.
ESPColorSettable:
- Pass
ColorandESPHSVColorby value instead of by reference because they fit in a register and it results in smaller code. - Added
set_channel()to set an individual color channel and redirected the other accessors through it. - Deprecated
set(ESPHSVColor)in favor of the existingset_hsv(ESPHSVColor)method to reduce API confusion among the subclasses:ESPColorViewpreviously hid theset(ESPHSVColor)overload, exposing it now would break callers ofset(Color)passing brace-initialized valueESPRangeViewpreviously exposedset(Color)andset(ESPHSVColor), soset(ESPHSVColor)is now deprecated
ESPColorView:
- Marked
finalfor devirtualization. - Added
operator=(const ESPColorView&)for consistency withESPRangeView. - Added
get_raw()andset_raw()for more efficient access of a complete color value. - Added
get_channel(),get_channel_raw(),set_channel(), andset_channel_raw()that operate on a channel index. - Added
set_red_raw(),set_green_raw(),set_blue_raw(), andset_white_raw()for consistency, cheap to do now that the colors are looked up by channel index.
ESPRangeView:
- Marked
finalfor devirtualization. - Now holds a pointer to an
ESPColorAccessorinstead of anAddressableLight. - Inlined
interpret_index()to avoid calculatingsize()in the common case where the index is positive. - Added
get_channel()andset_channel()that operate on individual color channels, redirected the other accessors.
ESPRangeIterator:
- Now holds a pointer to an
ESPColorAccessorinstead of copying the wholeESPRangeView.
ESPColorCorrection:
- Added
color_correct_channel()andcolor_uncorrect_channel()to operate on an individual color channel. - Redirected the existing methods over to the per-channel flavor.
AddressableLight:
- Added an
accessor_field to hold theESPColorAccessor*, it initially points toNullColorAccessor::instance. - Added
constqualifier toget(int32_t)to matchoperator[]. - Inlined
interpret_index()to avoid calculatingsize()in the common case where the index is positive.
Miscellaneous changes:
- Avoided accessing pixel color channels individually in core code when the whole
Colorcould be used instead. - Updated all LED strip components in tree.
Migration Guide
Section titled “Migration Guide”Required changes
Section titled “Required changes”-
Determine the type of
ESPColorAccessorthat your component will need.The color accessor will be doing the work that was previously done by your component’s implementation of
AddressableLight::get_view_internal(int32_t). If, as in most cases, your buffer is a linear array of RGB or RGBW pixels packed into 3 or 4 bytes per pixel, then you can useCorrectedBufferColorAccessorand initialize it with aChannelColorsobject that describes the layout.If
CorrectedBufferColorAccessorisn’t compatible with your buffer’s layout or you need to do something special when accessing pixels then you can implement a new subclass ofESPColorAccessor(as was done forPartitionLightOutput).Refer to the API documentation in esp_color_view.h and channel_colors.h for details.
-
Declare the color accessor.
Add a protected member variable to your component to hold the color accessor.
class MyComponent final : public esphome::light::AddressableLight {// ...protected:esphome::light::CorrectedBufferColorAccessor internal_accessor_;}; -
Describe the layout of the color data if needed by your accessor.
Use
ChannelColorsto describe the layout of the R, G, B, W channels. The constructor arguments specify the offset of the R, G, B, and (optionally) W channels. Here are some examples:constexpr esphome::light::ChannelColors RGB(0, 1, 2); // 3-arg constructor implies NO_WHITEconstexpr esphome::light::ChannelColors BGR(2, 1, 0, esphome::light::ChannelColors::NO_WHITE);constexpr esphome::light::ChannelColors WRGB(1, 2, 3, 0);constexpr esphome::light::ChannelColors GRBX(1, 0, 2, esphome::light::ChannelColors::RESERVED_WHITE); -
Initialize the color accessor and assign
this->accessor_.Once your component has allocated whatever the color accessor needs (such as the color data and effect data buffers), then it can setup the color accessor. In this example, the component splits the work across its constructor and
setup().MyComponent::MyComponent(/*...*/): internal_accessor_{nullptr, nullptr, RGB, &this->correction_} {}void MyComponent::setup() {// Allocate the color data and effect data buffers using the new helper on `CorrectedBufferColorAccessor`.esphome::RAMAllocator<uint8_t> allocator;if (!this->internal_accessor_.allocate(allocator, this->num_leds_)) {this->mark_failed(LOG_STR("Allocation failed"));return;}// Begin using the accessor now that it has been fully initializedthis->accessor_ = &this->internal_accessor_;} -
Delete your
get_view_internal()implementation.Review your
get_view_internal()implementation to confirm that your color accessor will match its behavior. Then delete it. So long, farewell!
Complete migration example
Section titled “Complete migration example”Before
Section titled “Before”Before migration, the component implements AddressableLight::get_view_internal().
class MyComponent final : public esphome::light::AddressableLight { // ...
void set_channel_colors(esphome::light::ChannelColors channel_colors) { this->channel_colors_ = channel_colors; }
void clear_effect_data() override { if (this->effect_data_ != nullptr) memset(this->effect_data_, 0, this->num_leds_); }
protected: esphome::light::ESPColorView get_view_internal(int32_t index) const override;
uint8_t *color_data_{nullptr}; uint8_t *effect_data_{nullptr}; esphome::light::ChannelColors channel_colors_{0, 1, 2, esphome::light::ChannelColors::NO_WHITE}; size_t num_leds_{0};};
void MyComponent::setup() { esphome::RAMAllocator<uint8_t> allocator; this->color_data_ = allocator.allocate(this->num_leds_ * this->channel_colors_.bytes_per_led()); this->effect_data_ = allocator.allocate(this->num_leds_);}
esphome::light::ESPColorView MyComponent::get_view_internal(int32_t index) const { const esphome::light::ChannelColors &colors = this->channel_colors_; uint8_t *led = this->color_data_ + (index * colors.bytes_per_led()); return {led + colors.r, led + colors.g, led + colors.b, colors.has_white() ? led + colors.w : nullptr, &this->effect_data_[index], &this->correction_};}After migration, the component incrementally initializes a color accessor in internal_accessor_ then assigns accessor_ to point to it.
class MyComponent final : public esphome::light::AddressableLight { // ...
void set_channel_colors(esphome::light::ChannelColors channel_colors) { this->internal_accessor_.channel_colors = channel_colors; }
void clear_effect_data() override { this->internal_accessor_.clear_effect_data(this->num_leds_); }
protected: esphome::light::CorrectedBufferColorAccessor internal_accessor_{nullptr, nullptr, {0, 1, 2}, &this->correction_}; size_t num_leds_{0};};
void MyComponent::setup() { esphome::RAMAllocator<uint8_t> allocator; if (!this->internal_accessor_.allocate(allocator, this->num_leds_)) { this->mark_failed(LOG_STR("Allocation failed")); return; } this->accessor_ = &this->internal_accessor_;}Backwards compatible alternative
Section titled “Backwards compatible alternative”This example shows how to support older and newer versions of ESPHome, when desired, by wrapping the modified portions with #if to preserve backwards compatibility.
#include "esphome/core/version.h"
class MyComponent final : public esphome::light::AddressableLight { // ...
void set_channel_colors(esphome::light::ChannelColors channel_colors) {#if ESPHOME_VERSION_CODE >= VERSION_CODE(2026, 11, 0) this->internal_accessor_.channel_colors = channel_colors;#else this->channel_colors_ = channel_colors;#endif }
void clear_effect_data() override {#if ESPHOME_VERSION_CODE >= VERSION_CODE(2026, 11, 0) this->internal_accessor_.clear_effect_data(this->num_leds_);#else if (this->effect_data_ != nullptr) memset(this->effect_data_, 0, this->num_leds_);#endif }
protected:#if ESPHOME_VERSION_CODE >= VERSION_CODE(2026, 11, 0) esphome::light::CorrectedBufferColorAccessor internal_accessor_{nullptr, nullptr, {0, 1, 2}, &this->correction_};#else esphome::light::ESPColorView get_view_internal(int32_t index) const override;
uint8_t *color_data_{nullptr}; uint8_t *effect_data_{nullptr}; esphome::light::ChannelColors channel_colors_{0, 1, 2, esphome::light::ChannelColors::NO_WHITE};#endif size_t num_leds_{0};};
void MyComponent::setup() { esphome::RAMAllocator<uint8_t> allocator;#if ESPHOME_VERSION_CODE >= VERSION_CODE(2026, 11, 0) if (!this->internal_accessor_.allocate(allocator, this->num_leds_)) { this->mark_failed(LOG_STR("Allocation failed")); return; } this->accessor_ = &this->internal_accessor_;#else const size_t color_data_bytes = this->num_leds_ * this->channel_colors_.bytes_per_led(); const size_t effect_data_bytes = this->num_leds_; this->color_data_ = allocator.allocate(color_data_bytes + effect_data_bytes); if (this->color_data_ == nullptr) { this->mark_failed(LOG_STR("Allocation failed")); return; } this->effect_data_ = this->color_data_ + color_data_bytes;#endif}
#if ESPHOME_VERSION_CODE < VERSION_CODE(2026, 11, 0)esphome::light::ESPColorView MyComponent::get_view_internal(int32_t index) const { // ESPColorView checks whether white or effect_data are nullptr during access but it doesn't check the // red, green, or blue pointers, so return pointers to scratch memory if allocation failed. static uint8_t scratch[3]; if (this->color_data_ == nullptr) return {&scratch[0], &scratch[1], &scratch[2], nullptr, nullptr, &this->correction_};
const esphome::light::ChannelColors &colors = this->channel_colors_; uint8_t *led = this->color_data_ + (index * colors.bytes_per_led()); return {led + colors.r, led + colors.g, led + colors.b, colors.has_white() ? led + colors.w : nullptr, &this->effect_data_[index], &this->correction_};}#endifQuestions?
Section titled “Questions?”If you have questions about migrating your external component, please ask in:
- ESPHome Discord - #devs channel
- ESPHome GitHub Discussions
Related Documentation
Section titled “Related Documentation”Copyright © 2026 ESPHome - A project from the Open Home Foundation
Comments
Feel free to leave a comment here to discuss this post with others. You can ask questions, share your experience, or suggest improvements. If you have a question about a specific feature or issue, please consider using the ESPHome Discord. Stick to English and follow ESPHome's code of conduct. These comments exist on a discussion on GitHub, so you can also comment there directly if you prefer.