diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index a3ffe22591..39b7bb50ed 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,40 @@ 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 + return {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + 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/script/ci-custom.py b/script/ci-custom.py index e726fcefc0..6f3c513f42 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -197,7 +197,7 @@ def lint_content_find_check(find, only_first=False, **kwargs): find_ = find(fname, content) errs = [] for line, col in find_all(content, find_): - err = func(fname) + err = func(fname, line, col, content) errs.append((line + 1, col + 1, err)) if only_first: break @@ -264,12 +264,12 @@ def lint_executable_bit(fname): "esphome/dashboard/static/ext-searchbox.js", ], ) -def lint_tabs(fname): +def lint_tabs(fname, line, col, content): return "File contains tab character. Please convert tabs to spaces." @lint_content_find_check("\r", only_first=True) -def lint_newline(fname): +def lint_newline(fname, line, col, content): return "File contains Windows newline. Please set your editor to Unix newline mode." @@ -512,7 +512,7 @@ def relative_cpp_search_text(fname, content): @lint_content_find_check(relative_cpp_search_text, include=["esphome/components/*.cpp"]) -def lint_relative_cpp_import(fname): +def lint_relative_cpp_import(fname, line, col, content): return ( "Component contains absolute import - Components must always use " "relative imports.\n" @@ -529,6 +529,20 @@ def relative_py_search_text(fname, content): return f"esphome.components.{integration}" +def convert_path_to_relative(abspath, current): + """Convert an absolute path to a relative import path.""" + if abspath == current: + return "." + absparts = abspath.split(".") + curparts = current.split(".") + uplen = len(curparts) + while absparts and curparts and absparts[0] == curparts[0]: + absparts.pop(0) + curparts.pop(0) + uplen -= 1 + return "." * uplen + ".".join(absparts) + + @lint_content_find_check( relative_py_search_text, include=["esphome/components/*.py"], @@ -537,14 +551,19 @@ def relative_py_search_text(fname, content): "esphome/components/web_server/__init__.py", ], ) -def lint_relative_py_import(fname): +def lint_relative_py_import(fname, line, col, content): + import_line = content.splitlines()[line] + abspath = import_line[col:].split(" ")[0] + current = fname.removesuffix(".py").replace(os.path.sep, ".") + replacement = convert_path_to_relative(abspath, current) + newline = import_line.replace(abspath, replacement) return ( "Component contains absolute import - Components must always use " "relative imports within the integration.\n" "Change:\n" - ' from esphome.components.abc import abc_ns"\n' + f" {import_line}\n" "to:\n" - " from . import abc_ns\n\n" + f" {newline}\n" ) @@ -588,7 +607,7 @@ def lint_namespace(fname, content): @lint_content_find_check('"esphome.h"', include=cpp_include, exclude=["tests/custom.h"]) -def lint_esphome_h(fname): +def lint_esphome_h(fname, line, col, content): return ( "File contains reference to 'esphome.h' - This file is " "auto-generated and should only be used for *custom* " @@ -679,7 +698,7 @@ def lint_trailing_whitespace(fname, match): "tests/custom.h", ], ) -def lint_log_in_header(fname): +def lint_log_in_header(fname, line, col, content): return ( "Found reference to ESP_LOG in header file. Using ESP_LOG* in header files " "is currently not possible - please move the definition to a source file (.cpp)" 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(