diff --git a/esphome/codegen.py b/esphome/codegen.py index daf53db43fd..ec1ac161875 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -45,6 +45,7 @@ from esphome.cpp_generator import ( # noqa: F401 static_const_array, static_function, templatable, + unique_global_id, variable, with_local_variable, ) diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index 0b82a7d9b90..9bc60d79a7d 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -1,6 +1,5 @@ from esphome import automation import esphome.codegen as cg -from esphome.config import iter_ids import esphome.config_validation as cv from esphome.const import ( CONF_ALPHA, @@ -30,9 +29,8 @@ from esphome.const import ( CONF_WHITE, CONF_WIDTH, ) -from esphome.core import CORE, ID +from esphome.core import CORE from esphome.cpp_generator import MockObjClass -from esphome.helpers import ensure_unique_string from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import ConfigType from esphome.util import Registry @@ -494,7 +492,7 @@ async def addressable_color_wipe_effect_to_code(config, effect_id): int(round(color[CONF_BLUE] * color_brightness * 255)), int(round(color[CONF_WHITE] * 255)), ) - random_slot = NO_RANDOM_SLOT + random_slot = cg.RawExpression("light::NO_RANDOM_SLOT") if color[CONF_RANDOM]: random_slot = len(random_colors) random_colors.append(rgbw) @@ -518,14 +516,7 @@ async def addressable_color_wipe_effect_to_code(config, effect_id): random_storage = cg.nullptr if random_colors: # One RAM slot per random entry; it starts at the configured color, as before - used = {str(i) for i, _ in iter_ids(CORE.config)} | { - str(i) for i in CORE.variables - } - id_ = ID( - ensure_unique_string(f"{effect_id.id}_random_colors", used), - is_declaration=True, - type=Color, - ) + id_ = cg.unique_global_id(f"{effect_id.id}_random_colors", Color) values = cg.ArrayInitializer( *(Color(r, g, b, w) for r, g, b, w in random_colors) ) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index a5ffaefe0be..76d1db8d110 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -468,6 +468,17 @@ def progmem_array(id_, rhs) -> "MockObj": return obj +def unique_global_id(name: str, type_: "MockObjClass") -> ID: + """A declaration ID for a generated global, unique against config ids, variables and reserved names.""" + from esphome.config import iter_ids + from esphome.config_validation import RESERVED_IDS + + used = {str(i) for i, _ in iter_ids(CORE.config)} + used |= {str(i) for i in CORE.variables} + used |= set(RESERVED_IDS) | CORE.loaded_integrations + return ID(ensure_unique_string(name, used), is_declaration=True, type=type_) + + def shared_progmem_array( name: str, type_: "MockObjClass", rhs: SafeExpType, *, share: bool = True ) -> "MockObj": @@ -477,18 +488,12 @@ def shared_progmem_array( must be captureless. Its name is made unique against every config id and variable. ``share=False`` always emits a new array, e.g. for lambdas that may keep static state. """ - from esphome.config import iter_ids - from esphome.config_validation import RESERVED_IDS - arrays: dict[str, MockObj] = CORE.data.setdefault("shared_progmem_array", {}) rhs = safe_exp(rhs) key = f"{type_} {rhs}" if share and (array := arrays.get(key)) is not None: return array - used = {str(i) for i, _ in iter_ids(CORE.config)} - used |= {str(i) for i in CORE.variables} - used |= set(RESERVED_IDS) | CORE.loaded_integrations - id_ = ID(ensure_unique_string(name, used), is_declaration=True, type=type_) + id_ = unique_global_id(name, type_) # Global, so any scope can use it; anything a lambda references is already declared. CORE.add_global(ProgmemAssignmentExpression(type_, id_, rhs)) array = MockObj(id_, ".") diff --git a/tests/component_tests/light/test_effect_color_tables.py b/tests/component_tests/light/test_effect_color_tables.py index 1f20519674f..cf422bb0a7a 100644 --- a/tests/component_tests/light/test_effect_color_tables.py +++ b/tests/component_tests/light/test_effect_color_tables.py @@ -6,6 +6,10 @@ from collections.abc import Callable from pathlib import Path import re +import pytest + +import esphome.config_validation as cv + def test_effect_colors_use_flash_tables( generate_main: Callable[[str | Path], str], @@ -26,4 +30,23 @@ def test_effect_colors_use_flash_tables( # Only the random entry gets a RAM slot, starting at its configured color assert re.search(r"static Color \w+_random_colors\[\] = \{Color\(", main_cpp) assert ".random_slot = 0," in main_cpp - assert ".random_slot = 255," in main_cpp + assert ".random_slot = light::NO_RANDOM_SLOT," in main_cpp + + +def _color_wipe_colors(colors: list[dict]) -> list[dict]: + from esphome.components.light.effects import EFFECTS_REGISTRY + + entry = EFFECTS_REGISTRY["addressable_color_wipe"] + return entry.schema({"name": "Wipe", "colors": colors})["colors"] + + +def test_color_wipe_requires_a_color() -> None: + with pytest.raises(cv.Invalid): + _color_wipe_colors([]) + + +def test_color_wipe_random_entry_limit() -> None: + random_color = {"random": True, "num_leds": 1} + assert len(_color_wipe_colors([random_color] * 254)) == 254 + with pytest.raises(cv.Invalid, match="At most 254 colors can be random"): + _color_wipe_colors([random_color] * 255)