mirror of
https://github.com/esphome/esphome.git
synced 2026-09-29 07:50:21 +00:00
635 lines
22 KiB
Python
635 lines
22 KiB
Python
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
|
|
from esphome.config_helpers import filter_source_files_from_defines
|
|
import esphome.config_validation as cv
|
|
from esphome.const import (
|
|
CONF_COLD_WHITE_COLOR_TEMPERATURE,
|
|
CONF_COLOR_CORRECT,
|
|
CONF_DEFAULT_TRANSITION_LENGTH,
|
|
CONF_EFFECTS,
|
|
CONF_ENTITY_CATEGORY,
|
|
CONF_FLASH_TRANSITION_LENGTH,
|
|
CONF_GAMMA_CORRECT,
|
|
CONF_ICON,
|
|
CONF_ID,
|
|
CONF_INITIAL_STATE,
|
|
CONF_IS_RGBW,
|
|
CONF_MQTT_ID,
|
|
CONF_NAME,
|
|
CONF_ON_STATE,
|
|
CONF_ON_TURN_OFF,
|
|
CONF_ON_TURN_ON,
|
|
CONF_OUTPUT_ID,
|
|
CONF_POWER_SUPPLY,
|
|
CONF_RESTORE_MODE,
|
|
CONF_RESTORE_STATE,
|
|
CONF_RGB_ORDER,
|
|
CONF_TRIGGER_ID,
|
|
CONF_WARM_WHITE_COLOR_TEMPERATURE,
|
|
CONF_WEB_SERVER,
|
|
)
|
|
from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority
|
|
from esphome.core.entity_helpers import (
|
|
entity_duplicate_validator,
|
|
queue_entity_register,
|
|
setup_entity,
|
|
)
|
|
from esphome.cpp_generator import MockObjClass
|
|
import esphome.final_validate as fv
|
|
from esphome.types import ConfigType
|
|
|
|
from .automation import LIGHT_STATE_SCHEMA
|
|
from .effects import (
|
|
ADDRESSABLE_EFFECTS,
|
|
BINARY_EFFECTS,
|
|
EFFECTS_REGISTRY,
|
|
MONOCHROMATIC_EFFECTS,
|
|
RGB_EFFECTS,
|
|
validate_effects,
|
|
)
|
|
from .restore_state import (
|
|
LEGACY_RESTORE_MODES,
|
|
RESTORE_STATE_NONE,
|
|
RESTORE_STATE_SCHEMA,
|
|
_build_state_lambda,
|
|
_initial_state_overridden_by_legacy_mode,
|
|
_initial_state_statements,
|
|
_legacy_cold_boot_statements,
|
|
_legacy_restore_statements,
|
|
_restore_state_statements,
|
|
)
|
|
from .types import ( # noqa: F401
|
|
AddressableLight,
|
|
AddressableLightState,
|
|
ChannelColors,
|
|
ColorMode,
|
|
GammaTable,
|
|
LightOutput,
|
|
LightState,
|
|
LightStateRTCState,
|
|
LightStateTrigger,
|
|
LightTurnOffTrigger,
|
|
LightTurnOnTrigger,
|
|
light_ns,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CODEOWNERS = ["@esphome/core"]
|
|
IS_PLATFORM_COMPONENT = True
|
|
|
|
DOMAIN = "light"
|
|
CONF_GAMMA_TABLE_ID = "gamma_table_id"
|
|
|
|
|
|
@dataclass
|
|
class EffectRef:
|
|
"""A pending effect name reference from a light action to validate."""
|
|
|
|
light_id: ID
|
|
effect_name: str
|
|
component_path: list[str | int] # path_context when the action was validated
|
|
|
|
|
|
@dataclass
|
|
class EffectCycleRef:
|
|
"""A pending light.effect.next/previous action to validate.
|
|
|
|
Records that the referenced light needs at least one effect configured.
|
|
"""
|
|
|
|
light_id: ID
|
|
component_path: list[str | int]
|
|
|
|
|
|
@dataclass
|
|
class LightData:
|
|
gamma_tables: dict = field(default_factory=dict) # gamma_value -> fwd_arr
|
|
effect_refs: list[EffectRef] = field(default_factory=list)
|
|
effect_cycle_refs: list[EffectCycleRef] = field(default_factory=list)
|
|
|
|
|
|
def _get_data() -> LightData:
|
|
if DOMAIN not in CORE.data:
|
|
CORE.data[DOMAIN] = LightData()
|
|
return CORE.data[DOMAIN]
|
|
|
|
|
|
def generate_gamma_table(gamma_correct: float) -> list[HexInt]:
|
|
"""Generate a 256-entry uint16 gamma lookup table.
|
|
|
|
For gamma > 0, non-zero indices are clamped to a minimum of 1 to preserve
|
|
the invariant that non-zero input always produces non-zero output. Without
|
|
this, small brightness values (e.g. 1%) get quantized to exactly 0.0,
|
|
which breaks zero_means_zero logic in FloatOutput.
|
|
"""
|
|
if gamma_correct > 0:
|
|
return [
|
|
HexInt(
|
|
max(1, min(65535, int(round((i / 255.0) ** gamma_correct * 65535))))
|
|
if i > 0
|
|
else HexInt(0)
|
|
)
|
|
for i in range(256)
|
|
]
|
|
return [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)]
|
|
|
|
|
|
def gamma_table_initializer(gamma_correct: float) -> str:
|
|
"""C++ initializer for a light::GammaTable: the lookup table, then gamma * 100."""
|
|
lut = ", ".join(f"0x{int(v):04X}" for v in generate_gamma_table(gamma_correct))
|
|
# gamma_x100 is a uint16_t; platforms that redefine gamma_correct leave it unbounded, so saturate here
|
|
return f"{{{{{lut}}}, {min(0xFFFF, round(gamma_correct * 100))}}}"
|
|
|
|
|
|
def _get_or_create_gamma_table(gamma_correct: float, table_id: ID) -> cg.RawExpression:
|
|
data = _get_data()
|
|
if gamma_correct in data.gamma_tables:
|
|
return data.gamma_tables[gamma_correct]
|
|
|
|
# table_id is generated and resolved against every declared ID, so it can't collide with a
|
|
# YAML ID; lights sharing a gamma reuse the first light's table.
|
|
cg.add(
|
|
cg.RawStatement(
|
|
f"static constexpr light::GammaTable {table_id} PROGMEM = "
|
|
f"{gamma_table_initializer(gamma_correct)};"
|
|
)
|
|
)
|
|
table = cg.RawExpression(f"&{table_id}")
|
|
data.gamma_tables[gamma_correct] = table
|
|
return table
|
|
|
|
|
|
def find_effect_index(effects: list, effect_name: str) -> int | None:
|
|
"""Find the 1-based index of an effect by name (case-insensitive).
|
|
|
|
Returns the 1-based index if found, or None if not found.
|
|
"""
|
|
effect_name_lower = effect_name.lower()
|
|
for i, effect_conf in enumerate(effects):
|
|
key = next(iter(effect_conf))
|
|
if effect_conf[key][CONF_NAME].lower() == effect_name_lower:
|
|
return i + 1
|
|
return None
|
|
|
|
|
|
def available_effects_str(effects: list) -> str:
|
|
"""Return a comma-separated string of available effect names."""
|
|
available = [
|
|
effect_conf[next(iter(effect_conf))][CONF_NAME] for effect_conf in effects
|
|
]
|
|
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 every configured light's own resolved config, and all recorded
|
|
effect name references against their target lights.
|
|
|
|
FINAL_VALIDATE_SCHEMA for a platform-based domain like `light:` runs once for
|
|
the whole domain, not once per entry -- `config` is the full list of light
|
|
platform entries across the file, not a single light's own config.
|
|
"""
|
|
for light_config in config:
|
|
restore_mode = light_config.get(CONF_RESTORE_MODE)
|
|
if restore_mode is not None:
|
|
legacy = LEGACY_RESTORE_MODES[restore_mode]
|
|
if _initial_state_overridden_by_legacy_mode(
|
|
legacy, light_config.get(CONF_INITIAL_STATE)
|
|
):
|
|
_LOGGER.warning(
|
|
"[%s] 'initial_state: state' is ignored because 'restore_mode: %s' "
|
|
"always sets the light %s at boot; use 'restore_state:' instead for "
|
|
"per-field control",
|
|
light_config.get(CONF_NAME) or light_config[CONF_ID],
|
|
restore_mode,
|
|
"ON" if legacy.cold_boot_state else "OFF",
|
|
)
|
|
|
|
data = _get_data()
|
|
if not data.effect_refs and not data.effect_cycle_refs:
|
|
return
|
|
|
|
# Drain the lists so each recorded reference is only validated once.
|
|
refs = data.effect_refs
|
|
data.effect_refs = []
|
|
cycle_refs = data.effect_cycle_refs
|
|
data.effect_cycle_refs = []
|
|
|
|
fconf = fv.full_config.get()
|
|
|
|
for ref in refs:
|
|
try:
|
|
light_path = fconf.get_path_for_id(ref.light_id)[:-1]
|
|
light_config = fconf.get_config_for_path(light_path)
|
|
except KeyError:
|
|
# Light ID not found — ID validation will have already reported this
|
|
continue
|
|
|
|
effects = light_config.get(CONF_EFFECTS, [])
|
|
|
|
if find_effect_index(effects, ref.effect_name) is None:
|
|
raise cv.FinalExternalInvalid(
|
|
f"Effect '{ref.effect_name}' not found for light "
|
|
f"'{ref.light_id}'. "
|
|
f"Available effects: {available_effects_str(effects)}",
|
|
path=[cv.ROOT_CONFIG_PATH] + ref.component_path,
|
|
)
|
|
|
|
for ref in cycle_refs:
|
|
try:
|
|
light_path = fconf.get_path_for_id(ref.light_id)[:-1]
|
|
light_config = fconf.get_config_for_path(light_path)
|
|
except KeyError:
|
|
continue
|
|
|
|
if not light_config.get(CONF_EFFECTS):
|
|
raise cv.FinalExternalInvalid(
|
|
f"Light '{ref.light_id}' has no effects configured, but a "
|
|
f"'light.effect.next' or 'light.effect.previous' action "
|
|
f"references it. Add at least one effect to the light.",
|
|
path=[cv.ROOT_CONFIG_PATH] + ref.component_path,
|
|
)
|
|
|
|
|
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
|
|
|
|
|
# Schema default that also matches the C++ initializer in light_state.h; codegen
|
|
# skips the setter when the config equals it.
|
|
DEFAULT_FLASH_TRANSITION_LENGTH = "0s"
|
|
CONF_TRANSITION_STATE_PUBLISH_INTERVAL = "transition_state_publish_interval"
|
|
|
|
LIGHT_SCHEMA = (
|
|
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
|
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
|
.extend(
|
|
{
|
|
cv.GenerateID(): cv.declare_id(LightState),
|
|
cv.GenerateID(CONF_GAMMA_TABLE_ID): cv.declare_id(GammaTable),
|
|
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
|
mqtt.MQTTJSONLightComponent
|
|
),
|
|
cv.Exclusive(CONF_RESTORE_MODE, "restore"): cv.one_of(
|
|
*LEGACY_RESTORE_MODES, upper=True, space="_"
|
|
),
|
|
cv.Exclusive(CONF_RESTORE_STATE, "restore"): RESTORE_STATE_SCHEMA,
|
|
cv.Optional(CONF_ON_TURN_ON): auto.validate_automation(
|
|
{
|
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOnTrigger),
|
|
}
|
|
),
|
|
cv.Optional(CONF_ON_TURN_OFF): auto.validate_automation(
|
|
{
|
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOffTrigger),
|
|
}
|
|
),
|
|
cv.Optional(CONF_ON_STATE): auto.validate_automation(
|
|
{
|
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightStateTrigger),
|
|
}
|
|
),
|
|
cv.Optional(CONF_INITIAL_STATE): LIGHT_STATE_SCHEMA,
|
|
}
|
|
)
|
|
)
|
|
|
|
LIGHT_SCHEMA.add_extra(entity_duplicate_validator("light"))
|
|
|
|
BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend(
|
|
{
|
|
cv.Optional(CONF_EFFECTS): validate_effects(BINARY_EFFECTS),
|
|
}
|
|
)
|
|
|
|
BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend(
|
|
{
|
|
cv.Optional(CONF_GAMMA_CORRECT, default=2.8): cv.positive_float,
|
|
cv.Optional(
|
|
CONF_DEFAULT_TRANSITION_LENGTH, default="1s"
|
|
): cv.positive_time_period_milliseconds,
|
|
cv.Optional(
|
|
CONF_FLASH_TRANSITION_LENGTH, default=DEFAULT_FLASH_TRANSITION_LENGTH
|
|
): cv.positive_time_period_milliseconds,
|
|
# Below 150ms a device cannot publish any faster and only spends CPU and traffic
|
|
cv.Optional(CONF_TRANSITION_STATE_PUBLISH_INTERVAL): cv.All(
|
|
cv.positive_time_period_milliseconds,
|
|
cv.Range(min=cv.TimePeriod(milliseconds=150)),
|
|
),
|
|
cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS),
|
|
}
|
|
)
|
|
|
|
RGB_LIGHT_SCHEMA = BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend(
|
|
{
|
|
cv.Optional(CONF_EFFECTS): validate_effects(RGB_EFFECTS),
|
|
}
|
|
)
|
|
|
|
ADDRESSABLE_LIGHT_SCHEMA = RGB_LIGHT_SCHEMA.extend(
|
|
{
|
|
cv.GenerateID(): cv.declare_id(AddressableLightState),
|
|
# The addressable transformer writes the LED buffer directly, so there is no
|
|
# intermediate state to publish
|
|
cv.Optional(CONF_TRANSITION_STATE_PUBLISH_INTERVAL): cv.invalid(
|
|
"transition_state_publish_interval is not supported on addressable lights"
|
|
),
|
|
cv.Optional(CONF_EFFECTS): validate_effects(ADDRESSABLE_EFFECTS),
|
|
cv.Optional(CONF_COLOR_CORRECT): cv.All(
|
|
[cv.percentage], cv.Length(min=3, max=4)
|
|
),
|
|
cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply),
|
|
}
|
|
)
|
|
|
|
|
|
class LightType(enum.IntEnum):
|
|
"""Light type enum."""
|
|
|
|
BINARY = 0
|
|
BRIGHTNESS_ONLY = 1
|
|
RGB = 2
|
|
ADDRESSABLE = 3
|
|
|
|
|
|
def _apply_default_restore_mode(
|
|
default_restore_mode: str,
|
|
) -> Callable[[ConfigType], ConfigType]:
|
|
# cv.Exclusive has no default, so apply the default here if neither key is configured.
|
|
def validator(config: ConfigType) -> ConfigType:
|
|
if CONF_RESTORE_MODE not in config and CONF_RESTORE_STATE not in config:
|
|
config[CONF_RESTORE_MODE] = cv.one_of(
|
|
*LEGACY_RESTORE_MODES, upper=True, space="_"
|
|
)(default_restore_mode)
|
|
return config
|
|
|
|
return validator
|
|
|
|
|
|
_BASE_SCHEMAS: dict[LightType, cv.Schema] = {
|
|
LightType.BINARY: BINARY_LIGHT_SCHEMA,
|
|
LightType.BRIGHTNESS_ONLY: BRIGHTNESS_ONLY_LIGHT_SCHEMA,
|
|
LightType.RGB: RGB_LIGHT_SCHEMA,
|
|
LightType.ADDRESSABLE: ADDRESSABLE_LIGHT_SCHEMA,
|
|
}
|
|
|
|
|
|
def light_schema(
|
|
class_: MockObjClass,
|
|
type_: LightType,
|
|
*,
|
|
entity_category: str = cv.UNDEFINED,
|
|
icon: str = cv.UNDEFINED,
|
|
default_restore_mode: str = cv.UNDEFINED,
|
|
) -> cv.Schema:
|
|
schema = {
|
|
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(class_),
|
|
}
|
|
|
|
for key, default, validator in [
|
|
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
|
(CONF_ICON, icon, cv.icon),
|
|
]:
|
|
if default is not cv.UNDEFINED:
|
|
schema[cv.Optional(key, default=default)] = validator
|
|
|
|
result = _BASE_SCHEMAS[type_].extend(schema)
|
|
if default_restore_mode is not cv.UNDEFINED:
|
|
result.add_extra(_apply_default_restore_mode(default_restore_mode))
|
|
return result
|
|
|
|
|
|
def validate_color_temperature_channels(value):
|
|
if (
|
|
CONF_COLD_WHITE_COLOR_TEMPERATURE in value
|
|
and CONF_WARM_WHITE_COLOR_TEMPERATURE in value
|
|
and value[CONF_COLD_WHITE_COLOR_TEMPERATURE]
|
|
>= value[CONF_WARM_WHITE_COLOR_TEMPERATURE]
|
|
):
|
|
raise cv.Invalid(
|
|
"Color temperature of the cold white channel must be colder than that of the warm white channel.",
|
|
path=[CONF_COLD_WHITE_COLOR_TEMPERATURE],
|
|
)
|
|
return value
|
|
|
|
|
|
@setup_entity("light")
|
|
async def setup_light_core_(light_var, config, output_var):
|
|
# All 8 legacy restore_mode values, and the restore_state key, are just different
|
|
# ways to build the same state callback and save_enabled flag that LightState's
|
|
# runtime actually understands.
|
|
initial_state_config = config.get(CONF_INITIAL_STATE)
|
|
initial_statements = await _initial_state_statements(initial_state_config)
|
|
|
|
restore_mode = config.get(CONF_RESTORE_MODE)
|
|
restore_state_config = config.get(CONF_RESTORE_STATE)
|
|
if restore_state_config == RESTORE_STATE_NONE:
|
|
# restore_state: none is explicit shorthand for "no restoring at all" --
|
|
restore_state_config = None
|
|
|
|
if restore_mode is not None:
|
|
legacy = LEGACY_RESTORE_MODES[restore_mode]
|
|
initial_statements.extend(
|
|
_legacy_cold_boot_statements(legacy, initial_state_config)
|
|
)
|
|
restore_statements = _legacy_restore_statements(legacy)
|
|
save_enabled = legacy.save_enabled
|
|
elif restore_state_config is not None:
|
|
restore_statements = await _restore_state_statements(
|
|
restore_state_config, initial_state_config
|
|
)
|
|
save_enabled = True
|
|
else:
|
|
# Neither key configured: no persistence, and no cold-boot forcing either.
|
|
restore_statements = []
|
|
save_enabled = False
|
|
|
|
if (
|
|
lamb := await _build_state_lambda(
|
|
initial_statements, restore_statements, save_enabled
|
|
)
|
|
) is not None:
|
|
cg.add(light_var.set_state_callback(lamb))
|
|
if save_enabled: # matches LightState::save_enabled_'s own default of false
|
|
cg.add(light_var.set_save_enabled(save_enabled))
|
|
|
|
if (
|
|
default_transition_length := config.get(CONF_DEFAULT_TRANSITION_LENGTH)
|
|
) is not None:
|
|
cg.add(light_var.set_default_transition_length(default_transition_length))
|
|
# Skip the setter when the config matches the C++ initializer.
|
|
if (
|
|
flash_transition_length := config.get(CONF_FLASH_TRANSITION_LENGTH)
|
|
) is not None and flash_transition_length != cv.time_period(
|
|
DEFAULT_FLASH_TRANSITION_LENGTH
|
|
):
|
|
cg.add_define("USE_LIGHT_FLASH_TRANSITION_LENGTH")
|
|
cg.add(light_var.set_flash_transition_length(flash_transition_length))
|
|
# Setting an interval opts this light in and compiles the feature in
|
|
if (interval := config.get(CONF_TRANSITION_STATE_PUBLISH_INTERVAL)) is not None:
|
|
cg.add(light_var.set_transition_state_publish_interval(interval))
|
|
cg.add_define("USE_LIGHT_TRANSITION_PUBLISH_INTERVAL")
|
|
if (gamma_correct := config.get(CONF_GAMMA_CORRECT)) is not None:
|
|
fwd_arr = _get_or_create_gamma_table(gamma_correct, config[CONF_GAMMA_TABLE_ID])
|
|
cg.add(light_var.set_gamma_table(fwd_arr))
|
|
cg.add_define("USE_LIGHT_GAMMA_LUT")
|
|
effects = await cg.build_registry_list(
|
|
EFFECTS_REGISTRY, config.get(CONF_EFFECTS, [])
|
|
)
|
|
if effects:
|
|
cg.add(light_var.add_effects(effects))
|
|
|
|
for conf in config.get(CONF_ON_TURN_ON, []):
|
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var)
|
|
await auto.build_automation(trigger, [], conf)
|
|
for conf in config.get(CONF_ON_TURN_OFF, []):
|
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var)
|
|
await auto.build_automation(trigger, [], conf)
|
|
for conf in config.get(CONF_ON_STATE, []):
|
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var)
|
|
await auto.build_automation(trigger, [], conf)
|
|
|
|
if (color_correct := config.get(CONF_COLOR_CORRECT)) is not None:
|
|
cg.add(output_var.set_correction(*color_correct))
|
|
|
|
if (power_supply_id := config.get(CONF_POWER_SUPPLY)) is not None:
|
|
var_ = await cg.get_variable(power_supply_id)
|
|
cg.add(output_var.set_power_supply(var_))
|
|
|
|
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
|
mqtt_ = cg.new_Pvariable(mqtt_id, light_var)
|
|
await mqtt.register_mqtt_component(mqtt_, config)
|
|
|
|
if web_server_config := config.get(CONF_WEB_SERVER):
|
|
await web_server.add_entity_config(light_var, web_server_config)
|
|
|
|
|
|
async def register_light(output_var, config):
|
|
light_var = cg.new_Pvariable(config[CONF_ID], output_var)
|
|
queue_entity_register("light", config)
|
|
CORE.register_platform_component("light", light_var)
|
|
await cg.register_component(light_var, config)
|
|
await setup_light_core_(light_var, config, output_var)
|
|
|
|
|
|
async def new_light(config, *args):
|
|
output_var = cg.new_Pvariable(config[CONF_OUTPUT_ID], *args)
|
|
await register_light(output_var, config)
|
|
return output_var
|
|
|
|
|
|
@coroutine_with_priority(CoroPriority.CORE)
|
|
async def to_code(config):
|
|
cg.add_global(light_ns.using)
|
|
|
|
|
|
# light_json_schema.cpp is only used by mqtt and web_server, which both
|
|
# auto load json; USE_JSON alone is too broad since other components load it.
|
|
FILTER_SOURCE_FILES = filter_source_files_from_defines(
|
|
{"light_json_schema.cpp": ("USE_MQTT", "USE_WEBSERVER")}
|
|
)
|