Files
esphome-lsm6ds/esphome/components/light/automation.py
T

361 lines
13 KiB
Python

from esphome import automation
import esphome.codegen as cg
from esphome.config import path_context
import esphome.config_validation as cv
from esphome.const import (
CONF_BLUE,
CONF_BRIGHTNESS,
CONF_BRIGHTNESS_LIMITS,
CONF_COLD_WHITE,
CONF_COLOR_BRIGHTNESS,
CONF_COLOR_MODE,
CONF_COLOR_TEMPERATURE,
CONF_EFFECT,
CONF_EFFECTS,
CONF_FLASH_LENGTH,
CONF_GREEN,
CONF_ID,
CONF_LIMIT_MODE,
CONF_MAX_BRIGHTNESS,
CONF_MIN_BRIGHTNESS,
CONF_RANGE_FROM,
CONF_RANGE_TO,
CONF_RED,
CONF_STATE,
CONF_TRANSITION_LENGTH,
CONF_WARM_WHITE,
CONF_WHITE,
)
from esphome.core import CORE, EsphomeError, Lambda
from esphome.cpp_generator import LambdaExpression
from esphome.types import ConfigType
from .types import (
COLOR_MODES,
LIMIT_MODES,
AddressableLightState,
AddressableSet,
ColorMode,
DimRelativeAction,
LightControlAction,
LightIsOffCondition,
LightIsOnCondition,
LightState,
ToggleAction,
)
@automation.register_action(
"light.toggle",
ToggleAction,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(LightState),
cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(
cv.positive_time_period_milliseconds
),
}
),
synchronous=True,
)
async def light_toggle_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
has_transition_length = CONF_TRANSITION_LENGTH in config
toggle_template_arg = cg.TemplateArguments(has_transition_length, *template_arg)
var = cg.new_Pvariable(action_id, toggle_template_arg, paren)
if has_transition_length:
template_ = await cg.templatable(
config[CONF_TRANSITION_LENGTH], args, cg.uint32
)
cg.add(var.set_transition_length(template_))
return var
LIGHT_STATE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_COLOR_MODE): cv.enum(COLOR_MODES, upper=True, space="_"),
cv.Optional(CONF_STATE): cv.templatable(cv.boolean),
cv.Optional(CONF_BRIGHTNESS): cv.templatable(cv.percentage),
cv.Optional(CONF_COLOR_BRIGHTNESS): cv.templatable(cv.percentage),
cv.Optional(CONF_RED): cv.templatable(cv.percentage),
cv.Optional(CONF_GREEN): cv.templatable(cv.percentage),
cv.Optional(CONF_BLUE): cv.templatable(cv.percentage),
cv.Optional(CONF_WHITE): cv.templatable(cv.percentage),
cv.Optional(CONF_COLOR_TEMPERATURE): cv.templatable(cv.color_temperature),
cv.Optional(CONF_COLD_WHITE): cv.templatable(cv.percentage),
cv.Optional(CONF_WARM_WHITE): cv.templatable(cv.percentage),
}
)
LIGHT_CONTROL_ACTION_SCHEMA = LIGHT_STATE_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.use_id(LightState),
cv.Exclusive(CONF_TRANSITION_LENGTH, "transformer"): cv.templatable(
cv.positive_time_period_milliseconds
),
cv.Exclusive(CONF_FLASH_LENGTH, "transformer"): cv.templatable(
cv.positive_time_period_milliseconds
),
cv.Exclusive(CONF_EFFECT, "transformer"): cv.templatable(cv.string),
}
)
def _record_effect_ref(config: ConfigType) -> ConfigType:
"""Record a static effect name reference for later cross-component validation."""
if CONF_EFFECT not in config:
return config
effect = config[CONF_EFFECT]
if isinstance(effect, Lambda):
return config # Lambda effects resolved at runtime
if effect.lower() == "none":
return config # "None" is always valid
from . import EffectRef, _get_data
_get_data().effect_refs.append(
EffectRef(
light_id=config[CONF_ID],
effect_name=effect,
component_path=path_context.get(),
)
)
return config
LIGHT_CONTROL_ACTION_SCHEMA.add_extra(_record_effect_ref)
LIGHT_TURN_OFF_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(LightState),
cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(
cv.positive_time_period_milliseconds
),
cv.Optional(CONF_STATE, default=False): False,
}
)
LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id(
LIGHT_CONTROL_ACTION_SCHEMA.extend(
{
cv.Optional(CONF_STATE, default=True): True,
}
)
)
def _resolve_effect_index(config: ConfigType) -> int:
"""Resolve a static effect name to its 1-based index at codegen time.
Effect index 0 means "None" (no effect). Effects are 1-indexed matching
the C++ convention in LightState.
"""
from . import available_effects_str, find_effect_index
original_name = config[CONF_EFFECT]
if original_name.lower() == "none":
return 0
light_id = config[CONF_ID]
light_path = CORE.config.get_path_for_id(light_id)[:-1]
light_config = CORE.config.get_config_for_path(light_path)
effects = light_config.get(CONF_EFFECTS, [])
index = find_effect_index(effects, original_name)
if index is not None:
return index
# Should never reach here — effect names are validated during config
# validation in FINAL_VALIDATE_SCHEMA. This is a safety net.
raise EsphomeError(
f"Effect '{original_name}' not found for light '{light_id}'. "
f"Available effects: {available_effects_str(effects)}"
)
@automation.register_action(
"light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"light.turn_on", LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA, synchronous=True
)
async def light_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
# Order/bits must match LIGHT_CONTROL_FIELDS in automation.h.
# EFFECT has special handling below; setter=None skips the generic loop.
FIELDS = (
(CONF_COLOR_MODE, "set_color_mode", ColorMode),
(CONF_STATE, "set_state", cg.bool_),
(CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32),
(CONF_FLASH_LENGTH, "set_flash_length", cg.uint32),
(CONF_BRIGHTNESS, "set_brightness", cg.float_),
(CONF_COLOR_BRIGHTNESS, "set_color_brightness", cg.float_),
(CONF_RED, "set_red", cg.float_),
(CONF_GREEN, "set_green", cg.float_),
(CONF_BLUE, "set_blue", cg.float_),
(CONF_WHITE, "set_white", cg.float_),
(CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_),
(CONF_COLD_WHITE, "set_cold_white", cg.float_),
(CONF_WARM_WHITE, "set_warm_white", cg.float_),
(CONF_EFFECT, None, cg.uint32),
)
# Bitmask is passed as uint16_t in C++ — must stay within 16 bits.
assert len(FIELDS) <= 16, "LightControlAction Fields bitmask exceeds uint16_t"
field_mask = sum(1 << i for i, (k, _, _) in enumerate(FIELDS) if k in config)
control_template_arg = cg.TemplateArguments(
cg.RawExpression(f"static_cast<uint16_t>({field_mask})"), *template_arg
)
var = cg.new_Pvariable(action_id, control_template_arg, paren)
for conf_key, setter, type_ in FIELDS:
if conf_key in config and setter is not None:
template_ = await cg.templatable(config[conf_key], args, type_)
cg.add(getattr(var, setter)(template_))
if CONF_EFFECT in config:
if isinstance(config[CONF_EFFECT], Lambda):
# Lambda returns a string — wrap in a C++ lambda that resolves
# the effect name to its uint32_t index at runtime
inner_lambda = await cg.process_lambda(
config[CONF_EFFECT], args, return_type=cg.std_string
)
fwd_args = ", ".join(n for _, n in args)
# capture="" is correct: paren is a global variable name
# string-interpolated into the body at codegen time, not a
# C++ runtime capture.
wrapper = LambdaExpression(
f"auto __effect_s = ({inner_lambda})({fwd_args});\n"
f"return {paren}->get_effect_index("
f"__effect_s.c_str(), __effect_s.size());",
args,
capture="",
return_type=cg.uint32,
)
cg.add(var.set_effect(wrapper))
else:
# Static string — resolve effect name to index at codegen time
template_ = await cg.templatable(
_resolve_effect_index(config), args, cg.uint32
)
cg.add(var.set_effect(template_))
return var
CONF_RELATIVE_BRIGHTNESS = "relative_brightness"
LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(LightState),
cv.Required(CONF_RELATIVE_BRIGHTNESS): cv.templatable(
cv.possibly_negative_percentage
),
cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(
cv.positive_time_period_milliseconds
),
cv.Optional(CONF_BRIGHTNESS_LIMITS): cv.Schema(
{
cv.Optional(CONF_MIN_BRIGHTNESS, default="0%"): cv.percentage,
cv.Optional(CONF_MAX_BRIGHTNESS, default="100%"): cv.percentage,
cv.Optional(CONF_LIMIT_MODE, default="CLAMP"): cv.enum(
LIMIT_MODES, upper=True, space="_"
),
}
),
}
)
@automation.register_action(
"light.dim_relative",
DimRelativeAction,
LIGHT_DIM_RELATIVE_ACTION_SCHEMA,
synchronous=True,
)
async def light_dim_relative_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
templ = await cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, cg.float_)
cg.add(var.set_relative_brightness(templ))
if CONF_TRANSITION_LENGTH in config:
templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32)
cg.add(var.set_transition_length(templ))
if conf := config.get(CONF_BRIGHTNESS_LIMITS):
cg.add(
var.set_min_max_brightness(
conf[CONF_MIN_BRIGHTNESS], conf[CONF_MAX_BRIGHTNESS]
)
)
cg.add(var.set_limit_mode(conf[CONF_LIMIT_MODE]))
return var
LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(AddressableLightState),
cv.Optional(CONF_RANGE_FROM): cv.templatable(cv.positive_int),
cv.Optional(CONF_RANGE_TO): cv.templatable(cv.positive_int),
cv.Optional(CONF_COLOR_BRIGHTNESS): cv.templatable(cv.percentage),
cv.Optional(CONF_RED): cv.templatable(cv.percentage),
cv.Optional(CONF_GREEN): cv.templatable(cv.percentage),
cv.Optional(CONF_BLUE): cv.templatable(cv.percentage),
cv.Optional(CONF_WHITE): cv.templatable(cv.percentage),
}
)
@automation.register_action(
"light.addressable_set",
AddressableSet,
LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA,
synchronous=True,
)
async def light_addressable_set_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_RANGE_FROM in config:
templ = await cg.templatable(config[CONF_RANGE_FROM], args, cg.int32)
cg.add(var.set_range_from(templ))
if CONF_RANGE_TO in config:
templ = await cg.templatable(config[CONF_RANGE_TO], args, cg.int32)
cg.add(var.set_range_to(templ))
if CONF_COLOR_BRIGHTNESS in config:
templ = await cg.templatable(config[CONF_COLOR_BRIGHTNESS], args, cg.float_)
cg.add(var.set_color_brightness(templ))
if CONF_RED in config:
templ = await cg.templatable(config[CONF_RED], args, cg.float_)
cg.add(var.set_red(templ))
if CONF_GREEN in config:
templ = await cg.templatable(config[CONF_GREEN], args, cg.float_)
cg.add(var.set_green(templ))
if CONF_BLUE in config:
templ = await cg.templatable(config[CONF_BLUE], args, cg.float_)
cg.add(var.set_blue(templ))
if CONF_WHITE in config:
templ = await cg.templatable(config[CONF_WHITE], args, cg.float_)
cg.add(var.set_white(templ))
return var
@automation.register_condition(
"light.is_on",
LightIsOnCondition,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(LightState),
}
),
)
@automation.register_condition(
"light.is_off",
LightIsOffCondition,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(LightState),
}
),
)
async def light_is_on_off_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)