From 463e3833dae23329ad484c1a549dab13c2de7541 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:19:48 +1200 Subject: [PATCH] [light] Replace rgb_order/is_rgbw/is_wrgb with channel_colors (#18474) --- .../beken_spi_led_strip/led_strip.cpp | 75 ++------- .../beken_spi_led_strip/led_strip.h | 23 +-- .../components/beken_spi_led_strip/light.py | 41 +++-- esphome/components/const/__init__.py | 2 + .../esp32_rmt_led_strip/led_strip.cpp | 86 ++--------- .../esp32_rmt_led_strip/led_strip.h | 29 +--- .../components/esp32_rmt_led_strip/light.py | 65 ++------ esphome/components/light/__init__.py | 106 +++++++++++++ esphome/components/light/channel_colors.h | 41 +++++ esphome/components/light/types.py | 3 + .../rp2040_pio_led_strip/led_strip.cpp | 59 ++----- .../rp2040_pio_led_strip/led_strip.h | 42 +---- .../components/rp2040_pio_led_strip/light.py | 33 ++-- .../common-ard-esp32_rmt_led_strip.yaml | 2 +- .../common-idf-esp32_rmt_led_strip.yaml | 2 +- .../beken_spi_led_strip/test.bk72xx-ard.yaml | 2 +- .../validate-legacy.bk72xx-ard.yaml | 10 ++ tests/components/e131/common-ard.yaml | 2 +- tests/components/e131/common-idf.yaml | 2 +- tests/components/e131/test.rp2040-ard.yaml | 2 +- .../esp32_rmt_led_strip/common.yaml | 4 +- .../test.esp32-s3-idf.yaml | 4 +- .../validate-legacy.esp32-idf.yaml | 23 +++ tests/components/partition/common-ard.yaml | 2 +- tests/components/partition/common-idf.yaml | 2 +- .../rp2040_pio_led_strip/common.yaml | 4 +- .../validate-legacy.rp2040-ard.yaml | 18 +++ tests/components/wled/test.esp32-ard.yaml | 2 +- .../components/light/test_channel_colors.py | 144 ++++++++++++++++++ .../components/test_esp32_rmt_led_strip.py | 57 ------- 30 files changed, 454 insertions(+), 433 deletions(-) create mode 100644 esphome/components/light/channel_colors.h create mode 100644 tests/components/beken_spi_led_strip/validate-legacy.bk72xx-ard.yaml create mode 100644 tests/components/esp32_rmt_led_strip/validate-legacy.esp32-idf.yaml create mode 100644 tests/components/rp2040_pio_led_strip/validate-legacy.rp2040-ard.yaml create mode 100644 tests/unit_tests/components/light/test_channel_colors.py delete mode 100644 tests/unit_tests/components/test_esp32_rmt_led_strip.py diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp index 9e14615d7a..0cf970b3cc 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.cpp +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -300,46 +300,12 @@ void BekenSPILEDStripLightOutput::write_state(light::LightState *state) { } light::ESPColorView BekenSPILEDStripLightOutput::get_view_internal(int32_t index) const { - int32_t r = 0, g = 0, b = 0; - switch (this->rgb_order_) { - case ORDER_RGB: - r = 0; - g = 1; - b = 2; - break; - case ORDER_RBG: - r = 0; - g = 2; - b = 1; - break; - case ORDER_GRB: - r = 1; - g = 0; - b = 2; - break; - case ORDER_GBR: - r = 2; - g = 0; - b = 1; - break; - case ORDER_BGR: - r = 2; - g = 1; - b = 0; - break; - case ORDER_BRG: - r = 1; - g = 2; - b = 0; - break; - } - uint8_t multiplier = this->is_rgbw_ || this->is_wrgb_ ? 4 : 3; - uint8_t white = this->is_wrgb_ ? 0 : 3; - - return {this->buf_ + (index * multiplier) + r + this->is_wrgb_, - this->buf_ + (index * multiplier) + g + this->is_wrgb_, - this->buf_ + (index * multiplier) + b + this->is_wrgb_, - this->is_rgbw_ || this->is_wrgb_ ? this->buf_ + (index * multiplier) + white : nullptr, + const light::ChannelColors &colors = this->channel_colors_; + uint8_t *led = this->buf_ + (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_}; } @@ -349,35 +315,12 @@ void BekenSPILEDStripLightOutput::dump_config() { "Beken SPI LED Strip:\n" " Pin: %u", this->pin_); - const char *rgb_order; - switch (this->rgb_order_) { - case ORDER_RGB: - rgb_order = "RGB"; - break; - case ORDER_RBG: - rgb_order = "RBG"; - break; - case ORDER_GRB: - rgb_order = "GRB"; - break; - case ORDER_GBR: - rgb_order = "GBR"; - break; - case ORDER_BGR: - rgb_order = "BGR"; - break; - case ORDER_BRG: - rgb_order = "BRG"; - break; - default: - rgb_order = "UNKNOWN"; - break; - } + char channel_colors[5]; ESP_LOGCONFIG(TAG, - " RGB Order: %s\n" + " Channel colors: %s\n" " Max refresh rate: %" PRIu32 "\n" " Number of LEDs: %u", - rgb_order, this->max_refresh_rate_.value_or(0), this->num_leds_); + this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_.value_or(0), this->num_leds_); } float BekenSPILEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/beken_spi_led_strip/led_strip.h b/esphome/components/beken_spi_led_strip/led_strip.h index 909634e266..1496e65d4d 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.h +++ b/esphome/components/beken_spi_led_strip/led_strip.h @@ -3,6 +3,7 @@ #ifdef USE_BK72XX #include "esphome/components/light/addressable_light.h" +#include "esphome/components/light/channel_colors.h" #include "esphome/components/light/light_output.h" #include "esphome/core/color.h" #include "esphome/core/component.h" @@ -10,15 +11,6 @@ namespace esphome::beken_spi_led_strip { -enum RGBOrder : uint8_t { - ORDER_RGB, - ORDER_RBG, - ORDER_GRB, - ORDER_GBR, - ORDER_BGR, - ORDER_BRG, -}; - class BekenSPILEDStripLightOutput final : public light::AddressableLight { public: void setup() override; @@ -28,7 +20,7 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight { int32_t size() const override { return this->num_leds_; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->is_rgbw_ || this->is_wrgb_) { + if (this->channel_colors_.has_white()) { traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE}); } else { traits.set_supported_color_modes({light::ColorMode::RGB}); @@ -38,16 +30,13 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight { void set_pin(uint8_t pin) { this->pin_ = pin; } void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; } - void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } - void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; } + void set_channel_colors(light::ChannelColors channel_colors) { this->channel_colors_ = channel_colors; } /// Set a maximum refresh rate in µs as some lights do not like being updated too often. void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; } void set_led_params(uint8_t bit0, uint8_t bit1, uint32_t spi_frequency); - void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } - void clear_effect_data() override { for (int i = 0; i < this->size(); i++) this->effect_data_[i] = 0; @@ -58,7 +47,7 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight { protected: light::ESPColorView get_view_internal(int32_t index) const override; - size_t get_buffer_size_() const { return this->num_leds_ * (this->is_rgbw_ || this->is_wrgb_ ? 4 : 3); } + size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); } uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; @@ -66,13 +55,11 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight { uint8_t pin_; uint16_t num_leds_; - bool is_rgbw_; - bool is_wrgb_; uint32_t spi_frequency_{6666666}; uint8_t bit0_{0xE0}; uint8_t bit1_{0xFC}; - RGBOrder rgb_order_; + light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE}; uint32_t last_refresh_{0}; optional max_refresh_rate_{}; diff --git a/esphome/components/beken_spi_led_strip/light.py b/esphome/components/beken_spi_led_strip/light.py index 9093b08b62..2be5842818 100644 --- a/esphome/components/beken_spi_led_strip/light.py +++ b/esphome/components/beken_spi_led_strip/light.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from esphome import pins import esphome.codegen as cg from esphome.components import libretiny, light +from esphome.components.const import CONF_CHANNEL_COLORS, CONF_IS_WRGB import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, @@ -13,6 +14,7 @@ from esphome.const import ( CONF_PIN, CONF_RGB_ORDER, ) +from esphome.types import ConfigType CODEOWNERS = ["@Mat931"] DEPENDENCIES = ["libretiny"] @@ -22,17 +24,6 @@ BekenSPILEDStripLightOutput = beken_spi_led_strip_ns.class_( "BekenSPILEDStripLightOutput", light.AddressableLight ) -RGBOrder = beken_spi_led_strip_ns.enum("RGBOrder") - -RGB_ORDERS = { - "RGB": RGBOrder.ORDER_RGB, - "RBG": RGBOrder.ORDER_RBG, - "GRB": RGBOrder.ORDER_GRB, - "GBR": RGBOrder.ORDER_GBR, - "BGR": RGBOrder.ORDER_BGR, - "BRG": RGBOrder.ORDER_BRG, -} - @dataclass class LEDStripTimings: @@ -57,8 +48,6 @@ CHIPSETS = { } -CONF_IS_WRGB = "is_wrgb" - SUPPORTED_PINS = { libretiny.const.FAMILY_BK7231N: [16], libretiny.const.FAMILY_BK7231T: [16], @@ -79,10 +68,9 @@ def _validate_pin(value): return value -def _validate_num_leds(value): - max_num_leds = 165 # 170 - if value[CONF_IS_RGBW] or value[CONF_IS_WRGB]: - max_num_leds = 123 # 127 +def _validate_num_leds(value: ConfigType) -> ConfigType: + # A white channel makes each LED one byte wider, so fewer of them fit in the DMA buffer. + max_num_leds = 123 if "W" in value[CONF_CHANNEL_COLORS] else 165 # 127 / 170 if value[CONF_NUM_LEDS] > max_num_leds: raise cv.Invalid( f"The maximum number of LEDs for this configuration is {max_num_leds}.", @@ -99,18 +87,23 @@ CONFIG_SCHEMA = cv.All( pins.internal_gpio_output_pin_number, _validate_pin ), cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, - cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), + cv.Optional(CONF_CHANNEL_COLORS): light.validate_channel_colors, + # Deprecated in favour of CONF_CHANNEL_COLORS, remove in 2027.3.0 + cv.Optional(CONF_RGB_ORDER): cv.one_of(*light.RGB_ORDERS, upper=True), + cv.Optional(CONF_IS_RGBW): cv.boolean, + cv.Optional(CONF_IS_WRGB): cv.boolean, cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Required(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), - cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, - cv.Optional(CONF_IS_WRGB, default=False): cv.boolean, } ), + light.migrate_channel_colors( + removed_in="2027.3.0", component="beken_spi_led_strip" + ), _validate_num_leds, ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) await light.register_light(var, config) await cg.register_component(var, config) @@ -130,6 +123,6 @@ async def to_code(config): ) ) - cg.add(var.set_rgb_order(config[CONF_RGB_ORDER])) - cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) - cg.add(var.set_is_wrgb(config[CONF_IS_WRGB])) + cg.add( + var.set_channel_colors(light.channel_colors_struct(config[CONF_CHANNEL_COLORS])) + ) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 3ba89d2838..10710c8d29 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -10,6 +10,7 @@ CONF_ACCELEROMETER_RANGE = "accelerometer_range" CONF_B_CONSTANT = "b_constant" CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent" CONF_BYTE_ORDER = "byte_order" +CONF_CHANNEL_COLORS = "channel_colors" CONF_CLIMATE_ID = "climate_id" CONF_CO2_EQUIVALENT = "co2_equivalent" CONF_COLOR_DEPTH = "color_depth" @@ -22,6 +23,7 @@ CONF_GYROSCOPE_ODR = "gyroscope_odr" CONF_GYROSCOPE_RANGE = "gyroscope_range" CONF_IAQ = "iaq" CONF_IGNORE_NOT_FOUND = "ignore_not_found" +CONF_IS_WRGB = "is_wrgb" CONF_LABEL = "label" CONF_LIBRETINY = "libretiny" CONF_LOOP = "loop" diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 95391ef100..7cac1dfb41 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -221,46 +221,12 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { } light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index) const { - int32_t r = 0, g = 0, b = 0; - switch (this->rgb_order_) { - case ORDER_RGB: - r = 0; - g = 1; - b = 2; - break; - case ORDER_RBG: - r = 0; - g = 2; - b = 1; - break; - case ORDER_GRB: - r = 1; - g = 0; - b = 2; - break; - case ORDER_GBR: - r = 2; - g = 0; - b = 1; - break; - case ORDER_BGR: - r = 2; - g = 1; - b = 0; - break; - case ORDER_BRG: - r = 1; - g = 2; - b = 0; - break; - } - uint8_t multiplier = this->is_rgbw_ || this->is_wrgb_ ? 4 : 3; - uint8_t white = this->is_wrgb_ ? 0 : this->white_index_; - - return {this->buf_ + (index * multiplier) + r + (white <= r), - this->buf_ + (index * multiplier) + g + (white <= g), - this->buf_ + (index * multiplier) + b + (white <= b), - this->is_rgbw_ || this->is_wrgb_ ? this->buf_ + (index * multiplier) + white : nullptr, + const light::ChannelColors &colors = this->channel_colors_; + uint8_t *led = this->buf_ + (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_}; } @@ -271,46 +237,12 @@ void ESP32RMTLEDStripLightOutput::dump_config() { " Pin: %u", this->pin_); ESP_LOGCONFIG(TAG, " RMT Symbols: %" PRIu32, this->rmt_symbols_); - const char *rgb_order; - switch (this->rgb_order_) { - case ORDER_RGB: - rgb_order = "RGB"; - break; - case ORDER_RBG: - rgb_order = "RBG"; - break; - case ORDER_GRB: - rgb_order = "GRB"; - break; - case ORDER_GBR: - rgb_order = "GBR"; - break; - case ORDER_BGR: - rgb_order = "BGR"; - break; - case ORDER_BRG: - rgb_order = "BRG"; - break; - default: - rgb_order = "UNKNOWN"; - break; - } - if (this->is_rgbw_ || this->is_wrgb_) { - char rgbw_order[5]; - uint8_t white = this->is_wrgb_ ? 0 : this->white_index_; - uint8_t rgb_index = 0; - for (uint8_t i = 0; i < 4; i++) { - rgbw_order[i] = i == white ? 'W' : rgb_order[rgb_index++]; - } - rgbw_order[4] = '\0'; - ESP_LOGCONFIG(TAG, " RGBW Order: %s", rgbw_order); - } else { - ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order); - } + char channel_colors[5]; ESP_LOGCONFIG(TAG, + " Channel colors: %s\n" " Max refresh rate: %" PRIu32 "\n" " Number of LEDs: %u", - this->max_refresh_rate_.value_or(0), this->num_leds_); + this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_.value_or(0), this->num_leds_); } float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h index 3e31309bff..61aac06d76 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.h +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -3,6 +3,7 @@ #ifdef USE_ESP32 #include "esphome/components/light/addressable_light.h" +#include "esphome/components/light/channel_colors.h" #include "esphome/components/light/light_output.h" #include "esphome/core/color.h" #include "esphome/core/component.h" @@ -15,15 +16,6 @@ namespace esphome::esp32_rmt_led_strip { -enum RGBOrder : uint8_t { - ORDER_RGB, - ORDER_RBG, - ORDER_GRB, - ORDER_GBR, - ORDER_BGR, - ORDER_BRG, -}; - struct LedParams { rmt_symbol_word_t bit0; rmt_symbol_word_t bit1; @@ -39,7 +31,7 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight { int32_t size() const override { return this->num_leds_; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->is_rgbw_ || this->is_wrgb_) { + if (this->channel_colors_.has_white()) { traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE}); } else { traits.set_supported_color_modes({light::ColorMode::RGB}); @@ -50,13 +42,7 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight { void set_pin(uint8_t pin) { this->pin_ = pin; } void set_inverted(bool inverted) { this->invert_out_ = inverted; } void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; } - void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } - void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; } - void set_rgbw_order(uint8_t white_index) { - this->is_rgbw_ = true; - this->is_wrgb_ = false; - this->white_index_ = white_index; - } + void set_channel_colors(light::ChannelColors channel_colors) { this->channel_colors_ = channel_colors; } void set_use_dma(bool use_dma) { this->use_dma_ = use_dma; } void set_use_psram(bool use_psram) { this->use_psram_ = use_psram; } @@ -66,7 +52,6 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight { void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low); - void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; } void clear_effect_data() override { @@ -79,7 +64,7 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight { protected: light::ESPColorView get_view_internal(int32_t index) const override; - size_t get_buffer_size_() const { return this->num_leds_ * (this->is_rgbw_ || this->is_wrgb_ ? 4 : 3); } + size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); } uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; @@ -94,15 +79,11 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight { uint32_t rmt_symbols_{48}; uint8_t pin_; uint16_t num_leds_; - bool is_rgbw_{false}; - bool is_wrgb_{false}; - // An index after the RGB channels makes offset adjustment a no-op for three-channel strips. - uint8_t white_index_{3}; bool use_dma_{false}; bool use_psram_{false}; bool invert_out_{false}; - RGBOrder rgb_order_{ORDER_RGB}; + light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE}; uint32_t last_refresh_{0}; optional max_refresh_rate_{}; diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 2722a9b656..571b7d93b8 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -1,10 +1,9 @@ from dataclasses import dataclass -import logging from esphome import pins import esphome.codegen as cg from esphome.components import esp32, esp32_rmt, light -from esphome.components.const import CONF_USE_PSRAM +from esphome.components.const import CONF_CHANNEL_COLORS, CONF_IS_WRGB, CONF_USE_PSRAM from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( @@ -22,8 +21,6 @@ from esphome.const import ( ) from esphome.types import ConfigType -_LOGGER = logging.getLogger(__name__) - CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["esp32"] @@ -32,17 +29,6 @@ ESP32RMTLEDStripLightOutput = esp32_rmt_led_strip_ns.class_( "ESP32RMTLEDStripLightOutput", light.AddressableLight ) -RGBOrder = esp32_rmt_led_strip_ns.enum("RGBOrder") - -RGB_ORDERS = { - "RGB": RGBOrder.ORDER_RGB, - "RBG": RGBOrder.ORDER_RBG, - "GRB": RGBOrder.ORDER_GRB, - "GBR": RGBOrder.ORDER_GBR, - "BGR": RGBOrder.ORDER_BGR, - "BRG": RGBOrder.ORDER_BRG, -} - @dataclass class LEDStripTimings: @@ -62,8 +48,6 @@ CHIPSETS = { "SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0), } -CONF_IS_WRGB = "is_wrgb" -CONF_RGBW_ORDER = "rgbw_order" CONF_BIT0_HIGH = "bit0_high" CONF_BIT0_LOW = "bit0_low" CONF_BIT1_HIGH = "bit1_high" @@ -72,26 +56,6 @@ CONF_RESET_HIGH = "reset_high" CONF_RESET_LOW = "reset_low" -def _validate_rgbw_order(value: str) -> str: - value = cv.string(value).upper() - if len(value) != 4 or set(value) != set("RGBW"): - raise cv.Invalid("RGBW order must be a permutation of RGBW") - return value - - -def _split_rgbw_order(rgbw_order: str) -> tuple[str, int]: - return rgbw_order.replace("W", ""), rgbw_order.index("W") - - -def _validate_rgbw_order_exclusivity(config: ConfigType) -> ConfigType: - if CONF_RGBW_ORDER in config and (config[CONF_IS_RGBW] or config[CONF_IS_WRGB]): - raise cv.Invalid( - f"'{CONF_RGBW_ORDER}' cannot be used with '{CONF_IS_RGBW}' or " - f"'{CONF_IS_WRGB}'" - ) - return config - - CONFIG_SCHEMA = cv.All( esp32.only_on_variant( unsupported=list(esp32_rmt.VARIANTS_NO_RMT), @@ -102,8 +66,11 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, - cv.Optional(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), - cv.Optional(CONF_RGBW_ORDER): _validate_rgbw_order, + cv.Optional(CONF_CHANNEL_COLORS): light.validate_channel_colors, + # Deprecated in favour of CONF_CHANNEL_COLORS, remove in 2027.3.0 + cv.Optional(CONF_RGB_ORDER): cv.one_of(*light.RGB_ORDERS, upper=True), + cv.Optional(CONF_IS_RGBW): cv.boolean, + cv.Optional(CONF_IS_WRGB): cv.boolean, cv.SplitDefault( CONF_RMT_SYMBOLS, esp32=192, @@ -117,8 +84,6 @@ CONFIG_SCHEMA = cv.All( ): cv.int_range(min=2), cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), - cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, - cv.Optional(CONF_IS_WRGB, default=False): cv.boolean, cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] @@ -153,12 +118,13 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH), - cv.has_exactly_one_key(CONF_RGB_ORDER, CONF_RGBW_ORDER), - _validate_rgbw_order_exclusivity, + light.migrate_channel_colors( + removed_in="2027.3.0", component="esp32_rmt_led_strip" + ), ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) include_builtin_idf_component("esp_driver_rmt") @@ -198,14 +164,9 @@ async def to_code(config): ) ) - if (rgbw_order := config.get(CONF_RGBW_ORDER)) is not None: - rgb_order, white_index = _split_rgbw_order(rgbw_order) - cg.add(var.set_rgb_order(RGB_ORDERS[rgb_order])) - cg.add(var.set_rgbw_order(white_index)) - else: - cg.add(var.set_rgb_order(config[CONF_RGB_ORDER])) - cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) - cg.add(var.set_is_wrgb(config[CONF_IS_WRGB])) + cg.add( + var.set_channel_colors(light.channel_colors_struct(config[CONF_CHANNEL_COLORS])) + ) cg.add(var.set_use_psram(config[CONF_USE_PSRAM])) cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) if CONF_USE_DMA in config: diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index b5b3d7c905..175f5b43cf 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -1,9 +1,12 @@ +from collections.abc import Callable from dataclasses import dataclass, field import enum +import logging import esphome.automation as auto import esphome.codegen as cg from esphome.components import mqtt, power_supply, web_server +from esphome.components.const import CONF_CHANNEL_COLORS, CONF_IS_WRGB import esphome.config_validation as cv from esphome.const import ( CONF_BLUE, @@ -23,6 +26,7 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_INITIAL_STATE, + CONF_IS_RGBW, CONF_MQTT_ID, CONF_NAME, CONF_ON_STATE, @@ -32,6 +36,7 @@ from esphome.const import ( CONF_POWER_SUPPLY, CONF_RED, CONF_RESTORE_MODE, + CONF_RGB_ORDER, CONF_STATE, CONF_TRIGGER_ID, CONF_WARM_WHITE, @@ -61,6 +66,7 @@ from .effects import ( from .types import ( # noqa: F401 AddressableLight, AddressableLightState, + ChannelColors, ColorMode, LightOutput, LightState, @@ -71,6 +77,8 @@ from .types import ( # noqa: F401 light_ns, ) +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -165,6 +173,104 @@ def available_effects_str(effects: list) -> str: return ", ".join(f"'{name}'" for name in available) if available else "none" +# Accepted values of the deprecated `rgb_order` key. +RGB_ORDERS = ("RGB", "RBG", "GRB", "GBR", "BGR", "BRG") + +_RGB_CHANNELS = frozenset("RGB") +_RGBW_CHANNELS = frozenset("RGBW") + + +def validate_channel_colors(value: str) -> str: + """Validate the channel order of an addressable strip, e.g. "GRB" or "WRGB".""" + value = cv.string_strict(value).upper() + channels = frozenset(value) + if len(channels) != len(value) or channels not in (_RGB_CHANNELS, _RGBW_CHANNELS): + raise cv.Invalid( + f"'{value}' is not a valid channel order. List each of R, G and B exactly " + "once, optionally with a single W, in the order the strip expects them " + "(for example GRB, GRBW or WRGB)" + ) + return value + + +def channel_colors_struct(value: str) -> cg.StructInitializer: + """Build the C++ `light::ChannelColors` for a validated channel order string.""" + return cg.StructInitializer( + ChannelColors, + ("r", value.index("R")), + ("g", value.index("G")), + ("b", value.index("B")), + ( + "w", + value.index("W") + if "W" in value + else cg.RawExpression(f"{ChannelColors}::NO_WHITE"), + ), + ) + + +def _quote_and_join(keys: list[str]) -> str: + """Quote each key and join them into a readable list, e.g. "'a', 'b' and 'c'".""" + quoted = [f"'{key}'" for key in keys] + if len(quoted) == 1: + return quoted[0] + return f"{', '.join(quoted[:-1])} and {quoted[-1]}" + + +def migrate_channel_colors( + *, removed_in: str, component: str +) -> Callable[[ConfigType], ConfigType]: + """Fold the deprecated `rgb_order`, `is_rgbw` and `is_wrgb` keys into `channel_colors`. + + This also enforces that `channel_colors` is set, which the schema cannot do on its + own while the deprecated keys are still accepted. After this runs, `to_code` only + ever sees `channel_colors`. + """ + + def validator(config: ConfigType) -> ConfigType: + config = config.copy() + deprecated = [ + key for key in (CONF_RGB_ORDER, CONF_IS_RGBW, CONF_IS_WRGB) if key in config + ] + if CONF_CHANNEL_COLORS in config: + if deprecated: + raise cv.Invalid( + f"'{CONF_CHANNEL_COLORS}' cannot be combined with " + f"{_quote_and_join(deprecated)}" + ) + return config + if CONF_RGB_ORDER not in config: + raise cv.Invalid( + f"'{CONF_CHANNEL_COLORS}' is required", path=[CONF_CHANNEL_COLORS] + ) + rgb_order = config.pop(CONF_RGB_ORDER) + is_rgbw = config.pop(CONF_IS_RGBW, False) + is_wrgb = config.pop(CONF_IS_WRGB, False) + if is_rgbw and is_wrgb: + raise cv.Invalid( + f"'{CONF_IS_RGBW}' and '{CONF_IS_WRGB}' cannot both be enabled" + ) + if is_wrgb: + channel_colors = f"W{rgb_order}" + elif is_rgbw: + channel_colors = f"{rgb_order}W" + else: + channel_colors = rgb_order + _LOGGER.warning( + "[%s] %s %s deprecated, use '%s: %s'. Will be removed in %s", + component, + _quote_and_join(deprecated), + "are" if len(deprecated) > 1 else "is", + CONF_CHANNEL_COLORS, + channel_colors, + removed_in, + ) + config[CONF_CHANNEL_COLORS] = channel_colors + return config + + return validator + + def _final_validate(config: ConfigType) -> None: """Validate all recorded effect name references against their target lights. diff --git a/esphome/components/light/channel_colors.h b/esphome/components/light/channel_colors.h new file mode 100644 index 0000000000..9d8f46d575 --- /dev/null +++ b/esphome/components/light/channel_colors.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +namespace esphome::light { + +/// Which byte of an addressable LED's data carries each colour. +/// +/// Built from a configuration string such as "GRB" or "WRGB": every field holds the +/// position that colour occupies in the bytes the strip expects. `w` is NO_WHITE when +/// the strip has no separate white channel. +struct ChannelColors { + /// Value of `w` for a strip that only has red, green and blue channels. + static constexpr uint8_t NO_WHITE = 0xFF; + + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t w; + + bool has_white() const { return this->w != NO_WHITE; } + + uint8_t bytes_per_led() const { return this->has_white() ? 4 : 3; } + + /// Write the order back out as text, e.g. "GRBW". + /// + /// `buf` must have room for at least 5 characters. Returns `buf` so the result can be + /// passed straight to a log call. + const char *to_string(char *buf) const { + buf[this->r] = 'R'; + buf[this->g] = 'G'; + buf[this->b] = 'B'; + if (this->has_white()) { + buf[this->w] = 'W'; + } + buf[this->bytes_per_led()] = '\0'; + return buf; + } +}; + +} // namespace esphome::light diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index 9c1c7331d1..1778aa8410 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -16,6 +16,9 @@ LightColorValues = light_ns.class_("LightColorValues") LightStateRTCState = light_ns.struct("LightStateRTCState") LightCall = light_ns.class_("LightCall") +# Addressable strips +ChannelColors = light_ns.struct("ChannelColors") + # Color modes ColorMode = light_ns.enum("ColorMode", is_class=True) COLOR_MODES = { diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index cf7041931e..1f4bea9ecd 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -107,10 +107,10 @@ void RP2040PIOLEDStripLightOutput::setup() { pio_get_dreq(this->pio_, this->sm_, true)); // set the DREQ to the state machine's TX FIFO dma_channel_configure(this->dma_chan_, &this->dma_config_, - &this->pio_->txf[this->sm_], // write to the state machine's TX FIFO - this->buf_, // read from memory - this->is_rgbw_ ? num_leds_ * 4 : num_leds_ * 3, // number of bytes to transfer - false // don't start yet + &this->pio_->txf[this->sm_], // write to the state machine's TX FIFO + this->buf_, // read from memory + this->get_buffer_size_(), // number of bytes to transfer + false // don't start yet ); // Initialize the semaphore for this DMA channel @@ -142,58 +142,25 @@ void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) { } light::ESPColorView RP2040PIOLEDStripLightOutput::get_view_internal(int32_t index) const { - int32_t r = 0, g = 0, b = 0; - switch (this->rgb_order_) { - case ORDER_RGB: - r = 0; - g = 1; - b = 2; - break; - case ORDER_RBG: - r = 0; - g = 2; - b = 1; - break; - case ORDER_GRB: - r = 1; - g = 0; - b = 2; - break; - case ORDER_GBR: - r = 2; - g = 0; - b = 1; - break; - case ORDER_BGR: - r = 2; - g = 1; - b = 0; - break; - case ORDER_BRG: - r = 1; - g = 2; - b = 0; - break; - } - uint8_t multiplier = this->is_rgbw_ ? 4 : 3; - return {this->buf_ + (index * multiplier) + r, - this->buf_ + (index * multiplier) + g, - this->buf_ + (index * multiplier) + b, - this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr, + const light::ChannelColors &colors = this->channel_colors_; + uint8_t *led = this->buf_ + (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_}; } void RP2040PIOLEDStripLightOutput::dump_config() { + char channel_colors[5]; ESP_LOGCONFIG(TAG, "RP2040 PIO LED Strip Light Output:\n" " Pin: GPIO%d\n" " Number of LEDs: %d\n" - " RGBW: %s\n" - " RGB Order: %s\n" + " Channel colors: %s\n" " Max Refresh Rate: %f Hz", - this->pin_, this->num_leds_, YESNO(this->is_rgbw_), rgb_order_to_string(this->rgb_order_), - this->max_refresh_rate_); + this->pin_, this->num_leds_, this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_); } float RP2040PIOLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.h b/esphome/components/rp2040_pio_led_strip/led_strip.h index c499f0a7ca..b2162f641d 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.h +++ b/esphome/components/rp2040_pio_led_strip/led_strip.h @@ -7,6 +7,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/light/addressable_light.h" +#include "esphome/components/light/channel_colors.h" #include "esphome/components/light/light_output.h" #include @@ -18,15 +19,6 @@ namespace esphome::rp2040_pio_led_strip { -enum RGBOrder : uint8_t { - ORDER_RGB, - ORDER_RBG, - ORDER_GRB, - ORDER_GBR, - ORDER_BGR, - ORDER_BRG, -}; - enum Chipset : uint8_t { CHIPSET_WS2812, CHIPSET_WS2812B, @@ -36,25 +28,6 @@ enum Chipset : uint8_t { CHIPSET_CUSTOM = 0xFF, }; -inline const char *rgb_order_to_string(RGBOrder order) { - switch (order) { - case ORDER_RGB: - return "RGB"; - case ORDER_RBG: - return "RBG"; - case ORDER_GRB: - return "GRB"; - case ORDER_GBR: - return "GBR"; - case ORDER_BGR: - return "BGR"; - case ORDER_BRG: - return "BRG"; - default: - return "UNKNOWN"; - } -} - using init_fn = void (*)(PIO pio, uint sm, uint offset, uint pin, float freq); class RP2040PIOLEDStripLightOutput final : public light::AddressableLight { @@ -66,13 +39,14 @@ class RP2040PIOLEDStripLightOutput final : public light::AddressableLight { int32_t size() const override { return this->num_leds_; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - this->is_rgbw_ ? traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE}) - : traits.set_supported_color_modes({light::ColorMode::RGB}); + this->channel_colors_.has_white() + ? traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE}) + : traits.set_supported_color_modes({light::ColorMode::RGB}); return traits; } void set_pin(uint8_t pin) { this->pin_ = pin; } void set_num_leds(uint32_t num_leds) { this->num_leds_ = num_leds; } - void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } + void set_channel_colors(light::ChannelColors channel_colors) { this->channel_colors_ = channel_colors; } void set_max_refresh_rate(float interval_us) { this->max_refresh_rate_ = interval_us; } @@ -81,7 +55,6 @@ class RP2040PIOLEDStripLightOutput final : public light::AddressableLight { void set_init_function(init_fn init) { this->init_ = init; } void set_chipset(Chipset chipset) { this->chipset_ = chipset; }; - void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } void clear_effect_data() override { for (int i = 0; i < this->size(); i++) { this->effect_data_[i] = 0; @@ -93,7 +66,7 @@ class RP2040PIOLEDStripLightOutput final : public light::AddressableLight { protected: light::ESPColorView get_view_internal(int32_t index) const override; - size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } + size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); } static void dma_write_complete_handler(); @@ -102,14 +75,13 @@ class RP2040PIOLEDStripLightOutput final : public light::AddressableLight { uint8_t pin_; uint32_t num_leds_; - bool is_rgbw_; pio_hw_t *pio_; uint sm_; uint dma_chan_; dma_channel_config dma_config_; - RGBOrder rgb_order_{ORDER_RGB}; + light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE}; Chipset chipset_{CHIPSET_CUSTOM}; uint32_t last_refresh_{0}; diff --git a/esphome/components/rp2040_pio_led_strip/light.py b/esphome/components/rp2040_pio_led_strip/light.py index b3f816102a..9f7479edd0 100644 --- a/esphome/components/rp2040_pio_led_strip/light.py +++ b/esphome/components/rp2040_pio_led_strip/light.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from esphome import pins import esphome.codegen as cg from esphome.components import light, rp2 +from esphome.components.const import CONF_CHANNEL_COLORS import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, @@ -13,6 +14,7 @@ from esphome.const import ( CONF_PIN, CONF_RGB_ORDER, ) +from esphome.types import ConfigType from esphome.util import _LOGGER @@ -37,7 +39,7 @@ def get_nops(timing): return nops -def generate_assembly_code(id, rgbw, t0h, t0l, t1h, t1l): +def generate_assembly_code(id, t0h, t0l, t1h, t1l): """ Generate assembly code with the given timing values. """ @@ -139,8 +141,6 @@ RP2040PIOLEDStripLightOutput = rp2040_pio_led_strip_ns.class_( "RP2040PIOLEDStripLightOutput", light.AddressableLight ) -RGBOrder = rp2040_pio_led_strip_ns.enum("RGBOrder") - Chipset = rp2040_pio_led_strip_ns.enum("Chipset") CHIPSETS = { @@ -159,15 +159,6 @@ class LEDStripTimings: T1L: int -RGB_ORDERS = { - "RGB": RGBOrder.ORDER_RGB, - "RBG": RGBOrder.ORDER_RBG, - "GRB": RGBOrder.ORDER_GRB, - "GBR": RGBOrder.ORDER_GBR, - "BGR": RGBOrder.ORDER_BGR, - "BRG": RGBOrder.ORDER_BRG, -} - CHIPSET_TIMINGS = { "WS2812": LEDStripTimings(20, 40, 46, 34), "WS2812B": LEDStripTimings(23, 49, 46, 26), @@ -199,10 +190,12 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RP2040PIOLEDStripLightOutput), cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, - cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), + cv.Optional(CONF_CHANNEL_COLORS): light.validate_channel_colors, + # Deprecated in favour of CONF_CHANNEL_COLORS, remove in 2027.3.0 + cv.Optional(CONF_RGB_ORDER): cv.one_of(*light.RGB_ORDERS, upper=True), + cv.Optional(CONF_IS_RGBW): cv.boolean, cv.Required(CONF_PIO): cv.one_of(0, 1, int=True), cv.Optional(CONF_CHIPSET): cv.enum(CHIPSETS, upper=True), - cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, cv.Inclusive( CONF_BIT0_HIGH, "custom", @@ -222,10 +215,13 @@ CONFIG_SCHEMA = cv.All( } ), cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH), + light.migrate_channel_colors( + removed_in="2027.3.0", component="rp2040_pio_led_strip" + ), ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) id = config[CONF_ID].id await light.register_light(var, config) @@ -234,8 +230,9 @@ async def to_code(config): cg.add(var.set_num_leds(config[CONF_NUM_LEDS])) cg.add(var.set_pin(config[CONF_PIN])) - cg.add(var.set_rgb_order(config[CONF_RGB_ORDER])) - cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) + cg.add( + var.set_channel_colors(light.channel_colors_struct(config[CONF_CHANNEL_COLORS])) + ) cg.add(var.set_pio(config[CONF_PIO])) cg.add(var.set_program(cg.RawExpression(f"&rp2040_pio_led_strip_{id}_program"))) @@ -255,7 +252,6 @@ async def to_code(config): key, generate_assembly_code( id, - config[CONF_IS_RGBW], CHIPSET_TIMINGS[chipset].T0H, CHIPSET_TIMINGS[chipset].T0L, CHIPSET_TIMINGS[chipset].T1H, @@ -270,7 +266,6 @@ async def to_code(config): key, generate_assembly_code( id, - config[CONF_IS_RGBW], time_to_cycles(config[CONF_BIT0_HIGH]), time_to_cycles(config[CONF_BIT0_LOW]), time_to_cycles(config[CONF_BIT1_HIGH]), diff --git a/tests/components/addressable_light/common-ard-esp32_rmt_led_strip.yaml b/tests/components/addressable_light/common-ard-esp32_rmt_led_strip.yaml index a071f9df91..d21c4b61b9 100644 --- a/tests/components/addressable_light/common-ard-esp32_rmt_led_strip.yaml +++ b/tests/components/addressable_light/common-ard-esp32_rmt_led_strip.yaml @@ -3,7 +3,7 @@ light: id: led_matrix_32x8 default_transition_length: 500ms chipset: ws2812 - rgb_order: GRB + channel_colors: GRB num_leds: 256 pin: ${pin} diff --git a/tests/components/addressable_light/common-idf-esp32_rmt_led_strip.yaml b/tests/components/addressable_light/common-idf-esp32_rmt_led_strip.yaml index a071f9df91..d21c4b61b9 100644 --- a/tests/components/addressable_light/common-idf-esp32_rmt_led_strip.yaml +++ b/tests/components/addressable_light/common-idf-esp32_rmt_led_strip.yaml @@ -3,7 +3,7 @@ light: id: led_matrix_32x8 default_transition_length: 500ms chipset: ws2812 - rgb_order: GRB + channel_colors: GRB num_leds: 256 pin: ${pin} diff --git a/tests/components/beken_spi_led_strip/test.bk72xx-ard.yaml b/tests/components/beken_spi_led_strip/test.bk72xx-ard.yaml index 15409caeaf..2bb831848c 100644 --- a/tests/components/beken_spi_led_strip/test.bk72xx-ard.yaml +++ b/tests/components/beken_spi_led_strip/test.bk72xx-ard.yaml @@ -1,6 +1,6 @@ light: - platform: beken_spi_led_strip - rgb_order: GRB + channel_colors: GRB pin: P16 num_leds: 30 chipset: ws2812 diff --git a/tests/components/beken_spi_led_strip/validate-legacy.bk72xx-ard.yaml b/tests/components/beken_spi_led_strip/validate-legacy.bk72xx-ard.yaml new file mode 100644 index 0000000000..3ca78398c3 --- /dev/null +++ b/tests/components/beken_spi_led_strip/validate-legacy.bk72xx-ard.yaml @@ -0,0 +1,10 @@ +# The deprecated rgb_order / is_rgbw / is_wrgb keys, kept working until 2027.3.0. +# Config-only, and only one strip because P16 is the sole supported pin. +light: + - platform: beken_spi_led_strip + name: Legacy RGBW + pin: P16 + num_leds: 30 + chipset: sk6812 + rgb_order: GRB + is_rgbw: true # -> GRBW diff --git a/tests/components/e131/common-ard.yaml b/tests/components/e131/common-ard.yaml index 8300dbb01b..48ccafc2d2 100644 --- a/tests/components/e131/common-ard.yaml +++ b/tests/components/e131/common-ard.yaml @@ -5,7 +5,7 @@ light: id: led_matrix_32x8 default_transition_length: 500ms chipset: ws2812 - rgb_order: GRB + channel_colors: GRB num_leds: 256 pin: ${pin} effects: diff --git a/tests/components/e131/common-idf.yaml b/tests/components/e131/common-idf.yaml index 8300dbb01b..48ccafc2d2 100644 --- a/tests/components/e131/common-idf.yaml +++ b/tests/components/e131/common-idf.yaml @@ -5,7 +5,7 @@ light: id: led_matrix_32x8 default_transition_length: 500ms chipset: ws2812 - rgb_order: GRB + channel_colors: GRB num_leds: 256 pin: ${pin} effects: diff --git a/tests/components/e131/test.rp2040-ard.yaml b/tests/components/e131/test.rp2040-ard.yaml index 4593784ef9..89255e2d87 100644 --- a/tests/components/e131/test.rp2040-ard.yaml +++ b/tests/components/e131/test.rp2040-ard.yaml @@ -6,7 +6,7 @@ light: pin: 2 pio: 0 num_leds: 256 - rgb_order: GRB + channel_colors: GRB chipset: WS2812 effects: - e131: diff --git a/tests/components/esp32_rmt_led_strip/common.yaml b/tests/components/esp32_rmt_led_strip/common.yaml index 701e513ebd..7f52d32229 100644 --- a/tests/components/esp32_rmt_led_strip/common.yaml +++ b/tests/components/esp32_rmt_led_strip/common.yaml @@ -3,13 +3,13 @@ light: id: led_strip1 pin: ${pin1} num_leds: 60 - rgb_order: GRB + channel_colors: GRB chipset: ws2812 - platform: esp32_rmt_led_strip id: led_strip2 pin: ${pin2} num_leds: 60 - rgbw_order: RWGB + channel_colors: RWGB bit0_high: 100us bit0_low: 100us bit1_high: 100us diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml index 6bf0639a52..132966eddf 100644 --- a/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml +++ b/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml @@ -8,14 +8,14 @@ light: id: led_strip1 pin: ${pin1} num_leds: 60 - rgb_order: GRB + channel_colors: GRB chipset: ws2812 use_dma: "true" - platform: esp32_rmt_led_strip id: led_strip2 pin: ${pin2} num_leds: 60 - rgb_order: RGB + channel_colors: RGB bit0_high: 100us bit0_low: 100us bit1_high: 100us diff --git a/tests/components/esp32_rmt_led_strip/validate-legacy.esp32-idf.yaml b/tests/components/esp32_rmt_led_strip/validate-legacy.esp32-idf.yaml new file mode 100644 index 0000000000..6dd1bcdad3 --- /dev/null +++ b/tests/components/esp32_rmt_led_strip/validate-legacy.esp32-idf.yaml @@ -0,0 +1,23 @@ +# The deprecated rgb_order / is_rgbw / is_wrgb keys, kept working until 2027.3.0. +# Config-only: each strip below must migrate to the channel_colors shown in the comment. +light: + - platform: esp32_rmt_led_strip + id: legacy_rgb + pin: GPIO13 + num_leds: 60 + chipset: ws2812 + rgb_order: GRB # -> GRB + - platform: esp32_rmt_led_strip + id: legacy_rgbw + pin: GPIO14 + num_leds: 60 + chipset: sk6812 + rgb_order: GRB + is_rgbw: true # -> GRBW + - platform: esp32_rmt_led_strip + id: legacy_wrgb + pin: GPIO15 + num_leds: 60 + chipset: sk6812 + rgb_order: GRB + is_wrgb: true # -> WGRB diff --git a/tests/components/partition/common-ard.yaml b/tests/components/partition/common-ard.yaml index b2ceadd6f7..8d39670e32 100644 --- a/tests/components/partition/common-ard.yaml +++ b/tests/components/partition/common-ard.yaml @@ -4,7 +4,7 @@ light: default_transition_length: 500ms chipset: ws2812 num_leds: 256 - rgb_order: GRB + channel_colors: GRB pin: ${pin} - platform: partition name: Partition Light diff --git a/tests/components/partition/common-idf.yaml b/tests/components/partition/common-idf.yaml index b2ceadd6f7..8d39670e32 100644 --- a/tests/components/partition/common-idf.yaml +++ b/tests/components/partition/common-idf.yaml @@ -4,7 +4,7 @@ light: default_transition_length: 500ms chipset: ws2812 num_leds: 256 - rgb_order: GRB + channel_colors: GRB pin: ${pin} - platform: partition name: Partition Light diff --git a/tests/components/rp2040_pio_led_strip/common.yaml b/tests/components/rp2040_pio_led_strip/common.yaml index 254ac0e13d..1cb5fe0737 100644 --- a/tests/components/rp2040_pio_led_strip/common.yaml +++ b/tests/components/rp2040_pio_led_strip/common.yaml @@ -4,14 +4,14 @@ light: pin: 4 num_leds: 60 pio: 0 - rgb_order: GRB + channel_colors: GRB chipset: WS2812 - platform: rp2040_pio_led_strip id: led_strip_custom_timings pin: 5 num_leds: 60 pio: 1 - rgb_order: GRB + channel_colors: GRB bit0_high: .1us bit0_low: 1.2us bit1_high: .69us diff --git a/tests/components/rp2040_pio_led_strip/validate-legacy.rp2040-ard.yaml b/tests/components/rp2040_pio_led_strip/validate-legacy.rp2040-ard.yaml new file mode 100644 index 0000000000..2ab124393b --- /dev/null +++ b/tests/components/rp2040_pio_led_strip/validate-legacy.rp2040-ard.yaml @@ -0,0 +1,18 @@ +# The deprecated rgb_order / is_rgbw keys, kept working until 2027.3.0. +# Config-only: each strip below must migrate to the channel_colors shown in the comment. +light: + - platform: rp2040_pio_led_strip + id: legacy_rgb + pin: 4 + num_leds: 60 + pio: 0 + chipset: WS2812 + rgb_order: GRB # -> GRB + - platform: rp2040_pio_led_strip + id: legacy_rgbw + pin: 5 + num_leds: 60 + pio: 1 + chipset: SK6812 + rgb_order: GRB + is_rgbw: true # -> GRBW diff --git a/tests/components/wled/test.esp32-ard.yaml b/tests/components/wled/test.esp32-ard.yaml index 156b31181e..ecab767812 100644 --- a/tests/components/wled/test.esp32-ard.yaml +++ b/tests/components/wled/test.esp32-ard.yaml @@ -9,7 +9,7 @@ light: id: led_matrix_32x8 default_transition_length: 500ms chipset: ws2812 - rgb_order: GRB + channel_colors: GRB num_leds: 256 pin: 2 effects: diff --git a/tests/unit_tests/components/light/test_channel_colors.py b/tests/unit_tests/components/light/test_channel_colors.py new file mode 100644 index 0000000000..0c129a8bb2 --- /dev/null +++ b/tests/unit_tests/components/light/test_channel_colors.py @@ -0,0 +1,144 @@ +"""Tests for the shared addressable-strip channel order helpers.""" + +import logging + +import pytest + +from esphome.components.const import CONF_CHANNEL_COLORS, CONF_IS_WRGB +from esphome.components.light import ( + channel_colors_struct, + migrate_channel_colors, + validate_channel_colors, +) +import esphome.config_validation as cv +from esphome.const import CONF_IS_RGBW, CONF_RGB_ORDER +from esphome.types import ConfigType + +NO_WHITE = "light::ChannelColors::NO_WHITE" + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("RGB", "RGB"), + ("grb", "GRB"), + ("BRG", "BRG"), + ("rgbw", "RGBW"), + ("WRGB", "WRGB"), + ("GWRB", "GWRB"), + ], +) +def test_validate_channel_colors(value: str, expected: str) -> None: + assert validate_channel_colors(value) == expected + + +@pytest.mark.parametrize( + "value", + [ + "RG", # missing a channel + "RGBB", # duplicate channel + "RRGB", # duplicate channel, correct length + "RGBWW", # two white channels + "RGBX", # unknown channel + "RGBWX", # unknown channel, correct length + "", + ], +) +def test_validate_channel_colors_rejects_invalid(value: str) -> None: + with pytest.raises(cv.Invalid, match="is not a valid channel order"): + validate_channel_colors(value) + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("RGB", (0, 1, 2, NO_WHITE)), + ("GRB", (1, 0, 2, NO_WHITE)), + ("BRG", (1, 2, 0, NO_WHITE)), + ("RGBW", (0, 1, 2, 3)), + ("GRBW", (1, 0, 2, 3)), + ("WRGB", (1, 2, 3, 0)), + ("GWRB", (2, 0, 3, 1)), + ], +) +def test_channel_colors_struct(value: str, expected: tuple[int, int, int, int]) -> None: + struct = channel_colors_struct(value) + assert str(struct.base) == "light::ChannelColors" + assert tuple(str(arg) for arg in struct.args.values()) == tuple( + str(field) for field in expected + ) + + +def _migrate(config: ConfigType) -> ConfigType: + return migrate_channel_colors(removed_in="2027.3.0", component="test_strip")(config) + + +def test_migrate_passes_through_channel_colors() -> None: + config = {CONF_CHANNEL_COLORS: "GRBW"} + assert _migrate(config) == {CONF_CHANNEL_COLORS: "GRBW"} + + +@pytest.mark.parametrize( + ("deprecated", "expected", "named"), + [ + ({}, "GRB", "'rgb_order' is"), + ( + {CONF_IS_RGBW: False, CONF_IS_WRGB: False}, + "GRB", + "'rgb_order', 'is_rgbw' and 'is_wrgb' are", + ), + ({CONF_IS_RGBW: True}, "GRBW", "'rgb_order' and 'is_rgbw' are"), + ({CONF_IS_WRGB: True}, "WGRB", "'rgb_order' and 'is_wrgb' are"), + ], +) +def test_migrate_folds_deprecated_keys( + deprecated: ConfigType, + expected: str, + named: str, + caplog: pytest.LogCaptureFixture, +) -> None: + config = {CONF_RGB_ORDER: "GRB", "num_leds": 1, **deprecated} + with caplog.at_level(logging.WARNING): + result = _migrate(config) + + assert result == {CONF_CHANNEL_COLORS: expected, "num_leds": 1} + assert f"[test_strip] {named} deprecated" in caplog.text + assert f"'{CONF_CHANNEL_COLORS}: {expected}'" in caplog.text + assert "2027.3.0" in caplog.text + + +def test_migrate_does_not_mutate_input() -> None: + config = {CONF_RGB_ORDER: "GRB", CONF_IS_RGBW: True} + _migrate(config) + assert config == {CONF_RGB_ORDER: "GRB", CONF_IS_RGBW: True} + + +@pytest.mark.parametrize("deprecated", [CONF_RGB_ORDER, CONF_IS_RGBW, CONF_IS_WRGB]) +def test_migrate_rejects_mixing_old_and_new(deprecated: str) -> None: + config = {CONF_CHANNEL_COLORS: "GRBW", deprecated: "GRB"} + with pytest.raises(cv.Invalid, match=f"cannot be combined with '{deprecated}'"): + _migrate(config) + + +def test_migrate_reports_every_conflicting_key() -> None: + config = { + CONF_CHANNEL_COLORS: "GRBW", + CONF_RGB_ORDER: "GRB", + CONF_IS_RGBW: True, + CONF_IS_WRGB: False, + } + with pytest.raises( + cv.Invalid, match="cannot be combined with 'rgb_order', 'is_rgbw' and 'is_wrgb'" + ): + _migrate(config) + + +def test_migrate_requires_channel_colors() -> None: + with pytest.raises(cv.Invalid, match=f"'{CONF_CHANNEL_COLORS}' is required"): + _migrate({"num_leds": 1}) + + +def test_migrate_rejects_is_rgbw_with_is_wrgb() -> None: + config = {CONF_RGB_ORDER: "GRB", CONF_IS_RGBW: True, CONF_IS_WRGB: True} + with pytest.raises(cv.Invalid, match="cannot both be enabled"): + _migrate(config) diff --git a/tests/unit_tests/components/test_esp32_rmt_led_strip.py b/tests/unit_tests/components/test_esp32_rmt_led_strip.py deleted file mode 100644 index e2cb513e3b..0000000000 --- a/tests/unit_tests/components/test_esp32_rmt_led_strip.py +++ /dev/null @@ -1,57 +0,0 @@ -import pytest - -from esphome.components.esp32_rmt_led_strip.light import ( - CONF_IS_WRGB, - CONF_RGBW_ORDER, - _split_rgbw_order, - _validate_rgbw_order, - _validate_rgbw_order_exclusivity, -) -import esphome.config_validation as cv -from esphome.const import CONF_IS_RGBW - - -def test_validate_rgbw_order() -> None: - assert _validate_rgbw_order("rwgb") == "RWGB" - - -@pytest.mark.parametrize("rgbw_order", ["RGB", "RRGB", "RGBWW"]) -def test_validate_rgbw_order_rejects_invalid_order(rgbw_order: str) -> None: - with pytest.raises(cv.Invalid, match="permutation of RGBW"): - _validate_rgbw_order(rgbw_order) - - -@pytest.mark.parametrize( - ("rgbw_order", "expected"), - [ - ("WRGB", ("RGB", 0)), - ("RWGB", ("RGB", 1)), - ("GWRB", ("GRB", 1)), - ("RGBW", ("RGB", 3)), - ], -) -def test_split_rgbw_order(rgbw_order: str, expected: tuple[str, int]) -> None: - assert _split_rgbw_order(rgbw_order) == expected - - -@pytest.mark.parametrize("conflict", [CONF_IS_RGBW, CONF_IS_WRGB]) -def test_rgbw_order_is_mutually_exclusive(conflict: str) -> None: - with pytest.raises(cv.Invalid, match="cannot be used with"): - _validate_rgbw_order_exclusivity( - { - CONF_RGBW_ORDER: "RGBW", - CONF_IS_RGBW: conflict == CONF_IS_RGBW, - CONF_IS_WRGB: conflict == CONF_IS_WRGB, - } - ) - - -@pytest.mark.parametrize("legacy_option", [CONF_IS_RGBW, CONF_IS_WRGB]) -def test_rgbw_order_allows_disabled_legacy_options(legacy_option: str) -> None: - config = { - CONF_RGBW_ORDER: "RGBW", - CONF_IS_RGBW: False, - CONF_IS_WRGB: False, - } - config[legacy_option] = False - assert _validate_rgbw_order_exclusivity(config) is config