From bcc56648c0482ca409b50d5deeee38c8f4cc7746 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 26 Jul 2025 18:56:35 -1000 Subject: [PATCH] [light] Reduce flash memory usage by optimizing validation and color mode logic (#9921) --- esphome/components/light/light_call.cpp | 66 +++++++++++++------------ tests/integration/test_light_calls.py | 63 +++++++++++++++++++++++ 2 files changed, 97 insertions(+), 32 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index a3ffe22591..1b856ad580 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -9,6 +9,11 @@ namespace light { static const char *const TAG = "light"; +// Helper function to reduce code size for validation warnings +static void log_validation_warning(const char *name, const char *param_name, float val, float min, float max) { + ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, param_name, val, min, max); +} + // Macro to reduce repetitive setter code #define IMPLEMENT_LIGHT_CALL_SETTER(name, type, flag) \ LightCall &LightCall::set_##name(optional(name)) { \ @@ -223,8 +228,7 @@ LightColorValues LightCall::validate_() { if (this->has_##name_()) { \ auto val = this->name_##_; \ if (val < (min) || val > (max)) { \ - ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_LITERAL(upper_name), val, \ - (min), (max)); \ + log_validation_warning(name, LOG_STR_LITERAL(upper_name), val, (min), (max)); \ this->name_##_ = clamp(val, (min), (max)); \ } \ } @@ -442,41 +446,39 @@ std::set LightCall::get_suitable_color_modes_() { bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) || (this->has_red() || this->has_green() || this->has_blue()); +// Build key from flags: [rgb][cwww][ct][white] #define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) -#define ENTRY(white, ct, cwww, rgb, ...) \ - std::make_tuple>(KEY(white, ct, cwww, rgb), __VA_ARGS__) - // Flag order: white, color temperature, cwww, rgb - std::array>, 10> lookup_table{ - ENTRY(true, false, false, false, - {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, - ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(false, true, false, false, - {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, - ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(true, true, false, false, - {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(false, false, true, false, {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(false, false, false, false, - {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, - ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}), - ENTRY(true, false, false, true, - {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(false, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(true, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(false, false, true, true, {ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(false, false, false, true, - {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), - }; + uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb); - auto key = KEY(has_white, has_ct, has_cwww, has_rgb); - for (auto &item : lookup_table) { - if (std::get<0>(item) == key) - return std::get<1>(item); + switch (key) { + case KEY(true, false, false, false): // white only + return {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(false, true, false, false): // ct only + return {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(true, true, false, false): // white + ct + return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(false, false, true, false): // cwww only + return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(false, false, false, false): // none + return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, + ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}; + case KEY(true, false, false, true): // rgb + white + return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(false, true, false, true): // rgb + ct + case KEY(true, true, false, true): // rgb + white + ct + return {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(false, false, true, true): // rgb + cwww + return {ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(false, false, false, true): // rgb only + return {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + default: + return {}; // conflicting flags } - // This happens if there are conflicting flags given. - return {}; +#undef KEY } LightCall &LightCall::set_effect(const std::string &effect) { diff --git a/tests/integration/test_light_calls.py b/tests/integration/test_light_calls.py index 1c56bbbf9e..1a0a9e553f 100644 --- a/tests/integration/test_light_calls.py +++ b/tests/integration/test_light_calls.py @@ -180,6 +180,69 @@ async def test_light_calls( state = await wait_for_state_change(rgb_light.key) assert state.state is False + # Test color mode combinations to verify get_suitable_color_modes optimization + + # Test 22: White only mode + client.light_command(key=rgbcw_light.key, state=True, white=0.5) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Test 23: Color temperature only mode + client.light_command(key=rgbcw_light.key, state=True, color_temperature=300) + state = await wait_for_state_change(rgbcw_light.key) + assert state.color_temperature == pytest.approx(300) + + # Test 24: Cold/warm white only mode + client.light_command( + key=rgbcw_light.key, state=True, cold_white=0.6, warm_white=0.4 + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.cold_white == pytest.approx(0.6) + assert state.warm_white == pytest.approx(0.4) + + # Test 25: RGB only mode + client.light_command(key=rgb_light.key, state=True, rgb=(0.5, 0.5, 0.5)) + state = await wait_for_state_change(rgb_light.key) + assert state.state is True + + # Test 26: RGB + white combination + client.light_command( + key=rgbcw_light.key, state=True, rgb=(0.3, 0.3, 0.3), white=0.5 + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Test 27: RGB + color temperature combination + client.light_command( + key=rgbcw_light.key, state=True, rgb=(0.4, 0.4, 0.4), color_temperature=280 + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Test 28: RGB + cold/warm white combination + client.light_command( + key=rgbcw_light.key, + state=True, + rgb=(0.2, 0.2, 0.2), + cold_white=0.5, + warm_white=0.5, + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Test 29: White + color temperature combination + client.light_command( + key=rgbcw_light.key, state=True, white=0.6, color_temperature=320 + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Test 30: No specific color parameters (tests default mode selection) + client.light_command(key=rgbcw_light.key, state=True, brightness=0.75) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + assert state.brightness == pytest.approx(0.75) + # Final cleanup - turn all lights off for light in lights: client.light_command(